2.3    Objektorientiert programmieren

Die Strukturierung von Code in Funktionen bzw. Methoden ist nur der erste Schritt auf dem Weg zur durchdachten Entwicklung komplexer, großer Programme. Noch einmal schwieriger ist es, Code sinnvoll objektorientiert zu konzipieren. Was gehört zu einer Klasse? Welche Daten sind öffentlich, welche privat? Um welche Details muss sich der Konstruktor kümmern? An welchen (Daten-)Grenzen werden Klassen voneinander abgegrenzt? Wo können Mechanismen wie Vererbung oder Schnittstellen zweckmäßig eingesetzt werden?

Wie schon in den vorangegangenen Abschnitten geht es uns hier nicht darum, Ihnen das Programmieren beizubringen. Ebenso wenig erschien es uns zweckmäßig, hier eine Menge Code abzudrucken. Unser primäres Ziel ist es, Sie anhand von Beispielen zu motivieren, sich von KI-Bots oder -Assistenten gezielt unter die Arme greifen zu lassen. Das funktioniert nicht nur bei der Ausformulierung des Codes, sondern – was uns noch wichtiger erscheint – auch bei der richtigen Konzeption von Klassen.

Warum Java?

Die folgenden Beispiele verwenden Java als Programmiersprache. Natürlich können Sie in Python ebenfalls Klassen definieren. Die Stärken von Python liegen aber woanders. Java bietet deutlich mehr objektorientierte Features.

Zugegebenermaßen ist Java eine ziemlich alte Sprache. Objektorientierten Code können Sie in neuen Programmiersprachen wie Swift oder Kotlin deutlich eleganter formulieren. Allerdings sind diese Sprachen relativ neu; Features ändern sich oder kommen hinzu. Aus diesem Grund und wegen der geringeren Verbreitung (d. h. dem nur recht eingeschränkt verfügbarem Trainingsmaterial) funktionieren Sprachmodelle bei neuen Sprachen weniger gut als bei lange etablierten Sprachen.

Kurz und gut: Auch wenn sich unsere Liebe zu Java in Grenzen hält, erschien uns diese weitverbreitete Sprache die beste Wahl für die folgenden Beispiele.

Beispiel: User-Account-Klasse

Die folgenden Prompts sollen Ihnen zeigen, wie Ihnen KI-Tools beim objektorientierten Design von Programmen helfen können. Die ersten Prompts betreffen einen Programmteil, bei dem ein Benutzeraccount durch eine Java-Klasse abgebildet wird. Im Prompt fassen wir die Zielsetzung der Klasse zusammen, listen ihre wichtigsten Eigenschaften auf und nennen die von uns eingesetzte Programmiersprache. Erst dann folgt die eigentliche Frage: Welche Art von Validierung sollte im Konstruktor der Klasse durchgeführt werden?

Prompt: I need to write a Java class for a user account. Important attributes are account name, full name, contact email, password hash, and a list of recent login times. What kind of validation should I do in the constructor?

When creating a Java class for a user account with attributes like account name, full name, contact email, password hash, and a list of recent login times, it’s important to include appropriate validations to ensure data integrity and security. Below is a suggested Java class with the required validations:

import ...;

public class UserAccount {
private String accountName;
private String fullName;
private String contactEmail;
private String passwordHash;
private List<String> recentLoginTimes;
// Constructor
public UserAccount(String accountName, ...) {
this.setAccountName(accountName);
this.setFullName(fullName);
...
}

// Validation methods used by constructor
private void setAccountName(String accountName) {
if (accountName == null ||
accountName.isEmpty() ||
accountName.length() < 3 ||
accountName.length() > 20)
{
throw new IllegalArgumentException(
"Name must be between 3 and 20 characters.");
}
this.accountName = accountName;
}
private void setFullName(String fullName) { ... }
private void setContactEmail(String contactEmail) { ... }
private void setPasswordHash(String passwordHash) { ... }
private void setRecentLoginTimes(
List<String> recentLoginTimes) { ... }

// Utility method to validate email format
private boolean isValidEmail(String email) { ... }

// Getters
public String getAccountName() {
return accountName;
}
public String getFullName() { ... }
public String getContactEmail() { ... }
public String getPasswordHash() { ... }
public List<String> getRecentLoginTimes() { ... }
}

