¿Puedo usar la inyección de dependencia sin romper la encapsulación?

14

Aquí está mi solución y proyectos:

  • BookStore (solución)
    • BookStore.Coupler (proyecto)
      • Bootstrapper.cs
    • BookStore.Domain (project)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (proyecto)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (proyecto)
      • Global.asax
    • BookStore.BatchProcesses (proyecto)
      • Program.cs

Bootstrapper.cs :

public static class Bootstrapper.cs 
{
    // I'm using SimpleInjector as my DI Container
    public static void Initialize(Container container) 
    {
        container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>), typeof(CreateBookCommandHandler).Assembly);
        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>));
        container.RegisterManyForOpenGeneric(typeof(IValidate<>),
            AccessibilityOption.PublicTypesOnly,
            (serviceType, implTypes) => container.RegisterAll(serviceType, implTypes),
            typeof(IValidate<>).Assembly);
        container.RegisterSingleOpenGeneric(typeof(IValidator<>), typeof(CompositeValidator<>));
    }
}

CreateBookCommandValidator.cs

public class CreateBookCommandValidator : IValidate<CreateBookCommand>
{
    public IEnumerable<IValidationResult> Validate(CreateBookCommand book)
    {
        if (book.Author == "Evan")
        {
            yield return new ValidationResult<CreateBookCommand>("Evan cannot be the Author!", p => p.Author);
        }
        if (book.Price < 0)
        {
            yield return new ValidationResult<CreateBookCommand>("The price can not be less than zero", p => p.Price);
        }
    }
}

CompositeValidator.cs

public class CompositeValidator<T> : IValidator<T>
{
    private readonly IEnumerable<IValidate<T>> validators;

    public CompositeValidator(IEnumerable<IValidate<T>> validators)
    {
        this.validators = validators;
    }

    public IEnumerable<IValidationResult> Validate(T instance)
    {
        var allResults = new List<IValidationResult>();

        foreach (var validator in this.validators)
        {
            var results = validator.Validate(instance);
            allResults.AddRange(results);
        }
        return allResults;
    }
}

IValidate.cs

public interface IValidate<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

IValidator.cs

public interface IValidator<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

ICommandHandler.cs

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

CreateBookCommandHandler.cs

public class CreateBookCommandHandler : ICommandHandler<CreateBookCommand>
{
    private readonly IBookStore _bookStore;

    public CreateBookCommandHandler(IBookStore bookStore)
    {
        _bookStore = bookStore;
    }

    public void Handle(CreateBookCommand command)
    {
        var book = new Book { Author = command.Author, Name = command.Name, Price = command.Price };
        _bookStore.SaveBook(book);
    }
}

ValidationCommandHandlerDecorator.cs

public class ValidationCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;
    private readonly IValidator<TCommand> validator;

    public ValidationCommandHandlerDecorator(ICommandHandler<TCommand> decorated, IValidator<TCommand> validator)
    {
        this.decorated = decorated;
        this.validator = validator;
    }

    public void Handle(TCommand command)
    {
        var results = validator.Validate(command);

        if (!results.IsValid())
        {
            throw new ValidationException(results);
        }

        decorated.Handle(command);
    }
}

Global.asax

// inside App_Start()
var container = new Container();
Bootstrapper.Initialize(container);
// more MVC specific bootstrapping to the container. Like wiring up controllers, filters, etc..

Program.cs

// Pretty much the same as the Global.asax

Lo siento por la larga configuración del problema, no tengo otra forma de explicar esto que no sea detallar mi problema real.

No quiero hacer mi CreateBookCommandValidator public . Preferiría que fuera internal , pero si lo hago internal , entonces no podré registrarlo con mi contenedor DI. La razón por la que me gustaría que fuera interno es porque el único proyecto que debería tener una noción de mi IValidate < > Las implementaciones están en el proyecto BookStore.Domain. Cualquier otro proyecto solo necesita consumir IValidator < > y se debe resolver el CompositeValidator que cumplirá todas las validaciones.

¿Cómo puedo usar la inyección de dependencia sin romper la encapsulación? ¿O voy a hacer esto mal?

    
pregunta Evan Larsen 30.12.2013 - 22:50
fuente

5 respuestas

11

Hacer que CreateBookCommandValidator sea público no viola la encapsulación, ya que

  

La encapsulación se utiliza para ocultar los valores o el estado de un dato estructurado   objeto dentro de una clase, evitando el acceso directo de personas no autorizadas   para ellos ( wikipedia )

Su CreateBookCommandValidator no permite el acceso a sus miembros de datos (actualmente no parece tener ninguno), por lo que no viola la encapsulación.

