Object-oriented Programming (OOP)

Objektorientierte Programmierung erklärt

27.03.2024
Von 
Matthew Tyson ist Java-Entwickler und schreibt unter anderem für unsere US-Schwesterpublikation Infoworld.com.
Lesen Sie, was Sie über objektorientierte Programmierung wissen sollten – inklusive anschaulicher Code-Beispiele in Java, Python und TypeScript.
Objektorientierte Programmierung wird oft als komplex dargestellt, nutzt jedoch ein vetrautes Modell, damit Applikationen leichter zu managen sind.
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.

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.

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.

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

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.