Validation Details

  • Account Name: must be between 3 and 20 characters, should not be null or empty

  • Full Name: should not be empty and must be less than 50 characters.

  • Contact Email: must be a valid email format using a regular expression.

  • Password Hash: must be a 64-character string assuming a SHA-256 hash, should not be null or empty.

  • Recent Login Times: Initializes an empty list if the provided list is null to avoid NullPointerException.

(more details …)

Wie so oft geht die Antwort ein wenig über die eigentliche Fragestellung hinaus. ChatGPT schlägt den kompletten Aufbau der Klasse vor, definiert die Instanzvariablen private und sieht Getter- und Setter-Methoden für den Datenzugriff zu. Die Validierung erfolgt in den Setter-Methoden. Wir hinterfragen dieses Konzept.

Prompt: Java: Using getters and setters only seems to complicate my code. What’s the benefit?

In seiner Antwort erläutert ChatGPT, dass die gewählte Vorgehensweise hier tatsächlich sinnvoll ist. Wir folgen daher dem Vorschlag und implementieren die Klasse inklusive Eigenschaften, Getter- und Setter-Methoden sowie dem Konstruktor.

Ausgabe des Objekts

Unser nächster Wunsch besteht darin, die Klasse mit toString auszustatten. Diese Methode ist vor allem bei Tests hilfreich, weil wir so unkompliziert ein Objekt mit all seinen Eigenschaften ausgeben können.

Im Prompt bitten wir ChatGPT, nicht jedes Mal den Code der gesamten Klasse auszugeben, sondern nur den Code der neuen toString-Methode. Wir haben die Code-Ausgaben stark gekürzt. Falls Sie den gesamten Code ansehen möchten, werfen Sie einen Blick in die Beispieldateien zu diesem Kapitel.

Prompt: Please add a toString method to easily output the account data in a readable form. Don’t answer with the entire code, only show the necessaray changes.

Here are the necessary changes to add a toString method for the UserAccount class:

@Override
public String toString() {
return "UserAccount{" +
"accountName='" + accountName + '\'' +
...
", recentLoginTimes=" + recentLoginTimes +
"}";
}

JSON-Export und -Konstruktor

Jetzt wollen wir die Klasse um eine Methode für den JSON-Export erweitern. Außerdem wollen wir einen zusätzlichen JSON-Konstruktor implementieren. Im Prompt bitten wir um eine möglichst einfache Lösung ohne die Verwendung externer Bibliotheken.

Prompt: I want to export all attributes as a JSON string. Can you provide a simple method without using an external library?

To export the UserAccount object to a JSON string without using any external libraries, you can manually construct the JSON string. Here’s how you can do it …

// Method to convert the object to JSON string
public String toJson() {
StringBuilder json = new StringBuilder();
json.append("{");
json.append("\"accountName\":\"").
append(escapeJson(accountName)).
append("\",");
...
return json.toString();
}
// Helper methods
private String escapeJson(String value) { ... }
private String listToJson(List<String> list) { ... }

Nachdem wir den Code getestet haben, wünschen wir uns im nächsten Prompt eine JSON-Importfunktion in Form eines zweiten Konstruktors.

Prompt: What might a second constructor that expects a JSON string look like?

To create a second constructor that initializes a UserAccount object from a JSON string, you need to parse the JSON string and extract the necessary fields. Since we are not using any external libraries, we will manually parse the JSON string. Here is how you can implement the second constructor:

