Czy mogę używać wstrzykiwania zależności bez przerywania enkapsulacji?

15

Oto moje rozwiązanie i projekty:

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

Przepraszam za długą konfigurację problemu, nie mam lepszego sposobu na wyjaśnienie tego niż opisanie mojego rzeczywistego problemu.

Nie chcę tworzyć mojego CreateBookCommandValidator public. Wolałbym, internalale jeśli to internalzrobię, nie będę mógł zarejestrować go w moim kontenerze DI. Powodem, dla którego chciałbym, aby był wewnętrzny, jest to, że jedynym projektem, który powinien mieć pojęcie o moich implementacjach IValidate <>, jest projekt BookStore.Domain. Każdy inny projekt musi po prostu zużyć IValidator <>, a CompositeValidator powinien zostać rozwiązany, co spełni wszystkie kryteria sprawdzania poprawności.

Jak korzystać z Dependency Injection bez przerywania enkapsulacji? A może źle to robię?

Evan Larsen
źródło
Tylko nitpick: to, czego używasz, nie jest prawidłowym wzorcem poleceń, więc wywołanie go może być błędną informacją. Ponadto CreateBookCommandHandler wydaje się łamać LSP: co się stanie, jeśli przekażesz obiekt, który pochodzi z CreateBookCommand? I myślę, że to, co tu robisz, to w rzeczywistości antypateriał Anemic Domain Model. Rzeczy takie jak zapisywanie powinny znajdować się w domenie, a sprawdzanie poprawności powinno być częścią encji.
Euforia
1
@Euphoric: Zgadza się. To nie jest wzorzec poleceń . W rzeczywistości OP działa według innego wzorca: wzorzec polecenia / procedury obsługi .
Steven
Było tak wiele dobrych odpowiedzi, które chciałbym oznaczyć jako odpowiedź. Dziękuję wszystkim za pomoc.
Evan Larsen,
@Euforia, po ponownym przemyśleniu układu projektu, myślę, że CommandHandlers powinien być w domenie. Nie jestem pewien, dlaczego umieściłem je w projekcie infrastruktury. Dzięki.
Evan Larsen,

Odpowiedzi:

11

Upublicznienie CreateBookCommandValidatornie narusza enkapsulacji, ponieważ

Hermetyzacja służy do ukrywania wartości lub stanu obiektu danych strukturalnych w klasie, zapobiegając bezpośredniemu dostępowi do nich przez osoby nieupoważnione ( wikipedia )

Twój CreateBookCommandValidatornie pozwala na dostęp do danych swoich członków (obecnie wydaje się nie mieć w ogóle), więc jej nie łamie hermetyzacji.

Upublicznienie tej klasy nie narusza żadnej innej zasady (takiej jak zasady SOLID ), ponieważ:

  • Klasa ta ma jedną ściśle określoną odpowiedzialność, a zatem jest zgodna z zasadą jednej odpowiedzialności.
  • Dodanie nowych walidatorów do systemu można wykonać bez zmiany jednego wiersza kodu, dlatego przestrzegasz zasady otwartej / zamkniętej.
  • Interfejs IValidator <T>, który implementuje ta klasa, jest wąski (ma tylko jednego członka) i jest zgodny z Zasadą Segregacji Interfejsu.
  • Twoi konsumenci zależą tylko od tego interfejsu IValidator <T> i dlatego stosują zasadę inwersji zależności.

Możesz zrobić CreateBookCommandValidatorwewnętrzny tylko wtedy, gdy klasa nie jest zużywana bezpośrednio spoza biblioteki, ale rzadko tak jest, ponieważ testy jednostkowe są ważnym konsumentem tej klasy (i prawie każdej klasy w twoim systemie).

Chociaż możesz ustawić klasę jako wewnętrzną i użyć [InternalsVisibleTo], aby umożliwić projektowi testu jednostkowego dostęp do wewnętrznych elementów projektu, po co zawracać sobie tym głowę?

Najważniejszym powodem, aby uczynić klasy wewnętrznymi, jest uniemożliwienie podmiotom zewnętrznym (nad którymi nie masz kontroli) uzależnienia się od takich klas, ponieważ uniemożliwiłoby to w przyszłości przejście do tej klasy bez zerwania czegokolwiek. Innymi słowy, dotyczy to tylko tworzenia biblioteki wielokrotnego użytku (takiej jak biblioteka wstrzykiwania zależności). W rzeczywistości Simple Injector zawiera elementy wewnętrzne, a projekt ich testów jednostkowych testuje te elementy wewnętrzne.

