Wie führt man eine Abhängigkeitsinjektion mit dem Kuchenmuster ohne Hardcodierung durch?

74

Ich habe gerade den Artikel über das Kuchenmuster gelesen und genossen . Meiner Meinung nach ist einer der Hauptgründe für die Verwendung der Abhängigkeitsinjektion, dass Sie die Komponenten variieren können, die entweder von einer XML-Datei oder von Befehlszeilenargumenten verwendet werden.

Wie wird dieser Aspekt von DI mit dem Kuchenmuster behandelt? Die Beispiele, die ich alle gesehen habe, beinhalten das statische Einmischen von Merkmalen.

Rechnung
quelle

Antworten:

56

Da das Einmischen von Merkmalen in Scala statisch erfolgt, erstellen Sie verschiedene Objekte basierend auf bestimmten Bedingungen, wenn Sie die in ein Objekt eingemischten Merkmale variieren möchten.

Nehmen wir ein Beispiel für ein kanonisches Kuchenmuster. Ihre Module sind als Merkmale definiert, und Ihre Anwendung besteht aus einem einfachen Objekt mit einer Reihe von Funktionen

val application =
    new Object
extends Communications
   with Parsing
   with Persistence
   with Logging
   with ProductionDataSource
application.startup

Jetzt haben alle diese Module nette Selbsttypdeklarationen, die ihre Abhängigkeiten zwischen Modulen definieren, sodass die Zeile nur kompiliert wird, wenn alle Abhängigkeiten zwischen Modulen vorhanden, eindeutig und gut typisiert sind. Insbesondere hat das Persistenzmodul einen Selbsttyp, der besagt, dass alles, was Persistenz implementiert, auch DataSource implementieren muss, ein abstraktes Modulmerkmal. Da ProductionDataSource von DataSource erbt, ist alles großartig und diese Anwendungskonstruktionslinie wird kompiliert.

Was aber, wenn Sie eine andere DataSource verwenden möchten, die zu Testzwecken auf eine lokale Datenbank verweist? Angenommen, Sie können ProductionDataSource nicht einfach mit verschiedenen Konfigurationsparametern wiederverwenden, die aus einer Eigenschaftendatei geladen wurden. In diesem Fall definieren Sie ein neues Merkmal TestDataSource, das DataSource erweitert, und mischen es stattdessen ein. Sie können dies sogar dynamisch basierend auf einem Befehlszeilenflag tun.

val application = if (test)
  new Object
    extends Communications
      with Parsing
      with Persistence
      with Logging
      with TestDataSource
else
  new Object
    extends Communications
      with Parsing
      with Persistence
      with Logging
      with ProductionDataSource

application.startup

Das sieht etwas ausführlicher aus, als wir es gerne hätten, insbesondere wenn Ihre Anwendung ihre Konstruktion auf mehreren Achsen variieren muss. Auf der positiven Seite haben Sie normalerweise nur einen Teil einer solchen bedingten Konstruktionslogik in einer Anwendung (oder im schlimmsten Fall einmal pro identifizierbarem Komponentenlebenszyklus), sodass zumindest die Schmerzen minimiert und vom Rest Ihrer Logik abgegrenzt werden.

Dave Griffith
quelle
Reines kanonisches Kuchenmuster, obwohl ich denke, dass das OP nach einer Möglichkeit suchte, einem bereits instanziierten Objekt ein Merkmal hinzuzufügen. Das Problem hierbei ist, dass dies nur zum Zeitpunkt des Baus möglich ist.
Kevin Wright
10
@ Dave: Schlagen Sie wirklich vor, dass Ihre "if" -Anweisung produktionsbereit ist und etwas, das Sie für Unternehmenssoftware bereitstellen würden? Dies ist meiner Meinung nach wirklich schlechter Code, da ein Bereitstellungsproblem (welche Datenbank) nicht von einem Codeproblem (wie eine Anwendung zusammengesetzt ist) getrennt werden kann. Das Nachschlagen der Datenbank sollte beispielsweise in einem JNDI-Baum erfolgen. Es sollte niemals fest codiert werden, da für eine Änderung eine erneute Bereitstellung erforderlich wäre.
Ant Kutschera
6
Ich würde es im Produktionscode nicht so machen, wenn es keine Alternative gäbe. Dies war einfach die beste Antwort, die ich auf die ursprüngliche Frage kenne, was wirklich die Notwendigkeit implizierte, die Zusammensetzung der Anwendung zur Laufzeit zu ändern. Die Frage stellt sich einfach: "Das müssen Sie nie tun, da alles durch individuelle Konfiguration der Komponenten erledigt werden kann."
Dave Griffith
1
Wenn Sie sich über den Software-Engineering-Aspekt des Kuchenmusters Gedanken machen, kann die "Test" -Klausel einfach in Ihre Komponententests eingebettet werden, während die else / Production-Klausel in der Hauptanwendung verbleibt.
Jhclark
2
@jhclark Produktion, Phase, Entwicklung, Test: Wie gehe ich mit diesen Szenarien um, wenn Sie abhängig von den Laufzeitbedingungen möglicherweise mit mehreren Datenbanken arbeiten? Die obige JNDI-Baumreferenz ist gut, aber ich bin mir nicht sicher, ob sie alle Grundlagen abdeckt, wohingegen eine Kuchenimplementierung alles abdecken kann, nur mit viel mehr Boilerplate, und wie Dave in seiner Antwort erwähnt, "Schmerz". ;-)
virtualeyes
29

