Pureline Java
Modernes Java ohne Enterprise-Zeremonie.
Pureline Java nutzt die moderne Sprache direkt: Records, Sealed Types, Pattern Matching, Switch Expressions und Virtual Threads. Framework- und Infrastrukturcode bleibt so nah wie möglich an der eigentlichen Aufgabe.
Profilregeln
Packages nach Verantwortung
Packages werden nach fachlicher Verantwortung geschnitten. Generische Sammelstellen wie util/, manager/, misc/ oder helper/ sind nur erlaubt, wenn das Package einen echten, engen Zweck hat.
src/main/java/
space/example/project/
util/
manager/
helper/
misc/
stuff/ src/main/java/
space/example/project/
api/
runtime/
service/
network/
storage/
model/ Naming
Klassen verwenden PascalCase, Methoden sind kurze Verben, Variablen benennen ihre Rolle. Interfaces tragen das Prefix I.
Ein-Buchstaben-Variablen sind nicht erlaubt, außer bei etablierten lokalen Konventionen wie Schleifenzählern.
public interface Session {
}
Session s = sessions.open(r);
class sessionService {
} SessionService
PacketRegistry
UserStorage
ConnectionState ISession
IStorage
IPacket
IConnection open()
close()
load()
save()
execute()
resolve() Records für immutable Daten
Wenn ein Record genügt, gibt es kein Boilerplate-POJO mit Feldern, Konstruktor und Gettern.
public final class SessionId {
private final UUID value;
public SessionId(UUID value) {
this.value = value;
}
public UUID getValue() {
return value;
}
} public record SessionId(UUID value) {
} Explizites Null-Handling
Objects.requireNonNull ist kein normaler Kontrollfluss. Null-Zustände werden sichtbar als Guard behandelt oder mit einer fachlichen Exception beantwortet.
Objects.requireNonNull(value); if (value == null) {
return;
} if (config == null) {
throw new ConfigurationException("Missing configuration");
} Imports statt FQCNs
Voll qualifizierte Klassennamen gehören in den Import-Block, nicht in den eigentlichen Code.
java.util.concurrent.CompletableFuture<Result> future; import java.util.concurrent.CompletableFuture; Modernes Java bevorzugen
Records, Sealed Types, Pattern Matching, Switch Expressions, var, Virtual Threads, Text Blocks und Streams werden genutzt, wenn sie den Code verbessern.
Moderne Syntax soll Lärm reduzieren, nicht Neuheit um ihrer selbst willen erzeugen.
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
return circle.area();
} if (shape instanceof Circle circle) {
return circle.area();
} switch für echte Zustandsverzweigung
Echte Zustandsverzweigungen werden als Switch Expression formuliert, statt als lange Kette unzusammenhängender Zustandsprüfungen.
if (state == State.READY) {
return start();
}
if (state == State.CLOSED) {
return stop();
}
if (state == State.FAILED) {
return recover();
} return switch (state) {
case READY -> start();
case CLOSED -> stop();
case FAILED -> recover();
}; Nebenläufigkeit
Main- und Tick-Threads werden nie durch HTTP, Datenbankzugriffe, Dateisystem oder blockierende Netzwerk-I/O blockiert.
Für blockierende I/O sind Virtual Threads die bevorzugte Wahl, wo es passt.
public void tick() {
var profile = http.send(request, BodyHandlers.ofString());
players.update(profile.body());
} new Thread(() -> storage.save(session)).start(); try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> storage.save(session));
} Streams nur, wenn klarer
Streams sind willkommen, wenn sie die Lesbarkeit verbessern. Eine Schleife ist die bessere Wahl, wenn sie klarer, leichter zu debuggen oder für den Hot Path besser geeignet ist.
sessions.stream()
.filter(Session::expired)
.peek(session -> log.info("closing {}", session.id()))
.forEach(session -> {
session.close();
storage.delete(session.id());
}); var names = users.stream().map(User::name).toList(); for (var session : sessions) {
if (!session.expired()) {
continue;
}
session.close();
storage.delete(session.id());
} Lifecycle-Symmetrie
Alles, was geöffnet, registriert, gestartet, allokiert oder abonniert wird, braucht einen ebenso klaren Gegenpfad: close, unregister, stop, release oder unsubscribe.
public void start() {
events.register(listener);
scheduler.scheduleAtFixedRate(this::tick, 0, 50, MILLISECONDS);
} var reader = Files.newBufferedReader(path);
return reader.readLine(); public void start() {
events.register(listener);
}
public void stop() {
events.unregister(listener);
} try (var reader = Files.newBufferedReader(path)) {
return reader.readLine();
} Core-Regeln im Java-Checker
Zusätzlich zu den Profilregeln prüft der Checker für Java diese Regeln aus dem Pureline Core: