Montag, 30. September 2013

Das Data Transfer Object – Be or not to be.

Praktisches Beispiel 2.2.1

Artikelübersicht
1. Teil Das Data Transfer Object – Be or not to be.
2. Teil Java Beans und Use Cases.
3. Teil Data Transfer Objects - ein Antipattern?


In dieser kleinen Reihe möchte ich den Einsatz des Data Transfer Objects diskutieren. Einer meiner Absichten ist es, mit Hilfe der Community, einige Unklarheiten in meinem eigenen Kopf zu beseitigen. Nachdem ich mein eigenes Wissen über dieses Entwurfsmuster aufbereitet habe, möchte ich auf einige Einsatzbeispiele kommen und anhand dieser diskutieren, ob man dieses Entwurfsmuster so anwendet oder nicht.

Fowler [FOWL03] benutzt ein Data Transfer Object (DTO) für die Übertragung von Daten zwischen Prozessen. Im Prozess werden die Daten des DTO mit Hilfe eines Assemblers auf die Objektstruktur im Prozess gemappt. Das DTO selbst besteht aus einer Menge von Attributen, welche Setter und Getter besitzen, also eine einfache JavaBean.

Mit Hilfe einer Fassade wird der Zugriff auf einen Prozess bzw. auf eine Komponente konzentriert. Dazu ist es hilfreich die Artikelreihe zum Architekturentwurf zu lesen. Die angebotenen Methoden der Fassade stellen den Vertrag mit der Außenwelt dar. Die Methoden verwenden DTOs. Damit wird die Anzahl der Methodenaufrufe verringert.

"Verwenden Sie ein Data Transfer Object, wenn Sie in einem einzigen Methodenaufruf mehrere Datenelemente zwischen zwei Prozessen übertragen müssen." [S.448, FOWL03] Ein weiteres Argument für den Einsatz von DTOs bei Fowler ist, "wenn sie per XML zwischen Komponenten kommunizieren wollen." [S.449, FOWL03] Dabei kapselt er die Aufgabe der Manipulation eines XML-DOM im DTO. Außerdem kann ein DTO "als gemeinsame Datenquelle für verschiedene Komponenten in verschiedenen Schichten verwendet" [S.449, FOWL03] werden. "Jede Komponente fügt einige Änderungen in das Data Transfer Object ein und übergibt es dann an die nächste Schicht."

Im weiteren Artikel wollen wir zwei der drei genannten Hauptgründe für das Verwenden von Data Transfer Objects im Auge behalten, die Vereinfachung des Methodenaufrufes und das Datenobjekt, welches durch alle Schichten manipuliert werden kann. Dazu gehört auch die Aufgabe des Assemblers innerhalb der Komponente, der DTOs auf die entsprechende Objektstruktur der Komponente abbildet.


Die Fassade liefert an die aufrufende Komponente ein DTO zurück. "Die zurückgelieferten Daten sollten in sich abgeschlossen sein, sodass eine darüberliegende Schicht mit diesen Informationen arbeiten kann." [S.144, FILD12] Damit weist Ulf Fildebrandt darauf hin, dass Komponenten zu genau einer Schicht gehören sollten. Der Zugriff darf in einer Schichtenarchitektur (Layers) immer nur von der oberen auf die untere Schicht erfolgen. Dazu wird "ein Konstrukt definiert …, das auch als Data Transfer Objekt in der Softwareentwicklung bekannt ist." [S.145, FILD12] Ein Data Transfer Object wird also als Rückgabewert für eine Schicht definiert.

In den folgenden Artikeln möchte ich zum einen ein Beispiel definieren, in dem die Businessobjekte in JavaBeans und UseCase-Objekte getrennt wurden. Dabei wurden die Daten im JavaBean konzentriert. Das Verhalten wurde in sogenannten UseCases separiert. Die JavaBeans konnten über alle Schichten verwendet werden. Dieses Beispiel möchte ich im nächsten Post dieser Reihe diskutieren. Der Grund ist, dass ich diese Anwendung selbst erlebt habe und die Vor- und Nachteile gerne in einem größeren Erfahrungshorizont betrachten möchte, nämlich mit Hilfe Eures Feedbacks.