Scala ist auch eine Skriptsprache. Ihr Konfigurations-XML kann also ein Scala-Skript sein. Es ist typsicher und keine andere Sprache.

Schauen Sie sich einfach den Start an:

scala -cp first.jar:second.jar startupScript.scala

ist nicht so anders als:

java -cp first.jar:second.jar com.example.MyMainClass context.xml

Sie können immer DI verwenden, aber Sie haben noch ein Werkzeug.

Shellholic
quelle
5

Die kurze Antwort lautet, dass Scala derzeit keine integrierte Unterstützung für dynamische Mixins bietet.

Ich arbeite an dem Autoproxy-Plugin, um dies zu unterstützen, obwohl es derzeit bis zur Version 2.9 angehalten wird, wenn der Compiler über neue Funktionen verfügt, die es viel einfacher machen.

In der Zwischenzeit können Sie fast genau die gleiche Funktionalität erreichen, indem Sie Ihr dynamisch hinzugefügtes Verhalten als Wrapper-Klasse implementieren und anschließend dem Wrap-Member eine implizite Konvertierung hinzufügen.

Kevin Wright
quelle
Werden diese neuen Funktionen irgendwo erklärt?
Pedrofurla
@pedrofurla - Im Quellcode :) Der 2.9-Compiler bietet bessere Möglichkeiten zum Zurücksetzen, nachdem eine Einheit in der Typer-Phase ausgefallen ist. Dies wurde größtenteils für die Verwendung durch den Präsentations-Compiler implementiert (wie von Eclipse und Ensime verwendet). Dies ist für mich relevant, da das Autoproxy-Plugin eine Technik zum Eingeben in zwei Durchgängen verwendet, einmal zum Generieren von Typinformationen, die für Delegatmethoden benötigt werden, und erneut für Einheiten, bei denen die Eingabe fehlgeschlagen ist, bevor die Delegaten synthetisiert wurden. Ich hatte Probleme mit Inkonsistenzen, die nach dem ersten fehlgeschlagenen Durchgang nicht in der Symboltabelle vorhanden waren.
Kevin Wright
1
@pedrofurla - Nicht, dass dies für die allgemeine Scala-Programmierung relevant wäre, nur für Leute, die eine bestimmte Klasse von Tricks mit Compiler-Plugins ausführen.
Kevin Wright
3

Bis das AutoProxy-Plugin verfügbar ist, können Sie den Effekt mithilfe der Delegierung erzielen:

trait Module {
  def foo: Int
}

trait DelegatedModule extends Module {
  var delegate: Module = _
  def foo = delegate.foo
}

class Impl extends Module {
  def foo = 1
}

// later
val composed: Module with ... with ... = new DelegatedModule with ... with ...
composed.delegate = choose() // choose is linear in the number of `Module` implementations

Aber Vorsicht, der Nachteil ist, dass es ausführlicher ist und Sie bei der Initialisierungsreihenfolge vorsichtig sein müssen, wenn Sie vars innerhalb eines Merkmals verwenden. Ein weiterer Nachteil ist, dass ModuleSie die Delegierung nicht so einfach verwenden können , wenn oben pfadabhängige Typen vorhanden sind .

Wenn es jedoch eine große Anzahl unterschiedlicher Implementierungen gibt, die variiert werden können, kostet dies wahrscheinlich weniger Code als das Auflisten von Fällen mit allen möglichen Kombinationen.

axel22
quelle
neues delegiertes Modul? Sie können "neue" Eigenschaften deklarieren? Was bedeutet der Unterstrich in diesem speziellen Kontext? Ich bin so verwirrt.
Ryan The Leach
0

In Lift ist etwas in diese Richtung eingebaut. Es besteht hauptsächlich aus Scala-Code, aber Sie haben eine gewisse Laufzeitkontrolle. http://www.assembla.com/wiki/show/liftweb/Dependency_Injection

sblundy
quelle
Hmm, ich mag im Allgemeinen sowohl Lift als Framework als auch Lift als Bibliothek. Aber genau in diesem Fall ist es aus meiner Sicht keine wirklich gute Antwort. Der Hauptgrund ist, dass die Programmkorrektheit vom Compiler nicht überprüft wird. Die Bibliothek erwartet, dass ein DI-Aufruf fehlschlägt, auch wenn das Ergebnis jedes DI-Aufrufs "Option" -al ist.
VasiliNovikov