Der Core.
Diese Regeln gelten für alle unterstützten Sprachen, soweit die Sprache sie natürlich erlaubt. Die Sprachprofile bauen darauf auf.
Pureline standardisiert Prinzipien, nicht Syntax. Jede Sprache bleibt idiomatisch: Pureline entfernt unnötigen Lärm, ohne die Features wegzunehmen, die Java, JavaScript, TypeScript, Go, Crystal, Emvus und Rust stark machen.
Kernregeln
Kontrollfluss
Guard Clauses bevorzugen
Sonderfälle verlassen die Funktion früh. Der Happy Path bleibt so nah wie möglich an der Basis-Einrückung.
Lesefluss: Input → Validation → Transformation → Execution → Result. Nicht: if → if → else → if → callback → branch.
if (request != null) {
if (request.valid()) {
var session = sessions.create(request.user());
return executor.execute(session, request);
} else {
return Result.invalid();
}
}
return Result.empty(); if (request == null) {
return Result.empty();
}
if (!request.valid()) {
return Result.invalid();
}
var session = sessions.create(request.user());
return executor.execute(session, request); else vermeiden
Statt eines else-Zweigs wird die Bedingung umgedreht, der Sonderfall erledigt und der Block verlassen.
Für einfache Wertauswahl ist ein Ternary erlaubt. Verschachtelte Ternaries sind nicht Pureline.
if (valid) {
execute();
} else {
reject();
} var state = valid ? (open ? State.READY : State.CLOSED) : failed ? State.FAILED : State.INVALID; if (!valid) {
reject();
return;
}
execute(); var state = valid ? State.READY : State.INVALID; Explizite Blöcke in Klammer-Sprachen
In Java, JavaScript, TypeScript, Go, Emvus und anderen Sprachen mit geschweiften Klammern bekommt jeder Kontrollfluss-Body einen expliziten Block, auch bei genau einer Anweisung. Das gilt für if, for, while und alle vergleichbaren Konstrukte.
Die Regel verbietet keine idiomatischen Kurzformen in Sprachen, in denen sie ein normaler, lesbarer Teil der Sprache sind. Einfache Crystal-Postfix-Guards wie return unless valid sind Pureline-konform.
if (!valid) return; if (!valid)
return; for (const user of users) notify(user); if (!valid) {
return;
} for (const user of users) {
notify(user);
} return unless valid Idiomatische Kurzformen, wenn sie sofort lesbar bleiben
Pureline standardisiert Klarheit und Struktur, nicht identische Syntax über alle Sprachen. Kurzformen, die in einer Sprache etabliert sind, bleiben erlaubt, solange Bedingung und Aktion einfach sind.
Sobald eine Kurzform Nachdenken erfordert, wird sie zum expliziten Block.
notify_admins(session, reason: :expired) unless session.valid? || session.renewable? && !user.banned? return unless session.valid? if session.closed? || session.expired?
logger.info("Session is no longer usable")
return
end Namen, Funktionen & Struktur
Namen tragen Bedeutung
So kurz wie möglich, so explizit wie nötig. Kryptische Kürzel sind genauso schlecht wie Namen, die zu Sätzen werden.
Ausnahmen sind etablierte lokale Konventionen wie Schleifenzähler (i, j, k).
r s p x tmp obj data2 thing currentIncomingPlayerConnectionRequestObject var tmp = sessions.find(r.id());
var data2 = tmp.user(); request session player packet handler
storage result connection worker Eine klare Verantwortung pro Funktion
Eine Funktion steht für eine klar erkennbare Aktion. Vage Namen verraten, dass sie zu viel tut.
Größen-Heuristik: ~5–30 Zeilen normal, ~30–50 Zeilen Review, ab 50 Zeilen in der Regel teilen. Das sind Heuristiken, keine absoluten Limits.
handleEverything()
process()
doStuff()
executeAll() public void process(Request request) {
validate(request);
var session = sessions.create(request.user());
storage.save(session);
mailer.sendWelcome(request.user());
metrics.increment("sessions");
} validateRequest()
createSession()
sendPacket()
loadUser()
closeConnection() public Session openSession(Request request) {
var session = sessions.create(request.user());
storage.save(session);
return session;
} Eine klare Verantwortung pro Klasse und Modul
Manager-Klassen, die Datenbank, Spieler, Pakete, Commands, Konfiguration, HTTP und Logging bündeln, sind God Classes. Code wird nach fachlicher Verantwortung geschnitten.
Eine Klasse hat eine klare Verantwortung, wenn ihre Member und ihr interner Zustand zu einem zusammenhängenden Anliegen beitragen. Die Zeilenzahl allein entscheidet nicht, ob eine Klasse gut entworfen ist; Größenorientierung steht in PL-STRUCT-003.
ServerManager
├─ database
├─ players
├─ packets
├─ commands
├─ configuration
├─ HTTP
└─ logging server/ ServerRuntime
network/ Connection, PacketRegistry, PacketDispatcher
session/ Session, SessionService
storage/ UserStorage, SessionStorage Pakete und Ordner klein genug zum Überblicken
Datei- und Typ-Dichte gehören zur Lesbarkeit. Ein Paket oder Quellordner soll so klein sein, dass man seinen Inhalt auf einen Blick versteht, ohne eine lange flache Liste unzusammenhängender Dateien zu durchsuchen.
Wird das Limit überschritten, wird nach Verantwortung geteilt, statt den Ordner zur Müllhalde werden zu lassen. Ein Split soll eine echte Verantwortungsgrenze einführen, nicht willkürliche Verschachtelung erzeugen.
Default-Limits: Java 8 Produktions-Top-Level-Typen/Quelldateien pro Paket; Emvus 8 .ev-Dateien pro Quell-/Namespace-Ordner; JavaScript und TypeScript 10 Produktionsmodule pro Feature-/Quellordner; Go 12 Nicht-Test-, nicht generierte .go-Dateien pro Paketverzeichnis; Crystal 10 .cr-Dateien pro Ordner; Rust 10 handgepflegte .rs-Dateien pro Feature-/Quellordner.
Nicht mitgezählt werden: Tests und Specs, generierter Code, reine Metadaten-Dateien, package-info.java, module-info.java, Barrel-Dateien wie index.ts/index.js, generierte Bindings oder Protokoll-Ausgaben sowie Migrations-Snapshots und vergleichbare maschinell erzeugte Artefakte.
Ein Projekt darf die Schwellen künftig über eine Pureline-Konfigurationsdatei überschreiben; der Default bleibt bewusst streng.
network/
Connection.java
ConnectionFactory.java
ConnectionState.java
Packet.java
PacketCodec.java
PacketDecoder.java
PacketEncoder.java
PacketRegistry.java
PacketDispatcher.java
PacketHandler.java
PacketQueue.java
PacketMetrics.java
SocketConfig.java
SocketRuntime.java network/
Connection.java
ConnectionFactory.java
ConnectionState.java
network/packet/
Packet.java
PacketCodec.java
PacketDecoder.java
PacketEncoder.java
PacketRegistry.java
PacketDispatcher.java
network/runtime/
PacketQueue.java
PacketMetrics.java
SocketConfig.java
SocketRuntime.java Zusammengehörigkeit vor Zeilenbegrenzung
Cohesion over Size: Die bevorzugte Maximalgröße einer Klasse ist eine Orientierung, niemals ein hartes Limit. Eng zusammengehörige Logik bleibt zusammen, wenn ein Split den Code schwerer lesbar, wartbar, testbar oder nachvollziehbar machen würde. Eine größere kohäsive Klasse ist besser als mehrere künstliche Klassen, die durch unnötige Delegation verbunden sind.
Orientierung: bis 200 Zeilen ist der bevorzugte Normalfall; 201–400 Zeilen sind unproblematisch bei klarer Verantwortung; 401–600 Zeilen sind eine bewusste Architekturentscheidung, deren Kohäsion verständlich bleiben soll; über 600 Zeilen wird ein Review empfohlen, aber nichts ist verboten. Das sind Heuristiken: Eine überschrittene Schwelle ist für sich kein Stilverstoß und erzwingt keinen Split.
Größere Klassen sind gerechtfertigt bei Parsern und Lexern (eine kohäsive State Machine wird komplizierter, wenn Zustände und Übergänge über künstliche Abstraktionen verstreut sind), bei Algorithmen mit eng gekoppelten Verarbeitungsphasen, bei zentralen Datenstrukturen, deren Methoden dieselben Invarianten und denselben internen Zustand teilen, sowie bei komplexer, aber kohäsiver Logik, bei der ein Split nur Indirektion, Delegation und Koordinationsaufwand ohne mehr Klarheit bringt.
Trotzdem gesplittet wird entlang echter Verantwortungsgrenzen, nicht nach Zeilenzahl. Hinweise sind eigenständige Subsysteme mit unabhängiger Entwicklung, unzusammenhängende Änderungsgründe, schwach gekoppelte Methodengruppen oder getrennte Lebenszyklen, die eine Klasse verdeckt.
Tooling: Pureline Checker und IDE-Inspektionen dürfen einen informativen Größenhinweis mit Kohäsions-Review zeigen, besonders über 600 Zeilen. Sie dürfen die Zeilenzahl nicht als hinreichenden Beleg für einen Verstoß werten, keine Klassen automatisch extrahieren und keine Suppressions verlangen, nur um eine kohäsive Klasse intakt zu lassen. Das Prinzip gilt für alle Sprachprofile (Java, JavaScript, TypeScript, Go, Crystal, Rust, Emvus); sprachspezifische Syntax und Idiome haben weiterhin Vorrang.
Lexer
├─ LexerStateA (delegiert an LexerHelper)
├─ LexerStateB (delegiert an LexerHelper)
└─ LexerHelper (teilt alle Zustände mit beiden) Lexer // eine State Machine, ~500 Zeilen, eine Verantwortung ReportService
├─ PDF-Rendering → ReportRenderer
├─ Mail-Versand → ReportMailer
└─ Abrechnungsregeln → BillingPolicy Abstraktion braucht einen Grund
Duplikation allein rechtfertigt keine Abstraktion. Gute Gründe sind: mehrere echte Implementierungen, klare API-Grenzen, plattformspezifische Implementierungen, Test Seams, Dependency Inversion und gemeinsame Domain-Verträge.
AbstractBaseFactoryManagerProvider public interface ISessionFactory {
Session create(User user);
}
public final class SessionFactoryImpl implements ISessionFactory {
public Session create(User user) {
return new Session(user);
}
} var session = new Session(user); multiple real implementations
clear API boundaries
platform-specific implementations
test seams
dependency inversion
shared domain contracts Formatierung & Kommentare
Kompakte horizontale Aufrufe
Aufrufe bleiben horizontal, solange sie lesbar sind. Argumente werden nicht einzeln untereinander geschrieben, nur weil es geht.
Vertikale Formatierung ist erlaubt, wenn sie echte semantische Struktur zeigt, etwa bei Buildern.
var box = Block.box(
min,
min,
max,
max,
max,
16
); var box = Block.box(min, min, max, max, max, 16); var server = Server.builder()
.host(config.host())
.port(config.port())
.executor(executor)
.build(); Kommentare erklären das Warum
Der Code erklärt, was passiert. Kommentare erklären, warum etwas Ungewöhnliches nötig ist: Protokoll-Constraints, Workarounds, externe Bugs, nicht offensichtliche Mathematik oder ungewöhnliche Performance-Anforderungen.
// Check if the player exists
if (player == null) {
return;
} // timeout in seconds for the handshake
var t = 30; // The protocol allows only one retry, a second one gets the client banned.
if (attempts > MAX_RETRIES) {
return;
} var handshakeTimeout = Duration.ofSeconds(30); Sprache & Werte
Moderne Features, wenn sie Klarheit schaffen
Moderne Sprachmittel werden genutzt, wenn sie Code kürzer, sicherer oder verständlicher machen, nicht um modern auszusehen.
String label;
switch (state) {
case READY:
label = "ready";
break;
default:
label = "unknown";
} var label = switch (state) {
case READY -> "ready";
default -> "unknown";
}; Framework-native APIs bevorzugen
Was die Plattform oder das Framework bereits sauber löst, wird nicht nachgebaut. Eigene Wrapper brauchen einen echten Grund.
function copy(value) {
return JSON.parse(JSON.stringify(value));
} const copy = structuredClone(value); Magic Values vermeiden
Werte mit Bedeutung bekommen einen Namen. Werte, die sich betrieblich ändern können, gehören in die Konfiguration statt tief in die Implementierung.
if (attempts > 3) {
connection.close(4008);
} if (attempts > MAX_LOGIN_ATTEMPTS) {
connection.close(CloseCode.POLICY_VIOLATION);
} Architektur & Laufzeit
Klare Abhängigkeitsrichtung
Abhängigkeiten fließen in eine Richtung: domain → service → infrastructure, oder feature-orientiert in Modulen wie session/, network/, storage/, command/, runtime/, api/.
Zyklen wie network → session → storage → network werden vermieden.
network → session → storage → network public record Session(UUID id, JdbcTemplate jdbc) {
} domain
↓
service
↓
infrastructure Seiteneffekte sichtbar halten
Pureline verlangt nicht, jede interne Operation offenzulegen. Überraschendes, verstecktes Verhalten wird aber vermieden.
sessions.createAndPersistAndNotify(user); var user = users.load(id);
var session = sessions.create(user);
sessions.save(session);
events.publish(new SessionCreated(session.id())); Immutability als Default
Werte sind standardmäßig unveränderlich. Mutation ist bewusst, lokal und leicht erkennbar: Java Records, readonly in TypeScript, const in JavaScript, immutable Value Objects und kurzlebiger lokaler State.
Geteilter veränderlicher Zustand wird vermieden, wo es praktikabel ist.
public static List<Session> ACTIVE = new ArrayList<>();
session.setState(State.CLOSED); public record Session(SessionId id, State state) {
public Session close() {
return new Session(id, State.CLOSED);
}
} interface Session {
readonly id: string;
readonly state: SessionState;
} Explizite Abhängigkeiten
Abhängigkeiten sind in Konstruktoren, Parametern oder expliziter Modul-Verdrahtung sichtbar. Versteckter globaler Zugriff und unnötige Service Locator werden vermieden.
Dependency Injection ist willkommen, ein schweres DI-Framework ist aber keine Pflicht.
public Session open(User user) {
var storage = ServiceLocator.get(IStorage.class);
return storage.create(user);
} public final class SessionService {
private final IStorage storage;
public SessionService(IStorage storage) {
this.storage = storage;
}
} Performance mit Absicht
Lesbarkeit ist der Default. In Hot Paths wird bewusst optimiert: unnötige Allokationen vermeiden, keine wiederholte Reflection, teure Auflösung cachen, passende Collections wählen, kein verstecktes Blocking, weniger Kopien und klare Ownership.
Optimierungen müssen verständlich bleiben.
public void dispatch(Packet packet) {
var handler = packet.getClass().getMethod("handle");
handler.invoke(packet);
} private final Map<Class<?>, IPacketHandler> handlers;
public void dispatch(Packet packet) {
handlers.get(packet.getClass()).handle(packet);
} Ressourcen mit klarem Lebenszyklus
Was geöffnet, registriert, gestartet, allokiert oder abonniert wird, hat einen ebenso klaren Weg zurück. Die Sprachprofile konkretisieren das: try-with-resources in Java, defer in Go, Aufräumpfade in JavaScript.
function watch(element) {
window.addEventListener("resize", () => layout(element));
} function watch(element) {
const onResize = () => layout(element);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
} Review-Checkliste
Ist der Code Pureline? Die Checkliste deckt ab, was kein Checker automatisch beurteilen kann.