Zum Inhalt
wannacode

SOLID ohne Dogma

Kurz gesagt: SOLID sind fünf Prinzipien für Code, der Änderungen übersteht: ein Grund zur Änderung, Erweiterung ohne Änderung, austauschbare Untertypen, kleine Interfaces und Abhängigkeit von Abstraktionen.

Problem

Die E-Mail-Änderung, die die Buchhaltung gebrochen hat

Der Online-Shop schickt dem Kunden eine Rechnung per E-Mail. Jeden Abend gibt er der Buchhaltung außerdem eine Datei mit einer Zeile pro Bestellung. Eine Klasse macht beides:

class Order {
constructor(number, customer, net) {
this.number = number;
this.customer = customer;
this.net = net;
}
}
class InvoiceService {
vat(order) {
return order.net * 0.2;
}
// Die Buchhaltung: eine Zeile für die Datei, die sie in ihre Software einliest.
exportLine(order) {
const total = order.net + this.vat(order);
return [order.number, this.#money(order.net), this.#money(this.vat(order)), this.#money(total)].join(";");
}
// Der Kunde: der Text der E-Mail mit der Rechnung.
emailText(order) {
const total = order.net + this.vat(order);
return `Hallo, ${order.customer}! Bestellung ${order.number}: ${this.#money(total)} €`;
}
#money(amount) {
return amount.toFixed(2);
}
}
const invoices = new InvoiceService();
const order = new Order("2026-0142", "Maria", 80);
console.log(invoices.exportLine(order));
console.log(invoices.emailText(order));
Ausgabe
2026-0142;80.00;16.00;96.00
Hallo, Maria! Bestellung 2026-0142: 96.00 €

Marketing sagt: „Schreibt in der E-Mail bitte 96 €, nicht 96,00 €. Das sieht sauberer aus.“ Jemand ändert die Hilfsmethode Methode #money:

#money(amount) {
if (Number.isInteger(amount)) {
return String(amount);
}
return amount.toFixed(2);
}
Ausgabe
2026-0142;80;16;96
Hallo, Maria! Bestellung 2026-0142: 96 €

Die E-Mail ist so, wie sie sie wollten. Am nächsten Morgen lehnt die Buchhaltungssoftware die Datei ab: Sie erwartet 80.00 und bekommt 80. Niemand hat die Zeile der Buchhaltung angefasst. Geändert hat sich die Hilfsmethode, die sich beide Zeilen teilen. Der Test der E-Mail läuft durch, und für die Datei gibt es keinen Test, denn „daran hat ja niemand etwas geändert“.

Die Idee

Ein Grund zur Änderung

Die Klasse bedient zwei Parteien, die einander nicht kennen. Die Buchhalterin braucht ein exaktes Format, das ihre Software liest. Marketing will einen angenehmen Text für den Kunden. Sie verlangen Änderungen zu verschiedenen Zeiten und aus verschiedenen Gründen. Solange ihr Code in einer Klasse lebt, kann eine Änderung für den einen den anderen brechen.

Das ist das erste der fünf SOLID-Prinzipien (gesammelt von Robert Martin; der Name entsteht aus ihren Anfangsbuchstaben). Das Single Responsibility Principle (SRP): Eine Klasse hat einen Grund zur Änderung.

„Grund“ heißt, wer die Änderung verlangt: eine Person, eine Abteilung, ein Gesetz. Es heißt nicht „eine Methode“, und es heißt nicht „eine kleine Klasse“. Die Frage an dich selbst: „Wer wird kommen und hier eine Änderung verlangen?“ Wenn die Antwort „die Buchhaltung oder das Marketing“ lautet, sind das zwei Klassen.

Sieh es dir an

Jeder bekommt seine eigene Klasse

Die Mehrwertsteuer und die Summe sind eine Regel des Steuerrechts, und beide Parteien brauchen sie. Sie gehören an einer Stelle in die Bestellung:

class Order {
constructor(number, customer, net) {
this.number = number;
this.customer = customer;
this.net = net;
}
vat() {
return this.net * 0.2;
}
total() {
return this.net + this.vat();
}
}

Die Zeile für die Buchhaltung und der Text der E-Mail werden getrennte Klassen. Jede formatiert Beträge auf ihre Art:

