22 Objektorientierung mit TypeScript

TypeScript, als Superset von JavaScript, bietet alle objektorientierten Features von JavaScript und erweitert diese um einige zusätzliche Funktionen. Diese zusätzlichen Funktionen sind stark von klassenbasierten objektorientierten Sprachen wie Java und C# inspiriert und sollen die Entwicklung großer und komplexer Anwendungen erleichtern.

22.1 Klassen

Klassen in TypeScript ähneln Klassen in anderen objektorientierten Sprachen wie Java oder C#. Sie ermöglichen die Erstellung von komplexen benutzerdefinierten Typen mit Eigenschaften (auch als Felder oder Instanzvariablen bezeichnet) und Methoden.

Ein Beispiel für eine Klasse in TypeScript könnte so aussehen:

class Animal {
  name: string;

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

  makeSound(): void {
    console.log(this.name + ' makes a sound.');
  }
}

let dog = new Animal('Dog');
dog.makeSound(); // Ausgabe: "Dog makes a sound."

Hier definieren wir eine Klasse Animal mit einer Eigenschaft name und einer Methode makeSound.

Klassen in TypeScript unterstützen auch Konzepte wie Vererbung und Polymorphie:

class Dog extends Animal {
  makeSound(): void {
    console.log(this.name + ' barks.');
  }
}

let dog = new Dog('Dog');
dog.makeSound(); // Ausgabe: "Dog barks."

In diesem Beispiel erweitert die Dog-Klasse die Animal-Klasse und überschreibt die makeSound-Methode.

Insgesamt sind sowohl Interfaces als auch Klassen wichtige Konzepte in TypeScript, die dazu beitragen, den Code strukturierter, sicherer und wiederverwendbarer zu gestalten.

22.2 Vererbung

Vererbung ist ein grundlegendes Konzept der objektorientierten Programmierung und es ist auch in TypeScript vorhanden. Durch die Vererbung können Klassen Eigenschaften und Methoden von anderen Klassen erben und dabei den Code wiederverwenden und besser strukturieren.

In TypeScript wird die Vererbung mit den Schlüsselwörtern extends und super umgesetzt.

Hier ist ein einfaches Beispiel für die Vererbung in TypeScript:

class Animal {
  name: string;

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

  move(distance: number) {
    console.log(`${this.name} moved ${distance} meters.`);
  }
}

class Dog extends Animal {
  bark() {
    console.log('Woof! Woof!');
  }
}

const dog = new Dog('Max');
dog.bark();        // "Woof! Woof!"
dog.move(10);      // "Max moved 10 meters."

In diesem Beispiel erstellen wir zunächst eine Basisklasse Animal mit Eigenschaften und Methoden, die alle Tiere gemeinsam haben könnten, wie z.B. einen Namen und eine move-Methode. Dann erstellen wir eine Dog-Klasse, die die Animal-Klasse erweitert. Das bedeutet, dass die Dog-Klasse alle Eigenschaften und Methoden der Animal-Klasse erbt und zusätzliche Eigenschaften und Methoden hinzufügen kann, die spezifisch für Hunde sind.

Das Schlüsselwort super wird verwendet, um auf den Konstruktor der Basisklasse zuzugreifen:

class Cat extends Animal {
  constructor(name: string) {
    super(name);  // ruft den Konstruktor der Animal-Klasse auf
  }

  meow() {
    console.log('Meow!');
  }
}

const cat = new Cat('Felix');
cat.meow();       // "Meow!"
cat.move(5);      // "Felix moved 5 meters."

In diesem Beispiel erstellen wir eine Cat-Klasse, die ebenfalls die Animal-Klasse erweitert. Im Konstruktor der Cat-Klasse verwenden wir super, um den Konstruktor der Animal-Klasse aufzurufen und den Namen des Tieres zu setzen.

Die Vererbung in TypeScript ermöglicht es, hierarchische Strukturen zu erstellen und den Code zu organisieren, indem gemeinsame Eigenschaften und Verhaltensweisen in Basisklassen kapsuliert und spezifische Eigenschaften und Verhaltensweisen in abgeleiteten Klassen hinzugefügt werden.

