Reusable-Code-Richtlinien

8 Wege zu wiederverwendbarem Java-Code

03.01.2024 von Rafael  Del Nero
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.
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:

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:

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:

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:

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:

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):

Strukturmuster (Structural Patterns):

Verhaltensmuster (Behavioral Patterns):

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.