class AccountingExport {
line(order) {
return [order.number, order.net.toFixed(2), order.vat().toFixed(2), order.total().toFixed(2)].join(";");
}
}
class InvoiceEmail {
text(order) {
return `Hallo, ${order.customer}! Bestellung ${order.number}: ${this.#money(order.total())} €`;
}
#money(amount) {
if (Number.isInteger(amount)) {
return String(amount);
}
return amount.toFixed(2);
}
}
const order = new Order("2026-0142", "Maria", 80);
console.log(new AccountingExport().line(order));
console.log(new InvoiceEmail().text(order));
Ausgabe
2026-0142;80.00;16.00;96.00
Hallo, Maria! Bestellung 2026-0142: 96 €

Die E-Mail hat ganze Beträge, und die Datei hat weiter zwei Nachkommastellen. Morgen ändert die Buchhaltung das Format, und du fasst nur AccountingExport an. Der Code der E-Mail liegt gar nicht vor dir.

Die zwei Arten, einen Betrag zu schreiben, werden nicht mehr geteilt. Das ist keine Duplizierung: Sie sehen heute ähnlich aus, aber es sind zwei Regeln, die sich aus verschiedenen Gründen ändern.

Wann es wehtut. Das andere Extrem ist eine Klasse für jede Handlung: VatCalculator, TotalCalculator, NumberFormatter, GreetingBuilder, je drei Zeilen. Sie ändern sich immer zusammen, weil dieselbe Person sie verlangt, und du öffnest vier Dateien, um eine Regel zu lesen. Eine Klasse mit 200 Zeilen, die eine Person bedient, ist besser als fünf Klassen, die du immer zusammen öffnest. Was sich zusammen ändert, bleibt zusammen.

Genauer, für Neugierige
  • Robert Martin selbst schreibt, dass „eine Verantwortung“ oft als „macht eine Sache“ gelesen wird. Seine spätere Klarstellung: Ein Modul antwortet einem Akteur, einer Gruppe von Menschen, die Änderungen verlangen. „Macht eine Sache“ ist eine Regel für Funktionen, nicht für Klassen.
  • „Don't repeat yourself“ (DRY) geht um Wissen, nicht um Text. Zwei Stücke Code, die gleich aussehen, sich aber aus verschiedenen Gründen ändern, sind keine Duplizierung. Wenn du sie in eine Hilfsmethode zusammenlegst, bindest du zwei Menschen mit einem Knoten aneinander. Genau das ist im Problem passiert.

Probier es aus

Wer verlangt die Änderung

1. Welche Frage verrät dir, ob eine Klasse nur einen Grund zur Änderung hat?
2. ShippingLabel berechnet den Preis der Lieferung (der Vertrieb verlangt Änderungen daran), zeichnet das Etikett (das Format gehört der Kurierfirma) und schreibt die Zeile fürs Lager (das Lager verlangt sie). Wie ordnest du das?

Sieh es dir an

Eine neue Lieferung, ohne die Summe anzufassen

Das zweite Prinzip ist open/closed (Open/Closed Principle, OCP): Code ist offen für Erweiterung und geschlossen für Änderung. Eine neue Variante kommt als neuer Code, und der Code, der schon funktioniert und getestet ist, wird nicht angefasst.

Du hast es in der Lektion Strategy gesehen: Die Kette von ifs für die Zahlungsart wurde zu einer Tabelle Name → Strategie. Mit den Lieferungen ist es dasselbe. Jede Lieferung kennt ihren Preis:

function toAddress(amount) {
return amount >= 100 ? 0 : 5.9;
}
function toOffice(amount) {
return 3.9;
}

Die Summe weiß nicht, welche Lieferungen es gibt:

function orderTotal(amount, delivery) {
return amount + delivery(amount);
}

Der Shop bekommt die Lieferung an eine Packstation. Das ist eine neue Funktion:

function toLocker(amount) {
return 2.9;
}

orderTotal, die Lieferung an eine Adresse und ins Büro werden nicht angefasst. Es bleibt, die neue Lieferung in die Tabelle zu setzen, aus der der Kunde wählt. Das ist die nächste Karte.

Wann es wehtut. Du kannst den Code nicht gegen alles schließen, und du sollst es auch nicht. Jeder Erweiterungspunkt ist eine Funktion und eine Zeile in einer Tabelle, die jemand lesen muss. Du schließt dort, wo Änderungen wirklich kommen: Neue Lieferungen kommen jede Saison. Die Faustregel: Beim ersten Mal schreibst du ein if. Wenn die dritte Variante kommt, machst du die Tabelle (Replace Conditional with Polymorphism aus der Lektion zum Refactoring).

