c# wcf dependency-injection refactoring cqrs

c# - Refactorización del servicio WCF "procedimental"



dependency-injection refactoring (1)

Estoy tratando de refactorizar un monstruoso servicio WCF en algo más manejable. En el momento de escribir, el servicio toma alrededor de 9 dependencias a través de un constructor, lo que hace que las pruebas unitarias sean muy difíciles.

El servicio maneja el estado local a través de la máquina de estado, realiza la validación de los parámetros, arroja excepciones de fallas, realiza la operación real y dispara eventos de publicación a través de un canal pub / sub. Este código es muy similar en todas las demás llamadas de servicio.

Me doy cuenta de que puedo hacer varias de esas cosas (validación de argumentos, notificaciones de pub / sub) de manera diferente, tal vez a través de la programación orientada a aspectos o comportamientos de WCF, pero mi instinto me dice que el enfoque general es incorrecto; esto parece demasiado "procedimental" .

Mi objetivo es separar la ejecución de la operación real de cosas como notificaciones de pub / sub, y quizás incluso el manejo de errores.

Me pregunto si acrónimos como DDD o CQRS u otras técnicas pueden ayudar aquí. Desafortunadamente, no estoy muy familiarizado con esos conceptos más allá de la definición.

Aquí hay un ejemplo (simplificado) de una de tales operaciones de WCF:

