Objektorientierte Programmierung wird oft als komplex dargestellt, nutzt jedoch ein vetrautes Modell, damit Applikationen leichter zu managen sind.
Foto: Pavel_D – shutterstock.com
Object-oriented Programming (OOP; auch objektorientierte Programmierung) stellt eine der wichtigsten und nützlichsten Innovationen im Bereich der Softwareentwicklung dar. In diesem Artikel lernen Sie die wesentlichen Elemente der objektorientierten Programmierung kennen – und erfahren, wie diese in den gängigen Sprachen Java, Python und TypeScript angewendet werden.
Objekte als Grundlage
Im täglichen Leben agieren wir mit Objekten, die bestimmte Eigenschaften aufweisen. Ein Hund etwa hat eine bestimmte Fellfarbe und Rasse. In der Welt der Softwareentwicklung heißen diese Attribute Properties. Entsprechend würden wir in JavaScript ein Objekt erstellen, das einen Hund sowie dessen Rasse und Farbe abbildet:
let dog = {
color: “cream”,
breed: “shih tzu”
}
Die Variable Dog ist in diesem Beispiel ein Objekt mit zwei Properties, Farbe (color) und Rasse (breed). Das ist die wichtigste Grundlage der objektorientierten Programmierung. In JavaScript können wir eine Property mit dem Punkt-Operator erzeugen: dog.color.
Schickes Wort: Encapsulation
Im obigen Codebeispiel ist zu erkennen, dass das Objekt Dog alle properties zusammenhält. Dafür gibt es in der OOP ein schickes Wort: Encapsulation (oder Datenkapselung). Ähnlich wie eine Vitaminkapsel hält das Objekt alles in einem Container zusammen. Zu diesem Thema an späterer Stelle mehr.
Objektklassen erstellen
Zunächst lohnt es sich, über die Grenzen des Objekts Dog nachzudenken. Das größte Problem dabei: Jedes Mal, wenn wir ein neues Objekt erstellen wollen, müssen wir eine neue Variable erstellen. Wenn (wie oft der Fall) viele Objekte der gleichen Art zu erstellen sind, kann das mühsam werden. Für diesen Fall sehen JavaScript und viele andere Programmiersprachen Classes (Objektklassen) vor. Nachfolgend sehen Sie, wie die Objektklasse Dog in JavaScript zu erstellen ist:
class Dog {
color;
breed;
}
Das Keyword class bedeutet “eine Klasse von Objekten” – jede Klasseninstanz stellt ein Objekt dar. Die Objektklasse definiert die allgemeinen Eigenschaften, die ihre Instanzen aufweisen werden. In JavaScript könnten wir etwa eine Instanz der Objektklasse Dog erstellen und ihre Eigenschaften wie folgt verwenden:
let suki = new Dog();
suki.color = “cream”
console.log(suki.color); // outputs “cream”
Classes stellen die gängigste Art dar, Objekttypen zu definieren. Die meisten Sprachen, die Objekte verwenden – inklusive Java, Python und C++ – unterstützen Klassen mit einer ähnlichen Syntax (JavaScript verwendet auch Prototypes, was ein anderer Stil ist). Konventionell wird der erste Buchstabe des Namens einer Objektklasse groß geschrieben, Objektinstanzen hingegen klein.
Zu beachten ist beim obigen Beispiel auch, dass die Objektlasse Dog mit dem Keyword new als Funktion aufgerufen wird, um ein neues Objekt zu erhalten. Die auf diese Weise erzeugten Objekte sind “Instanzen” der Objektklasse – so stellt das Objekt suki eine Instanz der Objektklasse Dog dar.
Schickes Wort: Member
In der objektorientierten Programmierung werden die Properties eines Objekts auch als Members (Mitglieder) bezeichnet.
Verhalten hinzufügen
Bisher ist die Objektklasse Dog nützlich, um alle unsere Eigenschaften zusammenzuhalten, (Stichwort Datenkapselung). Sie lässt sich auch leicht weiterverwenden, um viele Objekte mit ähnlichen Eigenschaften (Mitgliedern) zu erstellen. Was aber, wenn unsere Objekte nun etwas “tun” sollen? Nehmen wir an, wir wollen den Instanzen der Objektklasse Dog erlauben, zu “sprechen”. In diesem Fall erweitern wir die Class um eine Funktion:
class Dog {
color;
breed;
speak() {
console.log(`Barks!`);
}
Nun weisen alle Instanzen von Dog bei ihrer Erstellung eine Funktion auf, auf die über den Punkt-Operator zugegriffen werden kann:
set suki = new Dog();
suki.speak() // outputs “Suki barks!”
State und Behavior
Beim Object-oriented Programming werden Objekte manchmal über State (Zustand) und Behavior (Verhalten) beschrieben. Dabei handelt es sich um die Mitglieder (Members) und Methoden (Methods) des Objekts. Das ist insofern nützlich, als dass wir die Objekte selbst und den größeren Kontext der Applikation unabhängig voneinander betrachten können.
Schickes Wort: Method
In der OOP werden die zu einem Objekt gehörenden Funktionen Methoden (Methods) genannt. Objekte haben also Mitglieder und Methoden.
Private und Public Methods
Bislang haben wir ausschließlich sogenannte Public Members und Methods verwendet. Das bedeutet lediglich, dass Code außerhalb des Objekts mit dem Punktoperator direkt auf diese zugreifen kann. In der objektorientierten Programmierung gibt es jedoch auch Modifikatoren (Modifiers), die die Sichtbarkeit von Mitgliedern und Methoden steuern. Einige Sprachen – etwa Java – arbeiten mit Modifikatoren wie private und public. Dabei gilt:
Ein private Member (oder eine private Method) ist nur für die anderen Methoden des Objekts sichtbar.
Ein public Member (oder eine public Method) ist für die Außenwelt sichtbar.
JavaScript unterstützte für längere Zeit offiziell ausschließlich Public Members und Methods. Inzwischen lässt sich mit Hilfe des Hashtag-Symbols auch privater Zugriff definieren:
class Dog {
#color;
#breed;
speak() {
console.log(`Barks!`);
}
}
Wenn Sie in diesem Beispiel versuchen, direkt auf die Property suki.color zuzugreifen, wird das nicht funktionieren. Der private Zugriff wirkt als Verstärker für die Datenkapselung und reduziert die Menge an Informationen, die zwischen den verschiedenen Teilen der Applikation verfügbar sind.
Getter und Setter
Weil Members in der objektorientierten Programmierung in der Regel private sind, werden Ihnen regelmäßig Public Methods begegnen, die Ihre Variablen mit get holen und mit set setzen:
class Dog {
#color;
#breed;
get color() {
return this.#color;
}
set color(newColor) {
this.#color = newColor;
}
}
In diesem Beispiel haben wir einen Getter und einen Setter (diese werden auch als Accessors und Mutators bezeichnet) für die Property color bereitgestellt. So können wir nun mit suki.getColor() auf die Farbe zugreifen. Auf diese Weise bleibt die Privatsphäre der Variablen gewahrt, während der Zugriff auf sie weiterhin möglich ist. Langfristig kann das dazu beitragen, die Code-Strukturen sauber(er) zu halten.
Constructors
Ein weiteres gemeinsames Merkmal von objektorientierten Programmierklassen ist der Constructor (Konstruktor). Zur Erklärung: Wenn wir ein neues Objekt erstellen, rufen wir erst das Keyword new auf und dann die Objektklasse wie eine Funktion:
new Dog()
Das Schlüsselwort new erzeugt ein neues Objekt, während Dog() eine spezielle Methode aufruft, den Constructor. In diesem Fall handelt es sich dabei um den Standardkonstruktor, der nichts tut. Ein Konstruktor lässt sich wie folgt bereitstellen:
class Dog {
constructor(color, breed) {
this.#color = color;
this.#breed = breed;
}
let suki = new Dog(“cream”, “Shih Tzu”);
Indem wir den Konstruktor hinzufügen, können wir Objekte mit bereits definierten Werten erstellen.
In TypeScript heißt der Konstruktor constructor.
In Java und JavaScript ist es eine Funktion mit demselben Namen wie die Objektklasse.
In Python ist es die Funktion __init__.
Schickes Schlüsselwort: this
Bestimmt haben Sie im letzten Beispiel das Keyword this bemerkt. Dieses Schlüsselwort kommt in diversen objektorientierten Programmiersprachen vor und besagt im Wesentlichen, sich auf das aktuelle Objekt zu beziehen. In einigen Sprachen wie Python ist dieses Keyword nicht this, sondern self.
Private Members nutzen
Darüber hinaus ist es auch möglich, Private Members innerhalb der Objektklasse mit weiteren Methoden zu verwenden – nicht nur Getter und Setter:
class Dog {
// … same
speak() {
console.log(`The ${breed} Barks!`);
}
}
let suki = new Dog(“cream”, “Shih Tzu”);
suki.speak(); // Outputs “The Shih Tzu Barks!”
OOP-Beispiele in TypeScript, Java und Python
Eine der positiven Eigenschaften von Object-oriented Programming: Das Konzept lässt sich mit verschiedenen Sprachen nutzen. Oft ist die Syntax dabei auch recht ähnlich. Um das zu belegen, hier ein Beispiel für unseren Tutorial-Hund in TypeScript, Java und Python:
// Typescript
class Dog {
private breed: string;
constructor(breed: string) {
this.breed = breed;
}
speak() { console.log(`The ${this.breed} barks!`); }
}
let suki = new Dog(“Shih Tzu”);
suki.speak(); // Outputs “The Shih Tzu Barks!”
// Java
public class Dog {
private String breed;
public Dog(String breed) {
this.breed = breed;
}
public void speak() {
System.out.println(“The ” + breed + ” barks!”);
}
public static void main(String[] args) {
Dog suki = new Dog(“cream”, “Shih Tzu”);
suki.speak(); // Outputs “The Shih Tzu barks!”
}
}
// Python
class Dog:
def __init__(self, breed: str):
self.breed = breed
def speak(self):
print(f”The {self.breed} barks!”)
suki = Dog(“Shih Tzu”)
suki.speak()
Die Syntax mag unter Umständen ungewohnt sein, aber Objekte als konzeptionellen Rahmen zu verwenden, hilft dabei, die Struktur nahezu jeder objektorientierten Programmiersprache zu verstehen.
Supertypes und Inheritance
Mit der Klasse Dog können wir so viele Objektinstanzen erstellen, wie wir wollen. Es kann auch vorkommen, dass Sie viele Instanzen erstellen wollen, die in einigen Punkten identisch sind, sich aber in anderen unterscheiden. An dieser Stelle kommen Supertypes ins Spiel. In der klassenbasierten objektorientierten Programmierung stellt ein Supertype eine Objektklasse dar, von der andere Klassen abstammen. Im OOP-Jargon spricht man auch davon, dass die Subclass von der Superclass erbt (Inheritance) – beziehungsweise diese erweitert.
JavaScript unterstützt (noch) keine klassenbasierte Vererbung, aber TypeScript. Nehmen wir an, wir möchten eine Superclass Animal mit zwei Subclasses – Dog und Cat – definieren. Diese Objektklassen ähneln sich, weil sie beide die Property breed aufweisen – unterscheiden sich aber hinsichtlich der speak()-Methode:
// Animal superclass
class Animal {
private breed: string;
constructor(breed: string) {
this.breed = breed;
}
// Common method for all animals
speak() {
console.log(`The ${this.breed} makes a sound.`);
}
}
// Dog subclass
class Dog extends Animal {
constructor(breed: string) {
super(breed); // Call the superclass constructor
}
// Override the speak method for dogs
speak() {
console.log(`The ${this.breed} barks!`);
}
}
// Cat subclass
class Cat extends Animal {
constructor(breed: string) {
super(breed); // Call the superclass constructor
}
// Override the speak method for cats
speak() {
console.log(`The ${this.breed} meows!`);
}
}
// Create instances of Dog and Cat
const suki = new Dog(“Shih Tzu”);
const whiskers = new Cat(“Siamese”);
// Call the speak method for each instance
suki.speak(); // Outputs “The Shih Tzu Barks!”
whiskers.speak(); // Outputs “The Siamese meows!”
Im Grunde ist es ganz einfach: Inheritance – oder Vererbung – bedeutet lediglich, dass ein Typ alle Properties des Typs übernimmt, den er erweitert (außer es wurde entsprechend anders definiert).
Inheritance-Konzepte
Im letzten Beispiel haben wir zwei neue speak()-Methoden definiert. Das bezeichnet man auch als Method Override – beziehungsweise eine Mthode überschreiben. Dabei wird eine Property der Superclass mit einer gleichnamigen Property der Subclass überschrieben. In einigen Sprachen ist es auch möglich Methoden zu überladen, indem Sie denselben Namen mit unterschiedlichen Argumenten verwenden. Allerdings macht es einen Unterschied, ob Sie eine Methode überschreiben oder überladen.
Das Beispiel demonstriert darüber hinaus auch eines der komplexeren Konzepte von Object-Oriented Programming: die Polymorphie (wörtlich: viele Formen). Das besagt im Wesentlichen, dass ein Subtype ein unterschiedliches Verhalten aufweisen kann, aber dennoch gleich behandelt wird, insofern er mit seinem Supertype konform geht.
Angenommen, wir haben eine Funktion, die eine Animal-Referenz verwendet. Dann können wir der Funktion einen Subtype (wie Cat oder Dog) übergeben. Das eröffnet wiederum die Möglichkeit, generischer zu coden:
function talkToPet(pet: Animal) {
pet.speak(); // This will work because speak() is defined in the Animal class
}
Abstract Types
Die Grundidee der Supertypes lässt sich weiterführen – mit Abstract Types. Abstrakt bedeutet in diesem Fall lediglich, dass ein Typ nicht all seine Methoden implementiert, sondern deren Signatur definiert. Die eigentliche Arbeit wird den Subclasses überlassen. Abstrakte Typen stehen im Gegensatz zu Concrete Types. Bisher waren alle Typen, die uns begegnet sind, Concrete Classes. Hier ist eine abstrakte Version der Objektklasse Animal (TypeScript):
abstract class Animal {
private breed: string;
abstract speak(): void;
}
Neben dem Keyword abstract fällt auf, dass die abstrakte speak()-Methode nicht implementiert ist. Sie definiert, welche Argumente sie benötigt (keine) und ihren Rückgabewert (void). Aus diesem Grund können abstrakte Klassen nicht instanziiert werden. Sie können lediglich Verweise auf sie erstellen oder sie erweitern.
Davon abgesehen ist zu beachten, dass unsere abstrakte Objektklasse Animal die Funktion speak() nicht implementiert, aber die Property breed definiert. Daher können die Subclasses von Animal mit dem Keyword super auf breed zugreifen. Das funktioniert wie das Schlüsselwort this, allerdings für die Parent Class.
Interfaces
Ganz generell ermöglicht eine abstrakte Objektklasse, konkrete und abstrakte Properties zu vermischen. Diese Abstraktheit lässt sich noch weiter ausbauen, indem Sie ein Interface definieren (es gibt keine konkrete Implementierung). Ein Beispiel in TypeScript:
interface Animal {
breed: string;
speak(): void;
}
Beachten Sie, dass Property und Method dieses Interfaces das Keyword abstract nicht deklarieren – das ist automatisch so, weil sie Teil einer Schnittstelle sind.
Overengineering
Das Ideal abstrakter Typen besteht darin, so viel wie möglich in Richtung Supertype zu verschieben, um Code wiederzuverwenden. Entsprechend könnten Sie Hierarchien definieren, die die allgemeinsten Teile eines Modells in den höheren Typen enthalten und erst nach und nach die Spezifika in den niedrigeren Typen definieren. Ein Beispiel dafür ist die Object-Klasse in Java und JavaScript, von der alle anderen Typen abstammen und die eine generische toString()-Methode definiert.
In der Praxis besteht jedoch oft eine Tendenz zum Overengineering – in Form tiefer und extravaganter Type-Hierarchien. Allerdings sind flache Hierarchien vorzuziehen: In der Praxis haben Softwareentwickler festgestellt, dass die Vererbung zu einer starken Kopplung zwischen den Members führt. Deshalb lassen diese sich im Laufe der Zeit nicht mehr verändern. Außerdem neigen ausufernde Hierarchien zur Komplexität, und übersteigen so den eigentlichen Zweck des Codes unter Umständen bei weitem. (fm)
Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.
Objektorientierte Programmierung wird oft als komplex dargestellt, nutzt jedoch ein vetrautes Modell, damit Applikationen leichter zu managen sind.
Foto: Pavel_D – shutterstock.comObject-oriented Programming (OOP; auch objektorientierte Programmierung) stellt eine der wichtigsten und nützlichsten Innovationen im Bereich der Softwareentwicklung dar. In diesem Artikel lernen Sie die wesentlichen Elemente der objektorientierten Programmierung kennen – und erfahren, wie diese in den gängigen Sprachen Java, Python und TypeScript angewendet werden.Objekte als GrundlageIm täglichen Leben agieren wir mit Objekten, die bestimmte Eigenschaften aufweisen. Ein Hund etwa hat eine bestimmte Fellfarbe und Rasse. In der Welt der Softwareentwicklung heißen diese Attribute Properties. Entsprechend würden wir in JavaScript ein Objekt erstellen, das einen Hund sowie dessen Rasse und Farbe abbildet:let dog = { color: “cream”, breed: “shih tzu”}Die Variable Dog ist in diesem Beispiel ein Objekt mit zwei Properties, Farbe (color) und Rasse (breed). Das ist die wichtigste Grundlage der objektorientierten Programmierung. In JavaScript können wir eine Property mit dem Punkt-Operator erzeugen: dog.color.Schickes Wort: EncapsulationIm obigen Codebeispiel ist zu erkennen, dass das Objekt Dog alle properties zusammenhält. Dafür gibt es in der OOP ein schickes Wort: Encapsulation (oder Datenkapselung). Ähnlich wie eine Vitaminkapsel hält das Objekt alles in einem Container zusammen. Zu diesem Thema an späterer Stelle mehr.Objektklassen erstellenZunächst lohnt es sich, über die Grenzen des Objekts Dog nachzudenken. Das größte Problem dabei: Jedes Mal, wenn wir ein neues Objekt erstellen wollen, müssen wir eine neue Variable erstellen. Wenn (wie oft der Fall) viele Objekte der gleichen Art zu erstellen sind, kann das mühsam werden. Für diesen Fall sehen JavaScript und viele andere Programmiersprachen Classes (Objektklassen) vor. Nachfolgend sehen Sie, wie die Objektklasse Dog in JavaScript zu erstellen ist:class Dog { color; breed;}Das Keyword class bedeutet “eine Klasse von Objekten” – jede Klasseninstanz stellt ein Objekt dar. Die Objektklasse definiert die allgemeinen Eigenschaften, die ihre Instanzen aufweisen werden. In JavaScript könnten wir etwa eine Instanz der Objektklasse Dog erstellen und ihre Eigenschaften wie folgt verwenden:let suki = new Dog();suki.color = “cream”console.log(suki.color); // outputs “cream”Classes stellen die gängigste Art dar, Objekttypen zu definieren. Die meisten Sprachen, die Objekte verwenden – inklusive Java, Python und C++ – unterstützen Klassen mit einer ähnlichen Syntax (JavaScript verwendet auch Prototypes, was ein anderer Stil ist). Konventionell wird der erste Buchstabe des Namens einer Objektklasse groß geschrieben, Objektinstanzen hingegen klein.Zu beachten ist beim obigen Beispiel auch, dass die Objektlasse Dog mit dem Keyword new als Funktion aufgerufen wird, um ein neues Objekt zu erhalten. Die auf diese Weise erzeugten Objekte sind “Instanzen” der Objektklasse – so stellt das Objekt suki eine Instanz der Objektklasse Dog dar.Schickes Wort: MemberIn der objektorientierten Programmierung werden die Properties eines Objekts auch als Members (Mitglieder) bezeichnet.Verhalten hinzufügenBisher ist die Objektklasse Dog nützlich, um alle unsere Eigenschaften zusammenzuhalten, (Stichwort Datenkapselung). Sie lässt sich auch leicht weiterverwenden, um viele Objekte mit ähnlichen Eigenschaften (Mitgliedern) zu erstellen. Was aber, wenn unsere Objekte nun etwas “tun” sollen? Nehmen wir an, wir wollen den Instanzen der Objektklasse Dog erlauben, zu “sprechen”. In diesem Fall erweitern wir die Class um eine Funktion:class Dog { color; breed; speak() { console.log(`Barks!`);}Nun weisen alle Instanzen von Dog bei ihrer Erstellung eine Funktion auf, auf die über den Punkt-Operator zugegriffen werden kann:set suki = new Dog();suki.speak() // outputs “Suki barks!”State und BehaviorBeim Object-oriented Programming werden Objekte manchmal über State (Zustand) und Behavior (Verhalten) beschrieben. Dabei handelt es sich um die Mitglieder (Members) und Methoden (Methods) des Objekts. Das ist insofern nützlich, als dass wir die Objekte selbst und den größeren Kontext der Applikation unabhängig voneinander betrachten können.Schickes Wort: MethodIn der OOP werden die zu einem Objekt gehörenden Funktionen Methoden (Methods) genannt. Objekte haben also Mitglieder und Methoden.Private und Public MethodsBislang haben wir ausschließlich sogenannte Public Members und Methods verwendet. Das bedeutet lediglich, dass Code außerhalb des Objekts mit dem Punktoperator direkt auf diese zugreifen kann. In der objektorientierten Programmierung gibt es jedoch auch Modifikatoren (Modifiers), die die Sichtbarkeit von Mitgliedern und Methoden steuern. Einige Sprachen – etwa Java – arbeiten mit Modifikatoren wie private und public. Dabei gilt:Ein private Member (oder eine private Method) ist nur für die anderen Methoden des Objekts sichtbar.Ein public Member (oder eine public Method) ist für die Außenwelt sichtbar.JavaScript unterstützte für längere Zeit offiziell ausschließlich Public Members und Methods. Inzwischen lässt sich mit Hilfe des Hashtag-Symbols auch privater Zugriff definieren:class Dog { #color; #breed; speak() { console.log(`Barks!`); }}Wenn Sie in diesem Beispiel versuchen, direkt auf die Property suki.color zuzugreifen, wird das nicht funktionieren. Der private Zugriff wirkt als Verstärker für die Datenkapselung und reduziert die Menge an Informationen, die zwischen den verschiedenen Teilen der Applikation verfügbar sind.Getter und SetterWeil Members in der objektorientierten Programmierung in der Regel private sind, werden Ihnen regelmäßig Public Methods begegnen, die Ihre Variablen mit get holen und mit set setzen:class Dog { #color; #breed; get color() { return this.#color; } set color(newColor) { this.#color = newColor; }}In diesem Beispiel haben wir einen Getter und einen Setter (diese werden auch als Accessors und Mutators bezeichnet) für die Property color bereitgestellt. So können wir nun mit suki.getColor() auf die Farbe zugreifen. Auf diese Weise bleibt die Privatsphäre der Variablen gewahrt, während der Zugriff auf sie weiterhin möglich ist. Langfristig kann das dazu beitragen, die Code-Strukturen sauber(er) zu halten.ConstructorsEin weiteres gemeinsames Merkmal von objektorientierten Programmierklassen ist der Constructor (Konstruktor). Zur Erklärung: Wenn wir ein neues Objekt erstellen, rufen wir erst das Keyword new auf und dann die Objektklasse wie eine Funktion:new Dog()Das Schlüsselwort new erzeugt ein neues Objekt, während Dog() eine spezielle Methode aufruft, den Constructor. In diesem Fall handelt es sich dabei um den Standardkonstruktor, der nichts tut. Ein Konstruktor lässt sich wie folgt bereitstellen:class Dog { constructor(color, breed) { this.#color = color; this.#breed = breed;}let suki = new Dog(“cream”, “Shih Tzu”);Indem wir den Konstruktor hinzufügen, können wir Objekte mit bereits definierten Werten erstellen.In TypeScript heißt der Konstruktor constructor.In Java und JavaScript ist es eine Funktion mit demselben Namen wie die Objektklasse.In Python ist es die Funktion __init__.Schickes Schlüsselwort: thisBestimmt haben Sie im letzten Beispiel das Keyword this bemerkt. Dieses Schlüsselwort kommt in diversen objektorientierten Programmiersprachen vor und besagt im Wesentlichen, sich auf das aktuelle Objekt zu beziehen. In einigen Sprachen wie Python ist dieses Keyword nicht this, sondern self.Private Members nutzenDarüber hinaus ist es auch möglich, Private Members innerhalb der Objektklasse mit weiteren Methoden zu verwenden – nicht nur Getter und Setter:class Dog { // … same speak() { console.log(`The ${breed} Barks!`); }}let suki = new Dog(“cream”, “Shih Tzu”);suki.speak(); // Outputs “The Shih Tzu Barks!”OOP-Beispiele in TypeScript, Java und PythonEine der positiven Eigenschaften von Object-oriented Programming: Das Konzept lässt sich mit verschiedenen Sprachen nutzen. Oft ist die Syntax dabei auch recht ähnlich. Um das zu belegen, hier ein Beispiel für unseren Tutorial-Hund in TypeScript, Java und Python:// Typescriptclass Dog { private breed: string; constructor(breed: string) { this.breed = breed;}speak() { console.log(`The ${this.breed} barks!`); }}let suki = new Dog(“Shih Tzu”);suki.speak(); // Outputs “The Shih Tzu Barks!”// Javapublic class Dog { private String breed; public Dog(String breed) { this.breed = breed; } public void speak() { System.out.println(“The ” + breed + ” barks!”); } public static void main(String[] args) { Dog suki = new Dog(“cream”, “Shih Tzu”); suki.speak(); // Outputs “The Shih Tzu barks!” }}// Pythonclass Dog: def __init__(self, breed: str): self.breed = breed def speak(self): print(f”The {self.breed} barks!”)suki = Dog(“Shih Tzu”)suki.speak()Die Syntax mag unter Umständen ungewohnt sein, aber Objekte als konzeptionellen Rahmen zu verwenden, hilft dabei, die Struktur nahezu jeder objektorientierten Programmiersprache zu verstehen.Supertypes und InheritanceMit der Klasse Dog können wir so viele Objektinstanzen erstellen, wie wir wollen. Es kann auch vorkommen, dass Sie viele Instanzen erstellen wollen, die in einigen Punkten identisch sind, sich aber in anderen unterscheiden. An dieser Stelle kommen Supertypes ins Spiel. In der klassenbasierten objektorientierten Programmierung stellt ein Supertype eine Objektklasse dar, von der andere Klassen abstammen. Im OOP-Jargon spricht man auch davon, dass die Subclass von der Superclass erbt (Inheritance) – beziehungsweise diese erweitert.JavaScript unterstützt (noch) keine klassenbasierte Vererbung, aber TypeScript. Nehmen wir an, wir möchten eine Superclass Animal mit zwei Subclasses – Dog und Cat – definieren. Diese Objektklassen ähneln sich, weil sie beide die Property breed aufweisen – unterscheiden sich aber hinsichtlich der speak()-Methode:// Animal superclassclass Animal { private breed: string; constructor(breed: string) { this.breed = breed; } // Common method for all animals speak() { console.log(`The ${this.breed} makes a sound.`); }}// Dog subclassclass Dog extends Animal { constructor(breed: string) { super(breed); // Call the superclass constructor } // Override the speak method for dogs speak() { console.log(`The ${this.breed} barks!`); }}// Cat subclassclass Cat extends Animal { constructor(breed: string) { super(breed); // Call the superclass constructor } // Override the speak method for cats speak() { console.log(`The ${this.breed} meows!`); }}// Create instances of Dog and Catconst suki = new Dog(“Shih Tzu”);const whiskers = new Cat(“Siamese”);// Call the speak method for each instancesuki.speak(); // Outputs “The Shih Tzu Barks!”whiskers.speak(); // Outputs “The Siamese meows!”Im Grunde ist es ganz einfach: Inheritance – oder Vererbung – bedeutet lediglich, dass ein Typ alle Properties des Typs übernimmt, den er erweitert (außer es wurde entsprechend anders definiert).Inheritance-KonzepteIm letzten Beispiel haben wir zwei neue speak()-Methoden definiert. Das bezeichnet man auch als Method Override – beziehungsweise eine Mthode überschreiben. Dabei wird eine Property der Superclass mit einer gleichnamigen Property der Subclass überschrieben. In einigen Sprachen ist es auch möglich Methoden zu überladen, indem Sie denselben Namen mit unterschiedlichen Argumenten verwenden. Allerdings macht es einen Unterschied, ob Sie eine Methode überschreiben oder überladen.Das Beispiel demonstriert darüber hinaus auch eines der komplexeren Konzepte von Object-Oriented Programming: die Polymorphie (wörtlich: viele Formen). Das besagt im Wesentlichen, dass ein Subtype ein unterschiedliches Verhalten aufweisen kann, aber dennoch gleich behandelt wird, insofern er mit seinem Supertype konform geht.Angenommen, wir haben eine Funktion, die eine Animal-Referenz verwendet. Dann können wir der Funktion einen Subtype (wie Cat oder Dog) übergeben. Das eröffnet wiederum die Möglichkeit, generischer zu coden:function talkToPet(pet: Animal) { pet.speak(); // This will work because speak() is defined in the Animal class}Abstract TypesDie Grundidee der Supertypes lässt sich weiterführen – mit Abstract Types. Abstrakt bedeutet in diesem Fall lediglich, dass ein Typ nicht all seine Methoden implementiert, sondern deren Signatur definiert. Die eigentliche Arbeit wird den Subclasses überlassen. Abstrakte Typen stehen im Gegensatz zu Concrete Types. Bisher waren alle Typen, die uns begegnet sind, Concrete Classes. Hier ist eine abstrakte Version der Objektklasse Animal (TypeScript):abstract class Animal { private breed: string; abstract speak(): void;}Neben dem Keyword abstract fällt auf, dass die abstrakte speak()-Methode nicht implementiert ist. Sie definiert, welche Argumente sie benötigt (keine) und ihren Rückgabewert (void). Aus diesem Grund können abstrakte Klassen nicht instanziiert werden. Sie können lediglich Verweise auf sie erstellen oder sie erweitern.Davon abgesehen ist zu beachten, dass unsere abstrakte Objektklasse Animal die Funktion speak() nicht implementiert, aber die Property breed definiert. Daher können die Subclasses von Animal mit dem Keyword super auf breed zugreifen. Das funktioniert wie das Schlüsselwort this, allerdings für die Parent Class.InterfacesGanz generell ermöglicht eine abstrakte Objektklasse, konkrete und abstrakte Properties zu vermischen. Diese Abstraktheit lässt sich noch weiter ausbauen, indem Sie ein Interface definieren (es gibt keine konkrete Implementierung). Ein Beispiel in TypeScript:interface Animal { breed: string; speak(): void;}Beachten Sie, dass Property und Method dieses Interfaces das Keyword abstract nicht deklarieren – das ist automatisch so, weil sie Teil einer Schnittstelle sind.OverengineeringDas Ideal abstrakter Typen besteht darin, so viel wie möglich in Richtung Supertype zu verschieben, um Code wiederzuverwenden. Entsprechend könnten Sie Hierarchien definieren, die die allgemeinsten Teile eines Modells in den höheren Typen enthalten und erst nach und nach die Spezifika in den niedrigeren Typen definieren. Ein Beispiel dafür ist die Object-Klasse in Java und JavaScript, von der alle anderen Typen abstammen und die eine generische toString()-Methode definiert.In der Praxis besteht jedoch oft eine Tendenz zum Overengineering – in Form tiefer und extravaganter Type-Hierarchien. Allerdings sind flache Hierarchien vorzuziehen: In der Praxis haben Softwareentwickler festgestellt, dass die Vererbung zu einer starken Kopplung zwischen den Members führt. Deshalb lassen diese sich im Laufe der Zeit nicht mehr verändern. Außerdem neigen ausufernde Hierarchien zur Komplexität, und übersteigen so den eigentlichen Zweck des Codes unter Umständen bei weitem. (fm)Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.