SemaTrain Logo Ein Fachportal von SemaTrain

Best Practices & Clean Code

Sie professionalisieren Ihren Java-Code: Lesbarkeit, Struktur, Refactoring und Team-Standards (Reviews, Tests, Logging) – damit Code wartbar bleibt, auch wenn mehrere Personen daran arbeiten (kursnah im SemaTrain-Kontext).

Hinweis: Clean Code bedeutet nicht „perfekt“, sondern verständlich, testbar und änderbar – mit pragmatischen Regeln, nicht mit Dogmen.

Java Fortgeschrittenen Schulung – Kursbezug

Dieses Kapitel bündelt die wichtigsten Qualitätsregeln, die Sie im Kurs durchgängig anwenden: kleine Einheiten, Tests, Logging, Reviews, Refactoring – damit Ihr Team-Code in der SemaTrain-Trainingsplattform langfristig wartbar bleibt.

Ziel dieses Kapitels: Sie können Code lesbar strukturieren, technische Schulden reduzieren und pragmatische Standards im Team etablieren (Naming, Tests, Logging, Reviews, CI-Gates).

Worum geht’s?

Lehr-/Lernziele

Nach diesem Kapitel können Sie …

1) Naming & Absicht (Intent)

Gute Namen reduzieren Kommentare – weil der Code selbst erklärt, was passiert.

Vorher (unklar)

Schlechter Stil: kryptische Namen + Magic Number (Java)
public double calc(double p, int t){
  if(t == 1) return p * 0.9;
  return p;
}

Nachher (klar)

Clean: sprechende Namen + Enum statt Magic Number (Java)
public double rabattAnwenden(double basisPreis, RabattTyp typ){
  if(typ == RabattTyp.ONLINE_10_PROZENT) return basisPreis * 0.90;
  return basisPreis;
}

public enum RabattTyp { KEINER, ONLINE_10_PROZENT }

2) Kleine Funktionen & Single Responsibility

Eine Methode sollte eine Sache tun – und diese gut.

Aufteilen: Validierung, Preis, Persistenz, Logging getrennt (SemaTrain-Use-Case) (Java)
public class BuchungsService {

  public BuchungsBestaetigung buchen(BuchungsAnfrage anfrage) {
    validieren(anfrage);
    double preis = preisBerechnen(anfrage);
    speichern(anfrage, preis);
    erfolgLoggen(anfrage, preis);
    return new BuchungsBestaetigung(preis);
  }

  private void validieren(BuchungsAnfrage a) { /* ... */ }
  private double preisBerechnen(BuchungsAnfrage a) { /* ... */ return 0; }
  private void speichern(BuchungsAnfrage a, double preis) { /* ... */ }
  private void erfolgLoggen(BuchungsAnfrage a, double preis) { /* ... */ }
}

3) Fehler & Logging (pragmatisch)

Logging: Kontext + Level + Throwable (ohne printStackTrace) (Java)
import java.util.logging.*;

public class BuchungsService {
  private static final Logger LOG = Logger.getLogger(BuchungsService.class.getName());

  public void buchen(BuchungsAnfrage anfrage) {
    try {
      validieren(anfrage);

      // ... Buchungslogik (Preis, Persistenz, etc.)
      LOG.info(() -> "Buchung ok | kurs=" + anfrage.kursTitel() + " | kunde=" + anfrage.kundenId());

    } catch (IllegalArgumentException ex) {
      LOG.log(Level.WARNING,
        "Buchung abgelehnt | grund=" + ex.getMessage()
          + " | kurs=" + safe(anfrage != null ? anfrage.kursTitel() : null)
          + " | kunde=" + safe(anfrage != null ? anfrage.kundenId() : null),
        ex
      );
      throw ex;

    } catch (RuntimeException ex) {
      LOG.log(Level.SEVERE,
        "Buchung fehlgeschlagen | kurs=" + safe(anfrage != null ? anfrage.kursTitel() : null)
          + " | kunde=" + safe(anfrage != null ? anfrage.kundenId() : null),
        ex
      );
      throw ex;
    }
  }

