Montag, 9. Dezember 2013

Data Transfer Objects – ein Antipattern?

Praktisches Beipiel 2.2.3

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 einer Anwendung treffen wir oft eine Trennung zwischen der Geschäftslogik und der Präsentation an. Die Präsentation greift auf die Geschäftslogik über definierte Schnittstellen zu. Diese definierte Schnittstelle können wir als Fassade implementieren. In der Fassade stellt die Businessschicht Zugriffsmethoden auf die Geschäftslogik bereit.

Zwischen diesen Schichten können wir nun ein Data Transfer Objekt einsetzen. In der Businessschicht gibt es eine Reihe von Businessobjekten, die mit Hilfe eines Assemblers in Data Transfer Objects umgebaut und dann als Rückgabewert der Fassadenmethoden an die Präsentation gereicht werden.

Diese anzutreffende Lösung kann in einer Architektur viel Verwirrung stiften. Hauptsächlich dient das Data Transfer Object jedoch dem optimierten Datenverkehr zwischen verschiedenen Tiers in einer verteilten Rechnerarchitektur. Die Daten werden in Data Transfer Objects komprimiert und dann durch das Netz geschickt. Dabei wird der Datenverkehr auf ein Minimum reduziert. Innerhalb einer Layerarchitektur existiert dieses Problem jedoch nicht. Die Lösung erzeugt jedoch ebenfalls Probleme und es ist abzuwägen, ob sie kleiner sind als der Gewinn.

Mit den Data Transfer Objects und den Businessobjekten bauen wir eine Doppelstruktur der Daten auf. Diese Objekte sind miteinander verbunden und sie müssen hin und her gemappt werden. Dazu muss Assemblercode geschrieben werden. Diese offensichtlichen Nachteile werden in verteilten Anwendungen anscheinend in Kauf genommen, da der Datentransport sonst zu langsam wäre. Data Transfer Objects sind also sinnvoll in verteilten Anwendungen.

Welche Vorteile bieten die DTOs jedoch innerhalb einer Layerstruktur. Innerhalb der Businessschicht wird das Domänenmodell abgebildet. Die Präsentation könnte nun verschiedene Sichten auf dieses Domänenmodell abbilden. Diese Sichten werden in DTOs zusammengefasst und an die Präsentation geschickt. Ist das jedoch die Aufgabe der Businessschicht? Ich denke, es ist eine Darstellungsaufgabe und somit Teil der Präsentationsschicht. Wir müssen also innerhalb der Präsentation Filter auf das Domänenmodell bauen. Diese Filter liefern die eingeschränkte oder umgebaute Sicht auf die Daten.

Wozu könnten wir nun noch DTOs in unserer Layerstruktur benötigen? Wenn wir statt der DTOs Businessobjekte an die Präsentation liefern, dann können wir innerhalb der Präsentation auf Businessmethoden zugreifen. In diesem Fall würden wir nur das Verhalten aus dem Objekt entfernen und es bei der Rückkehr in die Businessschicht im Assembler wieder hinzufügen. Ist das genug Grund um eine Doppelstruktur und das Mappen der Daten zu rechtfertigen?

Man könnte im Team natürlich auch eine Regel aufstellen, die besagt, es dürfen keine Businessmethoden in der Präsentationsschicht aufgerufen werden. Diese Regel könnte nicht nur verabschiedet werden, sie könnte natürlich auch am Code geprüft werden.

Im Allgemeinen programmieren wir jedoch gegen ein Interface. Jedes Businessobjekt hätte dann ein solches Interface und innerhalb des Interfaces müssten nur die Methoden erscheinen, die wir innerhalb der Präsentationsschicht verwenden dürften. In der festgelegten Fassade, also den definierten Zugriffsmethoden auf die Businessschicht, wären als Rückgabetypen genau diese Interfaces eingetragen.

Auch künftige Änderungen an den Businessobjekten sind kein Grund für DTOs. Durch den Einsatz von Interfaces in der Fassade als Rückgabewert ist diese Problem gelöst. Aufwändige Änderungen würden beim Einsatz von DTOs sogar erheblich mehr Änderungsaufwand an dem Businessobjekt erfordern, am DTO und am Assember. Wir hätten also einen vielfachen Aufwand.

In einer Architektur mit Layern gibt es also fast keinen Grund Data Transfer Objects zu verwenden. Wir würden in die prozedurale Programmierung zurückfallen und Daten und Verhalten trennen und außerdem eine Doppelstruktur der Daten anlegen, die wir auch noch mappen müssten.

Im Folgenden möchte ich den Code des Beispiels dieser Reihe auch noch für eine DTO-freie Architektur zeigen:

Die Präsentationsschicht bildet ein Pseudodisplay. Dort wurden die DTOs durch die Interfaces aus der Fassade ersetzt.

 

package pseudoview;

public class PseudoDisplay {
  private TableItems tableItems = new DefaultTableItems();
 
  public void show() {
    List<CustomerItem> customerItems = tableItems.getTableItems(); 
    for (CustomerItem customerItem : customerItems) {
      System.out.println(customerItem.getFirstname() + 
            " " + customerItem.getLastname() + 
            " hat " + customerItem.getAsset() + " €.");
    }  
  }

  public static void main(String[] args) {
    new PseudoDisplay().show();
  }
}



Der Zugriff der Präsentationsschicht auf die Businessschicht erfolgt über eine Fassade. Dort ist auch das Interface für den Kunden vereinbart, welches die eingeschränkte Funktionalität für die Präsentationsschicht bietet.

 

