Suche
Java muss nicht objektorientiert sein

Java Prozedural: Es muss nicht immer Objektorientierung sein

Christoph Pater
java-prozedural

© Shutterstock / McIek

Java wird klassischerweise mit der objektorientierten Programmierung in Verbindung gebracht. Auch aspektorientierte und seit Java 8 funktionale Programmierung gehören zum Java-Universum. Obwohl sie bei Enterprise-Anwendungen durchaus vermehrt anzutreffen ist, wird die prozedurale Programmierung aber meist unterschlagen.

Als ich 1999 in der Ausbildung den ersten Java-Kurs machte, so war dies ein Kurs in objektorientierter Programmierung. Genau wie später an der Uni war Objektorientierung zum einen ganz klar ein Update zur bis dahin gelernten prozeduralen Programmierung und zum anderen fest mit Java verbunden. Java ist objektorientiert – Punkt. So war es auch lange normal, dass, wer Java programmiert, objektorientierte Software schreibt und sich spätestens als Seniorentwickler OOA/OOD (objektorientierte Architektur bzw. Design) in die Skill-Liste schreibt.

Auch wenn es sich nie „richtig“ anfühlte, zu jedem privaten Feld auch immer Getter und Setter generieren zu lassen, blieb es erst einmal dabei: Java ist objektorientiert. In den letzten Jahren habe ich mich vermehrt mit agiler Softwareentwicklung, Extreme Programming und Software Craftmanship beschäftigt. Je mehr ich dabei über Clean Code, Test-driven Development (TDD), Code Smells und objektorientierte Prinzipien gelernt habe, desto mehr hatte ich Probleme damit, diese im beruflichen Java-Enterprise-Umfeld anzuwenden. Zu groß wurde der Designbruch zum bestehenden Legacy-Code. Schließlich war es ein Ruby-Buch [1], welches mich darauf brachte, warum dies so ist: Das Design der Software ist eher prozedural als objektorientiert. Und das trifft bisher auf jede Enterprise-(Spring-)Codebasis zu, die ich in den letzten zehn Jahren gesehen habe.

Programmierparadigmen

Bevor wir tiefer in die Thematik einsteigen, wollen wir die zwei betrachteten Programmierparadigmen „prozedural“ und „objektorientiert“ genauer voneinander abgrenzen. Dabei ist es gar nicht so einfach, eine passende Definition zu finden. Die vorhandenen reichen von rein technischen bis hin zu recht theoretischen Definitionen. Meiner Ansicht nach trifft es die folgende am besten: „The focus of procedural programming is to break down a programming task into a collection of variables, data structures, and subroutines, whereas in object-oriented programming it is to break down a programming task into objects that expose behavior (methods) and data (members or attributes) using interfaces. The most important distinction is that while procedural programming uses procedures to operate on data structures, object-oriented programming bundles the two together, so an “object”, which is an instance of a class, operates on its “own” data structure.“ Wikipedia.org

Bei der prozeduralen Programmierung gibt es eine Trennung zwischen den Daten und Methoden, welche auf diesen angewendet werden, während bei der Objektorientierung die Daten und darauf angewendeten Methoden zu Objekten zusammengefasst werden. Es geht also weniger um die technischen Möglichkeiten als um das zugrunde liegende Designprinzip.

Objektorientiert vs. prozedural

Ein gutes Beispiel zur tiefen Verankerung der Trennung von Daten und darauf angewendeten Routinen im Java-Universum bietet uns die Klasse java.lang.String und die Hilfsmethoden, welche in so ziemlich jeder Bibliothek, sei es von Apache, Google oder Spring, dafür zur Verfügung gestellt werden. Hier gibt es statische (objektexterne) Methoden wie z. B. isEmpty, isBlank usw., welche auf die Daten, also den String, angewendet werden. Eine objektorientierte Lösung könnte eine bestimmte Wrapper-Klasse sein (Listing 1).

Listing 1: Text.java

public class Text {
  private final String text;
  
  public Text (final String text) {
    this.text = text == null ? "" : text;
  }
  
  public Text concat(final String append) {
    return concat(new Text(append));
  }
  
  public Text concat(final Text append) {
    return new Text(text.concat(append.text));
  }
  
  // ... more needed methods from String

  public boolean isEmpty() {
    return "".equals(text);
  }
  
  public boolean isBlank() {
    return "".equals(text.trim());
  }

