C# Dependency Injection – Can It Be Used Without Breaking Encapsulation?

cdependency-injectionencapsulation

Here is my Solution and projects:

  • BookStore (solution)
    • BookStore.Coupler (project)
      • Bootstrapper.cs
    • BookStore.Domain (project)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (project)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (project)
      • Global.asax
    • BookStore.BatchProcesses (project)
      • 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

Sorry for the long setup to the problem, I have no better way of explaining this other than detailing my actual problem.

I don't want to make my CreateBookCommandValidator public. I would rather it be internal but if I make it internal then I will not be able to register it with my DI Container. The reason I would like it to be internal is because the only project that should have notion of my IValidate<> implementations are in the BookStore.Domain project. Any other project just needs to consume IValidator<> and the CompositeValidator should be resolved which will fulfill all validations.

How can I use Dependency Injection without breaking encapsulation? Or am I going about this all wrong?

Best Answer

Making the CreateBookCommandValidator public does not violate encapsulation, since

Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties direct access to them (wikipedia)

Your CreateBookCommandValidator doesn't allow access to its data members (it currently doesn't seem to have any) so its not violating encapsulation.

Making this class public does not violate any other principle (such as the SOLID principles), because:

  • That class has a single well defined responsibility and therefore follows the Single Responsibility Principle.
  • Adding new validators to the system can be done without changing a single line of code and you therefore follow the Open/Closed Principle.
  • That IValidator<T> interface that this class implements is narrow (has only one member) and follows the Interface Segregation Principle.
  • Your consumers only depend on that IValidator<T> interface and therefore follow the Dependency Inversion Principle.

You can only make the CreateBookCommandValidator internal if the class isn't consumed directly from outside the library, but this hardly ever is the case, since your unit tests are an important consumer of this class (and almost every class in your system).

Although you can make the class internal and use [InternalsVisibleTo] to allow the unit test project to access your project's internals, why bother?

The most important reason to make classes internal is to prevent external parties (that you don't have control over) to take a dependency on such class, because that would prevent you from making future changing to that class without breaking anything. In other words, this only holds when you're creating a reusable library (such as a dependency injection library). As a matter of fact, Simple Injector contains internal stuff and its unit test project tests these internals.

However, if you're not creating a reusable project, this problem does not exist. It does not exist, because you can change the projects that depend on it, and the other developers in your team will have to follow your guidelines. And one simple guideline will do: Program to an abstraction; not an implementation (the Dependency Inversion Principle).

So long story short, don't make this class internal unless you are writing a reusable library.

But if you still want to make this class internal, you can still register it with Simple Injector without any problem like this:

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

The only thing to make sure of is that your all validators have a public constructor, even though they are internal. If you really want your types to have an internal constructor (don't really know why you would want that) you can override the Constructor Resolution Behavior.

UPDATE

Since Simple Injector v2.6, the default behavior of RegisterManyForOpenGeneric is to register both public and internal types. So supplying AccessibilityOption.AllTypes is now redundant and the following statement will register both public and internal types:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
Related Topic