Sonntag, 12. Februar 2012


Artikel

Juli 2010 | Artikel

Dependency Injection mit Scala

(Link zum Artikel: http://www.entwickler-magazin.de/jaxenter//003215)

Advanced Scala - Teil 1

Text: Heiko Seeberger
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Ob man nun das Spring-Framework liebt oder schon wieder hasst, eines muss man ihm hoch anrechnen: Gutes Design im Sinne von loser Kopplung sowie das Prinzip der Dependency Injection gehören heute dank Spring zum Handwerkszeug eines jeden (Java-)Software-Entwicklers. Natürlich bietet sich der Einsatz von Dependency Injection auch für viele Scala-Projekte an und daher stellt sich die Frage, wie wir das am besten hinbekommen können. Dank der “Abwärtskompatibilität” zu Java könnten wir natürlich mit Spring oder Guice arbeiten, aber Scala bietet als mächtige Sprache noch eine andere Möglichkeit. Im Folgenden zeigen wir, wie wir in Scala Dependency Injection ganz ohne Container, XML oder Annotations realisieren können, indem wir nur Sprachmittel einsetzen.

Advanced Scala

Willkommen zu Teil 1 der neuen JAXenter-Serie "Advanced Scala" von Heiko Seeberger. Wer schon Erfahrungen mit Scala sammeln konnte, ist hier genau richtig.

Als Beispiel soll uns ein News-Service dienen, der die Messages mehrerer Channels zusammenfasst. Um das Beispiel einfach zu halten beschränken wir uns dabei auf zwei Channels, und zwar auf Sport und Musik (Gibt es etwa Wichtigeres?).

Beginnen wir mit dem Channel, den wir als Trait mit der abstrakten Methode messages definieren, welche eine Sequenz (Liste) von Strings zurückgibt.

  1. trait Channel {
  2. def messages: Seq[String]
  3. }

Damit könnte unser News-Service in etwa so aussehen:

  1. class News {
  2. def latestMessages: Seq[String] =
  3. channels flatMap { _.messages take numberOfMessages }
  4. }

Wir greifen einfach eine bestimmte Anzahl von Messages, definiert durch numberOfMessages, von jedem Channel ab und fügen diese zusammen . Es bleibt natürlich zu klären, wie wir an die Channels und die numberOfMessages kommen. Ganz offensichtlich wollen wir diese per Dependency Injection beisteuern.

Nun greifen wir tief in die Design-Trickkiste und wenden das sogenannte Cake-Pattern an. Dabei betten wir den News-Service in einen Trait ein, welcher die Dependencies als abstrakte Methoden beisteuert.

  1. trait NewsContext {
  2. class News {
  3. def latestMessages: Seq[String] =
  4. channels flatMap { _.messages take numberOfMessages }
  5. }
  6. protected val channels: Seq[Channel]
  7. protected val numberOfMessages: Int
  8. }

Damit sind wir schon nahe am Ziel, denn wir haben schon einmal eine Abstraktion für unsere Dependencies gefunden. Der Trait NewsContext fungiert hier quasi als Scope für den News-Service. Da channels und numberOfMessages ebenfalls im gleichen Scope liegt, können sie im News-Service verwendet werden.

Jetzt müssen wir nur noch unsere Dependencies für unterschiedliche Konfigurationen konkretisieren bzw. injizieren. Dazu bedienen wir uns eines Singleton Objects.

  1. object Configuration extends NewsContext {
  2. lazy val news = new News
  3. override protected val channels =
  4. new SportsChannel :: new MusicChannel :: Nil
  5. override protected val numberOfMessages = 2
  6. }

Diese Konfiguration verwendet die "richtigen" Channels, die folgende "gemockte"; beide sind hier nicht weiter aufgeführt.

  1. object TestConfiguration extends NewsContext {
  2. lazy val news = new News
  3. override protected val channels =
  4. new MockSportsChannel :: new MockMusicChannel :: Nil
  5. override protected val numberOfMessages = 2
  6. }

Die beiden Konfigurationen, als Singleton Objects umgesetzt, dienen also zum "Wiring" der Dependencies und ersetzen im Vergleich mit Spring die XML-Konfigurationsdateien bzw. die Annotations. Ohne jetzt auf Details einzugehen, sei abschließend eines herausgepickt: Mit dem Schlüsselwort lazy können wir sogar die Erzeugung der Objekte auf den Zeitpunkt der Nutzung verschieben.

  1. object Configuration extends NewsContext {
  2. ...
  3. override lazy protected val channels =
  4. new SportsChannel :: new MusicChannel :: Nil
  5. ...
  6. }

Der komplette Code dieses Beispiels ist auf github unter http://wiki.github.com/weiglewilczek/demo-scala verfügbar. Fragen oder Kommentare sind jederzeit erwünscht!

In der nächsten Folge wollen wir den Traits treu bleiben und zeigen, wie wir sogar aspektorientierte Programmierung mit reinen Scala-Sprachmitteln umsetzen können.

Heiko Seeberger ist geschäftsführender Gesellschafter der Weigle Wilczek GmbH und verantwortlich für die technologische Strategie des Unternehmens mit den Schwerpunkten Java, Scala, OSGi, Eclipse RCP, Lift und Akka. Zudem ist er aktiver Open Source Committer, Autor zahlreicher Fachartikel und Redner auf einschlägigen Konferenzen.

andere Artikel dieser Serie

Kommentare

Gravatar Thomas 09.07.2010
um 11:55 Uhr
Hallo,

vielen Dank für den Artikel. Wirklich mal eine andere Richtung, mit der man auf Scala guckt und versucht etwas altes mit neuen Mitteln darzustellen.

Was mir hier allerdings noch fehlt sind Erfahrungen in der Verwendung dieses Ansatzes:

Wird für ein Singelton-Context für eine Anwendung definiert?
Wenn ja, dann müsste er ja alle Contexte einmixen. Was ist dann mit gleichnamigen Membern (z.B. 2 x channels aus verschiedenen Context)

Wenn auf der anderen Seite verschiedene singelton Context Objekte (für verschiedene Pakete) definiert werden, wie funktioniert dann die Interaktion und DI zwischen diesen Objects?

Gibt es hierzu schon Erfahrungen?
#zitieren
Gravatar Heiko Seeberger 12.07.2010
um 09:56 Uhr
Hi Thomas,

danke für Dein Feedback!

Tja, Erfahrungen mit Scala: Da stehen wir alle wohl noch am Anfang. Zwar habe ich schon "echte" Projekte gemacht, aber im Vergleich zu 10+ Jahren Java ist der Erfahrungsschatz doch recht überschaubar.

Das gezeigte Pattern stellt nur das Grundprinzip dar. Je nach Bedarf können wir Variationen einsetzen. Z.B. könnten wir für das Problem der Namenskollision (wenn wir nur ein Configuration-Object verwenden wollen) einfach innerhalb der Configuration "Sub-Configurations" pro Service oder auch etwas grob-granularer erstellen:

object Configuration {
lazy val news = NewsConfiguration.news
private[Configuration] object NewsConfiguration extends NewsContext {
lazy val news = new News
override protected val channels =
new SportsChannel :: new MusicChannel :: Nil
override protected val numberOfMessages = 2
}
}

Wenngleich es vielleicht zunächst irritieren man, dass wir keine allzu starren Spielregeln haben, ist das doch eigentlich gerade das Schöne: Große Flexibilität!

Viele Grüße

Heiko
#zitieren
Gravatar Trepper 12.07.2010
um 11:28 Uhr
Es ist zwar schön, dass man DI so mit Scala-Bordmitteln umsetzen kann, aber ich frage mich, ob das wirklich einfacher als Guice ist. Zumindest beim Lesen des Beispiels kommt mir Guice _wesentlich_ einfacher vor.

Dependency Injection is ja an sich auch keine große Sache, die kann man in jeder Sprache mit Bordmitteln machen - nur eben mehr oder weniger flexibel bzw. elegant.
#zitieren
Gravatar Heiko Seeberger 12.07.2010
um 18:31 Uhr
Hi Trepper,

würde mich interessieren, wie Du das mit Guice einfacher machst.

Im obigen Beispiel besteht das "Wiring" im Configuration Object aus vier effektiven Zeilen Code (die abschließende Klammer sowie den Umbruch wg. Lesbarkeit lasse ich mal weg).

Wenn ich mich nicht täusche, dann musst Du in Guice zusätzlich zu den Annotations (@Inject) ein Module schreiben in dem Du das Binding definierst. Das ist ziemlich sicher mehr Code als mit dem Scala Cake Pattern. Und meiner Meinung nach auch nicht verständlicher, weil Du zusätzlich wissen musst, wie die Annotations und das Binding funktionieren.

Aber zeig doch mal, vielleicht kenne ich Guice auch nicht gut genug ...

Heiko
#zitieren
Gravatar Frager 12.07.2010
um 23:40 Uhr
Hallo,
also das sieht mir weniger nach DI aus als nach einer simplen uralten Factory.
Wenn nicht, wo sind die Unterschiede?
Und was genau ist es, warum man das nur mit Scala und nicht mit auch mit Java machen könnte?
Danke schon mal!
#zitieren
Gravatar Heiko Seeberger 14.07.2010
um 11:04 Uhr
Hi Frager,

die Dependencies des News-Service werden "von aussen" konfiguriert, also injected. Dass es so wohlbekannt aussieht, liegt daran, dass das Wiring direkt ersichtlich ist und nicht hinter einem "Container" verborgen wird. Was glaubst Du, was Spring oder Guice unter der Haube tun?

Warum man das nicht mit Java tun kann? Das gibt die Sprache nun mal nicht her. Versuch's doch mal, dann wirst Du merken, dass der Compiler etwas dagegen hat. Zumindest bei meinen Versuchen ...

Heiko
#zitieren
Gravatar Trepper 19.07.2010
um 10:11 Uhr
Hallo Heiko,

ich muss zugeben, dass mir das Cake-Pattern bei nochmaligem Lesen eleganter und einfacher erscheint als beim ersten Mal.

Mit Guice muss man das Beispiel etwas anders umsetzen, da man keine Collection durch Guice mit Elementen belegen kann, was bei "channels" nötig wäre. Dafür habe ich noch einen kleinen Trait "Channels" eingeführt. Weil Guice bei Konstanten einen Namen braucht, habe ich außerdem noch die Konstante "numberName" eingeführt.

Bei meine Implementierung braucht man also insgesamt 3 Annotationen, einen zusätzlichen Trait, eine zusätzliche Klasse ("ImportantChannels") und eine Klasse für das Guice-Modul. Dafür spart man sich den NewsContext-Trait und das Configuration-Objekt. Nach Zeilenzahl brauchen beide Lösungen gleich viel Platz, allerdings muss ich eingestehen, dass das Cake-Pattern doch etwas schöner ist und es außerdem vorzuziehen ist, wenn man etwas mit Sprachmitteln lösen kann.

class News {
@Inject var channels: Seq[Channel]
val numberName = "numberOfMessages"
@Inject @Named(numberName) var numberOfMessages: Int
def latestMessages: Seq[String] = channels flatMap { _.messages take }
}

trait Channels {
val channels: Seq[Channel]
}

class ImportantChannels extends Channels {
override channels = new SportsChannel :: new MusicChannel :: Nil
}

public class NewsModule extends AbstractModule {
override def configure() {
bind(Channels.class).to(ImportantChannels.class)
bindConstant().annotatedWith(News.numberName).to(2)
bind(News.class)
}
}
#zitieren
Gravatar Heiko Seeberger 19.07.2010
um 10:20 Uhr
Hi Trepper,

wow, ich bin begeistert: Es gelingt nicht allzu oft, jemanden zu überzeugen ;-)

