Kann ich Dependency Injection verwenden, ohne die Kapselung zu beschädigen?

15

Hier ist meine Lösung und Projekte:

  • BookStore (Lösung)
    • 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

Entschuldigung für die lange Einrichtung des Problems, ich habe keine bessere Möglichkeit, dies zu erklären, als mein eigentliches Problem zu beschreiben.

Ich möchte meinen CreateBookCommandValidator nicht erstellen public. Es wäre mir lieber, internalaber wenn ich es schaffe, kann internalich es nicht bei meinem DI Container registrieren. Der Grund, warum ich möchte, dass es intern ist, ist, dass sich das einzige Projekt, das von meinen IValidate <> -Implementierungen Kenntnis haben sollte, im BookStore.Domain-Projekt befindet. Jedes andere Projekt muss nur IValidator <> verbrauchen und der CompositeValidator sollte aufgelöst werden, der alle Validierungen erfüllt.

Wie kann ich Dependency Injection verwenden, ohne die Kapselung zu beschädigen? Oder mache ich das alles falsch?

Evan Larsen
quelle
Nur ein Trottel: Was Sie verwenden, ist kein korrektes Befehlsmuster. Das Aufrufen dieses Befehls kann daher zu Fehlinformationen führen. Außerdem scheint CreateBookCommandHandler LSP zu beschädigen: Was passiert, wenn Sie ein Objekt übergeben, das von CreateBookCommand stammt? Und ich denke, was Sie hier tun, ist tatsächlich das Antipattern des anämischen Domänenmodells. Dinge wie das Speichern sollten innerhalb der Domäne liegen und die Validierung sollte Teil der Entität sein.
Euphoric
1
@Euphoric: Das stimmt. Dies ist nicht das Befehlsmuster . Tatsächlich folgt das OP einem anderen Muster: dem Befehls- / Handlermuster .
Steven
Es gab so viele gute Antworten, ich wünschte, ich hätte mehr als die Antwort markieren können. Vielen Dank für Ihre Hilfe.
Evan Larsen
@Euphoric, nach Überarbeitung des Projektlayouts sollten sich die CommandHandler meiner Meinung nach in der Domain befinden. Ich bin nicht sicher, warum ich sie in das Infrastrukturprojekt eingefügt habe. Vielen Dank.
Evan Larsen

Antworten:

11

Das Publizieren CreateBookCommandValidatorverletzt die Kapselung nicht, da

Die Kapselung wird verwendet, um die Werte oder den Status eines strukturierten Datenobjekts innerhalb einer Klasse auszublenden und so den direkten Zugriff durch Unbefugte zu verhindern ( Wikipedia ).

Ihr CreateBookCommandValidatorerlaubt keinen Zugriff auf seine Datenmitglieder (derzeit scheint es keine zu geben), so dass die Kapselung nicht verletzt wird.

Die Veröffentlichung dieser Klasse verstößt nicht gegen ein anderes Prinzip (z. B. die SOLID- Prinzipien), da:

  • Diese Klasse hat eine klar definierte Verantwortung und folgt daher dem Prinzip der einheitlichen Verantwortung.
  • Das Hinzufügen neuer Validatoren zum System kann ohne Änderung einer einzelnen Codezeile erfolgen, und Sie folgen daher dem Open / Closed-Prinzip.
  • Die von dieser Klasse implementierte IValidator <T> -Schnittstelle ist eng (hat nur einen Member) und folgt dem Prinzip der Schnittstellensegregation.
  • Ihre Konsumenten sind nur von dieser IValidator <T> -Schnittstelle abhängig und folgen daher dem Prinzip der Abhängigkeitsinversion.

Sie können die CreateBookCommandValidatorinterne Klasse nur erstellen, wenn die Klasse nicht direkt von außerhalb der Bibliothek konsumiert wird. Dies ist jedoch selten der Fall, da Ihre Komponententests ein wichtiger Konsument dieser Klasse (und fast jeder Klasse in Ihrem System) sind.

Obwohl Sie die Klasse intern machen und [InternalsVisibleTo] verwenden können, um dem Komponententestprojekt den Zugriff auf die Interna Ihres Projekts zu ermöglichen, warum sollten Sie sich die Mühe machen?

Der wichtigste Grund, Klassen intern zu machen, besteht darin, zu verhindern, dass externe Parteien (über die Sie keine Kontrolle haben) eine Abhängigkeit von einer solchen Klasse eingehen, da dies verhindern würde, dass Sie zukünftig zu dieser Klasse wechseln, ohne irgendetwas zu beschädigen. Dies gilt mit anderen Worten nur, wenn Sie eine wiederverwendbare Bibliothek erstellen (z. B. eine Abhängigkeitsinjektionsbibliothek). Tatsächlich enthält Simple Injector internes Material und sein Unit-Test-Projekt testet diese Interna.

