Ich möchte sichergehen, dass ich das Konzept der Abhängigkeitsinjektion (DI) verstehe. Nun, ich verstehe das Konzept tatsächlich, DI ist nicht kompliziert: Sie erstellen eine Schnittstelle und übergeben dann die Implementierung meiner Schnittstelle an die Klasse, die sie verwendet. Die übliche Methode zum Übergeben ist der Konstruktor. Sie können ihn jedoch auch per Setter oder einer anderen Methode übergeben.
Ich bin mir nicht sicher, wann ich DI verwenden soll.
Verwendung 1: Natürlich erscheint die Verwendung von DI für den Fall, dass Ihre Schnittstelle mehrfach implementiert ist, logisch. Sie haben ein Repository für Ihren SQL Server und ein anderes für Ihre Oracle-Datenbank. Beide teilen sich die gleiche Schnittstelle und Sie "injizieren" (dies ist der verwendete Begriff) diejenige, die Sie zur Laufzeit möchten. Dies ist sogar nicht DI, dies ist hier die grundlegende OO-Programmierung.
Verwendung 2: Wenn Sie eine Geschäftsschicht mit vielen Diensten mit all ihren spezifischen Methoden haben, scheint es die gute Praxis zu sein, für jeden Dienst eine Schnittstelle zu erstellen und die Implementierung auch dann einzufügen, wenn diese eindeutig ist. Weil dies besser für die Wartung ist. Dies ist diese zweite Verwendung, die ich nicht verstehe.
Ich habe ungefähr 50 Businessklassen. Nichts ist zwischen ihnen gemeinsam. Einige sind Repositorys, die Daten in 3 verschiedenen Datenbanken abrufen oder speichern. Einige lesen oder schreiben Dateien. Einige machen reine Geschäftsaktionen. Es gibt auch spezielle Validatoren und Helfer. Die Herausforderung ist die Speicherverwaltung, da einige Klassen von verschiedenen Standorten aus instanziiert werden. Ein Validator kann mehrere Repositorys und andere Validatoren aufrufen, die dieselben Repositorys erneut aufrufen können.
Beispiel: Geschäftsschicht
public class SiteService : Service, ICrud<Site>
{
public Site Read(Item item, Site site)
{
return beper4DbContext.Site
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public Site Read(string itemCode, string siteCode)
{
using (var itemService = new ItemService())
{
var item = itemService.Read(itemCode);
return Read(item, site);
}
}
}
public class ItemSiteService : Service, ICrud<Site>
{
public ItemSite Read(Item item, Site site)
{
return beper4DbContext.ItemSite
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public ItemSite Read(string itemCode, string siteCode)
{
using (var itemService = new ItemService())
using (var siteService = new SiteService())
{
var item = itemService.Read(itemCode);
var site = siteService.Read(itemCode, siteCode);
return Read(item, site);
}
}
}
Regler
public class ItemSiteController : BaseController
{
[Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
public IHttpActionResult Get(string itemCode, string siteCode)
{
using (var service = new ItemSiteService())
{
var itemSite = service.Read(itemCode, siteCode);
return Ok(itemSite);
}
}
}
Dieses Beispiel ist sehr einfach, aber Sie sehen, wie ich leicht zwei Instanzen von itemService erstellen kann, um eine itemSite zu erhalten. Dann kommt auch jeder Dienst mit seinem DB-Kontext. Dieser Aufruf erstellt also 3 DbContext. 3 Verbindungen.
Meine erste Idee war, Singleton zu erstellen, um all diesen Code wie unten umzuschreiben. Der Code ist besser lesbar und am wichtigsten ist, dass das Singleton-System nur eine Instanz jedes verwendeten Dienstes erstellt und beim ersten Aufruf erstellt. Perfekt, außer dass ich immer noch einen anderen Kontext habe, aber ich kann das gleiche System für meine Kontexte verwenden. So fertig.
Geschäftsschicht
public class SiteService : Service, ICrud<Site>
{
public Site Read(Item item, Site site)
{
return beper4DbContext.Site
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public Site Read(string itemCode, string siteCode)
{
var item = ItemService.Instance.Read(itemCode);
return Read(item, site);
}
}
public class ItemSiteService : Service, ICrud<Site>
{
public ItemSite Read(Item item, Site site)
{
return beper4DbContext.ItemSite
.AsNoTracking()
.SingleOrDefault(y => y.SiteId == site.Id && y.ItemId == item.Id)
}
public ItemSite Read(string itemCode, string siteCode)
{
var item = ItemService.Instance.Read(itemCode);
var site = SiteService.Instance.Read(itemCode, siteCode);
return Read(item, site);
}
}
Regler
public class ItemSiteController : BaseController
{
[Route("api/Item/{itemCode}/ItemSite/{siteCode}")]
public IHttpActionResult Get(string itemCode, string siteCode)
{
var itemSite = service.Instance.Read(itemCode, siteCode);
return Ok(itemSite);
}
}
Einige Leute sagen mir nach guter Praxis, ich sollte DI mit einer einzelnen Instanz verwenden, und Singleton ist eine schlechte Praxis. Ich sollte für jede Business Class eine Schnittstelle erstellen und diese mithilfe eines DI-Containers instanziieren. "Ja wirklich?" Dieser DI vereinfacht meinen Code. Kaum zu glauben.
quelle
Antworten:
Der "beliebteste" Anwendungsfall für DI (abgesehen von der bereits beschriebenen "Strategie" -Musterverwendung) ist wahrscheinlich das Testen von Einheiten.
Selbst wenn Sie glauben, dass es nur eine "echte" Implementierung für eine injizierte Schnittstelle gibt, gibt es für Unit-Tests normalerweise eine zweite: eine "Schein" -Implementierung mit dem einzigen Zweck, einen isolierten Test zu ermöglichen. Dies gibt Ihnen den Vorteil, dass Sie sich nicht mit der Komplexität, den möglichen Fehlern und möglicherweise den Auswirkungen auf die Leistung der "echten" Komponente auseinandersetzen müssen.
Nein, DI dient nicht zur Verbesserung der Lesbarkeit, sondern zur Erhöhung der Testbarkeit (- natürlich nicht ausschließlich).
Dies ist kein Selbstzweck. Wenn Ihre Klasse
ItemService
sehr einfach ist und keinen externen Netzwerk- oder Datenbankzugriff ermöglicht, so dass das Schreiben von Komponententests für so etwas nicht behindert wird,SiteService
kann es sich lohnen, diese isoliert zu testen, sodass DI dies nicht ist notwendig. WennItemService
Sie jedoch über das Netzwerk auf andere Sites zugreifen, möchten Sie wahrscheinlich einenSiteService
von diesem entkoppelten Unit-Test durchführen. Dies kann erreicht werden, indem Sie das "echte"ItemService
durch ein a ersetzenMockItemService
, das einige fest codierte gefälschte Elemente liefert.Lassen Sie mich auf eine andere Sache hinweisen: In Ihren Beispielen könnte man argumentieren, dass DI hier nicht zum Testen der Kerngeschäftslogik benötigt wird - die Beispiele zeigen immer zwei Varianten der
Read
Methoden, eine mit der tatsächlichen Geschäftslogik (die es sein kann) Gerät ohne DI getestet) und eines, das nur der "Kleber" -Code zum Anschließen derItemService
an die vorherige Logik ist. Im gezeigten Fall ist dies in der Tat ein gültiges Argument gegen DI. Wenn DI vermieden werden kann, ohne die Testbarkeit auf diese Weise zu beeinträchtigen, fahren Sie fort. Aber nicht jeder reale Code ist so einfach, und oft ist DI die einfachste Lösung, um eine "ausreichende" Testbarkeit der Einheiten zu erreichen.quelle
Wenn Sie die Abhängigkeitsinjektion nicht verwenden, können Sie dauerhafte Verbindungen zu anderen Objekten herstellen. Verbindungen, die Sie verstecken können, wo sie Menschen überraschen. Verbindungen, die sie nur ändern können, indem Sie das, was Sie erstellen, neu schreiben.
Stattdessen können Sie die Abhängigkeitsinjektion (oder die Referenzübergabe, wenn Sie wie ich altmodisch sind) verwenden, um die Anforderungen eines Objekts explizit zu machen, ohne es zu zwingen, zu definieren, wie seine Anforderungen erfüllt werden müssen.
Dies zwingt Sie dazu, viele Parameter zu akzeptieren. Sogar solche mit offensichtlichen Standardeinstellungen. In C # haben Sie glückliche Soden benannte und optionale Argumente . Das heißt, Sie haben Standardargumente. Wenn es Ihnen nichts ausmacht, statisch an Ihre Standardeinstellungen gebunden zu sein, können Sie DI zulassen, ohne von Optionen überfordert zu sein, auch wenn Sie diese nicht verwenden. Dies folgt der Konvention über die Konfiguration .
Testen ist keine gute Rechtfertigung für DI. In dem Moment, in dem Sie glauben, dass jemand Ihnen ein Wiz-Bang-Mocking-Framework verkauft, das Reflexion oder eine andere Magie verwendet, um Sie davon zu überzeugen, dass Sie zu Ihrer vorherigen Arbeitsweise zurückkehren und den Rest mit Magie erledigen können.
Bei richtiger Anwendung kann das Testen ein guter Weg sein, um zu zeigen, ob ein Design isoliert ist. Aber darum geht es nicht. Es hindert Verkäufer nicht daran zu beweisen, dass mit genügend Magie alles isoliert ist. Halte die Magie auf ein Minimum.
Der Sinn dieser Isolation besteht darin, Veränderungen zu verwalten. Es ist schön, wenn eine Änderung an einem Ort vorgenommen werden kann. Es ist nicht schön, Datei für Datei durchgehen zu müssen, in der Hoffnung, dass der Wahnsinn endet.
Bringen Sie mich in einen Laden, der sich weigert, Unit-Tests durchzuführen, und ich mache trotzdem DI. Ich mache es, weil ich dadurch trennen kann, was benötigt wird und wie es gemacht wird. Testen oder kein Testen Ich möchte diese Isolation.
quelle
Die Hubschrauberansicht von DI ist einfach die Möglichkeit, eine Implementierung gegen eine Schnittstelle auszutauschen . Während dies natürlich ein Segen für das Testen ist, gibt es andere potenzielle Vorteile:
Versionierung von Implementierungen eines Objekts
Wenn Ihre Methoden Schnittstellenparameter in den mittleren Ebenen akzeptieren, können Sie beliebige Implementierungen in der obersten Ebene übergeben, wodurch sich die Menge an Code verringert, die zum Austauschen von Implementierungen geschrieben werden muss. Zugegeben, dies ist ohnehin ein Vorteil von Schnittstellen, aber wenn der Code unter Berücksichtigung von DI geschrieben wurde, werden Sie diesen Vorteil sofort erreichen.
Reduzieren der Anzahl der Objekte, die Ebenen durchlaufen müssen
Während dies hauptsächlich für DI-Frameworks gilt , ist es möglich, den Kernel (oder was auch immer) abzufragen, um Objekt B im laufenden Betrieb zu generieren , anstatt es durch die Ebenen zu leiten, wenn Objekt A Instanzen von Objekt B benötigt . Dies reduziert die Menge an Code, die geschrieben und getestet werden muss. Außerdem werden Ebenen, die sich nicht um Objekt B kümmern, sauber gehalten.
quelle
Es ist nicht erforderlich, Schnittstellen zu verwenden, um DI zu verwenden. Der Hauptzweck von DI besteht darin, die Konstruktion und Verwendung von Objekten zu trennen.
Die Verwendung von Singletons ist in den meisten Fällen zu Recht verpönt. Einer der Gründe ist, dass es sehr schwierig wird, sich einen Überblick über die Abhängigkeiten einer Klasse zu verschaffen.
In Ihrem Beispiel könnte der ItemSiteController einfach einen ItemSiteService als Konstruktorargument verwenden. Auf diese Weise können Sie Kosten für das Erstellen von Objekten vermeiden, aber die Inflexibilität eines Singletons vermeiden. Das Gleiche gilt für ItemSiteService. Wenn ein ItemService und ein SiteService benötigt werden, fügen Sie diese in den Konstruktor ein.
Der Vorteil ist am größten, wenn alle Objekte die Abhängigkeitsinjektion verwenden. Auf diese Weise können Sie die Konstruktion in einem dedizierten Modul zentralisieren oder an einen DI-Container delegieren.
Eine Abhängigkeitshierarchie könnte ungefähr so aussehen:
Beachten Sie, dass es nur eine Klasse ohne Konstruktorparameter und nur eine Schnittstelle gibt. Bei der Konfiguration des DI-Containers können Sie entscheiden, welcher Speicher verwendet werden soll oder ob Caching verwendet werden soll usw. Das Testen ist einfacher, da Sie entscheiden können, welche Datenbank verwendet werden soll oder welche andere Art von Speicher verwendet werden soll. Sie können den DI-Container auch so konfigurieren, dass Objekte bei Bedarf im Kontext des Containerobjekts als Singletons behandelt werden.
quelle
Sie isolieren externe Systeme.
Ja, hier DI verwenden. Wenn es sich um ein Netzwerk, eine Datenbank, ein Dateisystem, einen anderen Prozess, Benutzereingaben usw. handelt, möchten Sie es isolieren.
Die Verwendung von DI erleichtert das Testen, da Sie diese externen Systeme leicht verspotten können. Nein, ich sage nicht, dass dies der erste Schritt zum Testen von Einheiten ist. Sie können auch nicht testen, ohne dies zu tun.
Selbst wenn Sie nur eine Datenbank hätten, würde Ihnen die Verwendung von DI an dem Tag helfen, an dem Sie migrieren möchten. Also ja, DI.
Sicher, DI kann Ihnen helfen. Ich würde über die Container diskutieren.
Bemerkenswert ist vielleicht, dass die Abhängigkeitsinjektion mit konkreten Typen immer noch eine Abhängigkeitsinjektion ist. Wichtig ist, dass Sie benutzerdefinierte Instanzen erstellen können. Es muss keine Schnittstelleninjektion sein (obwohl die Schnittstelleninjektion vielseitiger ist, bedeutet dies nicht, dass Sie sie überall verwenden sollten).
Die Idee, eine explizite Schnittstelle für jede Klasse zu erstellen, muss sterben. In der Tat, wenn Sie nur eine Implementierung einer Schnittstelle haben würden ... YAGNI . Das Hinzufügen einer Schnittstelle ist relativ kostengünstig und kann bei Bedarf durchgeführt werden. In der Tat würde ich vorschlagen, zu warten, bis Sie zwei oder drei Kandidatenimplementierungen haben, damit Sie eine bessere Vorstellung davon haben, welche Dinge unter ihnen gemeinsam sind.
Die Kehrseite davon ist jedoch, dass Sie Schnittstellen erstellen können, die den Anforderungen des Client-Codes besser entsprechen. Wenn der Client-Code nur wenige Mitglieder einer Klasse benötigt, können Sie dafür eine Schnittstelle haben. Dies führt zu einer besseren Trennung der Schnittstellen .
Behälter?
Sie wissen, dass Sie sie nicht brauchen.
Lassen Sie es uns auf Kompromisse bringen. Es gibt Fälle, in denen sie es nicht wert sind. Ihre Klasse muss die Abhängigkeiten übernehmen, die sie vom Konstruktor benötigt. Und das könnte gut genug sein.
Ich bin wirklich kein Fan von Annotationsattributen für "Setter Injection", geschweige denn von Attributen von Drittanbietern. Ich verstehe, dass dies möglicherweise für Implementierungen erforderlich ist, die außerhalb Ihrer Kontrolle liegen. Wenn Sie sich jedoch entscheiden, die Bibliothek zu ändern, müssen sich diese ändern.
Schließlich werden Sie Routinen erstellen, um diese Objekte zu erstellen, denn um sie zu erstellen, müssen Sie zuerst diese anderen erstellen, und für diese benötigen Sie weitere ...
Nun, wenn das passiert, möchten Sie all diese Logik an einem einzigen Ort platzieren und wiederverwenden. Sie möchten eine einzige Quelle der Wahrheit darüber, wie Sie Ihr Objekt erstellen. Und du bekommst es, wenn du dich nicht wiederholst . Das vereinfacht Ihren Code. Macht Sinn, oder?
Wo setzen Sie diese Logik ein? Der erste Instinkt wird sein, einen Service Locator zu haben . Eine einfache Implementierung ist ein Singleton mit einem schreibgeschützten Wörterbuch von Fabriken . Eine komplexere Implementierung könnte Reflexion verwenden, um die Fabriken zu erstellen, wenn Sie keine bereitgestellt haben.
Die Verwendung eines Singleton- oder statischen Service-Locators bedeutet jedoch, dass Sie so etwas wie
var x = IoC.Resolve<?>
jeden Ort ausführen, an dem Sie eine Instanz erstellen müssen. Dies fügt Ihrem Service Locator / Container / Injektor eine starke Kupplung hinzu. Das kann Unit-Tests tatsächlich schwieriger machen.Sie möchten einen Injektor, den Sie instanziieren, und behalten ihn nur für die Verwendung auf dem Controller. Sie möchten nicht, dass es tief in den Code eindringt. Das könnte das Testen tatsächlich erschweren. Wenn ein Teil Ihres Codes es benötigt, um etwas zu instanziieren, sollte er eine Instanz (oder fast eine Factory) auf seinem Konstruktor erwarten.
Und wenn Sie viele Parameter im Konstruktor haben ... sehen Sie, ob Sie Parameter haben, die zusammen laufen. Möglicherweise können Sie Parameter zu Deskriptortypen zusammenführen (Werttypen idealerweise).
quelle