Reusable-Code-Richtlinien

8 Wege zu wiederverwendbarem Java-Code

02.01.2024
Von 
Rafael Del Nero ist Java-Entwickler und schreibt unter anderem für unsere US-Schwesterpublikation Infoworld.com.
Wiederverwendbarer (Java) Code birgt für Entwickler enorme Vorteile. Diese acht Richtlinien helfen dabei, das in der Praxis umzusetzen.
Reusable Code macht nicht nur Java-Entwicklern das Leben leichter.
Reusable Code macht nicht nur Java-Entwicklern das Leben leichter.
Foto: Roman Samborskyi - shutterstock.com

Wiederverwendbaren (Reusable) Code schreiben zu können, stellt für jeden Softwareentwickler eine wichtige Fähigkeit dar. Denn Developer, die Reusable Code einsetzen, kommen in den Genuss zahlreicher Vorteile. Zum Beispiel:

  • optimierte Code-Qualität,

  • reduzierte Redundanzen,

  • höhere Produktivität,

  • verbesserte Zusammenarbeit,

  • kürzere Entwicklungszyklen,

  • schnellere Projekt-Iterationen,

  • einfachere Wartung und

  • die Fähigkeit, existierende Lösungen (besser) weiterzuentwickeln.

Letztlich ermöglicht maximal wiederverwendbarer Code den Entwicklern, skalierbare, flexible und damit zukunftssichere Softwaresysteme zu entwickeln. Außerdem erfordern Fehlerbehebung oder das Hinzufügen neuer Funktionen erheblich mehr Aufwand und Zeit, wenn Ihr Code nicht von Grund auf auf Wiederverwendbarkeit ausgelegt und gut geschrieben ist. Im Extremfall kann es dazu kommen, dass komplette Code-Basen entsorgt werden müssen - und das Projekt noch einmal bei Null beginnen muss. Das kann nicht nur Zeit und Geld, sondern möglicherweise auch Jobs kosten.

Um das zu verhindern und effiziente, wartbare Systeme zu entwickeln, lohnt es sich, die wichtigsten Reusable-Code-Prinzipien zu verinnerlichen - und sie in der Praxis anzuwenden. Dieser Artikel stellt acht praxiserprobte Guidelines vor, um wiederverwendbaren Code in Java zu schreiben.

1. Code-Regeln definieren

Die erste Maßnahme, um wiederverwendbaren Code zu schreiben: Legen Sie gemeinsam mit Ihrem Developer-Team Code-Standards fest. Ansonsten droht das Chaos - und sinnlose Diskussionen über Implementierungen, die ohne Abstimmung stattfinden. Sinn macht darüber hinaus, ein grundlegendes Code-Design für die Probleme zu bestimmen, die die Software lösen soll. Ist das erledigt, beginnt die wirkliche Arbeit: Richtlinien für Ihren Code definieren. Diese bestimmen die Regeln für Ihren Code in verschiedener Hinsicht:

  • Benennung des Codes

  • Anzahl der Klassen- und Methodenzeilen

  • Behandlung von Ausnahmen

  • Package-Struktur

  • Programmiersprache und Version

  • Frameworks, Tools und Bibliotheken

  • Code-Testing-Standards

  • Code-Layer (Controller, Service, Repository, Domain etc.)

Sobald Sie sich auf die Regeln für Ihren Code geeinigt haben, kann die Verantwortung für die Code Reviews auf das gesamte Team verteilt werden. Das sollte sicherstellen, dass der Code gut, respektive wiederverwendbar geschrieben wird. Gibt es hinsichtlich der Code-Regeln keine Einigung innerhalb des Teams, wird aus Reusable Code nichts.

2. APIs dokumentieren

Bei der Kreation von Services und deren Offenlegung als API sollte letztere so dokumentiert werden, dass sie auch für Entwickler, die neu ins Team kommen, leicht zu verstehen und zu verwenden ist.

Gerade in Microservices-Architekturen kommen häufig APIs zum Einsatz. In diesen Fällen müssen andere Teams, die nicht viel über das initiale Projekt wissen, die API-Dokumentation lesen und verstehen können. Ist das nicht der Fall, wird der Code mit hoher Wahrscheinlichkeit noch einmal neu geschrieben. APIs ordentlich zu dokumentieren, ist also sehr wichtig.

Auf der anderen Seite ist ein Hang zur Mikrodokumentation ebensowenig hilfreich. Konzentrieren Sie sich deshalb auf das Wesentliche und erläutern Sie beispielsweise die Geschäftsprozesse in der API, ihre Parameter oder Rückgabeobjekte.

3. Namenskonventionen folgen

Simple, beschreibende Codenamen sind mysteriösen Akronymen unter allen Umständen vorzuziehen. Schreiben Sie also Customer und nicht Ctr, um klar und aussagekräftig zu bleiben. Schließlich könnte die Abkürzung für andere Entwickler etwas ganz anderes bedeuten.

