Ida – Logo
article_typescript.webp

SOLID Principles in TypeScript: Verbessern von Software-Design und Wartbarkeit

8 min

Einführung

In der sich ständig weiterentwickelnden Welt der Softwareentwicklung sind die SOLID-Prinzipien ein Leuchtfeuer, das Entwickler:innen den Weg zum Schreiben von besser wartbarem, skalierbarem und effizientem Code weist. Ursprünglich von Robert C. Martin eingeführt, sind diese Prinzipien entscheidend für jede:n, der:die seine Codierungspraktiken verfeinern möchte, insbesondere in vielseitigen Sprachen wie TypeScript. In diesem Artikel werden die einzelnen SOLID-Prinzipien anhand von praktischen TypeScript-Beispielen näher erläutert.

S: Single Responsibility Principle (SRP)

Dieses Prinzip besagt, dass eine Klasse nur einen Grund haben sollte, sich zu ändern, d. h. sie sollte nur eine Aufgabe oder Verantwortung haben. Durch die Einhaltung des SRP-Prinzips wird Software einfacher zu warten und zu verstehen, da jede Klasse einen klaren Zweck hat und weniger komplex ist.

Korrekte Verwendung:

// A class with a single responsibility
class User {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName(): string {
        return this.name;
    }
}

// Separate class for database operations
class UserDatabase {
    saveUser(user: User) {
        // Logic to save the user to a database
    }
}

Verstoss:

// A class with multiple responsibilities
class User {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName(): string {
        return this.name;
    }

    saveUser() {
        // Logic to save the user to a database
    }
} 

O: Open/Closed Principle (OCP)

Diesem Prinzip zufolge sollten Klassen offen für Erweiterungen, aber geschlossen für Änderungen sein. Das bedeutet, dass das Verhalten einer Klasse erweitert werden kann, ohne ihren Quellcode zu verändern, typischerweise durch die Verwendung von Schnittstellen oder abstrakten Klassen. Dieser Grundsatz trägt zur Verbesserung der Skalierbarkeit der Software bei und verringert das Risiko, bestehende Funktionen zu zerstören.

Korrekte Verwendung:

abstract class Shape {
    abstract calculateArea(): number;
}

class Rectangle extends Shape {
    constructor(private width: number, private height: number) {
        super();
    }

    calculateArea(): number {
        return this.width * this.height;
    }
}

class Circle extends Shape {
    constructor(private radius: number) {
        super();
    }

    calculateArea(): number {
        return Math.PI * this.radius * this.radius;
    }
}

Verstoss:

class Shape {
    constructor(private shapeType: string, private dimensions: number[]) {}

    calculateArea(): number {
        if (this.shapeType === "rectangle") {
            return this.dimensions[0] * this.dimensions[1];
        } else if (this.shapeType === "circle") {
            return Math.PI * this.dimensions[0] * this.dimensions[0];
        }
        return 0;
    }
}

L: Liskov Substitution Principle (LSP)

Dieses Prinzip besagt, dass Objekte einer Oberklasse durch Objekte einer Unterklasse ersetzt werden können sollten, ohne dass die Korrektheit des Programms beeinträchtigt wird. Es stellt sicher, dass eine Unterklasse für ihre Oberklasse einspringen kann, was zu robusterer und zuverlässigerer Software führt, da ein konsistentes Verhalten über verwandte Klassen hinweg erzwungen wird.

Korrekte Verwendung:

class Bird {
    fly() {
        // Flying logic
    }
}

class Duck extends Bird {}

function makeBirdFly(bird: Bird) {
    bird.fly();
}

const duck = new Duck();
makeBirdFly(duck); // Duck can substitute Bird

Verstoss:

class Bird {
    fly() {
        // Flying logic
    }
}

class Penguin extends Bird {
    fly() {
        throw new Error("Cannot fly");
    }
}

function makeBirdFly(bird: Bird) {
    bird.fly();
}

const penguin = new Penguin();
makeBirdFly(penguin); // Penguin cannot substitute Bird without altering the correctness of the program

I: Interface Segregation Principle (ISP)

Dieses Prinzip befürwortet die Schaffung kleiner, spezifischer Schnittstellen anstelle von großen, allgemein verwendbaren Schnittstellen. Auf diese Weise müssen Klassen keine Schnittstellen implementieren, die sie nicht verwenden, was die Belastung durch unnötige Abhängigkeiten verringert und die Software modularer und verständlicher macht.

Korrekte Verwendung:

