Могу ли я использовать Dependency Injection, не нарушая инкапсуляцию?


15

Вот мое решение и проекты:

  • Книжный магазин (решение)
    • BookStore.Coupler (проект)
      • Bootstrapper.cs
    • BookStore.Domain (проект)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (проект)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (проект)
      • Global.asax
    • BookStore.BatchProcesses (проект)
      • 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

Извините за долгую установку на проблему, у меня нет лучшего способа объяснить это, кроме подробного описания моей настоящей проблемы.

Я не хочу делать мой CreateBookCommandValidator public. Я бы предпочел, internalно если я сделаю это, internalя не смогу зарегистрировать его с моим DI-контейнером. Причина, по которой я хотел бы, чтобы он был внутренним, заключается в том, что единственный проект, который должен иметь представление о моих реализациях IValidate <>, находится в проекте BookStore.Domain. Любой другой проект просто должен использовать IValidator <>, и CompositeValidator должен быть решен, который будет выполнять все проверки.

Как я могу использовать Dependency Injection, не нарушая инкапсуляцию? Или я все об этом говорю не так?


Просто придирка: то, что вы используете, не является правильным шаблоном команды, поэтому вызывать его команда может быть дезинформацией. Кроме того, CreateBookCommandHandler, похоже, нарушает LSP: что произойдет, если вы передадите объект, производный от CreateBookCommand? И я думаю, что вы здесь делаете на самом деле антипаттерн Anemic Domain Model. Такие вещи, как сохранение, должны находиться внутри домена, а проверка должна быть частью сущности.
эйфория

1
@ Эйфорик: Это правильно. Это не шаблон команды . На самом деле, OP следует другому шаблону: шаблон команды / обработчика .
Стивен

Было так много хороших ответов, что хотелось бы отметить больше как ответ. Спасибо всем за вашу помощь.
Эван Ларсен

@Euphoric, после переосмысления макета проекта, я думаю, что CommandHandlers должны быть в домене. Не уверен, почему я положил их в проекте инфраструктуры. Благодарю.
Эван Ларсен

Ответы:


11

Публикация CreateBookCommandValidatorне нарушает инкапсуляцию, так как

Инкапсуляция используется для сокрытия значений или состояния объекта структурированных данных внутри класса, предотвращая прямой доступ к ним неавторизованных сторон ( википедия )

Ваш CreateBookCommandValidatorне разрешает доступ к своим членам данных (в настоящее время он, кажется, не имеет), так что это не нарушает инкапсуляцию.

Публикация этого класса не нарушает никаких других принципов (таких как принципы SOLID ), потому что:

  • Этот класс имеет одну четко определенную ответственность и, следовательно, следует принципу единой ответственности.
  • Добавление новых валидаторов в систему может быть выполнено без изменения одной строки кода, и поэтому вы следуете принципу Open / Closed.
  • Интерфейс IValidator <T>, который реализует этот класс, является узким (имеет только один член) и следует принципу разделения интерфейса.
  • Ваши потребители зависят только от этого интерфейса IValidator <T> и, следовательно, следуют принципу инверсии зависимости.

Вы можете только сделать CreateBookCommandValidator внутреннее случае, если класс не используется напрямую из-за пределов библиотеки, но это вряд ли когда-либо так, поскольку ваши модульные тесты являются важным потребителем этого класса (и почти каждого класса в вашей системе).

Хотя вы можете сделать класс внутренним и использовать [InternalsVisibleTo], чтобы позволить проекту модульного теста получить доступ к внутренним объектам вашего проекта, зачем беспокоиться?

Самая важная причина сделать классы внутренними - запретить внешним сторонам (которые вы не контролируете) взять зависимость от такого класса, потому что это помешает вам в будущем перейти на этот класс, не нарушая ничего. Другими словами, это имеет место только при создании многократно используемой библиотеки (такой как библиотека внедрения зависимостей). На самом деле, Simple Injector содержит внутреннее содержимое, и его проект модульного тестирования тестирует эти внутренние компоненты.