Achten Sie außerdem darauf, den Namenskonventionen Ihrer Programmiersprache zu folgen. Für Java gibt es beispielsweise die JavaBeans-Namenskonvention. Sie ist einfach zu verstehen und definiert, wie Klassen, Methoden, Variablen und Pakete in Java benannt werden:

  • Klassen: customerContract

  • Methoden und Variablen: customerContract

  • Packages: service

4. "Cohesive" coden

Zusammenhängender (Cohesive) Code ist zwar ein einfaches Konzept, doch auch erfahrene Entwickler halten sich nicht immer daran. Das führt zu Klassen, die "ultra-responsible" (auch: God Classes) sind. Einfach ausgedrückt: Sie tun zu viele Dinge. Um Ihren Code "cohesive" zu gestalten, müssen Sie ihn so aufteilen, dass jeder Klasse und Methode nur eine Aufgabe zukommt. Eine Methode mit dem Namen saveCustomer sollte also nur zum Einsatz kommen, um die Daten eines Kunden zu speichern - nicht, um diese zu aktualisieren oder zu löschen.

Ebenso sollte eine Klasse mit dem Namen CustomerService nur Funktionen aufweisen, die "zum Kunden" gehören. Eine Methode innerhalb der Klasse CustomerService, die Prozesse innerhalb der Produktdomäne übernimmt, sollte in die Klasse ProductService verschoben werden.

Um das Konzept besser durchdringen zu können, zunächst ein Beispiel für eine nicht-zusammenhängende Klasse:

public class CustomerPurchaseService {

public void saveCustomerPurchase(CustomerPurchase customerPurchase) {

// Does operations with customer

registerProduct(customerPurchase.getProduct());

// update customer

// delete customer

}

private void registerProduct(Product product) {

// Performs logic for product in the domain of the customer…

}

}

Die Probleme mit dieser Klasse kurz und bündig zusammengefasst:

  • Die saveCustomerPurchase-Methode registriert das Produkt, aktualisiert und löscht den Kunden. Diese Methode hat also zu viele Aufgaben.

  • Die registerProduct-Methode ist schwer zu finden. Deshalb ist die Wahrscheinlichkeit groß, dass ein Entwickler die Methode dupliziert.

  • Die registerProduct-Methode liegt in der falschen Domäne. CustomerPurchaseService sollte keine Produkte registrieren.

  • Die saveCustomerPurchase-Methode ruft eine private Methode auf, anstatt eine externe Klasse für die Produktprozesse zu verwenden.

Da wir die Probleme des Codes nun ermittelt haben, können wir ihn im Sinne der "Cohesiveness" umschreiben. Dazu verschieben wir die registerProduct-Methode in ihre richtige Domäne (ProductService). Das sorgt dafür, dass sich der Code viel einfacher durchsuchen und wiederverwenden lässt:

public class CustomerPurchaseService {

private ProductService productService;

public CustomerPurchaseService(ProductService productService) {

this.productService = productService;

}

public void saveCustomerPurchase(CustomerPurchase customerPurchase) {

// Does operations with customer

productService.registerProduct(customerPurchase.getProduct());

}

}

public class ProductService {

public void registerProduct(Product product) {

// Performs logic for product in the domain of the customer…

}

}

In diesem Beispiel hat saveCustomerPurchase ausschließlich eine Aufgabe: den Kauf des Kunden zu speichern. Zudem haben wir die Verantwortung für registerProduct an die ProductService-Klasse delegiert - was dazu führt dass beide Klassen "cohesive" sind und das tun, was man von ihnen erwartet.

5. Klassen entkoppeln

Stark gekoppelter Code weist zu viele Abhängigkeiten auf, was es erschwert, ihn zu warten. Je mehr Abhängigkeiten eine Klasse aufweist, desto stärker ist sie gekoppelt. Dieses Konzept der Kopplung wird auch im Zusammenhang mit der Softwarearchitektur verwendet. Die Microservices-Architektur etwa verfolgt das Ziel, Services zu entkoppeln. Wenn ein Microservice eine Verbindung zu vielen anderen Microservices aufweisen würde, wäre er stark gekoppelt.

Der beste Weg zu wiederverwendbarem Code besteht folglich darin, Systeme und Code so wenig wie möglich voneinander abhängig zu machen. Natürlich wird dabei immer ein gewisses Maß an Kopplung bestehen, weil Services und Quellcode miteinander kommunizieren müssen. Der Schlüssel liegt also darin, diese Dienste so unabhängig wie möglich zu gestalten. Zunächst ein Beispiel für eine stark gekoppelte Klasse:

