Abhängigkeitsinjektion für Datenobjekte verwenden?

11

Ich lerne gerade etwas über Abhängigkeitsinjektion und bin auf etwas fixiert. Dependency Injection empfiehlt, abhängige Klassen über den Konstruktor zu senden, aber ich frage mich, ob dies für Datenobjekte erforderlich ist. Da die Unit-Testbarkeit einer der Hauptvorteile von DI ist, würde ein Datenobjekt, das nur Daten speichert, und keine Prozeduren jemals Unit-getestet werden, was DI zu einer unnötigen Komplexitätsebene macht, oder hilft es dennoch, Abhängigkeiten selbst anzuzeigen mit Datenobjekten?

Class DO{
    DO(){
        DataObject2List = new List<DO2>();
    }

    public string Field1;
    public string Field2;
    public List<DO2> DataObject2List;
}

Class DO2{
    public DateTime Date;
    public double Value;
}
Sooprise
quelle
Was genau meinst du mit "Datenobjekt"? Das ist kein Standardbegriff. Sprechen Sie über ein DTO oder beziehen Sie sich nur auf eine Klasse ohne Methoden (z. B. einen besonders langweiligen Teil eines Domänenmodells)? Es gibt einen großen Unterschied zwischen den beiden.
Aaronaught
Klar, ich meine nur eine Klasse ohne Methoden, eine Klasse, die nur Daten speichert. Wenn Data Object nicht der richtige Begriff dafür ist, gibt es einen oder wird es nur eine Klasse ohne Methoden genannt?
Sooprise
@sooprise Das ist eine gute Frage. Gute Idee.
Matthew Rodatus
@sooprise Vielleicht ist meine Antwort falsch. Wo werden Sie die CRUD-Methoden platzieren? In einer separaten Datenzugriffsklasse, die die Datenobjekte in einer Datenbanktabelle speichern würde? Dh DataAccess.Create (<DataObject>)?
Matthew Rodatus
1
@Matthew, wäre dies ein sein Datenzugriffsobjekt - wenn das in der Tat ist das, was die OP redet, dann ist das nicht klar. Moderne Implementierungen neigen ohnehin dazu, sich von diesem Muster zu entfernen und sich auf Repositorys und / oder Arbeitseinheiten zu stützen.
Aaronaught

Antworten:

7

Ich möchte eine Klarstellung einiger der hier verwendeten Begriffe vorschlagen, insbesondere "Abhängigkeit" und "Abhängigkeitsinjektion".

Abhängigkeit:

Eine "Abhängigkeit" ist normalerweise ein komplexes Objekt, das einige Funktionen ausführt, von denen eine andere Klasse möglicherweise abhängen muss. Einige klassische Beispiele wären ein Logger oder ein Datenbank-Accessor oder eine Komponente, die eine bestimmte Geschäftslogik verarbeitet.

Ein Datenobjekt wie ein DTO oder ein Wertobjekt wird normalerweise nicht als "Abhängigkeit" bezeichnet, da sie keine erforderliche Funktion ausführen.

Wenn Sie es auf diese Weise sehen, was Sie in Ihrem Beispiel (tun Komponieren das DOObjekt mit einer Liste von D02Objekten durch den Konstruktor) nicht „dependecy Injektion“ in Betracht gezogen werden. Es wird nur eine Eigenschaft festgelegt. Es liegt an Ihnen, ob Sie es im Konstruktor oder auf andere Weise bereitstellen, aber wenn Sie es einfach durch den Konstruktor übergeben, wird es nicht zur Abhängigkeitsinjektion.

Abhängigkeitsspritze:

Wenn Ihre DO2Klasse tatsächlich einige zusätzliche Funktionen bereitstellen würde, die die DOKlasse benötigt, wäre dies wirklich eine Abhängigkeit. In diesem Fall sollte die abhängige Klasse DOvon einer Schnittstelle (wie ILogger oder IDataAccessor) abhängen und sich wiederum auf den aufrufenden Code stützen, um diese Schnittstelle bereitzustellen (mit anderen Worten, um sie in die DOInstanz einzufügen ).

Das Injizieren der Abhängigkeit auf diese Weise macht das DOObjekt flexibler, da jeder unterschiedliche Kontext seine eigene Implementierung der Schnittstelle zum DOObjekt bereitstellen kann. (Denken Sie an Unit-Tests.)