In einem weiteren Teil möchte ich die Anwendung des Data Transfer Objects im Allgemeinen diskutieren. Die Verfechter der reinen Objektorientierung betrachten dieses Pattern inzwischen als Antipattern. Es erinnert in vielem wirklich an einen Rückfall in prozedurale Programmierung. Auf der einen Seite nur noch dumme Daten, auf der anderen das Verhalten. Auch hier würde ich mich über ein breites Feedback freuen. Besonders würde ich mich über Code-Beispiele freuen, die Daten zwischen Komponenten austauschen.

Zum Abschluss des heutigen Posts ein Code-Beispiel zur Verdeutlichung des Geschriebenen.

Die View bildet ein Pseudodisplay:

 

package pseudoview;

public class PseudoDisplay {
  private TableItems tableItems = new DefaultTableItems(); 
  public void show() {
    List customerItemDTOs = tableItems.getTableItems(); 
    for (CustomerItemDTO customerItemDTO : customerItemDTOs) {
      System.out.println(customerItemDTO.getName() + " hat " 
            + customerItemDTO.getAmount() + " €.");
    }
  }
}



Innerhalb dieses Pseudodisplay wird eine DTO verwendet. Diese gibt den Namen und das Gesamtvermögen des Kunden zurück.

 

package dto;

public class CustomerItemDTO {
 private final String name;
 private final int amount;

 public CustomerItemDTO(String firstname, String lastname,
                               int amount) {
  name = firstname + " " + lastname;
  this.amount = amount;
 }

 public String getName() {
  return name;
 }

 public int getAmount() {
  return amount;
 }
}



Der Zugriff der View auf die Businesskomponente erfolgt über eine Fassade.

 

package business.facade;

public interface TableItems {
 List getTableItems();
}

public class DefaultTableItems implements TableItems {
 private Assembler assembler = new Assembler();

 @Override
 public List getTableItems() {
  return assembler.getTableItems();
 }

}



Die Fassade greift auf den Assembler zu. Dieser wandelt die Businessobjekte in die DTOs. Im folgenden Code holen wir diese aus einer simulierten Datenbank. In der Geschäftslogik befinden sich außerdem die Customer- und die Account-Objekte. Für diese ist ein Verhalten angedeutet.

 

package business;

public class Assembler {

 public List getTableItems() {
    List customerItemDTOs = new ArrayList();
    List customers = new DBTest().select();
    for(Customer customer : customers) {
       customerItemDTOs.add(new CustomerItemDTO(
                                     customer.getFirstname(), 
                       customer.getLastname(), 
                                     customer.getAsset()));
    }
    return customerItemDTOs;
 }
}

/** Bildet den Kunden ab */
public class Customer {
 /** Vorname des Kunden */
 private final String firstname;
 /** Nachname des Kunden */
 private final String lastname;
 /** Konten des Kunden */
 private List accounts = new ArrayList();

 public Customer(String firstname, String lastname) {
  this.firstname = firstname;
  this.lastname = lastname;
 }

 public String getFirstname() {
  return firstname;
 }

 public String getLastname() {
  return lastname;
 }

 public void setAccount(Account account) {
  accounts.add(account);
 }

 /** Gibt das Vermögen des Kunden zurück */
 public int getAsset() {
  int asset = 0;
  for (Account account : accounts) {
   asset += account.getAccountBalance();
  }
  return asset;
 }
}

/** Bildet ein Konto ab */
public class Account {
 private int accountBalance;
 private List bookings = new ArrayList();
 
 public int getAccountBalance() {
  return accountBalance;
 }
 
 public Integer getBooking(int index) {
  if (bookings.size() > index && index >= 0) {
   return bookings.get(index);
  }
  throw new IllegalArgumentException("...");
 }
 