  private void validieren(BuchungsAnfrage a) {
    if(a == null) throw new IllegalArgumentException("Anfrage fehlt");
    if(a.kundenId() == null || a.kundenId().isBlank()) throw new IllegalArgumentException("Kunden-ID fehlt");
    if(a.kursTitel() == null || a.kursTitel().isBlank()) throw new IllegalArgumentException("Kurs-Titel fehlt");
  }

  private static String safe(String s) { return (s == null || s.isBlank()) ? "(leer)" : s; }
}

record BuchungsAnfrage(String kundenId, String kursTitel) {}

4) Refactoring mit Sicherheitsnetz (Tests)

Regel: erst Tests (oder Golden Master), dann refactoren.

Test zuerst: Verhalten absichern (kursnah) (Java)
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

class PreisServiceTest {

  @Test
  void onlineRabattWirdAngewendet() {
    PreisService s = new PreisService();
    assertEquals(1874.25, s.endpreis(2082.50, RabattTyp.ONLINE_10_PROZENT), 0.001);
  }
}

class PreisService {
  double endpreis(double basisPreis, RabattTyp typ) {
    if(basisPreis < 0) throw new IllegalArgumentException("Basispreis muss >= 0 sein");
    return (typ == RabattTyp.ONLINE_10_PROZENT) ? basisPreis * 0.90 : basisPreis;
  }
}

enum RabattTyp { KEINER, ONLINE_10_PROZENT }

5) Team-Standards (CI/Review)

Mini-Checkliste fürs Review
  • Ist die Absicht der Methode/Klasse über Namen sofort klar?
  • Gibt es „God Methods“ oder zu viele if/else-Zweige? (Komplexität)
  • Gibt es Tests für kritische Pfade? (Happy Path + Fehlerfälle)
  • Ist Logging sinnvoll (Kontext + Level), ohne sensible Daten?
  • Sind Abhängigkeiten sauber gekapselt/injiziert? (weniger Kopplung)

Praxisaufgabe (Mini)

Beitrag zu den Lehr-/Lernzielen: LZ1 (Struktur) · LZ2 (Refactoring) · LZ3 (Standards)

  1. Nehmen Sie eine Methode mit vielen Verantwortlichkeiten (z.B. „buchenUndBenachrichtigen“).
  2. Extrahieren Sie 3–4 private Methoden (validieren, preisBerechnen, speichern, loggen/notify).
  3. Benennen Sie Variablen/Methoden so, dass Kommentare überflüssig werden.
  4. Schreiben Sie 2 JUnit-Tests: Happy Path + Fehlerfall.
  5. Optional: Definieren Sie 5 Review-Regeln als Team-Standard.
Lösung (Refactoring-Skizze)
Lösung: Responsibility Split + klare Namen + minimale Abhängigkeiten (Java)
import java.util.logging.*;

public class BuchungsServiceRefactored {

  private static final Logger LOG = Logger.getLogger(BuchungsServiceRefactored.class.getName());
  private final PreisService preisService = new PreisService();
  private final BuchungsRepository repo = new InMemoryBuchungsRepository();

  public BuchungsBestaetigung buchen(BuchungsAnfrage anfrage) {
    validieren(anfrage);

    double preis = preisBerechnen(anfrage);
    speichern(anfrage, preis);

    LOG.info(() -> "Buchung ok | kurs=" + anfrage.kursTitel() + " | kunde=" + anfrage.kundenId() + " | preis=" + preis);
    return new BuchungsBestaetigung(preis);
  }

  private void validieren(BuchungsAnfrage a) {
    if(a == null) throw new IllegalArgumentException("Anfrage fehlt");
    if(a.kundenId() == null || a.kundenId().isBlank()) throw new IllegalArgumentException("Kunden-ID fehlt");
    if(a.kursTitel() == null || a.kursTitel().isBlank()) throw new IllegalArgumentException("Kurs-Titel fehlt");
  }