Eric King
quelle
7

Ich werde mein Bestes geben, um die Verwirrung in der Frage zu beseitigen.

Erstens ist "Datenobjekt" kein aussagekräftiger Begriff. Wenn das einzige definierende Merkmal dieses Objekts ist, dass es keine Methoden hat, sollte es überhaupt nicht existieren . Ein nützliches verhaltensloses Objekt sollte in mindestens eine der folgenden Unterkategorien passen:

  • Wertobjekte oder "Datensätze" haben überhaupt keine Identität. Es sollten Werttypen mit Copy-on-Reference-Semantik sein, vorausgesetzt, die Umgebung unterstützt dies. Da es sich um feste Strukturen handelt, sollte ein VO immer nur ein primitiver Typ oder eine feste Folge von Primitiven sein. Daher sollte eine VO keine Abhängigkeiten oder Assoziationen haben. Jeder nicht standardmäßige Konstruktor würde nur zum Zweck der Initialisierung des Werts existieren, dh weil er nicht als Literal ausgedrückt werden kann.

  • Datenübertragungsobjekte werden häufig fälschlicherweise mit Wertobjekten verwechselt. DTOs tun Identitäten haben, oder zumindest sie können . Der einzige Zweck eines DTO besteht darin, den Informationsfluss von einer Domäne zur anderen zu erleichtern. Sie haben niemals "Abhängigkeiten". Sie können Assoziationen haben (dh zu einem Array oder einer Sammlung), aber die meisten Leute ziehen es vor, sie flach zu machen. Grundsätzlich sind sie analog zu Zeilen in der Ausgabe einer Datenbankabfrage. sie sind transiente Objekte , die in der Regel müssen beibehalten oder serialisiert werden und daher nicht können keine abstrakten Typen verweisen, da diese dadurch unbrauchbar machen würde.

  • Schließlich bieten Datenzugriffsobjekte einen Wrapper oder eine Fassade für eine Datenbank. Diese haben offensichtlich Abhängigkeiten - sie hängen von der Datenbankverbindung und / oder den Persistenzkomponenten ab. Ihre Abhängigkeiten werden jedoch fast immer extern verwaltet und sind für Anrufer völlig unsichtbar. Im Active Record- Muster ist es das Framework, das alles durch Konfiguration verwaltet. In älteren (nach heutigen Maßstäben alten) DAO-Modellen konnte man diese immer nur über den Container konstruieren. Wenn ich eines davon mit Konstruktorinjektion sehen würde, wäre ich sehr, sehr besorgt.

Möglicherweise denken Sie auch an ein Entitätsobjekt oder ein "Geschäftsobjekt" . In diesem Fall möchten Sie die Abhängigkeitsinjektion unterstützen, jedoch nicht so, wie Sie denken oder aus den Gründen, die Sie denken. Dies ist nicht zum Nutzen des Benutzercodes , sondern zum Nutzen eines Entity Managers oder ORM, der stillschweigend einen Proxy einfügt, den er abfängt, um ausgefallene Dinge wie das Verständnis von Abfragen oder das verzögerte Laden auszuführen.

In diesen Fällen geben Sie normalerweise keinen Konstruktor für die Injektion an. Stattdessen müssen Sie nur die Eigenschaft virtuell machen und einen abstrakten Typ verwenden (z . B. IList<T>anstelle von List<T>). Der Rest passiert hinter den Kulissen und niemand ist klüger.

Alles in allem würde ich also sagen, dass ein sichtbares DI-Muster, das auf ein "Datenobjekt" angewendet wird, unnötig ist und wahrscheinlich sogar eine rote Fahne; Dies liegt jedoch zum großen Teil daran, dass die Existenz des Objekts eine rote Fahne ist, außer in dem Fall, in dem es speziell zur Darstellung von Daten aus einer Datenbank verwendet wird. In fast allen anderen Fällen handelt es sich um einen Codegeruch, normalerweise um die Anfänge eines anämischen Domänenmodells oder zumindest eines Poltergeists .

