Artikel

 
November 2009 | Artikel

Mit Spring und AOP Domänenklassen verwalten

(Link zum Artikel: http://www.it-republik.de/jaxenter/artikel/2644)

Beam me up, Scotty!

Text: Thorsten Kamann
  • Teilen
  • kommentieren
  • empfehlen
  • Bookmark and Share
Mit Spring können Sie Anwendungen erstellen, die flexibel, modular und frei von Abhängigkeiten zu dem zugrundeliegenden Framework sind. Die typischen Objekttypen, die mit Spring verwaltet werden sind Services und Datenzugriffskomponenten. Nur Domänenobjekte – eigentlich die wichtigsten Elemente einer Anwendung – fallen etwas aus der Rolle.
Teil 1   Teil 2   Teil 3   

Domänenmodell und Objektorientierung
Domain Layer (or Model Layer): Responsible for representing concepts of the business, information about the business situation, and business rules. State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure. This layer is the heart of business software. (Eric Evans: Domain Driven Design)

Wie das Zitat schon sagt: Das Rückgrat einer Anwendung ist das Domänenmodell. Dieses Modell beschreibt die Datenstrukturen, die mit dieser Anwendung verwaltet werden sollen. Leider verkommen die Klassen, die dieses Modell repräsentieren immer mehr zu inhaltsleeren POJOs. Sie bestehen fast nur noch aus Getter- und Setter-Methoden und den zugehörigen Feldern. Martin Fowler hat das hier als Anemic Domainmodel beschrieben.

Stellen Sie sich vor, Sie haben ein Objekt Bestellung. Es wäre wünschenswert, dass dieses Objekt auch eine Methode zum Abschicken der Bestellung hat. In einer typischen Anwendung übernimmt diese Funktionalität allerdings eher ein Service.

Um so etwas zu ändern, braucht dieses Bestellobjekt eine Referenz auf die Datenzugriffsschicht. Das können Klassen übernehmen, die das DAO-Pattern implementieren. Solche DAOs sind zustandslos, müssen also nicht bei jedem Zugriff neu initialisiert werden.

Evaluierung der Möglichkeiten

Wie bekommt man dieses DAO in das neu initialisierte Objekt? Die Schwierigkeit besteht darin, dass immer wieder neu erstellte Objekte eine Referenz auf eine bestehende Instanz bekommen. Als Möglichkeiten gibt es:

  • Statische Methodenzugriffe
  • Singleton-Pattern implementieren

Beide Möglichkeiten sind eher schlecht. Ein K.O.-Kriterium ist dabei die fehlende Testbarkeit. Besser wäre es, eine Lösung zu finden, die mittels Dependency Injection die benötigten Referenzen setzt.

Denkt man an Dependency Injection, dann denkt man auch an Spring. Allerdings löst Spring dieses Problem nicht Out-of-the-box. Mit Spring werden normalerweise Klassen verwaltet, die nur einmal zur Laufzeit initialisiert werden müssen.

Exkurs: Anatomie einer Spring-Anwendung

Warum ist das so? Um das zu erklären, müssen wir uns gemeinsam eine typische Spring-Anwendung ansehen. Spring ist ein nicht invasives Framework. Das bedeutet, es erfordert nicht das Implementieren von Interfaces oder das Erben von Superklassen. Somit kann die Anwendung mit Java-Bordmitteln erstellt werden. Spring umgibt die Anwendung dann als Container und konfiguriert diese mit XML oder Annotations (Abb. 1).

Dabei sind die Spring Beans – die Klassen, die von Spring verwaltet werden –verwaltete Singletons, d.h. Klassen werden einmalig erzeugt und diese Instanz immer wieder verwendet. In Listing 1 sehen Sie das Prinzip veranschaulicht. Der OrderService benötigt ein Datenzugriffsobjekt (DAO). Dieses DAO ist zustandslos. Somit reicht eine Instanz. Spring verwaltet diese Instanz und injiziert sie den anfragenden Objekten – in diesem Fall der OrderService. Eine Einschränkung dabei ist, dass das anfragende Objekt ebenfalls eine Spring Bean sein muss.

Natürlich ist es auch möglich, ein Objekt bei jedem Aufruf neu instanziieren zu lassen. Allerdings muss auch hier der Aufrufer eine Spring Bean sein.

  1. public class OrderService{
  2. @Resource
  3. private OrderDao orderDao ;
  4. }
Listing 1
Kann denn Spring gar nicht helfen?

Auf den ersten Blick sieht es tatsächlich so aus, dass Spring uns bei unserem Problem nicht helfen kann. Aber dieser Artikel wäre völlig sinnlos, wenn es nicht doch einen Lösungsansatz gäbe.

Seit Spring 2.0 gibt es @Configurable Annotation. Mit dieser Annotation können Sie Klassen annotieren, die über das new-Schlüsselwort erzeugt werden, aber von Spring als Spring Beans behandelt werden sollen.

  1. @Configurable
  2. @Entity
  3. public class Order{
  4. @Resource
  5. private OrderDao orderDao ;
  6. public void saveOrder() throws SaveException{
  7. orderDao.saveOrder(this) ;
  8. }
  9. }
  10. @Repository
  11. public class OrderDao{
  12. public Order findOrderById(Long id){
  13. }
  14. }
Listing 2

In Listing 2 sehen Sie zwei Klassen, die miteinander in Beziehung stehen. Die Entität Order benötigt die Datenzugriffskomponente OrderDao, um die Bestellung zu speichern.

Teil 1   Teil 2   Teil 3   

Anzeige

Kommentare

Gravatar Entwickler 03.11.2009
um 23:28 Uhr
Das vorliegende Beispiel zeigt mal wieder wie uneinig sich die Architekten sind. Erst heißt es POJO dann heißt es Active Record Pattern. Dann heißt es wieder: Dumme Objekte alle nur per Service und nun das hier.Die Geschichte wiederholt sich. #zitieren
Gravatar Trepper 04.11.2009
um 14:03 Uhr
@EntwicklerDas Wort "POJO" sollte eigentlich wieder aus dem Sprachgebrauch verschwinden denn es war als Abgrenzung gegenüber den schwerfälligen EJBs 2x gedacht. Ansonsten sagt POJO einfach zu wenig aus um als Diskussionsgrundlage zu dienen. Insbesondere sagt POJO nichts zu Domain Driven Design aus.Die "dummen" Objekte die per Service verarbeitet werden sind meiner Meinung nach ein Widerspruch zur Objekt_orientierung_ denn letztlich trennt man das Programm in Datenstrukturen und Funktionen.-------------------------Zum Artikel selbst:Wenn Objekte die Methoden mitbringen die man intuitiv aufrufen möchte macht das die Entwicklung sehr angenehm und das System viel leichter verständlich. Das habe ich bei Grails gemerkt wo Methoden wie save() durch die dynamischen Fähigkeiten Groovys den Domain-Objekten zur Laufzeit hinzugefügt werden. Prinzipiell finde ich den hier vorgestellten Ansatz also gut aber mir erscheint er irgendwie etwas over-engineered. Warum sollte man in einem DAO oder Repository nicht einfach die abhängigen Objekte direkt injizieren? Dadurch ergibt sich auch eine natürliche Trennung von Methoden die zum Repository gehören und Methoden die zu einem Domain-Objekt gehören. findBy-Methoden wären im Repository zuhause und save() z. B. im Domain-Objekt. Andererseits passt der Begriff "Repository" bei diesem ganzen Ansatz nicht mehr so ganz denn ein Repository soll eine Collection-artige Schnittstelle über einem dauerhaften Speicher (Datenbank) darstellen. Wenn man das jetzt mal mit normalen Collections vergleich speichern sich ja beliebige Objekte auch nicht selbst in einer Liste wenn man save() auf ihnen aufruft (weil sie überhaupt keine solche Methode haben). Letztlich ist es merkwürdig wenn ein Objekt sich selbst speichern kann. #zitieren
Gravatar Entwickler 04.11.2009
um 15:57 Uhr
@TrepperWenn sich ein DAO um die Injizierung kümmern würde dann hätten wir einen Hybriden Ansatz bei dem man sich die Objekte per DAO holt das DAO sich selber in die Objekte injiziert und man dann die Methoden der Objekte verwendet die ihrerseits wiederum auf dem DAO arbeiten. Da beißt sich die Katze doch in den Schwanz. #zitieren
Gravatar Carsten 04.11.2009
um 16:00 Uhr
Trepper is ein schlauer Fuchs. Es ist sehr viel eingaengier Order.Load(4711) zu schreiben als mit irgentwelchen Services und und Repostitories etc zu hantieren. Und ein entscheidener Aspekt der Entwicklung ist die Lesbarkeit des Quellcodes. Ist sie gut genug braucht man wenig bis gar nicht zusaetzlich zu dokumentieren.Schlau ist ebenfalls die Bemerkung warum denn ein Objekt wissen muss das es gespeichert ist und warum es dafuer eine Methode bereitstellen muss.Wir haben das so geloest dass alle Objekte zusammen in einem gemeinsamen Kontext leben. Dieser Kontext kann dann als Ganzes persistiert werden. #zitieren
Gravatar Carsten 04.11.2009
um 16:18 Uhr
@Entwickler: ich weiss ja nicht was Du so entwickelst aber die meisten die ich so kenne arbeiten an Geschaeftsanwendungen. In meiner Schaffenszeit ist mir in dem Umfeld noch nie untergekommen dass Datenbanken oder OR-Mapper ausgetauscht worden sind. Ich kenne aber viele Projekte wo DAOs injeziert werden und es nur so wimmelt von Schnittstellen. Alles Quatsch wie ich finde.Es gibt auf eine komplexe Frage wie die Architektur eines Systems keine "So macht man das" Antwort auch wenn uns das durch solche Artikel weiss gemacht werden soll. #zitieren
Gravatar Trepper 04.11.2009
um 19:16 Uhr
@EntwicklerAuf den ersten Blick sieht es tatsächlich so aus als würde sich die Katze in den Schwanz beißen wenn das DAO sich selbst in ein Domain-Objekt injiziert. Aber abgesehen davon dass man das technisch trotzdem tun könnte könnte man das DAO auch in zwei Schnittstellen zerlegen - eine Repository-Schnittstelle und eine Domain-Objekt-Schnittstelle. Man ruft dann z. B. mit customerRepository.findByID(231) ein customer-Objekt aus dem Repository ab und diesem Objekt wird vorher noch eine Implementierung der CustomerDAO-Schnittstelle injiziert.Mir stellt sich allerdings die Frage ob man in einem Domain-Objekt wirklich solche Methoden wie save() haben sollte. Dafür kann man auch automatisches Dirty-Checking (bei Hibernate) verwenden und implizit speichern. @CarstenDie Sache mit dem Context klingt interessant aber wann wird der gespeichert? Prinzipiell müsste der ja während der gesamten Anwendungslaufzeit existieren und erst beim Beenden in die DB geschrieben werden. Das würde aber (je nach Anwendung) extrem viel Hauptspeicher benötigen und wohl auch die Datensicherheit (im Sinne von Verlust) bei Serverfehlern extrem gefährden.Eine Methode wie Order.load(123) müsste eine statische Methode sein weil es im allgemeinen unlogisch wäre wenn ein Order-Objekt ein anderes laden würde. Ich überlege gerade ob man bei statischen Methoden probleme mit dem Tausch von Implementierungen hinter Schnittstellen und Unit-Tests haben könnte. Wenn dem nicht so wäre wäre die Klasse eigentlich der perfekte Ort um Repository-Methoden bereitzustellen.Sehr viel eleganter finde ich in diesem Zusammenhang übrigens Scala wo es überhaupt keine statischen Elemente gibt sondern Objekte direkt definiert werden können.Zitat von Carsten: "In meiner Schaffenszeit ist mir in dem Umfeld noch nie untergekommen dass Datenbanken oder OR-Mapper ausgetauscht worden sind. Ich kenne aber viele Projekte wo DAOs injeziert werden und es nur so wimmelt von Schnittstellen. Alles Quatsch wie ich finde."Sicherlich kann man es übertreiben aber prinzipiell finde ich es schon sinnvoll konkrete Implementierungen hinter Schnittstellen zu verbergen. Allein schon beim Testen ist das hilfreich. Und bei Spring besteht ein wesentlicher Vorzug darin dass man hinter den Schnittstellen alles austauschen kann was einem eine enorme Flexiblität bringt. Allerdings muss man beachten dass Spring ein allgemeines Framework ist dass an viele Fälle angepasst werden muss. Wohingegen du von konkreten Geschäftsanwendungen sprichst die nicht so stark an geänderte Bedingungen angepasst werden müssen.Auch wenn ich ein Freund von Schnittstellen bin gebe ich dir absolut Recht dass in einigen System abstrahiert und entkoppelt wird ohne zu beachten dass das neue Komplexität mit sich bringt. Ich sage nur FactoryFactoryFactory ... http://discuss.joelonsoftware.com/default.asp?joel.3.219431.12 #zitieren
Gravatar Trepper 04.11.2009
um 19:22 Uhr
Kann es sein dass diese dämliche Blog-Software nicht nur Leerzeilen sondern auch Kommas entfernt?! #zitieren
Gravatar Trepper 04.11.2009
um 19:23 Uhr
Es ist so! Ist das schlecht! Und das bei einem Verlag im IT-Umfeld. Einfach nur peinlich. #zitieren
Gravatar d. 05.11.2009
um 08:51 Uhr
Weitere Überlegungen zu statischen Methoden für Repository-Funktionalität in Domain-Klassen: http://www.java-forum.org/allgemeines-ee/90804-repository-domain-klasse.html (kurz: http://tinyurl.com/ylhw4ub) #zitieren
Gravatar Entwickler 05.11.2009
um 10:03 Uhr
@Carsten: Wo gehe ich bitte auf die Punkte "OR-Mapper austauschen" oder "Geschäftsanwendungen" ein? Nicht sehr schlau... #zitieren
Gravatar Carsten 05.11.2009
um 12:07 Uhr
@Trepper: Der Context ist im Prinzip equivalent mit der UnitOfWork oder der Session in Hibernate. Diese muss natuerlich so kurz wie moeglich gehalten werden. Wann diese Synchonisationspunkte gesetzt werden haengt vom Anwendungsdesign ab. Die einfachste Moechlichkeit in einer Web-Anwendung ist die Session statisch an den Thread zu binden (OpenSessionInView). Damit ist es dann auch moeglich Order.Load als Factory Methode zu verwenden. Zum Thema Schnittstellen: Aus meiner Sicht sollte man eine Schnittstelle nur dann verwenden wenn man mindestens zwei unterschiedliche Implementierungen kennt (oder antizipieren kann). Zum Thema Flexibilitaet: Es gibt ganz sicher Anforderungen bei denen ein hohes Mass an Flexibilitaet gewuenscht und gefordert ist. Was ich meine ist dass sehr oft diese Muster eingesetzt werden ohne dass diese Flexibilitaet angefordert wurde. Es wird einfach so gemacht weil es eben "best practice" ist. Zum Thema testen: Es wird auch viel zu viel getestet: ich hatte mal ein Projekt bei dem eine Code-Coverage >60% festgestellt wurde (Aufwand ca. 3 Mann Monate). Leider war das was getestet wurde nicht das was gefordert war (Ergenis der Anwendertests). Jetzt koennte man natuerlich argumentieren dass dann die Anforderungen nicht klar definiert waren aber dass das in den meisten Faellen so ist ist nun auch keine neue Erkenntnis. Schlussendlich war der Druck so gross dass die Entwickler die Wartung ihrer Tests aufgeben haben (dies haette die Aufwaende nochmals potentiert) und letztendlich ist die Anwendung ohne jedwege Tests live gegangen. #zitieren
Gravatar Carsten 05.11.2009
um 12:11 Uhr
@Entwickler: Entschuldigung. War in der Tat doof und ne blanke Unterstellung #zitieren
Gravatar Carsten 05.11.2009
um 13:06 Uhr
@Trepper: Danke fuer den Link (http://discuss.joelonsoftware.com/default.asp?joel.3.219431.12). Stimmt den Spring verleitet den Fokus auf die Infrastruktur zu lenken um eine Flexibilitaet zu erreichen die man nicht braucht. Ausserdem wird konfiguriert und nicht mehr programmiert. Im Sinne des Artikels und dieser "best practice" Fallen koennte ich noch hinzufuegen: wenn man nur einen Hammer kennt sieht jedes Problem aus wie ein Nagel. #zitieren
Gravatar Florian R. 17.11.2009
um 08:36 Uhr
"Stellen Sie sich vor Sie haben ein Objekt Bestellung. Es wäre wünschenswert dass dieses Objekt auch eine Methode zum Abschicken der Bestellung hat. In einer typischen Anwendung übernimmt diese Funktionalität allerdings eher ein Service."..."Wenn man streng objektorientiert die Services und Entitäten betrachtet dann kann man sehr leicht den Eindruck gewinnen dass die Methoden eines Service eher in die Entitäten gehören."Oft genug wurde nun schon argumentiert dass es doch nicht der Objektorientierung entspräche wenn eine "Bestellung" sich nicht selber bestellt. Oder ein Objekt sich nicht selbst speichert. Aber was ist denn Objektorientierung? Alan Kay sagt: "Encapsulation polymorphism & inheritance" (http://c2.com/cgi/wiki?HeInventedTheTerm). OOA/D wird oft angewendet um ein Modell das der wirklichen Welt möglichst nahe kommt zu erstellen. Deswegen kommt man auf Domänenobjekte wie "Bestellung" oder "Kunde". Weil sie eben Dingen der wirklichen Welt entsprechen.Wie bestellt sich aber eine Bestellung in der wirklichen Welt? Sie bestellt sich nicht selbst! Ein irgendwie gearteter "Sachbearbeiter" bearbeitet eine Bestellung. Bei OOA/D geht es um die Aufteilung von Verantwortlichkeiten. Und die Verantwortung für die Bearbeitung einer Bestellung liegt beim "Information Expert" (siehe "GRASP"-Pattern) der alles Wissen hat um die Bearbeitung durchzuführen. Und wieso sollte die Bestellung irgendeine Ahnung von einer Datenbank einer Message Queue und weiteren Dingen haben die zur Durchführung notwendig sind? Folgt man dem elementaren Prinzip "Separation of Concerns" so sollte man das Erstellen einer Bestellung und deren Bearbeitung also auftrennen.Hier spiegelt sich die grundlegende Trennung zwischen Actio und Res (siehe "Enzyklopädie Philosophie und Wissenschaftstheorie Jürgen Mittelstrass") also Konzepten die etwas "tun" und Konzepten die etwas "sind" wieder. Ein Sachbearbeiter "tut" etwas eine Bestellung "ist" etwas. Letzteres wurde in J2EE durch die Trennung zwischen Entity- und Session-Bean meines Erachtens nach erkannt (dass es schlecht umgesetzt wurde hat damit nichts zu tun). Dieses "Sachbearbeiter"-Konzept wird im Artikel als "Service" bezeichnet was ich für sehr gefährlich halte da jeder unter diesem Wort etwas anderes versteht und es inzwischen emotional sehr aufgeladen ist.Meiner Meinung nach führt die Beachtung dieses Prinzips aber nicht zu dem von Fowler benannten "Anemic Domain Model". Natürlich sollen Objekte keine reinen Datenhalter sein "encapsulation" ist ja eines der Prinzipien der OO. Doch das Domänenmodell sollte sich auf die Business-Logik konzentrieren und technische Aspekte -- wie eine Datenbankanbindung -- die unweigerlich zur Bearbeitung einer Bestellung notwendig sind außen vor lassen.Diese Argumentation folgt auch dem Ansatz von Domain-Driven Design wo Eric Evans als Einleitung zum Punkt "Services" schreibt: "Sometimes it just isn't a thing. In some cases the clearest and most pragmatic design includes operations that do not conceptually belong to any object. Rather than force the issue we can follow the natural contours of the problem space and include Services explicitly in the model." Die Lösung ist also die "Sachbearbeiter" als Objekte in mein Domänenmodell aufzunehmen.Ich habe die vorgestellt Lösung der Konfiguration von Domänen-Objekten mit @Configurable auch schon eingesetzt. Der nicht-invasive Ansatz von Spring wird hier aufgegeben da man sich eng an Spring bindet. Bei anschließenden Code-Refactorings und weitergehenden Architekturüberlegungen wurde jedes Mal das System so umgebaut dass die Injizierung in Domänen-Objekte nicht mehr notwendig war. Ich würde inzwischen soweit gehen zu sagen dass der Einsatz von @Configurable einen jener "architectural smells" darstellt die dazu veranlassen sollten das Design zu überdenken.Injizierung von statischen Abhängigkeiten in Domänenobjekte rechtfertigt sich also meines Erachtens nach nur im (äußerst) gut begründeten Ausnahmefall. #zitieren
Gravatar lg 29.12.2009
um 16:36 Uhr
Warum macht man nicht einfach "static private OrderDao orderDao;", denn dann brauche ich auch nicht "@Configurable" . Ich habe dann immer eine Instanz, die initialisiert ist.????!?!?!?!? #zitieren
Gravatar RPR 30.12.2009
um 01:10 Uhr
@lg: Weil du deine Klassen dann nicht mehr einfach "mocken" kannst - z.B. mit einem OrderDaoMock.

@Florian R.: Danke für den guten Beitrag!!
#zitieren

Anzeige

zurück zum Seitenanfang