Probier es aus

Eine neue Zeile in der Tabelle

Die Tabelle aus der Strategy-Lektion listet die Lieferungen auf, aus denen der Kunde wählt. Füge die Packstation hinzu.

Was kommt in die Lücke?

const deliveries = new Map([
  ["Adresse", toAddress],
  ["Büro", toOffice],
  ["Packstation", ],
]);

for (const [choice, delivery] of deliveries) {
  console.log(`${choice}: ${orderTotal(80, delivery).toFixed(2)} €`);
}

Problem

Die Stornierung, die auf halbem Weg stehen blieb

Eine Kundin bezahlt eine Bestellung auf drei Arten: 60 € mit Karte, einen Gutschein über 20 € und 16 € in Punkten. Dann storniert sie. Jede Zahlungsart weiß, wie sie ihr Geld zurückgibt:

// Jede Zahlung hat refund(): Es erstattet dem Kunden das Geld auf dem Weg, auf dem er bezahlt hat.
class Payment {
constructor(amount) {
this.amount = amount;
}
}
class CardPayment extends Payment {
refund() {
console.log(`Karte: erstattet ${this.amount.toFixed(2)} €`);
}
}
class VoucherPayment extends Payment {
refund() {
throw new Error("ein Gutschein wird nicht erstattet");
}
}
class PointsPayment extends Payment {
refund() {
console.log(`Punkte: erstattet ${this.amount.toFixed(2)} €`);
}
}

Die Bestellung geht ihre Zahlungen durch und ruft bei jeder refund() auf:

class Order {
constructor(payments) {
this.payments = payments;
this.status = "bezahlt";
}
cancel() {
for (const payment of this.payments) {
payment.refund();
}
this.status = "storniert";
}
}
const order = new Order([
new CardPayment(60),
new VoucherPayment(20),
new PointsPayment(16),
]);
try {
order.cancel();
} catch (e) {
console.log(`Fehler: ${e.message}`);
}
console.log(`Status: ${order.status}`);
Ausgabe
Karte: erstattet 60.00 €
Fehler: ein Gutschein wird nicht erstattet
Status: bezahlt

Die Karte ist erstattet, die Punkte nicht, und die Bestellung ist noch „bezahlt“. Das Lager sieht „bezahlt“ und verschickt das Paket. Der Code von cancel() ist richtig. Der Fehler steckt im Gutschein: Er ist ein Payment, tut aber nicht, was Payment verspricht.

Sieh es dir an

Der Untertyp, der das Versprechen bricht

cancel() {
for (const payment of this.payments) {
payment.refund();
}
this.status = "storniert";
}
const order = new Order([
new CardPayment(60),
new VoucherPayment(20),
new PointsPayment(16),
]);
try {
order.cancel();
} catch (e) {
console.log(`Fehler: ${e.message}`);
}
console.log(`Status: ${order.status}`);
Schritt für Schritt, in Worten
  1. Die Bestellung wurde auf drei Arten bezahlt: 60.00 € mit Karte, ein Gutschein über 20.00 € und 16.00 € in Punkten. Alle drei sind Payment.
  2. Die Kundin storniert. cancel() geht die Zahlungen durch und ruft bei jeder refund() auf.
  3. Die Karte erstattet 60.00 €. Sie tut genau das, was Payment verspricht.
  4. Der Gutschein ist auch ein Payment, aber sein refund() wirft Error. Das Versprechen ist gebrochen.
  5. Die Exception verlässt die Schleife und cancel(). Die Punkte (16.00 €) werden nicht erstattet, und die Zeile, die status auf „storniert“ setzt, läuft nie.
  6. Das catch in dem Programm fängt sie ab und gibt den Fehler aus. Das Programm läuft weiter.
  7. Der Status ist noch immer „bezahlt“. Das Lager verschickt die Bestellung, und 60.00 € sind schon wieder auf der Karte.

Das ist das dritte Prinzip, Barbara Liskovs Substitutionsprinzip (Liskov Substitution Principle, LSP): Überall, wo der Code mit einem Typ arbeitet, kann jeder Untertyp einspringen, und der Code merkt es nicht. Der Untertyp darf die Arbeit auf seine Art erledigen, aber er hält das Versprechen des Typs.