package business.facade;

public interface TableItems {
  List<CustomerItem> getTableItems();
}

public class DefaultTableItems implements TableItems {
  private BusinessObject bo = new BusinessObject();

  @Override
  public List<CustomerItem> getTableItems() {
    return bo.getTableItems();
  }
}

public interface CustomerItem {
  String getFirstname(); 
  String getLastname();
  int getAsset();
}



Wir greifen also direkt auf das BussinesObject zu und holen uns die Daten z.B. aus einer Datenbank.

 

package business;

public class BusinessObject { 
  public List<CustomerItem> getTableItems() {
    List<CustomerItem> customerItems = new ArrayList<CustomerItem>();
    List<Customer> customers = new DBTest().select();
    for (Customer customer : customers) {
      customerItems.add((CustomerItem)customer);
    }
    return customerItems;
  }
}

public class Customer implements CustomerItem {
  private String firstname;
  private String lastname;
  private List<Account> accounts = new ArrayList<Account>();

  public String getFirstname() {
    return firstname;
  }

  public void setFirstname(String firstname) {
    this.firstname = firstname;
  }

  public String getLastname() {
    return lastname;
  }

  public void setLastname(String lastname) {
    this.lastname = lastname;
  }
 
  // Wegen des Kommentars geändert
  public void addAccount(Account account) {
    accounts.add((Account)account);
  }

  public int getAsset() {
    int asset = 0;
    for (Account account : accounts) {
      asset += account.getAccountBalance();
    }
    return asset;
  }
}

public class Account {
  private int accountBalance;
  private List<Integer> bookings = new ArrayList<Integer>();

  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("...");
    }
  }
}



Folgende Hinweise aus den Kommentaren, die ich in der Xing-Gruppe Clean Code Developer erhielt möchte ich Ihnen nicht vorenthalten: Bei den Kommentatoren möchte ich mich ganz herzlich bedanken.

vorheriger Post dieses Themas


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

1 Kommentar:

  1. Wenn man das Beispiel mal weiter denkt, dann kommt man um DTOs nicht herum.

    Customer ist ein Entity und wird hinter CustomerItem versteckt. Die Accounts wurden bereits mitgegeben. Lade ich nun die Namen in ein Formular, wo der Nutzer seinen Namen nochmal korrigieren kann, was passiert dann? Das Interface muss auch noch die setter offen legen. Oder biete ich dann eine Methode changeName(String firstname, String lastname) an?
    Das mag noch gehen, aber bei großen Formularen will ich keine Methode die 5 Strings und 3 Ints als Parameter braucht, anbieten. Und alle properties der Klasse per Interface offen legen lässt das Interface wieder sinnlos erscheinen.

    Und wie sieht in einer Welt ohne DTOs die Behandlung von konkurrierenden Zugriffen aus? Halten alle Klassen ein und das selbe Customer-Objekt mit id 1 in der Hand oder gibt es mehrere Instanzen davon? Z. B. wenn 2 Anwender ein und den selben Datensatz bearbeiten (z. B. Wiki-Eintrag).

    Was passiert dann eigentlich beim Speichern neuer Daten? Holt sich die View von einer CustomerFactory ein neues Objekt, um dies per setter zu befüllen und ruft dann direkt save am Customer-Entity auf?

    Und was mache ich beim debugging wenn ich nur CustomerItem in der Hand habe? Bei einem CustomerDto sehe ich zumindest ob schon nach dem Dto mapping falsche Daten im Objekt stehen. D. h. ich kann schnell abschätzen ob der gesuchte Fehler in der Präsentations- oder der Persistenzschicht liegt.

    Klar nervt es dto mapper zu schreiben inkl. Unit Test.

    Aber ich sehe trotzdem oft keine andere Möglichkeit als ein Dto zu nehmen, wenn
    - Daten aus verschiedenen Entities zusammengefasst werden sollen
    - nur Teil-Daten aus Entities benötigt werden
    - die Präsentationsschicht generell nicht wissen soll wie die Daten in der DB abgelegt / strukturiert sind
    - die Serialisierung von komplexen Entities nicht möglich ist
    - Methoden viele Parameter benötigen
    - ich beim debugging sehen will welche Daten von einer Schicht zur anderen wandern
    - bei externen Schnittstellenaufrufen wie z. B. REST, Soap Webservice etc.
    - ich einen einfachen Unit Test schreiben will, wo eine Methode einfach nur ein Dto bekommt und ich leicht den Rückgabewert prüfen kann
    - generell Unit Tests ohne DB Anbindung schreiben will um Geschäftslogik zu testen (mit den komplexen Objekten habe ich aber viele Abhängigkeiten und das Mocken wird elende aufwändig)

    Ich habe die Erfahrung gemacht dass solche Entities dann leicht immer größer und gigantischer werden, weil jeder glaubt gemäß tell, don't ask noch mehr Methoden dran basteln zu müssen.
    Das führte dazu, dass ich in Entities Code zur XSL Transformation fand.

    Ich finde es daher besser eine Ausnahme zu machen und die Entities in der Persistenzschicht zu belassen. Ein Refactoring ist dann auch einfacher möglich, da die Fassade das ganze abkapselt. In der Welt ohne Dtos haben leider Refactorings dann Auswirkungen auf alle Schichten. Es muss so viel geändert werden, dass jeder vor dem Aufwand zurück schreckt.

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

    => eine Methode die set heißt aber add macht? Das ist ganz schön fies.

    AntwortenLöschen