Zum Inhalt springen
TypeScript Fortgeschritten 25 min

Abstrakte Klassen

Lerne abstrakte Klassen und Methoden in TypeScript für flexible Basisklassen-Designs.

Aktualisiert:

Abstrakte Klassen

Abstrakte Klassen sind Klassen, die nicht direkt instanziiert werden können. Sie dienen als Vorlage für andere Klassen und können sowohl implementierte als auch abstrakte Methoden enthalten.

Grundlagen

// Abstrakte Klasse
abstract class Animal {
    constructor(public name: string) {}

    // Implementierte Methode
    move(): string {
        return `${this.name} is moving`;
    }

    // Abstrakte Methode - muss in Subklassen implementiert werden
    abstract speak(): string;
}

// Kann nicht instanziiert werden:
// const animal = new Animal("Generic");  // Fehler!

// Konkrete Klasse
class Dog extends Animal {
    // Muss speak() implementieren
    speak(): string {
        return `${this.name} says: Woof!`;
    }
}

class Cat extends Animal {
    speak(): string {
        return `${this.name} says: Meow!`;
    }
}

const dog = new Dog("Buddy");
console.log(dog.speak());  // "Buddy says: Woof!"
console.log(dog.move());   // "Buddy is moving"

Abstrakte Properties

abstract class Shape {
    abstract readonly name: string;
    abstract color: string;

    abstract getArea(): number;

    describe(): string {
        return `A ${this.color} ${this.name}`;
    }
}

class Rectangle extends Shape {
    readonly name = "rectangle";

    constructor(
        public color: string,
        public width: number,
        public height: number
    ) {
        super();
    }

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

class Circle extends Shape {
    readonly name = "circle";

    constructor(
        public color: string,
        public radius: number
    ) {
        super();
    }

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

const shapes: Shape[] = [
    new Rectangle("blue", 10, 5),
    new Circle("red", 7)
];

shapes.forEach(shape => {
    console.log(shape.describe(), "- Area:", shape.getArea().toFixed(2));
});

Abstrakte Getter und Setter

abstract class Component {
    abstract get isVisible(): boolean;
    abstract set visible(value: boolean);

    abstract render(): string;

    log(): void {
        console.log(`Component visible: ${this.isVisible}`);
    }
}

class Modal extends Component {
    private _visible = false;

    get isVisible(): boolean {
        return this._visible;
    }

    set visible(value: boolean) {
        this._visible = value;
        console.log(`Modal is now ${value ? "visible" : "hidden"}`);
    }

    render(): string {
        return `<Modal visible=${this._visible}>`;
    }
}

const modal = new Modal();
modal.visible = true;
console.log(modal.render());

Template Method Pattern

Ein klassisches Design Pattern mit abstrakten Klassen:

abstract class DataProcessor {
    // Template Method - definiert den Algorithmus
    process(data: string): string {
        const validated = this.validate(data);
        const transformed = this.transform(validated);
        const formatted = this.format(transformed);
        return formatted;
    }

    // Abstrakte Schritte - müssen implementiert werden
    protected abstract validate(data: string): string;
    protected abstract transform(data: string): string;

    // Optionaler Schritt mit Default-Implementierung
    protected format(data: string): string {
        return data.trim();
    }
}

class JsonProcessor extends DataProcessor {
    protected validate(data: string): string {
        JSON.parse(data);  // Throws if invalid
        return data;
    }

    protected transform(data: string): string {
        const obj = JSON.parse(data);
        return JSON.stringify(obj, null, 2);
    }
}

class CsvProcessor extends DataProcessor {
    protected validate(data: string): string {
        if (!data.includes(",")) {
            throw new Error("Invalid CSV");
        }
        return data;
    }

    protected transform(data: string): string {
        return data.split(",").map(v => v.trim()).join(";");
    }

    protected override format(data: string): string {
        return `CSV: ${super.format(data)}`;
    }
}

const jsonProcessor = new JsonProcessor();
const csvProcessor = new CsvProcessor();

console.log(jsonProcessor.process('{"name":"Max"}'));
console.log(csvProcessor.process("a, b, c"));

Factory Method Pattern

abstract class Creator {
    abstract createProduct(): Product;

    operate(): string {
        const product = this.createProduct();
        return `Creator produced: ${product.operation()}`;
    }
}

interface Product {
    operation(): string;
}

class ConcreteCreatorA extends Creator {
    createProduct(): Product {
        return new ConcreteProductA();
    }
}

class ConcreteCreatorB extends Creator {
    createProduct(): Product {
        return new ConcreteProductB();
    }
}

class ConcreteProductA implements Product {
    operation(): string {
        return "Result of ProductA";
    }
}

class ConcreteProductB implements Product {
    operation(): string {
        return "Result of ProductB";
    }
}

function clientCode(creator: Creator): void {
    console.log(creator.operate());
}

clientCode(new ConcreteCreatorA());  // "Creator produced: Result of ProductA"
clientCode(new ConcreteCreatorB());  // "Creator produced: Result of ProductB"

Praktische Beispiele

HTTP Client

abstract class HttpClient {
    protected baseUrl: string;

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