interface Printable {
    print(): void;
}

interface Scannable {
    scan(): void;
}

class AllInOnePrinter implements Printable, Scannable {
    print() { /* printing logic */ }
    scan() { /* scanning logic */ }
}

class SimplePrinter implements Printable {
    print() { /* printing logic */ }
}

Verstoss:

interface MultiFunctionDevice {
    print(): void;
    scan(): void;
}

class SimplePrinter implements MultiFunctionDevice {
    print() { /* printing logic */ }
    scan() {
        throw new Error("Scan function not supported");
    }
}

D: Dependency Inversion Principle (DIP)

Dieses Prinzip besagt, dass High-Level-Module nicht von Low-Level-Modulen abhängen sollten, sondern dass beide von Abstraktionen abhängen sollten. Ausserdem sollten diese Abstraktionen nicht von Details abhängen, sondern Details sollten von Abstraktionen abhängen. DIP führt zu entkoppeltem und damit leicht wartbarem und testbarem Code.

Korrekte Verwendung:

interface Database {
    save(data: string): void;
}

class MongoDatabase implements Database {
    save(data: string) { /* MongoDB saving logic */ }
}

class DataProcessor {
    private database: Database;

    constructor(database: Database) {
        this.database = database;
    }

    processData(data: string) {
        this.database.save(data);
    }
}

const mongoDB = new MongoDatabase();
const processor = new DataProcessor(mongoDB);

Verstoss:

class MongoDatabase {
    save(data: string) { /* MongoDB saving logic */ }
}

class DataProcessor {
    private database: MongoDatabase;

    constructor() {
        this.database = new MongoDatabase();
    }

    processData(data: string) {
        this.database.save(data);
    }
}

const processor = new DataProcessor();

 

 

Schlussfolgerung

Zusammenfassend lässt sich sagen, dass die SOLID-Prinzipien einen grundlegenden Rahmen für das Schreiben von sauberem, effizientem und wartbarem Code in TypeScript bieten. Jedes Prinzip - Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation und Dependency Inversion- befasst sich mit einem bestimmten Aspekt des Software-Designs und der Entwicklung. Durch die Einhaltung dieser Prinzipien können Entwickler:innen häufige Fallstricke wie eng gekoppelten Code, Unflexibilität angesichts sich ändernder Anforderungen und Schwierigkeiten beim Testen und Warten der Software vermeiden.

Die Übernahme der SOLID-Grundsätze erfordert einen disziplinierten Ansatz und eine Änderung der Denkweise. Dazu gehört, dass sich von Anfang an mehr Gedanken über das Design der Software gemacht werden und darauf geachtet wird, wie sich Änderungen in einem Teil des Systems auf andere auswirken können. Die langfristigen Vorteile wie leichteres Debugging, einfachere Aktualisierungen und besser lesbarer Code überwiegen jedoch bei weitem den anfänglichen Aufwand.

In TypeScript ist die Anwendung dieser Prinzipien besonders effektiv, da die Sprache starke Typisierung, Klassen und Schnittstellen unterstützt. Diese Unterstützung ermöglicht klare, explizite und durchsetzbare Kodierungspraktiken, die gut mit den SOLID-Prinzipien übereinstimmen. Die praktischen Beispiele in diesem Artikel zeigen, wie die Funktionen von TypeScript genutzt werden können, um diese Prinzipien einzuhalten, was letztlich zu einer höheren Softwarequalität führt.

Da sich die Softwareentwicklungslandschaft ständig weiterentwickelt, wird die Wartung und Skalierung komplexer Systeme immer wichtiger. In diesem Zusammenhang dienen die SOLID-Prinzipien nicht nur als Richtlinien, sondern als unverzichtbare Werkzeuge für jede:n Entwickler:in, um robuste, effiziente und anpassungsfähige Software zu entwickeln. Unabhängig davon, ob Du ein:e Anfänger:in oder ein:e erfahrene:r Entwickler:in bist, wird die Integration dieser Prinzipien in Ihre TypeScript-Codierungspraktiken zweifellos erhebliche Vorteile bringen, sowohl für dich als auch für die von dir entwickelte Software.


Mehr Beiträge

46.  Einführung von PartyTown

PartyTown optimiert die Web-Performance, indem es Skripte von Drittanbieter:innen vom Haupt-Thread des Browsers in einen Web-Worker im Hintergrund verlagert, wodurch der Wettbewerb um Ressourcen reduziert und die Ladezeiten sowie die Benutzungsfreundlichkeit verbessert werden.