Payment verspricht „Ich gebe dem Kunden das Geld zurück“. Was heißt das für einen Gutschein in einem echten Shop? Einen neuen Gutschein über denselben Betrag:

class VoucherPayment extends Payment {
refund() {
console.log(`Gutschein: neuer Gutschein über ${this.amount.toFixed(2)} €`);
}
}
const order = new Order([
new CardPayment(60),
new VoucherPayment(20),
new PointsPayment(16),
]);
try {
order.cancel();
} catch (e) {
console.log(`Fehler: ${e.message}`);
}
console.log(`Status: ${order.status}`);
Ausgabe
Karte: erstattet 60.00 €
Gutschein: neuer Gutschein über 20.00 €
Punkte: erstattet 16.00 €
Status: storniert

cancel() hat sich nicht geändert. Der Untertyp passt jetzt dorthin, wo der Typ erwartet wird.

Eine Verletzung erkennst du an drei Zeichen:

  • Der Untertyp wirft „nicht unterstützt“ (einen Fehler) bei einer Methode, die der Typ verspricht;
  • der Untertyp liefert etwas, das der Typ nicht verspricht (-1, null, einen leeren String), oder verlangt mehr vom Aufrufer;
  • der Code prüft mit instanceof, bevor er die Methode aufruft. Das ist ein Umweg um einen Untertyp, der nicht passt.
Die Sprache sieht keines der drei, und ohne ein Interface in JavaScript lebt das Versprechen nur im Namen der Methode und im Kommentar. Deshalb muss man es aufschreiben und mit einem Test prüfen.

Wann es wehtut. Hier geht das Dogma in die andere Richtung: etwas in die Hierarchie zu zwingen, „weil es auch eine Zahlung ist“. Wenn etwas wirklich kein Geld zurückgeben kann (etwa ein Geschenk des Shops), ist es kein Payment mit refund. Gib ihm lieber einen eigenen Typ, statt eine Methode zu schreiben, die wirft.