Однако, если вы не создаете повторно используемый проект, эта проблема не существует. Он не существует, потому что вы можете изменить проекты, которые зависят от него, и другие разработчики в вашей команде должны будут следовать вашим рекомендациям. И подойдет одно простое руководство: запрограммируйте абстракцию; не реализация (принцип инверсии зависимости).

Короче говоря, не делайте этот класс внутренним, если вы не пишете библиотеку многократного использования.

Но если вы все еще хотите сделать этот класс внутренним, вы все равно можете зарегистрировать его в Simple Injector без каких-либо проблем, например:

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

Единственное, в чем нужно убедиться, это то, что у всех ваших валидаторов есть открытый конструктор, хотя они и являются внутренними. Если вы действительно хотите, чтобы у ваших типов был внутренний конструктор (на самом деле не знаю, зачем вам это нужно), вы можете переопределить поведение разрешения конструктора .

ОБНОВИТЬ

Начиная с Simple Injector v2.6 , поведение по умолчанию RegisterManyForOpenGenericзаключается в регистрации как открытых, так и внутренних типов. Таким образом, поставка AccessibilityOption.AllTypesтеперь избыточна, и следующий оператор будет регистрировать как открытый, так и внутренний типы:

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

8

Это не имеет большого значения, что CreateBookCommandValidatorкласс является публичным.

Если вам нужно создать его экземпляры за пределами библиотеки, которая его определяет, вполне естественный подход - показать открытый класс и рассчитывать на клиентов, использующих только этот класс в качестве реализации IValidate<CreateBookCommand>. (Простое раскрытие типа не означает, что инкапсуляция нарушена, просто клиентам будет проще нарушить инкапсуляцию).

В противном случае, если вы действительно хотите заставить клиентов не знать о классе, вы можете также использовать публичный статический фабричный метод вместо предоставления класса, например:

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

Что касается регистрации в вашем DI-контейнере, то все DI-контейнеры, о которых я знаю, допускают создание с использованием статического фабричного метода.


Да, спасибо .. Я изначально думал то же самое, прежде чем я создал этот пост. Я думал о создании класса Factory, который бы возвращал соответствующую реализацию IValidate <>, но если какая-либо из реализаций IValidate <> имела какие-либо зависимости, это, вероятно, быстро стало бы довольно проблематичным.
Эван Ларсен

@EvanLarsen Почему? Если у IValidate<>реализации есть зависимости, поместите эти зависимости как параметры в фабричный метод.
23

5

Вы можете объявить CreateBookCommandValidatorв internalкачестве применения атрибут InternalsVisibleToAttribute , чтобы сделать его видимым для BookStore.Couplerсборки. Это также часто помогает при выполнении юнит-тестов.


Я понятия не имел о существовании этого атрибута. Спасибо
Эван Ларсен

4

Вы можете сделать его внутренним и использовать InternalVisibleToAttribute msdn.link, чтобы ваш тестовый фреймворк / проект мог получить к нему доступ.

У меня была связанная проблема -> ссылка .

Вот ссылка на другой вопрос Stackoverflow относительно проблемы:

И, наконец, статья в Интернете.


Я понятия не имел о существовании этого атрибута. Спасибо
Эван Ларсен

1

Другой вариант - сделать его публичным, но поместить его в другую сборку.

Таким образом, по сути, у вас есть сборка интерфейсов служб, сборка реализаций служб (которая ссылается на интерфейсы служб), сборка потребителей услуг (которая ссылается на интерфейсы служб) и сборка регистратора IOC (которая ссылается как на интерфейсы служб, так и на реализации служб для их соединения). ).

Я должен подчеркнуть, что это не всегда самое подходящее решение, но его стоит рассмотреть.


Сможет ли это сделать хоть какой-нибудь риск для безопасности, чтобы сделать внутренние компоненты видимыми?
Йоханнес

1
@ Йоханнес: Угроза безопасности? Если вы полагаетесь на модификаторы доступа для обеспечения безопасности, вам нужно беспокоиться. Вы можете получить доступ к любому методу через отражение. Но он удаляет простой / поощряемый доступ к внутренним компонентам, помещая реализацию в другую сборку, на которую нет ссылок.
фунтовые
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.