Danke für die faire Gegenüberstellung. Da möchte ich noch ergänzen, dass Guice und vor allem Spring noch eine Menge mehr bieten, z.B. Guice Peaberry eine OSGi-Integration und Spring, na ja, das muss man nicht extra erwähnen. Aber für das reine Prinzip der Dependency Injection ist das Cake Pattern schon eine tolle Sache, finde ich.

Viele Grüße

Heiko
#zitieren
Gravatar Trepper 19.07.2010
um 10:49 Uhr
Das Cake-Pattern kann man auch mit Java implementieren, wenn auch nicht ganz so schön:

public interface Channel {
List getMessages();
}


public abstract class NewsContext {

abstract List getChannels();
abstract int getNumberOfMessages();

protected News createNews() {
return new News();
}

public class News {
List getLatestMessages() {
List messages = new ArrayList();
for(Channel channel : getChannels())
messages.addAll(channel.getMessages()
.subList(0, getNumberOfMessages()));
return messages;
}
}
}


public class Configuration extends NewsContext {

@Override List getChannels() {
// der Einfachheit halber spare ich mir die Erzeugung der Channels
return new ArrayList();
}

@Override int getNumberOfMessages() {
return 2;
}

public static News getNews() {
return new Configuration().createNews();
}

}
#zitieren
Gravatar Heiko Seeberger 19.07.2010
um 10:55 Uhr
Ah, sehr schön. Ich habe das auch probiert, allerdings mit einem Interface für den NewsContext, und das hat mir der Compiler nicht geschluckt.

Ich sehe nur folgendes Problem: Im "echten Leben" haben wir sicher mehrere Services die wir konfigurieren möchten, daher muss Configuration mehrere Contexte erweitern/implementieren. Mit abstrakten Klassen als Context haut das aber nicht hin ...
#zitieren
Gravatar Trepper 19.07.2010
um 11:00 Uhr
Man könnte ja auch eine Konfiguration pro Service machen und dann noch ein "Meta-Konfiguration", die alle diese Konfigurationen zusammenfasst. Ob das schön ist, ist eine andere Frage ;-) #zitieren