public class CustomerOrderService {

private ProductService productService;

private OrderService orderService;

private CustomerPaymentRepository customerPaymentRepository;

private CustomerDiscountRepository customerDiscountRepository;

private CustomerContractRepository customerContractRepository;

private CustomerOrderRepository customerOrderRepository;

private CustomerGiftCardRepository customerGiftCardRepository;

// Other methods…

}

In diesem Beispiel ist die CustomerService-Klasse in hohem Maße mit vielen anderen Dienstklassen gekoppelt. Die vielen Abhängigkeiten führen dazu, dass die Klasse viele Codezeilen benötigt, was zu Erschwernissen in Sachen Testing und Wartung führt.

Der bessere Ansatz wäre, die Klasse in Services mit weniger Abhängigkeiten aufzuteilen. Ganz konkret verringern wir die Kopplung, indem wir die CustomerService-Klasse in separate Dienste aufteilen:

public class CustomerOrderService {

private OrderService orderService;

private CustomerPaymentService customerPaymentService;

private CustomerDiscountService customerDiscountService;

// Omitted other methods…

}

public class CustomerPaymentService {

private ProductService productService;

private CustomerPaymentRepository customerPaymentRepository;

private CustomerContractRepository customerContractRepository;

// Omitted other methods…

}

public class CustomerDiscountService {

private CustomerDiscountRepository customerDiscountRepository;

private CustomerGiftCardRepository customerGiftCardRepository;

// Omitted other methods…

}

Nach dem Refactoring sind CustomerService und andere Klassen wesentlich einfacher zu testen und auch leichter zu warten. Je spezialisierter und übersichtlicher Ihre Klasse ist, desto einfacher gestaltet es sich auch, neue Funktionen zu implementieren. Auch wenn es Bugs geben sollte, sind diese leichter zu beheben.

6. SOLID befolgen

Das Akronym SOLID steht für die fünf Design-Prinzipien der objektorientierten Programmierung. Diese zielen darauf ab, Softwaresysteme wartbarer, flexibler und verständlicher zu gestalten. Im Folgenden eine kurze Erläuterung:

  • Single-Responsibility-Prinzip: Eine Klasse sollte einen einzigen Zweck haben, beziehungsweise eine Verantwortung aufweisen und diese "kapseln". Dieses Prinzip fördert Cohesive Code und hilft dabei, Klassen überschaubar zu halten.

  • Open-Closed-Prinzip: Softwareeinheiten (Klassen, Module, Methoden etc.) sollten offen für Erweiterungen, aber geschlossen für Änderungen sein. Im Klartext: Sie sollten Ihren Code so gestalten, dass Sie neue Funktionen hinzufügen können, ohne den bestehenden Code zu ändern. Das verringert den Impact von Änderungen fördert wiederverwendbaren Code.

  • Liskovsches Substitutionsprinzip: Objekte einer Oberklasse sollten durch Objekte ihrer Unterklassen ersetzt werden können, ohne die Korrektheit des Programms zu beeinträchtigen. Mit anderen Worten: Jede Instanz einer Basisklasse sollte durch jede Instanz ihrer abgeleiteten Klassen ersetzbar sein, um sicherzustellen, dass das Applikationsverhalten konsistent bleibt.

  • Interface-Segregation-Prinzip: Clients sollten nicht von Schnittstellen abhängig sein, die sie nicht benutzen. Umfassende Interfaces sollten in kleinere und spezifischere Schnittstellen aufgeteilt werden, damit Clients nur von den für sie relevanten abhängig sind. Das fördert eine lose Kopplung und vermeidet unnötige Dependencies.

  • Dependency-Inversion-Prinzip: High-Level-Module sollten nicht von Low-Level-Modulen abhängig sein - stattdessen sollten beide von Abstraktionen abhängen. Dieses Prinzip fördert die Verwendung von Abstraktionen, um High-Level-Module von Low-Level-Implementierungsdetails zu entkoppeln. Das macht die Systeme flexibler und erleichtert Testing und Wartung.

Indem sie die SOLID-Grundsätze befolgen, kommen Developer zu modulare(re)m, wartbarem und erweiterbarem Code, der leichter zu verstehen ist. Das führt wiederum zu robusteren, flexibleren Softwaresystemen.

7. Design Patterns nutzen

Design Patterns (Entwurfsmuster) werden von erfahrenen Entwicklern erstellt und helfen - wenn sie richtig eingesetzt werden - dabei, Code wiederzuverwenden. Developer, die Design Patterns verstehen, beziehungsweise erkennen, weisen im Regelfall auch optimierte Skills auf, wenn es ganz allgemein darum geht, Code zu lesen und zu verstehen. Selbst Code aus dem Java Development Kit wirkt klarer, wenn Sie das zugrundeliegende Design Pattern erkennen.

