Чи можу я використовувати ін'єкційну залежність без порушення інкапсуляції?


15

Ось моє рішення та проекти:

  • BookStore (рішення)
    • BookStore.Coupler (проект)
      • Bootstrapper.cs
    • BookStore.Domain (проект)
      • Створити BookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (проект)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (проект)
      • Global.asax
    • BookStore.BatchProcess (проект)
      • 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<>));
    }
}

Створити BookCommandValidator.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 повинен бути вирішений, що виконає всі перевірки.

Як я можу використовувати ін'єкційну залежність без порушення інкапсуляції? Або я про все це помиляюся?


Лише нітрик: те, що ви використовуєте, не є правильним шаблоном команд, тому виклик команди може бути дезінформацією. Крім того, CreateBookCommandHandler здається, що він порушує LSP: що станеться, якщо ви передасте об'єкт, що походить від CreateBookCommand? І я думаю, що ти тут робиш насправді антипатерн Anemic Domain Model. Такі речі, як збереження, повинні знаходитися всередині домену, а перевірка повинна бути частиною сутності.
Ейфорія

1
@Euphoric: Це правильно. Це не командна схема . Власне, ОП дотримується іншої схеми: схеми команд / обробника .
Стівен

Було так багато хороших відповідей, я б хотів, щоб я міг позначити більше як відповідь. Дякуємо всім за допомогу.
Еван Ларсен

@Euphoric, після переосмислення макета проекту я думаю, що CommandHandlers повинен бути в домені. Не впевнений, чому я вклав їх у проект інфраструктури. Спасибі.
Еван Ларсен

Відповіді:


11

Оголошення CreateBookCommandValidatorгромадськості не порушує інкапсуляцію, оскільки

Інкапсуляція використовується для приховування значень або стану структурованого об'єкта даних всередині класу, запобігання прямому доступу до них сторонніх сторін ( wikipedia )

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

Оприлюднення цього класу не порушує жодного іншого принципу (наприклад, принципів SOLID ), оскільки:

  • Цей клас несе чітко визначену відповідальність і тому дотримується Єдиного принципу відповідальності.
  • Додавання нових валідаторів до системи можна зробити без зміни одного рядка коду, і тому ви слідуєте принципу відкритого / закритого типу.
  • Цей інтерфейс IValidator <T>, який реалізує цей клас, є вузьким (має лише одного члена) і керується принципом розділення інтерфейсу.
  • Ваші споживачі залежать лише від інтерфейсу IValidator <T> і тому слідують принципу інверсії залежності.

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

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

Найважливіша причина зробити заняття внутрішніми - це заборонити зовнішнім сторонам (над якими ви не маєте контролю) взяти залежність від такого класу, тому що це не дозволить вам змінити майбутній клас до цього класу, нічого не порушуючи. Іншими словами, це має місце лише тоді, коли ви створюєте бібліотеку для багаторазового використання (наприклад, бібліотеку ін'єкційних залежностей). Власне кажучи, Простий інжектор містить внутрішні речі, і його блок тестового проекту перевіряє ці внутрішні компоненти.

Однак якщо ви не створюєте багаторазовий проект, цієї проблеми не існує. Його не існує, тому що ви можете змінити проекти, від яких залежать, а інші розробники у вашій команді повинні будуть дотримуватися ваших вказівок. І один простий настанов буде робити: Програма на абстракцію; не реалізація (принцип інверсії залежності).

Якщо коротко розповісти, не робіть цей клас внутрішнім, якщо ви не пишете бібліотеку для багаторазового використання.

Але якщо ви все ще хочете зробити цей клас внутрішнім, ви все одно можете зареєструвати його за допомогою простого інжектора без будь-яких проблем, як це:

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<>реалізація має залежності, то слід вказати ці залежності як параметри до заводського методу.
Джомінал


4

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

У мене була пов’язана проблема -> посилання .

Ось посилання на інше питання Stackoverflow щодо проблеми:

І нарешті стаття в Інтернеті.


Я не мав уявлення про існування цього атрибута. Дякую
Еван Ларсен

1

Інший варіант - оприлюднити його, але помістити його в іншу збірку.

Отже, у вас є складання сервісних інтерфейсів, збірка реалізацій сервісу (на які посилаються сервісні інтерфейси), збірка споживачів послуг (на які посилаються сервісні інтерфейси) та збірка реєстратора IOC (яка посилається як на сервісні інтерфейси, так і на виконання служб, щоб з'єднати їх разом ).

Слід підкреслити, це не завжди є найбільш підходящим рішенням, але це варто розглянути.


Чи не усуне це ризик безпеки огляду, щоб зробити внутрішні видимими?
Йоганнес

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