Wiederholen:

  1. Erstellen Sie keine "Datenobjekte".
  2. Wenn Sie ein "Datenobjekt" erstellen müssen, stellen Sie sicher, dass es einen klar definierten Zweck hat . Zu diesem Zweck erfahren Sie, ob DI angemessen ist oder nicht. Es ist unmöglich, sinnvolle Entwurfsentscheidungen über ein Objekt zu treffen, das überhaupt nicht existieren sollte.

Flosse.

Aaronaught
quelle
0

In Ihrem Beispiel DOgibt es keine funktionalen Abhängigkeiten (im Grunde, weil es nichts tut). Es hängt vom konkreten Typ ab DO2, daher möchten Sie möglicherweise eine Schnittstelle zum Abstrakten einführen DO2, damit der Verbraucher seine eigene konkrete Implementierung der untergeordneten Klasse implementieren kann.

Wirklich, welche Abhängigkeit würden Sie hier injizieren?

Scott Whitlock
quelle
Bei einer anderen Frage von ihm, die ich beantwortet habe, bezieht sich die Frage meines Erachtens auf ein Datenobjekt mit integrierten CRUD-Operationen. Wie / wo wird die Datenbankabhängigkeit in das Datenobjekt eingefügt? In den Methoden? Im Konstruktor? Ein anderer Weg? Es wird davon ausgegangen, dass die Abhängigkeit nicht im Hauptteil der Methoden verborgen sein sollte. Dadurch wird die Trennung der Datenbankabhängigkeit vom Datenobjekt deaktiviert, sodass das Datenobjekt einem Komponententest unterzogen werden kann.
Matthew Rodatus
@ Matthew Rodatus - ich verstehe. Ja, in diesem Fall haben Sie zwei Möglichkeiten: Injizieren Sie den Persistenzdienst oder erstellen Sie eine andere Klasse namens DOPersister, die weiß, wie man persistiert, DOund lassen Sie sie als reines Datenobjekt (meiner Meinung nach besser). Im letzteren Fall DOPersisterwürde mit der Datenbankabhängigkeit injiziert.
Scott Whitlock
Nachdem ich seine Frage noch einmal gelesen habe, bin ich mir weniger sicher. Meine Analyse kann falsch sein. In seiner Frage sagte er, dass sein DO keine Verfahren haben würde. Das würde bedeuten, dass die Persistenz in der DO NICHT auftritt. In diesem Fall ist Ihre Antwort richtig - es gibt keine Abhängigkeiten, die injiziert werden müssen.
Matthew Rodatus
0

Da dies ein Datenobjekt in der Datenzugriffsschicht ist, sollte es direkt von einem Datenbankdienst abhängen. Sie können dem Konstruktor einen DatabaseService angeben:

DataObject dataObject = new DataObject(new DatabaseService());
dataObject.Update();

Die Injektion muss sich jedoch nicht im Konstruktor befinden. Alternativ können Sie die Abhängigkeit über jede CRUD-Methode bereitstellen. Ich bevorzuge diese Methode gegenüber der vorherigen, da Ihr Datenobjekt nicht wissen muss, wo es bestehen bleibt, bis Sie es tatsächlich beibehalten müssen.

DataObject dataObject = new DataObject();
dataObject.Update(new DatabaseService());

Sie möchten die Konstruktion definitiv nicht in den CRUD-Methoden verstecken!

public void Update()
{
    // DON'T DO THIS!
    using (DatabaseService dbService = new DatabaseService())
    {
        ...
    }
}

Eine alternative Option wäre, den DatabaseService über eine überschreibbare Klassenmethode zu erstellen.

public void Update()
{
    // GetDatabaseService() is protected virtual, so in unit testing
    // you can subclass the Data Object and return your own
    // MockDatabaseService.
    using (DatabaseService dbService = GetDatabaseService())
    {
        ...
    }
}

Eine letzte Alternative ist die Verwendung eines ServiceLocator im Singleton-Stil. Obwohl ich diese Option nicht mag, ist sie einheitlich testbar.

public void Update()
{
    // The ServiceLocator would not be a real singleton. It would have a setter
    // property so that unit tests can swap it out with a mock implementation
    // for unit tests.
    using (DatabaseService dbService = ServiceLocator.GetDatabaseService())
    {
        ...
    }
}
Matthew Rodatus
quelle