Blog

Sneaky Leaky Abstraktionen

Lars Vonk

Aktualisiert Oktober 23, 2025
4 Minuten
Einige undichte Abstraktionen sind heimtückisch, sie sind nicht sofort sichtbar. Bei meinem derzeitigen Auftrag denken wir darüber nach, einige gemeinsam genutzte Dienste so umzugestalten, dass nur noch ein einziger Dienst pro Rechner existiert. Eine Möglichkeit, dies zu erreichen, besteht darin, die Dienste mithilfe von RMI "fernzusteuern". Da der Dienst bereits eine Schnittstelle ist und in einem IOC-Container konfiguriert wurde, sollte man meinen, dass er bereit ist, die zugrunde liegende Implementierung zu ändern: von Co-Location zu Remoting. Ich bin jedoch auf einige undichte Abstraktionen gestoßen, die das Refactoring zeitaufwändiger machten, als ich erwartet hatte. Beginnen wir zunächst mit einer vereinfachten Version der Service-Schnittstelle. Derzeit ruft der Dienst einen SOAP-Dienst über Axis.

// Dienst
interface MyService {
  MyResult findSomething()
}
//zurückgegebene Klasse
public class MyResult {
  //enum type
  private MyDomainType Typ;
}
//die Aufzählung
enum MyDomainType {
  static MyDomainType convert(GenerateAxisClass other) {...}
}
1. Statische Fabrikmethoden auf der Domänenklasse Für unsere Domäne konvertieren wir zwischen generierten Axis-Klassen und unserer eigenen Domäne, sozusagen eine Art Antikorruptionsschicht. Für die Konvertierung können Sie zwischen verschiedenen Mustern wählen. Das Muster, das ich am häufigsten verwende und sehe, sind separate Fabrikklassen (oder Konverter) und statische Fabrikmethoden auf der Klasse selbst. Die statische Fabrikmethode ist oben dargestellt, eine separate Fabrik könnte etwa so aussehen:

public class MyDomainTypeFactory {
  public MyDomainType convertFrom(OtherDomain other) {...}
}
Obwohl beide Implementierungen in Ordnung zu sein scheinen, macht der Ansatz der statischen Methode Ihren Client von der zugrunde liegenden Implementierung, in diesem Fall Axis, abhängig. Da MyDomainType schließlich vom Dienst zurückgegeben wird, haben Sie Ihrem Client die zugrundeliegende Implementierung offengelegt. Der Client benötigt nun auch die von Axis generierten Klassen und ist daher in gewisser Weise von Axis abhängig. 2. Keine Verwendung von Serializable Vielleicht ist dies ein kleines Problem, aber die Tatsache, dass unsere Domänen-/Dto-Klassen nicht Serializable implementieren, schließt die Möglichkeit aus, etwas wie rmi remoting zu verwenden. Wir müssen zunächst alle dto/Domain-Klassen so ändern, dass sie Serializable implementieren.Wenn Sie also bereits jetzt wissen, dass Ihre Domain-Objekte von einem Service zurückgegeben werden und daher eine Art dto sind, ist es eine gute Praxis, sie bereits Serializable implementieren zu lassen. Es ist nicht viel Arbeit, die Klassen Serializable implementieren zu lassen, und spart Ihnen eine Menge Zeit bei der Aktualisierung aller Client-Module, wenn Sie sich für Remoting entschieden haben. 3. Verschachtelte und ungeprüfte Ausnahmen Im Internet gibt es eine Menge Diskussionen über geprüfte und ungeprüfte Ausnahmen. Ich verwende aus verschiedenen Gründen gerne ungeprüfte Ausnahmen. Aber die Verwendung von ungeprüften und auch verschachtelten Ausnahmen bringt eine große Verantwortung mit sich; insbesondere bei der Implementierung einer Dienstschicht. Lassen Sie mich dies anhand eines Beispiels erläutern:

Klasse HibernateDao {
  void persist(Person transient) throws HibernateException{}
}
interface PersonCreateService {
  Person create(....);
}
class PersonCreateServiceImpl implements PersonCreateService{
  Person erstellen(....) {
 
//..
  hibernateDao.persist(..);
  }
}
Die PersonCreateServiceImpl ist nicht verpflichtet, die HibernateException abzufangen. Wenn also in Hibernate etwas schief läuft, wird die HibernateException bis zum Client weitergegeben. Da der Client in der Regel die Implementierungsdetails eines Dienstes nicht kennt und möglicherweise keine eigene Abhängigkeit von Hibernate hat, kann dies zur Laufzeit zu einer ClassNotFoundException führen.Das Gleiche gilt für verschachtelte Ausnahmen. Betrachten Sie das vorherige Beispiel mit einem umstrukturierten PersonCreateService und PersonCreateServiceImpl.

Klasse HibernateDao {
  void persist(Person transient) throws HibernateException{}
}
interface PersonCreateService {
  Person create(....) throws CannotCreatePersonException;
}
class PersonCreateServiceImpl implements PersonCreateService{
  Person create(....) throws CannotCreatePersonException {
  versuchen {
  hibernateDao.persist(..);
  } catch(HibernateException e) {
  throw new CannotCreatePersonException("...", e);
  }
  }
}
Dadurch entsteht auch für den aufrufenden Client die gleiche versteckte Laufzeitabhängigkeit von Hibernate. Fazit Wenn Sie also einen Dienst implementieren, sollten Sie sicherstellen, dass Sie keine Implementierungsdetails preisgeben.
  • Wenn Sie Domain-/Dto-Klassen von einem Service zurückgeben möchten, sollten Sie keine Konvertierungsmethoden einfügen, die Ihre Domain-/Dto-Klassen von einer bestimmten Implementierung abhängig machen.
  • Lassen Sie Ihre dto/Domain-Klassen Serializable implementieren, wenn Sie sie aus einem Dienst zurückgeben wollen. Auch wenn dies anfangs so klingt, als würden Sie Ihren Dienst zu sehr entwerfen, hält es tatsächlich alle Optionen, wie z.B. Remoting, so lange wie möglich offen. Dies ermöglicht das schlanke Prinzip Entscheiden Sie so spät wie möglich.
  • Fangen Sie alle erwarteten RuntimeExceptions von Ihren zugrunde liegenden Implementierungen (wie Hibernate oder Spring usw.) in Ihrem Dienst ab. Verpacken Sie sie nicht in Ihre eigene (Runtime)Exception, sondern protokollieren Sie sie und werfen Sie eine neue NamedException an den Client.

Verfasst von

Lars Vonk

Contact

Let’s discuss how we can support your journey.