  // ... equals, hashCode, toString ...
}

Die Instanzen dieser Klasse können selbst die üblichen Fragen wie isEmpty oder isBlank zu einem Text beantworten. Der Konstruktor sorgt dafür, dass der interne Wert nicht null sein kann und verhindert so die unbeliebten NullPointerExceptions. Erweiterungen dieser Klasse können so auch direkt eine Validierung durchführen.

„Neutrale Objekte“

Dadurch, dass Java neben reinen Objekten auch primitive Datentypen, also die Zuweisung von reinen Werten, unterstützt, verschwimmen die Grenzen hier noch leichter. Ein häufiges Problem sind dabei die leidigen NullpointerExceptions. Es ist uns ja erlaubt, dort, wo ein Objekt erwartet wird, nur einen Wert, nämlich null, zuzuordnen. Wenn ich aber den Wert null zuweise, verschiebe ich lediglich eine Entscheidung – die Entscheidung nämlich, wie sich das System verhalten soll, wenn ein Objekt an dieser Stelle nicht vorhanden oder nicht definiert ist. Diese Entscheidung wird auf die Stellen im Code verschoben, an denen das Objekt verwendet werden soll. Wenn an dieser Stelle bekannt ist, dass diese Entscheidung zu treffen ist, dann führt das zu überall im Code verstreuten Nulltests. Wenn nicht, kommt es zur NullPointerException.

Eine Lösung für dieses Dilemma können „Neutrale Objekte“ oder auch „NullObjekte“ sein. Diese Objekte sind Instanzen der entsprechenden Klasse mit Werten, welche sich im System möglichst neutral bzw. definiert verhalten. Am Beispiel der Klasse Text wäre es z. B. eine Instanz mit leerem Text. In den meisten Fällen wird sich diese Instanz im System neutral verhalten, wenn deren Methoden aufgerufen werden. Es gibt erfahrungsgemäß sehr wenige Stellen, an denen die Entscheidung, ob der Wert vorhanden ist, wichtig ist. Um eine Abfragemöglichkeit zu schaffen, hinterlegt man diese Instanz einfach statisch in der definierenden Klasse. Anschließend kann man über equals abfragen, ob es sich um das neutrale Objekt handelt, oder man spendiert ihr eine Abfragemethode (Listing 2).

Listing 2: TextMitNeutralObj.java

public class TextMitNeutralObj {
  private final String text;
  public static  final TextMitNeutralObj NEUTRAL = new TextMitNeutralObj("");

  public TextMitNeutralObj(final String text) {
    this.text = text == null ? "" : text;
  }

  // ... all other methods ...

  public boolean isNeutral() {
    return this.equals(NEUTRAL);
  }
}

Bei Collections machen wir das übrigens meist schon instinktiv, indem wir anstelle von null eine leere Collection zurückgeben, welche sich dann z. B. in einem foreach neutral verhält.

POJOs

Aber auch unsere eigene Codebasis ist meistens eher prozedural als objektorientiert organisiert. Da werden unter dem Schlagwort „Service“ Methoden, welche auf den Daten arbeiten, in Klassen oder – prozedural gesprochen – Modulen zusammengefasst. Die gekapselten Daten sind hier entweder Konfigurationen oder weitere Services. Weil statische Variablen und Methoden aber eher als verrufen gelten, werden Singletons verwendet. Die Daten werden in eigenen Klassen zusammengefasst. Diese enthalten dann nur Daten. Weil wir aber objektorientiert arbeiten, werden diese brav gekapselt, und zwar indem sie privat definiert und mit (public) Gettern und Settern versehen werden. Das nennen wir dann gerne POJOs (Plain Old Java Objects).

Die ursprüngliche Bedeutung von POJO

Ursprünglich wurde der Begriff „POJO“ von Martin Fowler, Rebecca Parsons und Josh MacKenzie auf einer Konferenz 2000 verwendet: „The term was coined while Rebecca Parsons, Josh MacKenzie and I were preparing for a talk at a conference in September 2000. In the talk we were pointing out the many benefits of encoding business logic into regular java objects rather than using Entity Beans. We wondered why people were so against using regular objects in their systems and concluded that it was because simple objects lacked a fancy name. So we gave them one, and it’s caught on very nicely.“ martinfowler.com