    // Abstrakte Methode für die eigentliche Anfrage
    protected abstract sendRequest(
        method: string,
        url: string,
        body?: unknown
    ): Promise<unknown>;

    // Konkrete Methoden nutzen die abstrakte Methode
    async get<T>(endpoint: string): Promise<T> {
        return this.sendRequest("GET", endpoint) as Promise<T>;
    }

    async post<T>(endpoint: string, data: unknown): Promise<T> {
        return this.sendRequest("POST", endpoint, data) as Promise<T>;
    }

    async put<T>(endpoint: string, data: unknown): Promise<T> {
        return this.sendRequest("PUT", endpoint, data) as Promise<T>;
    }

    async delete(endpoint: string): Promise<void> {
        await this.sendRequest("DELETE", endpoint);
    }
}

class FetchHttpClient extends HttpClient {
    protected async sendRequest(
        method: string,
        endpoint: string,
        body?: unknown
    ): Promise<unknown> {
        const response = await fetch(`${this.baseUrl}${endpoint}`, {
            method,
            headers: { "Content-Type": "application/json" },
            body: body ? JSON.stringify(body) : undefined
        });
        return response.json();
    }
}

// Nutzung
const api = new FetchHttpClient("https://api.example.com");
// const users = await api.get<User[]>("/users");

Validation Framework

abstract class Validator<T> {
    protected errors: string[] = [];

    abstract validate(value: T): boolean;

    getErrors(): string[] {
        return [...this.errors];
    }

    isValid(value: T): boolean {
        this.errors = [];
        return this.validate(value);
    }

    protected addError(message: string): void {
        this.errors.push(message);
    }
}

class EmailValidator extends Validator<string> {
    validate(email: string): boolean {
        if (!email) {
            this.addError("Email is required");
            return false;
        }
        if (!email.includes("@")) {
            this.addError("Email must contain @");
            return false;
        }
        return true;
    }
}

class AgeValidator extends Validator<number> {
    constructor(
        private minAge: number,
        private maxAge: number
    ) {
        super();
    }

    validate(age: number): boolean {
        if (age < this.minAge) {
            this.addError(`Age must be at least ${this.minAge}`);
            return false;
        }
        if (age > this.maxAge) {
            this.addError(`Age must be at most ${this.maxAge}`);
            return false;
        }
        return true;
    }
}

const emailValidator = new EmailValidator();
console.log(emailValidator.isValid("test"));  // false
console.log(emailValidator.getErrors());      // ["Email must contain @"]

const ageValidator = new AgeValidator(18, 100);
console.log(ageValidator.isValid(16));        // false
console.log(ageValidator.getErrors());        // ["Age must be at least 18"]

State Machine

abstract class State {
    abstract enter(): void;
    abstract exit(): void;
    abstract update(): void;
    abstract canTransitionTo(state: State): boolean;
}

class IdleState extends State {
    enter(): void {
        console.log("Entering idle state");
    }

    exit(): void {
        console.log("Exiting idle state");
    }

    update(): void {
        console.log("Idle...");
    }

    canTransitionTo(state: State): boolean {
        return state instanceof WalkingState || state instanceof RunningState;
    }
}

class WalkingState extends State {
    enter(): void {
        console.log("Starting to walk");
    }

    exit(): void {
        console.log("Stopping walk");
    }

    update(): void {
        console.log("Walking...");
    }

    canTransitionTo(state: State): boolean {
        return state instanceof IdleState || state instanceof RunningState;
    }
}

class RunningState extends State {
    enter(): void {
        console.log("Starting to run");
    }

    exit(): void {
        console.log("Stopping run");
    }

    update(): void {
        console.log("Running...");
    }

    canTransitionTo(state: State): boolean {
        return state instanceof IdleState || state instanceof WalkingState;
    }
}

class Character {
    private currentState: State;

    constructor(initialState: State) {
        this.currentState = initialState;
        this.currentState.enter();
    }

    transitionTo(newState: State): boolean {
        if (this.currentState.canTransitionTo(newState)) {
            this.currentState.exit();
            this.currentState = newState;
            this.currentState.enter();
            return true;
        }
        return false;
    }

    update(): void {
        this.currentState.update();
    }
}

Abstract vs Interface

FeatureAbstract ClassInterface
Implementierte MethodenJaNein (meist)
KonstruktorJaNein
Mehrfach-VererbungNeinJa (implements)
Private/Protected MembersJaNein
Runtime-CodeJaNein

Zusammenfassung

  • abstract class: Kann nicht instanziiert werden
  • abstract method: Muss in Subklassen implementiert werden
  • Kombinierbar: Abstrakte und konkrete Members
  • Template Method: Algorithmus-Skelett mit variablen Teilen
  • Factory Method: Objekterzeugung an Subklassen delegieren

Im nächsten Modul lernst du fortgeschrittene Typen wie Type Guards und Mapped Types!

Zurück zum TypeScript Kurs