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
.
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.
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.
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__
.
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.