Kein Wort von „Getter“ oder „Setter“, sondern Businesslogik sollten diese Objekte enthalten. Auch Wikipedia verweist auf das Mehr als Getter und Setter: „Befreit von Konventionen wird ein POJO als ein Objekt im eigentlichen Sinne der Objektorientierung verstanden, d. h. eine Einheit bestehend aus Daten und Verhalten, auf die die bekannten Grundsätze niedrige Kopplung und starke Kapselung angewendet werden. Ein POJO ist somit im Regelfall mehr als nur eine Ansammlung von Gettern und Settern.“

Dabei waren mit POJOs ursprünglich explizit Objekte mit Businesslogik gemeint (Kasten: „Die ursprüngliche Bedeutung von POJO“). Wenn unsere POJOs nun eigentlich „nur“ Datenstrukturen sind und dafür gedacht, dass andere die Werte bearbeiten, warum „kapseln“ wir sie dann und gönnen uns die Getter und Setter? Es wäre viel einfacher, die Felder einfach public zu definieren – was verlieren wir da in der Praxis? Womit wir wieder beim anfangs erwähnten Unbehagen in Bezug auf Getter und Setter wären. Sie sind eben eher Makulatur als echte und sinnvolle Kapselung.

Wer sich nun nicht vorstellen kann, wie eine objektorientierte Alternative zu den POJOs und Services aussehen kann, sollte versuchen, ein Design mittels Class Responsibility Collaboration Cards (Kasten: „CRC-Karten“) oder Sequenzdiagrammen anstelle von Klassendiagrammen zu entwerfen. Auch das Buch „Growing Object-Oriented Software, Guided by Tests“ [2] ist hier zu empfehlen.

CRC-Karten

CRC (Class Responsibility Collaboration) Cards [6] helfen beim Brainstorming zu objektorientiertem Design. Dabei werden Karteikarten in eine Titelzeile und darunter in zwei Hälften aufgeteilt. Die Titelzeile erhält den Namen der Klasse. Auf der linken Hälfte trägt man die Verantwortlichkeiten der Klasse ein (Single-Responsibility-Prinzip beachten!). Auf der anderen Seite werden die Namen der Klassen eingetragen, mit denen diese Klasse zusammenarbeiten muss, um ihre Verantwortlichkeiten zu erfüllen.

Natürlich kann man es als Verantwortlichkeit ansehen, gewisse Daten zusammenzuhalten, aber in der Regel entsteht dabei kein Design mit reinen Datenobjekten und reinen Routinesammlungen.

Jenseits der Grabenkämpfe

Die Grabenkämpfe um das bessere Programmierparadigma überlasse ich gerne Leuten mit zu viel Zeit. Für mich entscheidend ist nur die Erkenntnis, dass ein vorliegender Code nicht zwingend objektorientiert sein muss, weil er in Java erstellt wurde und auch syntaktisch die entsprechenden Sprachelemente und Mechaniken wie Klassen und Vererbung benutzt.

Diese Erkenntnis ist schon bei der Umsetzung so einfacher und allgemein bekannter Prinzipien wie DRY (Don’t Repeat Yourself) hilfreich. In meinen Services komme ich immer wieder an den Punkt, an dem ich eine Routine auf Datenobjekte an mehreren Stellen, ggf. sogar in mehreren Services benötige. Im Sinne des DRY-Prinzips versuche ich diese dann zu extrahieren. In den meisten Umgebungen wird bei kleineren und isolierten Routinen kein neuer Service entstehen, sondern eine statische Methode. Und wo packe ich diese hin, damit sie an allen benötigten Stellen auch verfügbar ist? In eine Utility-Klasse, im Package commons. Objektorientiert gesehen, fühlt sich das zumindest verkehrt an, denn diese Logik gehört wohl eher zu den Daten und somit in das Objekt. Und commons.utility.irgendwas haben ohnehin einen schlechten Ruf. Bin ich mir aber bewusst, dass ich mich in einem prozeduralen Umfeld bewege, kann ich damit leben.

Wirklich blockiert hat mich das Fehlen dieser Erkenntnis beim Übergang von Test-First Development (TFD) zu TDD. Während Ersteres lediglich das Schreiben des Testcodes vor der Implementierung der Logik vorstellt, treibt TDD auch das Design. In Dojos oder bei Code-Retreats klappte es zwar recht schnell, dass sich aus den Tests mit dem erwarteten Ergebnis das Design ergab. Ich hatte mir bei der Umsetzung nur zu überlegen, welche Message (Methodenaufruf) an wen (Objekt) zu senden war, um das gewünschte Ergebnis zu erhalten. Im Job aber wollte das nicht so richtig klappen. Es ergab sich da kein Service, der ein Interface hatte und meine POJOs transformierte. Also lief es hier immer auf einen Test-First-Ansatz heraus, bei dem zuerst zu überlegen war, welche Interfaces und Klassen benötigt wurden, um dann zumindest noch auf Methodenebene den Test zu haben.