Genauer, für Neugierige
Auch die Sprache kennt so einen Fall. Ein eingefrorenes Array ist weiter ein Array (Array.isArray sagt true), nimmt aber push nicht an:
const items = Object.freeze(["Kaffee", "Tasse"]);
try {
items.push("Tee");
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Ausgabe
TypeError: Cannot add property 2, object is not extensible
In einem Modul (und mit "use strict") ist der Fehler ein TypeError. Außerhalb des Strict-Modus wirft push weiterhin, aber das Zuweisen eines Elements tut still nichts.

Formal, nach Liskov: Ein Untertyp verlangt nicht mehr als der Typ (verschärft die Bedingungen an die Eingabe nicht), liefert nicht weniger (schwächt das Versprechen über das Ergebnis nicht ab) und bewahrt, was für den Typ immer gilt.

Probier es aus

Die stille Verletzung

Die Lieferung an die Packstation gibt -1 zurück, wenn die Station voll ist. Die Summe addiert es, wie immer. Was gibt das Programm aus?

function toLocker(full) {
return (amount) => {
if (full) {
return -1; // -1 heißt „kein Platz“
}
return 2.9;
};
}
console.log(`${orderTotal(80, toLocker(true)).toFixed(2)} €`);
Antwort

Problem

Die SMS, die nie rausging

Der Shop schickt Kunden Benachrichtigungen. Jemand hat alles, was versendet werden kann, an einer Stelle beschrieben:

// Benachrichtigungen: Jeder, der Benachrichtigungen verschickt, hat email(to, text) und sms(phone, text).
Der E-Mail-Anbieter verschickt nur E-Mails. Der Versandcode ruft notifier.sms(...) auf, und ohne die Methode bricht das Programm mit einem TypeError ab. Also hat jemand sie geschrieben.

Probier es aus

Finde den Fehler

Der Kunde bekommt eine E-Mail, dass das Paket versandt wurde. Die SMS des Kuriers kommt nie:

const notifier = new MailProvider();
notifier.email("maria@example.com", "Bestellung 2026-0142 ist versandt.");
notifier.sms("0888 123 456", "Der Kurier kommt heute zwischen 14 und 16 Uhr.");
Ausgabe
E-Mail an maria@example.com: Bestellung 2026-0142 ist versandt.

In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Sieh es dir an

Kleine Interfaces

Das vierte Prinzip: Interface Segregation (Interface Segregation Principle, ISP). Niemand soll von Methoden abhängen, die er nicht nutzt. Der Vertrag wird aus der Sicht dessen geschrieben, der ihn nutzt, und er ist so groß, wie er ihn braucht.

JavaScript hat keine Interfaces. Der kleine Vertrag ist das, was der Code aufruft: Die Kurier-Nachricht ruft nur sms auf, also passt jedes Objekt mit sms. Hier heißt das Prinzip: Verlange nicht mehr, als du aufrufst, und lass niemanden leere Methoden schreiben.

Jeder Anbieter macht nur, was er kann, und die Kurier-Nachricht verlangt nur SMS:

class MailProvider {
email(to, text) {
console.log(`E-Mail an ${to}: ${text}`);
}
}
class SmsGateway {
sms(phone, text) {
console.log(`SMS an ${phone}: ${text}`);
}
}
class ShippingNotice {
constructor(sender) {
this.sender = sender;
}
courierToday(phone) {
this.sender.sms(phone, "Der Kurier kommt heute zwischen 14 und 16 Uhr.");
}
}
const email = new MailProvider();
email.email("maria@example.com", "Bestellung 2026-0142 ist versandt.");
new ShippingNotice(new SmsGateway()).courierToday("0888 123 456");
Ausgabe
E-Mail an maria@example.com: Bestellung 2026-0142 ist versandt.
SMS an 0888 123 456: Der Kurier kommt heute zwischen 14 und 16 Uhr.

Wenn jemand den E-Mail-Anbieter übergibt, wo eine SMS gebraucht wird, bricht das Programm beim ersten Aufruf ab, statt still zu bleiben:

try {
new ShippingNotice(new MailProvider()).courierToday("0888 123 456");
} catch (e) {
console.log(`${e.name}: ${e.message}`);
}
Ausgabe
TypeError: this.sender.sms is not a function

Kleine Interfaces helfen auch den Tests: Der Ersatz für die Nachricht implementiert eine Methode, nicht alles, was der Shop verschicken kann.

Wann es wehtut. Ein Interface für jede Methode „wegen ISP“ ist das andere Extrem: Readable, Writable, Deletable, Countable vor einem Repository, das an einer Stelle und immer als Ganzes benutzt wird. Wenn jeder, der den Vertrag nutzt, ihn ganz nutzt, ist es ein Vertrag.

Problem

Der Test, der eine echte E-Mail verschickt

Die Rechnung geht per E-Mail raus. Der Service baut sich seinen Mailer selbst:

class SmtpMailer {
send(to, text) {
console.log(`SMTP an ${to}: ${text}`);
}
}
class InvoiceService {
constructor() {
this.mailer = new SmtpMailer();
}
issue(order, email) {
const total = order.total().toFixed(2);
const text = `Rechnung ${order.number}: ${total} €`;
this.mailer.send(email, text);
}
}
const order = new Order("2026-0142", "Maria", 80);
new InvoiceService().issue(order, "maria@example.com");
Ausgabe
SMTP an maria@example.com: Rechnung 2026-0142: 96.00 €

Es funktioniert, aber:

  • Der Test der Rechnung verschickt eine echte E-Mail, und auf einem Rechner ohne SMTP-Server schlägt er fehl;
  • der Shop wechselt von SMTP zu einem Dienst mit API, und du änderst die Klasse mit den Regeln der Rechnung, um den Mailer zu wechseln.

Die Regel (was auf die Rechnung kommt) hängt vom Detail ab (wie die E-Mail rausgeht).

Sieh es dir an

Die Abstraktion gehört der Regel

Das fünfte Prinzip: Dependency Inversion (Dependency Inversion Principle, DIP). Der Code mit den Regeln hängt nicht von den Details ab. Beide hängen von einer Abstraktion ab, und sie gehört den Regeln: Sie sagt, was die Regeln brauchen („schicke diesen Text an diese Adresse“), nicht wie SMTP es macht.

class InvoiceService {
// sender ist jedes Objekt mit einer Methode send(to, text).
constructor(sender) {
this.sender = sender;
}
issue(order, email) {
const total = order.total().toFixed(2);
const text = `Rechnung ${order.number}: ${total} €`;
this.sender.send(email, text);
}
}

Die Details stehen getrennt. Der Mailer und der Testersatz haben die Methode, die der Service aufruft:

class SmtpSender {
send(to, text) {
console.log(`SMTP an ${to}: ${text}`);
}
}
class FakeSender {
constructor() {
this.sent = [];
}
send(to, text) {
this.sent.push(`${to} | ${text}`);
}
}
const order = new Order("2026-0142", "Maria", 80);
new InvoiceService(new SmtpSender())
.issue(order, "maria@example.com");
const fake = new FakeSender();
const service = new InvoiceService(fake);
service.issue(order, "maria@example.com");
console.log(`Der Test sah: ${fake.sent[0]}`);
Ausgabe
SMTP an maria@example.com: Rechnung 2026-0142: 96.00 €
Der Test sah: maria@example.com | Rechnung 2026-0142: 96.00 €

class InvoiceService {
// sender ist jedes Objekt mit einer Methode send(to, text).
constructor(sender) {
this.sender = sender;
}
issue(order, email) {
const total = order.total().toFixed(2);
const text = `Rechnung ${order.number}: ${total} €`;
this.sender.send(email, text);
}
}
class SmtpSender {
send(to, text) {
console.log(`SMTP an ${to}: ${text}`);
}
}
const order = new Order("2026-0142", "Maria", 80);
new InvoiceService(new SmtpSender())
.issue(order, "maria@example.com");
const fake = new FakeSender();
const service = new InvoiceService(fake);
service.issue(order, "maria@example.com");
console.log(`Der Test sah: ${fake.sent[0]}`);
Schritt für Schritt, in Worten
  1. Vorher zeigten die Rechnungen nach unten, auf SmtpMailer. Jetzt schreibt der Service auf, was er braucht: ein Objekt mit send(to, text).
  2. InvoiceService bekommt den Versand im Konstruktor und kennt nur send(to, text). Von SMTP weiß er nichts.
  3. SmtpSender hat send(to, text), den Vertrag, den der Service beschrieben hat. Der Pfeil zeigt nach oben: Das Detail hängt von der Regel ab. Das ist die Umkehrung.
  4. Zur Laufzeit geht der Aufruf weiter nach unten: vom Service zu SMTP. Umgekehrt ist die Abhängigkeit im Code, nicht die Richtung des Aufrufs.
  5. Im Test bekommt derselbe Service FakeSender: ein zweites Detail, das auf dieselbe Abstraktion zeigt. Es geht keine E-Mail raus: Der Ersatz schreibt in sent auf, was er bekommen hat.
  6. Der Test sieht genau den Text der Rechnung, ohne Mailserver.

Die „Inversion“ betrifft den Pfeil im Code. Vorher kannten die Rechnungen SMTP. Jetzt kennt SMTP den Vertrag send(to, text), der neben den Rechnungen steht. Zur Laufzeit geht der Aufruf weiter von den Rechnungen zu SMTP.

DIP und DI. In der Lektion Singleton und Dependency Injection hast du die Übergabe von außen gesehen: Das Objekt bekommt seine Abhängigkeit im Konstruktor. Das ist der Mechanismus. DIP sagt, wovon man abhängen soll: von einer Abstraktion, die dir gehört. Das eine geht auch ohne das andere:

  • Übergabe ohne DIP: Der Service bekommt den Mailer von außen, aber seine Datei importiert weiter import { SmtpMailer } from "./mail.js" und ruft etwas auf, das nur SMTP hat;
  • DIP ohne DI-Bibliothek: In dem Programm bekommt InvoiceService den SmtpSender, und das ist die ganze Verdrahtung. Es gibt eine Stelle, an der das Programm die Objekte zusammensetzt.

Wann es wehtut. Ein Interface vor jeder Klasse „wegen DIP“ ist das häufigste Dogma. Eine Abstraktion lohnt sich vor einem Detail, das langsam, extern oder veränderlich ist: Mail, Zahlung, Datenbank, Uhr, Netzwerk. Vor Order oder vor dem Formatieren eines Betrags ist sie Code zum Lesen ohne Nutzen.

Genauer, für Neugierige
In einem echten Projekt importiert invoices.js nicht smtp.js. main.js importiert beide und übergibt new SmtpSender() an den Service. Es gibt keine Abstraktion als Typ: Es ist die Methode send(to, text), im Kommentar beschrieben und vom Test mit dem Ersatz geprüft.

Probier es aus

Bring den Test in Ordnung

Der Test prüft die Rechnung ohne echten Mailer. Die order gibt es schon. Bring die Zeilen in die richtige Reihenfolge:

  1. const service = new InvoiceService(fake);
  2. service.issue(order, "maria@example.com");
  3. console.log(`Der Test sah: ${fake.sent[0]}`);
  4. const fake = new FakeSender();

Kurz gesagt

Wenn SOLID wehtut

Hier ist die Mehrwertsteuer, geschrieben „nach allen Regeln“:

class StandardVat {
vat(net) {
return net * 0.2;
}
}
class TaxPolicyFactory {
static create() {
return new StandardVat();
}
}
console.log(`MwSt.: ${TaxPolicyFactory.create().vat(80).toFixed(2)} €`);
Ausgabe
MwSt.: 16.00 €

Und dasselbe ohne Dogma:

const VAT_RATE = 0.2;
console.log(`MwSt.: ${(80 * VAT_RATE).toFixed(2)} €`);
Ausgabe
MwSt.: 16.00 €

Die Ausgabe ist dieselbe. Das Erste ist eine Klasse und eine Fabrik für eine Zahl, die sich einmal in zehn Jahren ändert, und ohnehin an einer Stelle. Das ist der Smell namens spekulative Allgemeinheit (speculative generality): Code für Änderungen, die niemand verlangt hat. Wenn ein zweiter Satz kommt (9 % für Lebensmittel), ist das der Moment für eine Tabelle.

Die Prinzipien sind Fragen, keine Regeln. Stell sie, wenn der Code wehtut:

Die Frage Das Dogma, das du meiden sollst
SRP Wer wird hier eine Änderung verlangen? Eine Partei oder zwei? Eine Klasse für jede Methode
OCP Was kommt wirklich jeden Monat neu? Erweiterungspunkte überall
LSP Kann jeder Untertyp hier ohne ein if stehen? Eine Hierarchie wegen des Wortes „ist“
ISP Nutzt dieser Client alles, was er verlangt? Ein Interface für jede Methode
DIP Ist das Detail langsam, extern oder austauschbar? Ein Interface vor jeder Klasse

Übung

Eine Geschenkkarte, ohne die Stornierung anzufassen

Löse die Aufgabe in deinem Editor.

Geschenkkarte. Der Shop nimmt auch Geschenkkarten an. Bei einer Stornierung geht das Geld zurück auf die Karte. Füge eine GiftCardPayment hinzu, die das Versprechen von Payment hält, und storniere eine Bestellung, die mit 40 € per Karte und 25 € per Geschenkkarte bezahlt wurde. cancel() und die anderen Zahlungen werden nicht angefasst.

Ausgabe
Karte: erstattet 40.00 €
Geschenkkarte: 25.00 € zurück auf die Karte
Status: storniert

Weitere Aufgaben

  • Der Monatsbericht. Eine Klasse berechnet den Umsatz des Monats, schreibt eine CSV für die Buchhaltung und einen Text für die Geschäftsführung und verschickt die E-Mail selbst. Teile sie danach, wer die Änderungen verlangt. Was bleibt gemeinsam?
  • Push-Nachricht. Füge in der mobilen App eine Benachrichtigung hinzu, wenn das Paket versandt wurde. Welchen kleinen Vertrag brauchst du, und welche Klassen fasst du nicht an?
  • Eine Abstraktion entfernen. Suche in deinem eigenen Projekt eine Klasse oder Funktion, die „für alle Fälle“ eingepackt wurde, hinter der kein langsames oder externes Detail steckt. Entferne die Hülle und lass die Tests laufen.

Test

Teste dich

1. Marketing verlangt eine Änderung am Text der E-Mail, und danach geht die Buchhaltungsdatei kaputt. Welches Prinzip ist verletzt?
2. Der Shop bekommt die Lieferung an eine Packstation. Was bedeutet „geschlossen für Änderungen“?
3. VoucherPayment ist ein Payment, aber refund wirft den Fehler „kann nicht erstattet werden“. Was passiert mit Code, der refund bei jeder Zahlung aufruft?
4. Der E-Mail-Anbieter hat eine leere Methode sms, weil der Code sms bei jedem aufruft, der Benachrichtigungen verschickt. Was sagt dir das?
5. InvoiceService macht im Konstruktor self.mailer = SmtpMailer() (this.mailer = new SmtpMailer()). Was bricht das?
6. Wann lohnt sich ein Interface mit nur einer Implementierung?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·