22.3 Interfaces

Interfaces in TypeScript ermöglichen es, benutzerdefinierte Typen zu definieren. Sie können als Verträge für Klassen dienen, die definieren, welche Eigenschaften und Methoden eine Klasse implementieren muss:

interface Drivable {
  drive(): void;
}

class Car implements Drivable {
  drive() {
    console.log('Driving a car.');
  }
}

22.4 Konstruktoren

Konstruktoren sind spezielle Methoden in einer Klasse, die aufgerufen werden, um ein neues Objekt der Klasse zu erstellen und zu initialisieren. In TypeScript, ähnlich wie in anderen objektorientierten Sprachen, definiert man Konstruktoren mit dem Schlüsselwort constructor.

Hier ist ein einfaches Beispiel für einen Konstruktor in TypeScript:

class Car {
  color: string;

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

let myCar = new Car('red');

In diesem Beispiel definiert die Car-Klasse einen Konstruktor, der einen Parameter color nimmt. Innerhalb des Konstruktors wird der Wert des color-Parameters auf die color-Eigenschaft des neu erstellten Car-Objekts gesetzt. Wenn ein neues Car-Objekt mit new Car('red') erstellt wird, wird der Konstruktor aufgerufen und das Car-Objekt wird mit der Farbe 'red' initialisiert.

In TypeScript ist es auch möglich, mehrere Konstruktorparameter zu definieren:

class Car {
  color: string;
  year: number;

  constructor(color: string, year: number) {
    this.color = color;
    this.year = year;
  }
}

let myCar = new Car('red', 2022);

In diesem Beispiel nimmt der Konstruktor der Car-Klasse zwei Parameter, color und year, und initialisiert die entsprechenden Eigenschaften des Car-Objekts.

Darüber hinaus bietet TypeScript eine verkürzte Syntax, um Konstruktoren zu definieren und gleichzeitig Klasseneigenschaften zu deklarieren:

class Car {
  constructor(public color: string, public year: number) {}
}

let myCar = new Car('red', 2022);

In diesem Beispiel werden color und year sowohl als Konstruktorparameter als auch als öffentliche Eigenschaften der Klasse definiert. Diese Syntax kann den Code verkürzen und übersichtlicher machen, insbesondere wenn eine Klasse viele Eigenschaften hat.

22.5 Access modifier

In TypeScript können Sie Zugriffsmodifikatoren verwenden, um zu bestimmen, welche Teile einer Klasse von außen zugänglich sind. Es gibt drei Zugriffsmodifikatoren: public, private und protected.

22.5.1 Public

Wenn ein Mitglied (d.h., eine Eigenschaft oder Methode) einer Klasse als public deklariert wird, kann es von überall aus zugänglich gemacht werden. Mitglieder sind standardmäßig public, wenn kein Zugriffsmodifikator angegeben wird.

class Car {
  public color: string;

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

let myCar = new Car('red');
console.log(myCar.color);  // Ausgabe: "red"

22.5.2 Private

Wenn ein Mitglied als private deklariert wird, kann es nur innerhalb der Klasse, in der es definiert ist, zugegriffen werden.

class Car {
  private color: string;

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