// Second constructor expecting a JSON string
public UserAccount(String jsonString) {
this(parseJson(jsonString, "accountName"),
...
parseJsonArray(jsonString, "recentLoginTimes"));
}
// Helper methods to parse JSON strings and arrays
private static String parseJson(
String jsonString, String key) { ... }
private static List<String> parseJsonArray(
String jsonString, String key) { ... }

Beispiel: Bankkonto

Bei der nächsten Übung geht es darum, ein Bankkonto als Java-Klasse abzubilden. Im Prompt beschreiben wir zuerst die Aufgabenstellung. Bevor wir richtig loslegen, haben wir aber noch grundsätzliche Fragen. Welche Datentypen sollen wir einsetzen? Sollen die Eigenschaften in Form privater oder öffentlicher Variablen realisiert werden?

Prompt: I have to write a Java class for a bank account. It stores the name of the account holder, the current balance, and a list of all previous transactions. Which data types should I use? Should I make my instance variables public or private?

To write a Java class for a bank account, you need to decide on appropriate data types for each attribute and determine the visibility of your instance variables. Here are the recommendations for both (more details …).

Instance variables should generally be private to encapsulate the data and protect it from unauthorized access or modification. Public access should be provided through getter and setter methods.

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

public class BankAccount {
private String accountHolderName;
private BigDecimal currentBalance;
private List<Transaction> transactionHistory;

// constructor
public BankAccount(String accountHolderName) { ... }

// methods
public void deposit(BigDecimal amount) { ... }
public void withdraw(BigDecimal amount) { ... }
public String getAccountHolderName() { ... }
public BigDecimal getCurrentBalance() { ... }
public List<Transaction> getTransactionHistory() { ... }

// nested class representing a transaction
public static class Transaction {
private BigDecimal amount;
private String type;
private String date;
...
}
}

ChatGPT entscheidet sich für private Klassenvariablen, was in Anbetracht der Fragestellung naheliegend ist. Bei unseren Tests hat ChatGPT übereifrig gleich auch Methoden beigesteuert, um Geld sicher auf- und abzubuchen. Insbesondere weigert sich die withdraw-Methode, mehr Geld abzubuchen, als momentan zur Verfügung steht. Die deposit-Methode überprüft, dass der Betrag der Einzahlung größer 0 ist.

Eigentlich wollten wir nach diesen Details erst im nächsten Schritt fragen – das erübrigt sich leider. Wenn Sie effizient und schnell programmieren möchten, dann sind die umfassenden Antworten von ChatGPT zweifellos hilfreich. Wollen Sie dagegen programmieren lernen und, so gut es geht, viel eigenen Code beitragen, ist der Übereifer von KI-Tools hinderlich.

Beispiel: Fotos anhand von EXIF-Daten sortieren

Das dritte Beispiel ist noch offener formuliert. Das Ziel ist ein Programm, um Fotos nach ihren Aufnahmedaten in Verzeichnisse zu sortieren. Der Prompt beschreibt ziemlich detailliert, was das Programm können soll und welche Bibliothek zum Auslesen der EXIF-Daten verwendet werden soll. Einmal mehr versuchen wir, unserem KI-Werkzeug ausdrücklich klar zu machen, dass wir keinen fertigen Code wollen, sondern einen Vorschlag zur Organisation des Programms.

Prompt: I am trying to write a Java program that loops through all the JPEG files in a directory. The program uses metadata-extractor to read the EXIF data of each file. Images with valid EXIF data are then moved to a new directory yyyy-mm, where yyyy is the year of creation, mm is the month.

Please help me organize the code in a proper object-oriented way. Just provide a framework for the code, no methods yet.

Certainly! To organize your Java program in a proper object-oriented way, we can create a few classes to handle different responsibilities. Here’s a framework with a brief explanation of each class’s role:

  • JPEGProcessor: Main class to initiate the process and handle the overall flow.

  • FileHandler: Responsible for file operations like reading and moving files.

  • EXIFExtractor: Uses metadata-extractor to read and parse EXIF data from JPEG files.

  • DirectoryOrganizer: Manages the creation of new directories based on EXIF data.

  • Logger: Handles logging of operations and errors.