public void DoSomething(DoSomethingData data) { if (!_stateMachine.CanFire(MyEvents.StartProcessing)) { throw new FaultException(...); } if (!ValidateArgument(data)) { throw new FaultException(...); } var transitionResult = _stateMachine.Fire(MyEvents.StartProcessing); if (!transitionResult.Accepted) { throw new FaultException(...); } try { // does the actual something DoSomethingInternal(data); _publicationChannel.StatusUpdate(new Info { Status = transitionResult.NewState }); } catch (FaultException<MyError> faultException) { if (faultException.Detail.ErrorType == MyErrorTypes.EngineIsOffline) { TryFireEvent(MyServiceEvent.Error, faultException.Detail); } throw; } }


Lo que tienes ahí es un gran ejemplo de un comando disfrazado. Lo bueno de lo que estás haciendo aquí es que tu método de servicio ya tiene un solo argumento DoSomethingData . Este es su mensaje de comando.

Lo que te falta aquí es una abstracción general sobre los manejadores de comandos:

public interface ICommandHandler<TCommand> { void Handle(TCommand command); }

Con un poco de refactorización, su método de servicio se vería así:

// Vanilla dependency. ICommandHandler<DoSomethingData> doSomethingHandler; public void DoSomething(DoSomethingData data) { this.doSomethingHandler.Handle(data); }

Y, por supuesto, necesita una implementación para ICommandHandler<DoSomethingData> . En tu caso, se verá así:

public class DoSomethingHandler : ICommandHandler<DoSomethingData> { public void Handle(DoSomethingData command) { // does the actual something DoSomethingInternal(command); } }

Ahora se estará preguntando qué pasa con las preocupaciones transversales que implementó, como la validación de argumentos, la activación, la actualización del estado del canal de publicación y el manejo de errores. Bueno, sí, todas son preocupaciones transversales, tanto su clase de servicio WCF COMO su lógica de negocio ( DoSomethingHandler ) no deberían preocuparse por eso.

Hay varias formas de aplicar la Programación Orientada a Aspectos. A algunos les gusta usar herramientas de código como PostSharp. La desventaja de estas herramientas es que hacen que las pruebas unitarias sean mucho más difíciles, ya que entretejen todas sus preocupaciones transversales.

La segunda forma es mediante el uso de interceptación. Usando la generación de proxy dinámico y algo de reflexión. Sin embargo, hay una variación de esto que me gusta más, y eso es mediante la aplicación de decoradores. Lo bueno de esto es que, en mi experiencia, esta es la forma más limpia de aplicar problemas transversales.

Echemos un vistazo a un decorador para su validación:

public class WcfValidationCommandHandlerDecorator<T> : ICommandHandler<T> { private IValidator<T> validator; private ICommandHandler<T> wrapped; public ValidationCommandHandlerDecorator(IValidator<T> validator, ICommandHandler<T> wrapped) { this.validator = validator; this.wrapped = wrapped; } public void Handle(T command) { if (!this.validator.ValidateArgument(command)) { throw new FaultException(...); } // Command is valid. Let''s call the real handler. this.wrapped.Handle(command); } }

Como este WcfValidationCommandHandlerDecorator<T> es un tipo genérico, podemos envolverlo en cada controlador de comando. Por ejemplo:

var handler = new WcfValidationCommandHandlerDecorator<DoSomethingData>( new DoSomethingHandler(), new DoSomethingValidator());

Y también puedes crear fácilmente un decorador que maneje cualquier excepción arrojada:

public class WcfExceptionHandlerCommandHandlerDecorator<T> : ICommandHandler<T> { private ICommandHandler<T> wrapped; public ValidationCommandHandlerDecorator(ICommandHandler<T> wrapped) { this.wrapped = wrapped; } public void Handle(T command) { try { // does the actual something this.wrapped.Handle(command); _publicationChannel.StatusUpdate(new Info { Status = transitionResult.NewState }); } catch (FaultException<MyError> faultException) { if (faultException.Detail.ErrorType == MyErrorTypes.EngineIsOffline) { TryFireEvent(MyServiceEvent.Error, faultException.Detail); } throw; } } }

¿Viste cómo envolví tu código en este decorador? Nuevamente podemos usar este decorador para envolver el original:

var handler = new WcfValidationCommandHandlerDecorator<DoSomethingData>( new WcfExceptionHandlerCommandHandlerDecorator<DoSomethingData>( new DoSomethingHandler()), new DoSomethingValidator());

Por supuesto, todo esto parece una gran cantidad de código y si todo lo que tiene es un único método de servicio WCF que sí, esto es probablemente demasiado. Pero comienza a ser realmente interesante si tienes una docena más o menos. Si tienes cientos? Bueno ... No quiero ser el desarrollador que mantiene esa base de código si no estás usando una técnica como esta.

Luego de unos minutos de refactorización terminas con clases de servicio WCF que solo dependen de las interfaces ICommandHandler<TCommand> . Todas las preocupaciones transversales se ubicarán en los decoradores y, por supuesto, su biblioteca de DI conectará todo. Creo que sabes algunos ;-)

Cuando haya hecho esto, probablemente haya algo que podría mejorar, ya que todas las clases de servicios de WCF comenzarán a parecer aburridamente iguales:

// Vanilla dependency. ICommandHandler<FooData> handler; public void Foo(FooData data) { this.handler.Handle(data); }

Empezará a aburrirse para escribir nuevos comandos y nuevos manejadores. Aún tendrá que mantener su servicio WCF.

Lo que puede hacer, en cambio, es crear un servicio WCF con una única clase con un único método, como este:

[ServiceKnownType("GetKnownTypes")] public class CommandService { [OperationContract] public void Execute(object command) { Type commandHandlerType = typeof(ICommandHandler<>) .MakeGenericType(command.GetType()); dynamic commandHandler = Bootstrapper.GetInstance(commandHandlerType); commandHandler.Handle((dynamic)command); } public static IEnumerable<Type> GetKnownTypes(ICustomAttributeProvider provider) { // create and return a list of all command types // dynamically using reflection that this service // must accept. } }

Ahora todo lo que tiene es un servicio WCF con un único método que nunca cambiará. ServiceKnownTypeAttribute apunta a GetKnownTypes . WCF llamará a este método al inicio para ver qué tipos debe aceptar. Cuando devuelve la lista basada en los metadatos de la aplicación, le permite agregar y quitar comandos al sistema, sin tener que cambiar una sola línea en su servicio WCF.

Es probable que agregue decoradores nuevos específicos de WCF de vez en cuando y que normalmente deberían colocarse en el servicio WCF. Otros decoradores probablemente serían más generales y podrían colocarse en la capa de negocios en sí. Ellos pueden ser reutilizados por su aplicación MVC, por ejemplo.

Su pregunta fue un poco sobre CQRS, pero mi respuesta no tiene nada que ver con eso. Bueno ... nada es una exageración. CQRS usa este patrón extensamente, pero CQRS va un paso más allá. CQRS se trata de dominios colaborativos que te obligan a poner en cola los comandos y procesarlos de forma asincrónica. Mi respuesta, por otro lado, es simplemente aplicar los principios de diseño SOLID . SOLID es bueno en todas partes. No solo en dominios colaborativos.

Si desea leer más sobre esto, lea mi artículo sobre cómo aplicar controladores de comando . Después de eso, ve y lee mi artículo sobre cómo aplicar este principio a los servicios de WCF . Mi respuesta es un resumen de esos artículos.

Buena suerte.