Fazit

Es ergibt sicherlich keinen Sinn, alle Anwendungen auf objektorientiert umzustellen oder gar völlig in Frage zu stellen. Sie funktionieren seit Jahren. Aber einzelne Punkte kann man nun durchaus anders betrachten: Brauche ich wirklich Getter und Setter? Ist eine Utility-Klasse schlecht? Sollte ich mein neues Wissen zu objektorientiertem Design hier anwenden oder zerbricht es das gesamte Design im Sinne der Wartbarkeit?

Und schließlich: „Mache ich das nun so, weil es Sinn ergibt, oder weil man das halt so tut“?

Aufmacherbild: Programming code abstract screen via Shutterstock / Urheberrecht: McIek

Geschrieben von
Christoph Pater

Christoph Pater entwickelt seit über fünfzehn Jahren Software. Als Initiator der lokalen Software-Craftsmanship-Community setzt er sich aktiv für die Professionalisierung der Softwareentwicklung ein.

Kommentare

Hinterlasse einen Kommentar

3 Kommentare auf "Java Prozedural: Es muss nicht immer Objektorientierung sein"

avatar
400
  Subscribe  
Benachrichtige mich zu:
Klaus Weise
Gast
Hallo Christoph, prozedurale Konstrukte sind in vielen Projekten (leider) gang und gäbe, da hast Du recht. Aber: Das Problem ist nicht das man es nicht objektorientiert hinbekäme, sondern dass viele kleine Details im Handwerkszeug fehlen, auch und gerade bei scheinbar „erfahrenen“ Experten. So wird z.B. gern ein POJO mit einer JavaBean verwechselt. Handwerkszeug – das fängt an mit einem gänzlichen Verbot von „null“ an. Statt „null“ nimmt man Option. Jede Referenz muss überprüft werden, gerade bei der Interaktion mit Drittsoftware/Libraries, z.B. mit Objects.requireNonNull(). Oft wird das abgelehnt um zwei Zeilen Code zu sparen, dass sind die Projekte die dann dauernd… Read more »
Frank Kleine
Gast
Ack! Sie haben beide recht. Man muß nicht immer objektorientiert programmieren, nur weil es gerade „in“ ist. Genausowenig muss man prozedural programmieren, nur weil alle anderen es auch so machen. Beide Ansätze haben allerdings ihren Sinn: Sie helfen Komplexität zu beherrschen, Codeverdoppelung zu vermeiden und erlauben es Entwicklungsarbeit auf mehrere Personen aufzuteilen, d.h. zu paralellisieren. Allerdings sollte man die Objektorientierung nicht als Konkurrenz zur prozeduralen Programmierung ansehen, sondern als deren konsequente Erweiterung bzw. Ergänzung. Tatsächlich hat der Autor des Artikels sehr schön das aktuell weit verbreitete Missverständnis über den POJO Begriff herausgearbeitet: >Die Daten werden in eigenen Klassen zusammengefasst. Diese… Read more »
Andreas
Gast
Hier werden Technologie und Verwendung derselben verwechselt (im Text und in den Kommentaren): prozedurale und objektorientierte Softwareentwicklung sind Technologien. Mit beiden kann Businesslogik gut oder schlecht abgebildet werden: Mit prozeduraler Softwareentwicklung können Verantwortlichkeiten genauso „sauber“ getrennt werden wie sie mit objektorientierter Softwareentwicklung „unsauber“ vermischt werden können. Abgesehen davon beinhaltet die objektorientierte Softwareentwicklung die prozedurale immer automatisch, denn jede Methode eines Obekts ist eine Prozedur bzw. Funktion und Events werden ebenso durch Prozeduren und Funktionen realisiert. Objektorientierung ohne Prozeduren/Funktionen ist nicht möglich. Den einzigen Vorteil in diesem Kontext von objektorientierter Softwareentwicklung sehe ich darin, dass Operationen, welche nur auf bestimmte Datenstrukturen… Read more »