(more details with code for all classes …)

Natürlich gibt es viele Möglichkeiten, den dafür notwendigen Code über mehrere Klassen zu verteilen. Der hier zusammengefasste KI-Vorschlag ist absolut brauchbar und ein guter Startpunkt. Ärgerlicherweise kann ChatGPT auch dieses Mal der Versuchung nicht widerstehen, gleich eine Menge Beispielcode mitzuliefern.

Beispiel: Umgang mit Singletons

Wenn Sie eine neue Programmiersprache lernen – egal, ob es die erste Sprache ist oder ob Sie schon mit anderen Sprachen programmieren können – werden Sie immer wieder auf neue Features stoßen. Sie werden in Büchern und im Internet Programmiertechniken sehen, deren Konzept Sie nicht auf Anhieb verstehen.

Durch das Pair Programming mit einem LLM können Sie im Dialog Konzepte ausprobieren und konkrete Rückfragen stellen, die sich an Ihrer Aufgabe orientieren. Je mehr Informationen Sie zu Ihrem Wissensstand und der Ausgangslage liefern, desto besser kann die KI auf Ihre Fragestellung eingehen.

Im folgenden Prompt gehen wir davon aus, dass Sie vom Singleton-Pattern gehört bzw. gelesen haben. Das Konzept klingt nützlich – aber wie sieht die Realisierung in Java aus?

Prompt: How can I realize the singleton pattern in Java?

ChatGPT interpretiert die Frage im Sinne von: »Wie viele Möglichkeiten gibt es, Singletons in Java zu realisieren, und präsentiert in seiner Antwort gleich sechs Varianten. Wir haben nur die erste wiedergegeben.

In Java, the Singleton pattern ensures that a class has only one instance and provides a global point of access to it. There are several ways to implement a Singleton pattern in Java, each with its own advantages and disadvantages. Here’s a detailed look at the most common implementations.

Eager Initialization: This approach creates the instance when the class is loaded. It’s simple but may lead to resource wastage if the instance is never used.

public class Singleton {
// private static instance, so it can be accessed
// by only getInstance() method
private static final Singleton instance =
new Singleton();
// private constructor to prevent instantiation from
// other classes
private Singleton() {}

// public method to provide access to the instance
public static Singleton getInstance() {
return instance;
}
}

(More details: lazy initialization, thread-safe Singleton, thread-safe Singleton with double-checked locking, Bill Pugh Singleton implementation, enum Singleton …)

ChatGPT schießt – wir müssen leider sagen: wie üblich – über das Ziel hinaus und bombardiert Sie mit Details zu sechs (!) Implementierungsvarianten. Das geht Ihnen zu weit. Sie denken daran, Singletons dazu verwenden, um zentral im Programm eine Datenbankverbindung zu speichern. Der konkrete Prompt führt zu einer deutlich konkreteren Antwort.

Prompt: Which variant is best if my program needs a single database connection?

If your program needs a single database connection, the Bill Pugh Singleton Implementation is generally the best variant to use (more details …)

Sie vertrauen der Antwort und verzichten auf einen Vergleich mit anderen Implementierungsvarianten. Stattdessen fragen Sie im nächsten Prompt, ob es nicht doch ausreichen würde, Ihre angedachte Datenbankklasse nur mit einem herkömmlichen Konstruktor auszustatten.

Prompt: I intend to write a database class that contains all database functions I need for my program. What are the advantages of the Bill Pugh Singleton Implementation over a traditional constructor?

Endgültig überzeugt konkretisieren Sie im nächsten Prompt die Zielsetzung weiter. Sie geben an, welches Datenbanksystem Sie verwenden möchten, und bitten das KI-Tool um eine einfache Beispielimplementierung.

Prompt: OK, let’s use the Bill Pugh Singleton Implementation. My database system is PostgreSQL. Show me a simple implementation. Be concise!