 public void setBooking(Integer booking) {
  if ((accountBalance + booking) >= 0 ) {
   accountBalance += booking;
   bookings.add(booking);
  } else {
   throw new IllegalArgumentException("...");
  }
 }
 
}




  • [FILD12] Martin Fowler : "Patterns für Enterprise-Application-Architekturen", 1. Auflage, mitp-Verlag, Bonn, 2003
  • [FOWL03] Ulf Fieldebrandt: "Software modular bauen", 1. Auflage, dpunkt.verlag GmbH, Heidelberg, 2012


folgender Post dieses Themas


Jörg Rückert schrieb in der Xing-Gruppe "Clean Code Developer" folgenden Kommentar zu meinem Artikel. Ich möchte mich hier ausdrücklich für die Erlaubnis bedanken, ihn hier zu veröffentlichen. Außerdem verweise ich auf seinen Post zum Thema: Tell don't ask.

Hallo Ralf,

wie Du schön in Deinem Post dargestellt hast, "kleben" DTOs an der Facade, d.h.: in den Methoden der Facade werden DTOs als Parameter oder Rückgabewerte genutzt, häufig in Form von Collections von DTOs. Warum nutzt man diese Schnittstellenobjekte?

Im Prinzip haben die DTOs eine ähnliche Aufgabe wie die Facade. Eine Facade bietet eine reduzierte Schnittstelle auf ein komplexes System an. Hintergrund dieser grobkörnigen Schnittstelle ist es, die Anzahl der Aufrufe gegen das komplexe System zu minimieren. DTOs unterstützen die Facade durch das Bündeln strukturierter Daten. Im Messaging-Umfeld würde das einer Composite Message entsprechen.

DTOs sind mit Entwicklungsaufwand für die Datentransformation über Assembler verbunden. In der Regel werden dabei Entity Beans oder SQL Resultsets in DTOs transformiert. Heute verwendet man eher direkt die serialisierbaren Entitäten als DTOs ohne dabei einen Assembler schreiben zu müssen.

In ihrer Reinform (Klassen mit Attributen und Getter/Setter) verstoßen DTOs gegen Tell don’t ask. Deshalb werden blutarme DTOs ohne Geschäftslogik kritisiert. Auf der anderen Seite hat man DTOs lange Zeit aber genauso gelehrt. DTOs und die Assembler waren deshalb gern gesehene Patterns. Heute denkt man weiter und propagiert „Rich Domain Modells“, die dem Anspruch von Tell don’t ask gerecht werden.

Übrigens können DTOs durchaus mit Geschäftslogik ausgestattet werden. Dem steht nichts entgegen. DTOs sind serialisierbare POJOs und können genauso behandelt werden. DTOs leben schichtenübergreifend und sind deshalb Kandidaten für "verteilte" (bitte mit Maß) Geschäftslogik.

Grüße Jörg


Print Friendly Version of this page Print Get a PDF version of this webpage PDF