Wenn Sie jedoch kein wiederverwendbares Projekt erstellen, besteht dieses Problem nicht. Es ist nicht vorhanden, da Sie die davon abhängigen Projekte ändern können und die anderen Entwickler in Ihrem Team Ihre Richtlinien befolgen müssen. Und eine einfache Richtlinie reicht aus: Programmiere auf eine Abstraktion; keine Implementierung (das Prinzip der Abhängigkeitsinversion).

Machen Sie diese Klasse erst dann intern, wenn Sie eine wiederverwendbare Bibliothek schreiben.

Wenn Sie diese Klasse dennoch intern machen möchten, können Sie sie dennoch problemlos mit Simple Injector registrieren:

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

Das einzige, was Sie sicherstellen müssen, ist, dass alle Validatoren einen öffentlichen Konstruktor haben, auch wenn sie intern sind. Wenn Sie wirklich möchten, dass Ihre Typen einen internen Konstruktor haben (wissen nicht genau, warum Sie das wollen), können Sie das Konstruktorauflösungsverhalten außer Kraft setzen .

AKTUALISIEREN

Seit Simple Injector v2.6 werden standardmäßig RegisterManyForOpenGenericsowohl öffentliche als auch interne Typen registriert. Somit ist die Bereitstellung AccessibilityOption.AllTypesüberflüssig und die folgende Anweisung registriert sowohl öffentliche als auch interne Typen:

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

Es ist keine große Sache, dass die CreateBookCommandValidatorKlasse öffentlich ist.

Wenn Sie Instanzen davon außerhalb der Bibliothek erstellen müssen, die es definiert, ist es ein ziemlich natürlicher Ansatz, die öffentliche Klasse verfügbar zu machen und sich darauf zu verlassen, dass die Clients nur diese Klasse als Implementierung von verwenden IValidate<CreateBookCommand>. (Wenn Sie nur einen Typ verfügbar machen, bedeutet dies nicht, dass die Kapselung beschädigt ist. Sie erleichtert es den Clients lediglich, die Kapselung zu beschädigen.)

Andernfalls können Sie, wenn Sie Clients wirklich dazu zwingen möchten, die Klasse nicht zu kennen, auch eine öffentliche statische Factory-Methode verwenden, anstatt die Klasse verfügbar zu machen. Beispiel:

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

Was die Registrierung in Ihrem DI-Container angeht, so ermöglichen alle mir bekannten DI-Container die Konstruktion mit einer statischen Factory-Methode.

jhominal
quelle
Ja, danke. Ich habe ursprünglich das Gleiche gedacht, bevor ich diesen Beitrag erstellt habe. Ich habe darüber nachgedacht, eine Factory-Klasse zu erstellen, die die entsprechende IValidate <> -Implementierung zurückgibt, aber wenn eine der IValidate <> -Implementierungen Abhängigkeiten aufweist, wird sie wahrscheinlich schnell ziemlich haarig.
Evan Larsen
@EvanLarsen Warum? Wenn die IValidate<>Implementierung Abhängigkeiten aufweist, legen Sie diese Abhängigkeiten als Parameter für die Factory-Methode fest.
31.
5

Sie können das InternalsVisibleToAttributeCreateBookCommandValidator als internalApply deklarieren , um es für die Assembly sichtbar zu machen . Dies hilft oft auch bei Unit-Tests.BookStore.Coupler

Olivier Jacot-Descombes
quelle
Ich hatte keine Ahnung von der Existenz dieses Attributs. Vielen Dank
Evan Larsen
1

Eine andere Möglichkeit besteht darin, es öffentlich zu machen, aber es in eine andere Assembly zu stellen.

Im Wesentlichen verfügen Sie über eine Dienstschnittstellenassembly, eine Dienstimplementierungsassembly (die auf Dienstschnittstellen verweist), eine Dienstkonsumentenassembly (die auf Dienstschnittstellen verweist) und eine IOC-Registrarassembly (die auf Dienstschnittstellen und Dienstimplementierungen verweist, um sie miteinander zu verknüpfen ).

Ich sollte betonen, dass dies nicht immer die am besten geeignete Lösung ist, aber es ist eine Überlegung wert.

pdr
quelle
Würde dies das Sicherheitsrisiko beseitigen, Interna sichtbar zu machen?
Johannes
1
@Johannes: Sicherheitsrisiko? Wenn Sie sich auf Zugriffsmodifikatoren verlassen, um Sicherheit zu gewährleisten, müssen Sie sich Sorgen machen. Sie können über Reflection auf jede Methode zugreifen. Es entfernt jedoch den einfachen / ermutigten Zugriff auf Interna, indem die Implementierung in eine andere Assembly gestellt wird, auf die nicht verwiesen wird.
pdr