Entwurfsmuster sind äußerst leistungsfähig, aber kein Allheilmittel - auch wenn diese zum Einsatz kommen, müssen Softwareentwickler genau darauf achten, wie sie diese verwenden. Es wäre zum Beispiel ein Fehler, ein Design Pattern zu verwenden, nur weil es bereits bekannt ist. Kommen die Muster in der falschen Situation zum Einsatz, können Sie den Code komplexer machen. Es kommt in Sachen Design Patterns insbesondere darauf an, sie für die richtigen Use Cases zu nutzen, um den Code flexibler für Erweiterungen zu gestalten. Im Folgenden eine kurze Zusammenfassung der Design Patterns in der objektorientierten Programmierung.

Erzeugungsmuster (Creational Patterns):

  • Einzelstück (Singleton): Stellt sicher, dass eine Klasse nur eine Instanz hat und bietet auf diese globalen Zugriff.

  • Fabrikmethode (Factory Method): Definiert eine Schnittstelle, um Objekte zu erstellen, überlässt es aber den Unterklassen, welche Klasse instanziiert wird.

  • Abstrakte Fabrik (Abstract Factory): Bietet ein Interface, um Familien von verwandten oder abhängigen Objekten zu erstellen.

  • Erbauer (Builder): Trennt die Konstruktion von komplexen Objekten von ihrer Darstellung.

  • Prototyp (Prototype): Erzeugt neue Objekte, indem es bestehende klont.

Strukturmuster (Structural Patterns):

  • Adapter: Konvertiert die Schnittstelle einer Klasse in eine andere, die die Kunden erwarten.

  • Dekorator (Decorator): Fügt einem Objekt dynamisch Verhalten hinzu.

  • Proxy: Stellt ein Surrogat oder einen Platzhalter für ein anderes Objekt zur Verfügung, um den Zugriff darauf zu kontrollieren.

  • Kompositum (Composite): Behandelt eine Gruppe von Objekten als ein einziges Objekt.

  • Brücke (Bridge): Entkoppelt eine Abstraktion von ihrer Implementierung.

Verhaltensmuster (Behavioral Patterns):

  • Beobachter (Observer): Definiert eine Eins-zu-viele-Abhängigkeit zwischen Objekten. Wenn sich der Zustand eines Objekts ändert, werden alle von ihm abhängigen Objekte benachrichtigt und automatisch aktualisiert.

  • Strategie (Strategy): Kapselt verwandte Algorithmen und ermöglicht deren Auswahl zur Laufzeit.

  • Schablonenmethode (Template Method): Definiert das Grundgerüst eines Algorithmus in einer Basisklasse und ermöglicht es Unterklassen, spezifische Implementierungsdetails bereitzustellen.

  • Kommando (Command): Kapselt eine Anfrage als Objekt, so dass Clients mit verschiedenen Anfragen parametrisiert werden können.

  • Zustand (State): Ermöglicht es einem Objekt, sein Verhalten zu ändern, wenn sich sein interner Zustand ändert.

  • Iterator: Ermöglicht den sequentiellen Zugriff auf die Elemente eines Aggregatobjekts, ohne dessen zugrundeliegende Darstellung offenzulegen.

  • Zuständigkeitskette (Chain of Responsibility): Ermöglicht es einem Objekt, eine Anfrage entlang einer Kette von potenziellen Bearbeitern weiterzuleiten, bis die Anfrage bearbeitet ist.

  • Vermittler (Mediator): Definiert ein Objekt, das kapselt, wie eine Gruppe von Objekten interagiert, und fördert die lose Kopplung zwischen ihnen.

  • Besucher (Visitor): Trennt einen Algorithmus von den Objekten, auf denen er arbeitet, indem der Algorithmus in separate Besucherobjekte ausgelagert wird.

Es besteht keine Notwendigkeit, all diese Design Patterns auswendig zu lernen. Es reicht, wenn Sie wissen, dass es diese Muster gibt - und sie bewirken.

8. Rad nicht neu erfinden

In vielen Unternehmen ist es immer noch Standard, ohne triftigen Grund auf intern entwickelte Frameworks zu setzen. Das ist in den allermeisten Fällen sinnlos - es sei denn, Ihr Unternehmen heißt Google oder Microsoft. Kleine oder auch mittlere Unternehmen sind nicht in der Lage, mit eigenen Lösungen in diesem Bereich zu konkurrieren. Anstatt also das Softwarerad neu erfinden zu wollen und damit Arbeit zu verursachen, die es nicht braucht, sollten Sie besser einfach die vorhandenen Tools verwenden.

Das ist auch für Softwareentwickler eine Wohltat, weil es ihnen erspart, ein Framework erlenen zu müssen, das außerhalb des eigenen Unternehmens keinerlei Rolle spielt. Nutzen Sie also die auf dem Markt weithin verfügbaren und populären Technologien und Tools. (fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.