Kommentare:

  1. DTOs sind gut an der Prozessgrenze Einsetzbar. z.B. verwenden wir in einem C# Programm einen REST-basierten Webservice der auf einem Java-Server läuft. Für Parameter- und Return-Typen verwenden wir in C# DTOs, die aus den serverseitigen Java-Annotations generiert wurden. Die generierten Klassen sind reine Datencontainer mit Properties (wie JavaBeans) die für die Serialisierung der Daten verwendet werden.

    Die Serialisierung ist jedoch nicht in den DTOs eingebaut, sondern wird über einen Serializer gemacht. Das hat den Hintergrund, dass wir sowohl XML als auch JSON als Übertragungsformat verwenden und der Serialisierungs-Concern damit besser ausserhalb der DTOs eingebaut ist.

    Die generierten Klassen enthalten keine Logik, und es kommt zu dem beschriebenen prozeduralen Design (anemic domain model). Es gibt dann Stellen wo man sich aber doch Logik für die DTOs wünscht. Statt configurator.ConfigureForAudioConference(reservation) wünscht man sich reservation.ConfigureForAudioConference(). Hier helfen Mechanismen wie partielle Klassen und Extension Methods um die generierten Klassen zu erweitern. Falls diese Mechanismen fehlen kann man Vererbung oder den Decorator verwenden.

    DTOs sind wie beschrieben auch an Fassaden-Interfaces sinnvoll, um den Clients der Komponente eine möglichst einfache Schnittstelle zu bieten (DTOs sind leicht zu erzeugen) und eine lose Kopplung zu bieten (DTOs haben keine komplizierten Dependencies, nur weitere DTOs). Innerhalb der Komponennte weiss ich aber nicht ob sie viel Sinn machen.

    Weiterhin sollte man zwischen Data Transfer Object und Value Object unterscheiden. VO hat durchaus Logik und Verhalten, ist aber immutable. DTO kann immutable sein oder nicht.

    AntwortenLöschen
  2. DTOs für bestimmte Anwendungsfälle ja oder nein? Kompliziert.

    Es gibt wenige Anwendungsfälle bei denen man das DTO wirklich braucht, beispielsweise Hibernate (CGLIB bean proxys) POJOs über Systemgrenzen (zb. Remoting).

    Aber: DTOs instruieren Mapping, somit wird der Sourcecode zur Bloatware. Ellenlange dto.setPrimaryContactAdress(customer.getPrimaryContactAdress),... Statements und Mapping-Klassen. Mit AOP, Reflection und Metaprogrammierung usw. kann man gegensteuern (wie GORM bei Groovy/Grails), aber auch das birgt seine Gefahren! Wer versteht schon alle Implikationen von AOP Interceptoren die Hierarchien kopieren? Sobald es komplexer wird als der Pet-Shop erlebt man wahrhaft einen Alptraum (AOP Debugging, Komplexe Strukturen und Reflection...).

    Wenn wir uns aktuelle Systeme mit DMZ, Frontend, Backend und weiteren Services, Hibernate DB-Backends und fachlichen Domänenobjekten ansehen, stellen wir schnell fest, dass ~80% unseres Sourcecodes reines Mapping ist und zwar von DB-Objekt(Hibernate) zu Fachobjekt zu DTO zu POJO zu XML-Repräsentationen als DTO.

    DTOs bringen nicht zwangsweise eine lose Kopplung, höchstens über zusätzliche Pattern die eine hohe systematische Komplexität instruieren. Oftmals gibt es Parameterobjekte die in Modulen hinter der Schnittstelle liegen, aber von Client und Schnittstelle genutzt werden. Wenn nun selbst die Parametrisierung von verteilten Modulen mit DTOs passiert erhöhen wir wieder den Mapping-Aufwand.

    Schau mal wie viele Klassen und wie viel Sourcecode es braucht um einen simplen Sachverhalt (siehe Beitrag) abzubilden.

    Mein Fazit ist: Überall wo es sich vermeiden lässt sollte man von DTOs absehen und einfache POJOs direkt durchs System reichen. Abstraktionen hin zu Systemgrenzen schaffen heutzutage viele Technologien (JSON Mapper, Google Protocol Buffer, ...) und das direkt aus POJOs ohne DTOs. Auch hier lässt sich data-hiding betreiben, ausnahmen lassen sich registrieren. Wir arbeiten doch eh mit anämischen Domänenmodellen, das erkaufen wir uns schon durch ORMs. Wenn Technologien den Zwang eröffnen DTOs zu nutzen, sollte man sich überlegen ob man auf das richtige Pferd gesetzt hat. Oftmals stellt sich die Frage leider nicht und man muss sich den Gegebenheiten beugen.

    @rfilipov: Wie soll ein VO Verhalten haben und immutable sein. Verhalten impliziert Zustände. Ich schätze du meinst Methoden? Mische bitte nicht DDD (Eric Evans) und anämische Domänenmodelle. Ein konzeptmix verheiratet die Nachteile beider Welten (wie auch SUVs).

    AntwortenLöschen
  3. Ja, ich meinte Methoden mit Business-Logik, nicht getter/setter. Mit immutable meinte ich z.B sowas:

    class Point2D
    {
    double distance() { ... }

    Point2D rotate(double angle) { ... }
    }

    Also objekte mit Value-Semantik, wo die Objektidentität keine Rolle spielt, sondern nur die Gleichheit ausreicht. Dann kann man Zustandsänderungen simulieren indem man neue Instanzen zurückgibt. Aber was meinst Du mit Domain-Driven Design vs anemisches Domänenmodell, wo ist hier der Konzeptmix?

    AntwortenLöschen
  4. Domain driven design:
    http://books.google.de/books/about/Domain_Driven_Design.html?id=hHBf4YxMnWMC&redir_esc=y

    Im Buch werden beide Typen recht gut beschrieben. Änamische Domänenmodelle beinhalten "Services" (zb. Spring/EJB3 Beans) und POJOs. Domänengetriebene Modelle enthalten intelligente Objekte, sprich ein Warenkorb besitzt seine Kalkulationslogik für Rabatte, nicht ein DiscountService.

    Wenn man beides Mischt wird man schnell feststellen, dass man beispielsweise mit Hibernate nicht weiterkommt da es per Reflection & Proxies in die Objekte eingreift und Schlüsselworte verbietet, öffentliche Konstruktoren erzwingt und eben domänengetriebene Modelle zu nichte macht. Es wurde für das andere Designkonzept entworfen, quasi für prozedurale Programmierung mit (Datenträger und Serivce-)Objekten.

    Immutability in dem Sinne (ohne STM etc.) lässt dir bei großen Applikationen schnell den Speicher voll laufen oder es erzwingt große GC-Zyklen die bei Java leider nicht selbst kontrolliert werden können (zumindest nicht zuverlässig). Gutes Design ist es sicher so viel wie möglich immutable zu halten. Clojure hat interessante Konzepte um so etwas möglichst schmerzfrei auf der JVM darzustellen.

    AntwortenLöschen
  5. Das Buch kenne ich, habe aber etwas anderes unter Konzeptmix verstanden. Jedoch sind Services laut Evans ein Objektstereotyp, der durchaus auch in reichhaltigen Domain Models vorkommt (also wie Entities sind bei ihm Services auch ein Bestandteil des Domain-Models). Ein Service kommt zum Einsatz, wenn z.B. die Logik mehrere Entities umschliesst und deren Domain-Logik nutzt, also etwas was nicht sinnvoll in einem einzelnen Entity gekapselt ist (z.B. CheckoutService verknüpft den Warenkorb mit dem Kunden und die Bezahlmethode und den Rechnungsgenerator). Beim anemischen Model sind die Entities stupide und die gesamte Logik wandert in den Services, darüber denke ich sind wir uns einig.

    Immutability muss andererseits nicht bedeuten dass der Spercher voll läuft. Das ist unabhängig davon und passiert nur wenn man Objekte länger im Speicher hält wie man sie braucht. String ist beispielsweise immutable, aber dadurch heisst es nicht automatisch dass wir Speicherlecks haben. Und große GC-Zyklen werden eher von langlebigen Objekten erzwungen, da sie langlebige Referenzen halten die auch überprüft werden müssen. Kurzlebige Objekte landen hingegen in der Young Generation und werden i.d.R. schnell frei gegeben.

    Hibernate sett

    AntwortenLöschen
  6. JAXB-Context, Data Mapping / Binding usw. lässt in der Praxis den Speicher schnell alt aussehen. Insbesondere die guten DTOs unter Last.

    GC Zyklen laufen in entsprechend großen Applikationen die viel Speicher reservieren, das ist in der Praxis harte Realität. Und der GC lauf ist nicht beeinflussbar (vgl. C / C++). Lediglich über JVM Parameter können andere Strategien genutzt werden, blockieren tun sie alle.

    Unter Konzeptmix verstehe ich Spring depdendency injection von DDD Objektstereotypen. Das ist selbst mit aspectj nicht nur unperformant, sondern auch hoch komplex und kaum wartbar.

    AntwortenLöschen