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

15

Aquí está mi solución y proyectos:

  • Librería (solución)
    • BookStore.Coupler (proyecto)
      • Bootstrapper.cs
    • BookStore.Domain (proyecto)
      • 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

Perdón por la larga configuración del problema, no tengo mejor manera de explicar esto que detallar mi problema real.

No quiero hacer mi CreateBookCommandValidator public. Preferiría que fuera, internalpero si lo internalhago, no podré registrarlo en mi contenedor DI. La razón por la que me gustaría que sea interna es porque el único proyecto que debería tener noción de mis implementaciones IValidate <> está en el proyecto BookStore.Domain. Cualquier otro proyecto solo necesita consumir IValidator <> y el CompositeValidator debe resolverse para cumplir con todas las validaciones.

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

Evan Larsen
fuente
Solo un poco: lo que está utilizando no es un patrón de comando correcto, por lo que llamarlo comando podría ser información errónea. Además, CreateBookCommandHandler parece que está rompiendo LSP: ¿qué sucederá, si pasa un objeto, que se deriva de CreateBookCommand? Y creo que lo que estás haciendo aquí es en realidad un antipatrón de modelo de dominio anémico. Cosas como guardar deberían estar dentro del dominio y la validación debería ser parte de la entidad.
Eufórico
1
@Euphoric: Eso es correcto. Este no es el patrón de comando . De hecho, el OP sigue un patrón diferente: el patrón de comando / controlador .
Steven
Hubo tantas buenas respuestas que desearía haber marcado más como respuesta. Gracias a todos por su ayuda.
Evan Larsen
@Euphoric, después de repensar el diseño del proyecto, creo que los CommandHandlers deberían estar en el dominio. No estoy seguro de por qué los puse en el proyecto de Infraestructura. Gracias.
Evan Larsen

Respuestas:

11

Hacer CreateBookCommandValidatorpúblico no viola la encapsulación, ya que

La encapsulación se usa para ocultar los valores o el estado de un objeto de datos estructurados dentro de una clase, evitando que personas no autorizadas accedan directamente a ellos ( wikipedia )

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

Hacer pública esta clase no viola ningún otro principio (como los principios SÓLIDOS ) porque:

  • Esa clase tiene una responsabilidad única bien definida y, por lo tanto, sigue el Principio de responsabilidad única.
  • Puede agregar nuevos validadores al sistema sin cambiar una sola línea de código y, por lo tanto, debe seguir el Principio de Abierto / Cerrado.
  • Esa interfaz IValidator <T> que implementa esta clase es estrecha (tiene un solo miembro) y sigue el Principio de segregación de interfaz.
  • Sus consumidores solo dependen de esa interfaz IValidator <T> y, por lo tanto, siguen el Principio de Inversión de Dependencia.

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

Aunque puede hacer que la clase sea interna y usar [InternalsVisibleTo] para permitir que el proyecto de prueba de la 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 evitar que las partes externas (sobre las que no tienes control) dependan de esa clase, porque eso evitaría que hagas cambios futuros en esa clase sin romper nada. En otras palabras, esto solo se cumple cuando está creando una biblioteca reutilizable (como una biblioteca de inyección de dependencia). De hecho, Simple Injector contiene material interno y su proyecto de prueba de unidad prueba estos elementos 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 otros desarrolladores de su equipo deberán seguir sus pautas. Y una simple guía servirá: programar para una abstracción; no es una implementación (el principio de inversión de dependencia).

En resumen, no haga esta clase interna a menos que esté escribiendo una biblioteca reutilizable.

Pero si todavía desea que esta clase sea interna, 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 debe asegurarse es que todos sus validadores tengan un constructor público, aunque sean internos. Si realmente desea que sus tipos tengan un constructor interno (no sé 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 RegisterManyForOpenGenerices registrar los tipos públicos e internos. Por lo tanto, el suministro AccessibilityOption.AllTypesahora es redundante y la siguiente declaración registrará los tipos públicos e internos:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
Steven
fuente
8

No es gran cosa que la CreateBookCommandValidatorclase sea pública.

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

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

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

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

jhominal
fuente
Sí, gracias. Originalmente estaba pensando lo mismo antes de crear esta publicación. Estaba pensando en hacer una clase Factory que devolviera la implementación apropiada de IValidate <> pero si alguna de las implementaciones de IValidate <> tuviera alguna dependencia, probablemente se volvería bastante difícil rápidamente.
Evan Larsen
@EvanLarsen ¿Por qué? Si la IValidate<>implementación tiene dependencias, coloque estas dependencias como parámetros del método de fábrica.
jhominal
5

Puede declarar CreateBookCommandValidatorcomo internaluna aplicación InternalsVisibleToAttribute para que sea visible para el BookStore.Couplerensamblado. Esto a menudo también ayuda al hacer pruebas unitarias.

Olivier Jacot-Descombes
fuente
No tenía idea de la existencia de ese atributo. Gracias
Evan Larsen
4

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

Tuve un problema relacionado -> enlace .

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

Y finalmente un artículo en la web.

Johannes
fuente
No tenía idea de la existencia de ese atributo. Gracias
Evan Larsen
1

Otra opción es hacerlo público pero ponerlo en otra asamblea.

Esencialmente, tiene un ensamblado de interfaces de servicio, un ensamblaje de implementaciones de servicio (que hace referencia a interfaces de servicio), un ensamblaje de consumidor de servicios (que hace referencia a interfaces de servicio) y un ensamblaje de registrador IOC (que hace referencia tanto a interfaces de servicio como a implementaciones de servicio para unirlos )

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

pdr
fuente
¿Eliminaría eso el leve riesgo de seguridad de hacer visibles los elementos internos?
Johannes
1
@Johannes: ¿Riesgo de seguridad? Si confía en modificadores de acceso para brindarle seguridad, debe preocuparse. Puede obtener acceso a cualquier método a través de la reflexión. Pero elimina el acceso fácil / alentado a las partes internas al colocar la implementación en otro ensamblado al que no se hace referencia.
pdr