  private double preisBerechnen(BuchungsAnfrage a) {
    // Beispiel: Online-Training bekommt 10% Rabatt
    RabattTyp rabatt = (a.online() ? RabattTyp.ONLINE_10_PROZENT : RabattTyp.KEINER);
    return preisService.endpreis(a.basisPreis(), rabatt);
  }

  private void speichern(BuchungsAnfrage a, double preis) {
    repo.speichern(new Buchung(a.kundenId(), a.kursTitel(), preis));
  }
}

/* ===== minimale Hilfsklassen für das Beispiel ===== */
record BuchungsAnfrage(String kundenId, String kursTitel, boolean online, double basisPreis) {}
record BuchungsBestaetigung(double preis) {}
record Buchung(String kundenId, String kursTitel, double preis) {}

interface BuchungsRepository { void speichern(Buchung b); }

class InMemoryBuchungsRepository implements BuchungsRepository {
  public void speichern(Buchung b) { /* Demo: no-op */ }
}

enum RabattTyp { KEINER, ONLINE_10_PROZENT }

class PreisService {
  double endpreis(double basisPreis, RabattTyp typ) {
    if(basisPreis < 0) throw new IllegalArgumentException("Basispreis muss >= 0 sein");
    return (typ == RabattTyp.ONLINE_10_PROZENT) ? basisPreis * 0.90 : basisPreis;
  }
}

Kurz-Takeaways

Quiz: Best Practices & Clean Code (Lehr-/Lernziele Check)

1. (LZ1) Was ist ein gutes Indiz für „Clean Code“?

2. (LZ2) Warum sind Tests beim Refactoring so wichtig?

3. (LZ3) Was ist eine sinnvolle Team-Regel für Reviews?

4. (LZ3) Was ist beim Logging eine gute Praxis?

Praxisaufgabe

Mini-Projekt: „Refactor & Review“ (Clean Code im SemaTrain-Kurskontext)

Beitrag zu den Lehr-/Lernzielen: LZ1 (Struktur & Naming) · LZ2 (Refactoring mit Tests) · LZ3 (Team-Standards: Review/CI/Logging)

Szenario: In der SemaTrain-Trainingsplattform ist eine Methode buchenUndBenachrichtigen „gewachsen“. Sie ist zu lang, macht zu viel (Validierung, Preis, Persistenz, Logging) und ist schwer zu testen. Ziel: lesbarer, testbarer, review-fähig.

Lösung anzeigen
Teil 1: Vorher – lange Methode („God Method“) (Java)
// ===== 1) VORHER: zu lang, zu viele Verantwortlichkeiten =====
import java.util.logging.*;

public class BuchungsServiceVorher {
  private static final Logger LOG = Logger.getLogger(BuchungsServiceVorher.class.getName());

  public double buchenUndBenachrichtigen(String kundenId, String kursTitel, boolean online, double basisPreis) {
    // Validierung
    if (kundenId == null || kundenId.isBlank()) throw new IllegalArgumentException("Kunden-ID fehlt");
    if (kursTitel == null || kursTitel.isBlank()) throw new IllegalArgumentException("Kurs-Titel fehlt");
    if (basisPreis < 0) throw new IllegalArgumentException("Basispreis muss >= 0 sein");

    // Preislogik
    double preis = online ? basisPreis * 0.90 : basisPreis;

    // Persistenz (hier nur Demo)
    // ... INSERT booking

    // Logging
    LOG.info(() -> "Buchung ok | kunde=" + kundenId + " | kurs=" + kursTitel + " | preis=" + preis);

    // Benachrichtigung (Demo)
    // ... E-Mail senden

    return preis;
  }
}
Teil 2: Nachher – kleine Methoden, klare Verantwortlichkeiten (Java)
// ===== 2) NACHHER: Responsibility Split + testbar =====
import java.util.logging.*;

public class BuchungsServiceNachher {
  private static final Logger LOG = Logger.getLogger(BuchungsServiceNachher.class.getName());

  private final PreisService preisService;
  private final BuchungsRepository repo;

  public BuchungsServiceNachher(PreisService preisService, BuchungsRepository repo) {
    this.preisService = preisService;
    this.repo = repo;
  }