Hacer pública esta clase no viola ningún otro principio (como los principios SOLID ) , porque:

  • Esa clase tiene una responsabilidad única bien definida y, por lo tanto, sigue el Principio de Responsabilidad Única.
  • Se puede agregar nuevos validadores al sistema sin cambiar una sola línea de código y, por lo tanto, sigue el principio de apertura / cierre.
  • Ese IValidator < T > La interfaz que implementa esta clase es estrecha (solo tiene un miembro) y sigue el Principio de Segregación de Interfaz.
  • Sus consumidores solo dependen de ese IValidator < T > Interfaz y, por lo tanto, siga el principio de inversión de dependencia.

Solo puede hacer que CreateBookCommandValidator sea interno si la clase no se consume directamente desde fuera de la biblioteca, pero este casi nunca es el caso, ya que sus pruebas de unidad son un consumidor importante de esta clase (y casi todas las clases en tu sistema).

Aunque puede hacer que la clase sea interna y usar [InternalsVisibleTo] para permitir que el proyecto de prueba de unidad acceda a las partes internas de su proyecto, ¿por qué molestarse?

La razón más importante para hacer que las clases sean internas es para evitar que las partes externas (sobre las que usted no tiene control) tomen una dependencia de dicha clase, porque eso evitaría que haga cambios futuros a esa clase sin romper nada. En otras palabras, esto solo se cumple cuando creas una biblioteca reutilizable (como una biblioteca de inyección de dependencias). De hecho, Simple Injector contiene contenido interno y su proyecto de prueba de unidad prueba estos internos.

Sin embargo, si no está creando un proyecto reutilizable, este problema no existe. No existe, porque puede cambiar los proyectos que dependen de él y los demás desarrolladores de su equipo tendrán que seguir sus pautas. Y una simple guía hará: Programar a una abstracción; no una implementación (el principio de inversión de dependencia).

En pocas palabras, no hagas que esta clase sea interna a menos que estés escribiendo una biblioteca reutilizable.

Pero si aún desea que esta clase sea interna, aún puede registrarla con Simple Injector sin ningún problema como este:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    AccessibilityOption.AllTypes,
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

Lo único de lo que hay que asegurarse es que todos los validadores tengan un constructor público, aunque sean internos. Si realmente desea que sus tipos tengan un constructor interno (no sabe realmente por qué querría eso), puede anular el Comportamiento de resolución del constructor .

ACTUALIZAR

Desde Simple Injector v2.6 , el comportamiento predeterminado de RegisterManyForOpenGeneric es registrar tanto público como interno tipos Por lo tanto, el suministro de AccessibilityOption.AllTypes ahora es redundante y la siguiente declaración registrará tanto los tipos públicos como los internos:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
    
respondido por el Steven 31.12.2013 - 13:02
fuente
8

No es gran cosa que la clase CreateBookCommandValidator sea pública.

Si necesita crear instancias de él fuera de la biblioteca que lo define, es un enfoque bastante natural para exponer la clase pública, y contar con los clientes que solo usan esa clase como una implementación de IValidate<CreateBookCommand> . (La simple exposición de un tipo no significa que la encapsulación se rompa, solo hace que sea más fácil para los clientes romper la encapsulación).

De lo contrario, si realmente desea forzar a los clientes a no saber acerca de la clase, también puede usar un método de fábrica estático público en lugar de exponer la clase, por ejemplo:

public static class Validators
{
    public static IValidate<CreateBookCommand> NewCreateBookCommandValidator()
    {
        return new CreateBookCommnadValidator();
    }
}

En cuanto al registro en su contenedor DI, todos los contenedores DI que conozco permiten la construcción utilizando un método de fábrica estática.

    
respondido por el jhominal 30.12.2013 - 23:49
fuente
5

Puede declarar CreateBookCommandValidator como internal y aplicar InternalsVisibleToAttribute para hacerlo visible al ensamblado BookStore.Coupler . Esto también suele ayudar cuando se realizan pruebas unitarias.

    
respondido por el Olivier Jacot-Descombes 30.12.2013 - 23:49
fuente
4

Puede hacerlo interno y usar InternalVisibleToAttribute msdn.link para que su marco / proyecto de prueba pueda acceder a él.

Tuve un problema relacionado: > link .

Aquí hay un enlace a otra pregunta de Stackoverflow relacionada con el problema:

Y finalmente un artículo en la web.

    
respondido por el Johannes 31.12.2013 - 03:55
fuente
1

Otra opción es hacerlo público pero colocarlo en otro ensamblaje.

Básicamente, tiene un conjunto de interfaces de servicio, un conjunto de implementaciones de servicio (que hace referencia a las interfaces de servicio), un conjunto de consumidor de servicios (que hace referencia a interfaces de servicio) y un conjunto de registrador IOC (que hace referencia tanto a las interfaces de servicio como a las implementaciones de servicio a Engancharlos juntos.

Debo enfatizar, esta no es siempre la solución más apropiada, pero vale la pena considerarla.

    
respondido por el pdr 01.01.2014 - 03:46
fuente

Lea otras preguntas en las etiquetas