  displayColor() {
    console.log('The color of the car is ' + this.color);
  }
}

let myCar = new Car('red');
// Ausgabe: "The color of the car is red"
myCar.displayColor();      
// Fehler: Property 'color' is private and only accessible 
// within class 'Car'.
console.log(myCar.color);  

22.5.3 Protected

Ein protected Mitglied ist ähnlich wie ein private Mitglied, kann aber auch in abgeleiteten Klassen zugegriffen werden.

class Car {
  protected color: string;

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

class SportsCar extends Car {
  displayColor() {
    console.log('The color of the sports car is ' + this.color);
  }
}

let mySportsCar = new SportsCar('red');
// Ausgabe: "The color of the sports car is red"
mySportsCar.displayColor();  

In diesem Beispiel kann die abgeleitete Klasse SportsCar auf das protected-Mitglied color der Basisklasse Car zugreifen.

Diese Zugriffsmodifikatoren helfen dabei, die Prinzipien der Datenkapselung und der Vererbung, die zentrale Konzepte der objektorientierten Programmierung, zu implementieren. Sie ermöglichen es Ihnen, die Interaktion mit den Mitgliedern einer Klasse zu steuern und zu gewährleisten, dass diese nur auf sichere und kontrollierte Weise verwendet werden.

22.6 Getter und Setter

Getter und Setter sind spezielle Methoden in einer Klasse, die zum Abrufen (Get) und Ändern (Set) der Werte von privaten Klasseneigenschaften verwendet werden. Sie sind ein Teil des Konzepts der Datenkapselung in der objektorientierten Programmierung.

In TypeScript können Sie Getter und Setter mit den Schlüsselwörtern get und set definieren.

Hier ist ein einfaches Beispiel:

class Car {
  private _color: string;

  constructor(color: string) {
    this._color = color;
  }

  // Getter
  get color(): string {
    return this._color;
  }

  // Setter
  set color(newColor: string) {
    if (!newColor) {
      throw new Error('Please provide a valid color.');
    }
    this._color = newColor;
  }
}

let myCar = new Car('red');
console.log(myCar.color);  // Ausgabe: "red"

myCar.color = 'blue';
console.log(myCar.color);  // Ausgabe: "blue"

myCar.color = '';  // Fehler: Please provide a valid color.

In diesem Beispiel hat die Car-Klasse eine private Eigenschaft _color und einen Getter und einen Setter für diese Eigenschaft. Der Getter color gibt den Wert von _color zurück, und der Setter color ändert den Wert von _color, aber nur wenn der neue Wert ein gültiger String ist.

Getter und Setter können verwendet werden, um die Datenintegrität zu gewährleisten, indem sie sicherstellen, dass ungültige Werte nicht gesetzt werden können. Sie können auch verwendet werden, um zusätzliche Logik auszuführen, wenn eine Eigenschaft abgerufen oder geändert wird, wie z.B. das Loggen von Änderungen oder das Auslösen von Ereignissen.

22.7 Abstrakte Klassen

Abstrakte Klassen sind in TypeScript eine fortgeschrittene Funktion der objektorientierten Programmierung. Sie dienen als Basis für andere Klassen und können nicht direkt instanziiert werden. Eine abstrakte Klasse kann sowohl konkrete Methoden und Eigenschaften (die Implementierungen enthalten) als auch abstrakte Methoden und Eigenschaften (die keine Implementierungen enthalten) enthalten.

Abstrakte Methoden und Eigenschaften müssen in jeder konkreten (nicht-abstrakten) Klasse, die von der abstrakten Klasse erbt, implementiert werden. Abstrakte Klassen werden mit dem Schlüsselwort abstract definiert.

Hier ist ein einfaches Beispiel für eine abstrakte Klasse in TypeScript:

abstract class Animal {
  abstract makeSound(): void;

  move(): void {
    console.log('Moving along...');
  }
}

class Dog extends Animal {
  makeSound() {
    console.log('Woof! Woof!');
  }
}

const myDog = new Dog();
myDog.makeSound();  // Ausgabe: "Woof! Woof!"
myDog.move();       // Ausgabe: "Moving along..."

In diesem Beispiel ist Animal eine abstrakte Klasse mit einer abstrakten Methode makeSound und einer konkreten Methode move. Die Dog-Klasse erbt von der Animal-Klasse und implementiert die abstrakte Methode makeSound.

Versuchen wir, ein Objekt der abstrakten Klasse Animal direkt zu instanziieren, erhalten wir einen Kompilierungsfehler:

// Fehler: Cannot create an instance of an abstract class.
const myAnimal = new Animal();  

Abstrakte Klassen sind besonders nützlich, wenn Sie eine gemeinsame Struktur und/oder Funktionalität für eine Gruppe von verwandten Klassen definieren möchten, aber sicherstellen möchten, dass bestimmte Methoden oder Eigenschaften in jeder spezifischen Klasse eindeutig implementiert werden.