  public double buchenUndBenachrichtigen(String kundenId, String kursTitel, boolean online, double basisPreis) {
    validieren(kundenId, kursTitel, basisPreis);

    double preis = preisBerechnen(online, basisPreis);
    speichern(kundenId, kursTitel, preis);
    erfolgLoggen(kundenId, kursTitel, preis);

    // Benachrichtigung wäre eine separate Abhängigkeit (z.B. NotificationService)
    return preis;
  }

  private void validieren(String kundenId, String kursTitel, double basisPreis) {
    if (kundenId == null || kundenId.isBlank()) throw new IllegalArgumentException("Kunden-ID fehlt");
    if (kursTitel == null || kursTitel.isBlank()) throw new IllegalArgumentException("Kurs-Titel fehlt");
    if (basisPreis < 0) throw new IllegalArgumentException("Basispreis muss >= 0 sein");
  }

  private double preisBerechnen(boolean online, double basisPreis) {
    RabattTyp typ = online ? RabattTyp.ONLINE_10_PROZENT : RabattTyp.KEINER;
    return preisService.endpreis(basisPreis, typ);
  }

  private void speichern(String kundenId, String kursTitel, double preis) {
    repo.speichern(new Buchung(kundenId, kursTitel, preis));
  }

  private void erfolgLoggen(String kundenId, String kursTitel, double preis) {
    LOG.info(() -> "Buchung ok | kunde=" + kundenId + " | kurs=" + kursTitel + " | preis=" + preis);
  }
}

record Buchung(String kundenId, String kursTitel, double preis) {}
enum RabattTyp { KEINER, ONLINE_10_PROZENT }

interface BuchungsRepository { void speichern(Buchung b); }

class PreisService {
  double endpreis(double basisPreis, RabattTyp typ) {
    if (basisPreis < 0) throw new IllegalArgumentException("Basispreis muss >= 0 sein");
    return (typ == RabattTyp.ONLINE_10_PROZENT) ? basisPreis * 0.90 : basisPreis;
  }
}
Teil 3: JUnit 5 – Happy Path + Fehlerfall (Regression-Schutz) (Java)
// ===== 3) Tests: Happy Path + Fehlerfall =====
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;

class BuchungsServiceNachherTest {

  @Test
  void buchen_online_hat10ProzentRabatt_undSpeichert() {
    FakeRepo repo = new FakeRepo();
    BuchungsServiceNachher svc = new BuchungsServiceNachher(new PreisService(), repo);

    double preis = svc.buchenUndBenachrichtigen("K-100", "Java Fortgeschritten", true, 1000.0);

    assertEquals(900.0, preis, 0.0001);
    assertNotNull(repo.last);
    assertEquals("K-100", repo.last.kundenId());
    assertEquals("Java Fortgeschritten", repo.last.kursTitel());
    assertEquals(900.0, repo.last.preis(), 0.0001);
  }

  @Test
  void buchen_wirftWennKundenIdFehlt() {
    FakeRepo repo = new FakeRepo();
    BuchungsServiceNachher svc = new BuchungsServiceNachher(new PreisService(), repo);

    assertThrows(IllegalArgumentException.class,
      () -> svc.buchenUndBenachrichtigen(" ", "Java Fortgeschritten", false, 1000.0)
    );
  }

  static class FakeRepo implements BuchungsRepository {
    Buchung last;
    public void speichern(Buchung b) { this.last = b; }
  }
}
Teil 4: Team-Standard + Gate (CI muss grün sein) (Text)
// ===== 4) Review-Checkliste (pragmatisch) =====
1) Methoden machen genau eine Sache (klein, fokussiert).
2) Namen erklären die Absicht (keine kryptischen Kürzel, keine Magic Numbers).
3) Kritische Logik ist getestet (Happy Path + mind. ein Fehlerfall).
4) Logging ist kontextreich, aber sparsam (richtiges Level, keine sensiblen Daten).
5) Abhängigkeiten bleiben klein (keine unnötige Kopplung, klare Schnittstellen).

Gate-Idee:
- Tests müssen grün sein (CI), sonst kein Merge.