Jeśli jednak nie tworzysz projektu wielokrotnego użytku, ten problem nie istnieje. Nie istnieje, ponieważ możesz zmieniać projekty od niego zależne, a pozostali programiści w twoim zespole będą musieli przestrzegać twoich wskazówek. I zrobi to jedna prosta wytyczna: Zaprogramuj abstrakcję; nie implementacja (zasada odwrócenia zależności).

Krótko mówiąc, nie wprowadzaj tej klasy wewnętrznej, chyba że piszesz bibliotekę wielokrotnego użytku.

Ale jeśli nadal chcesz uczynić tę klasę wewnętrzną, możesz zarejestrować ją w Simple Injector bez żadnego problemu:

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

Jedyną rzeczą do upewnienia się jest to, że wszystkie wasze walidatory mają publicznego konstruktora, nawet jeśli są wewnętrzne. Jeśli naprawdę chcesz, aby twoje typy miały wewnętrzny konstruktor (nie wiem, dlaczego tak chcesz), możesz przesłonić zachowanie rozdzielczości konstruktora .

AKTUALIZACJA

Od Simple Injector v2.6 domyślnym zachowaniem RegisterManyForOpenGenericjest rejestracja typów publicznych i wewnętrznych. Dostarczanie AccessibilityOption.AllTypesjest teraz zbędne, a następujące oświadczenie zarejestruje zarówno typy publiczne, jak i wewnętrzne:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
Steven
źródło
8

Nie jest wielkim problemem, że CreateBookCommandValidatorklasa jest publiczna.

Jeśli musisz utworzyć jego instancje poza biblioteką, która ją definiuje, jest to całkiem naturalne podejście do ujawnienia klasy publicznej i liczenie na klientów używających tej klasy tylko jako implementacji IValidate<CreateBookCommand>. (Samo ujawnienie typu nie oznacza, że ​​enkapsulacja jest zepsuta, po prostu ułatwia klientom jej przerwanie).

W przeciwnym razie, jeśli naprawdę chcesz zmusić klientów, aby nie wiedzieli o klasie, możesz także użyć publicznej statycznej metody fabrycznej zamiast ujawniać klasę, np .:

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

Jeśli chodzi o rejestrację w kontenerze DI, wszystkie kontenery DI, które znam, pozwalają na budowę przy użyciu statycznej metody fabrycznej.

jhominal
źródło
Tak, dziękuję .. Początkowo myślałem o tym samym, zanim stworzyłem ten post. Zastanawiałem się nad stworzeniem klasy Factory, która zwróciłaby odpowiednią implementację IValidate <>, ale jeśli którakolwiek z implementacji IValidate <> miałaby jakieś zależności, prawdopodobnie stałaby się dość owłosiona.
Evan Larsen,
@EvanLarsen Dlaczego? Jeśli IValidate<>implementacja ma zależności, umieść te zależności jako parametry w metodzie fabrycznej.
jhominal
5

Możesz zadeklarować CreateBookCommandValidatorjako internalzastosowanie InternalsVisibleToAttribute , aby był widoczny dla BookStore.Couplerzespołu. Często pomaga to również podczas przeprowadzania testów jednostkowych.

Olivier Jacot-Descombes
źródło
Nie miałem pojęcia o istnieniu tego atrybutu. Dziękuję
Evan Larsen,
4

Możesz ustawić go jako wewnętrzny i użyć pliku msdn.link InternalVisibleToAttribute, aby struktura / projekt testowy mógł uzyskać do niego dostęp.

Miałem powiązany problem -> link .

Oto link do innego pytania Stackoverflow dotyczącego problemu:

I wreszcie artykuł w Internecie.

Johannes
źródło
Nie miałem pojęcia o istnieniu tego atrybutu. Dziękuję
Evan Larsen,
1

Inną opcją jest upublicznienie go, ale umieszczenie go w innym zestawie.

Zasadniczo masz zestaw interfejsów usług, zestaw implementacji usług (który odwołuje się do interfejsów usług), zestaw konsumenta usług (który odwołuje się do interfejsów usług) oraz zestaw rejestratora IOC (który odwołuje się zarówno do interfejsów usług, jak i implementacji usług, aby je połączyć ).

Chciałbym podkreślić, że nie zawsze jest to najbardziej odpowiednie rozwiązanie, ale warto je rozważyć.

pdr
źródło
Czy usunęłoby to niewielkie ryzyko związane z widocznością elementów wewnętrznych?
Johannes
1
@Johannes: Ryzyko bezpieczeństwa? Jeśli zależy Ci na modyfikatorach dostępu, które zapewnią ci bezpieczeństwo, musisz się martwić. Możesz uzyskać dostęp do dowolnej metody poprzez odbicie. Ale usuwa łatwy / promowany dostęp do elementów wewnętrznych poprzez umieszczenie implementacji w innym zestawie, do którego nie ma odniesienia.
pdr