SOLID-Prinzipien¶
SOLID ist ein Akronym für die ersten fünf Prinzipien des objektorientierten Designs (OOD) von Robert C. Martin (auch bekannt als Uncle Bob).
Diese Prinzipien legen Praktiken für die Entwicklung von Software mit Überlegungen zur Wartung und Erweiterbarkeit fest, wenn das Projekt wächst. Die Übernahme dieser Prinzipien kann auch dazu beitragen, Code Smells zu vermeiden, Code zu refaktorisieren und agile oder adaptive Software zu entwickeln.
SOLID steht für:
- S – Single-Responsibility-Prinzip
Die Methoden einer Klasse sollten auf einen einzigen Zweck ausgerichtet sein.
- O – Open-Closed-Prinzip
Objekte sollten offen für Erweiterungen, aber geschlossen für Änderungen sein.
- L – Liskovsches Substitutionsprinzip
Unterklassen sollten durch ihre Oberklassen substituierbar sein.
- I – Interface-Segregation-Prinzip
Objekte sollten nicht von Methoden abzuhängen, die sie nicht verwenden.
- D – Dependency-Inversion-Prinzip
Abstraktionen sollten nicht von Details abhängen.
Single-Responsibility-Prinzip¶
Das Single-Responsibility-Prinzip besagt, dass jede Klasse nur eine Aufgabe erfüllen soll:
Es sollte nie mehr als einen Grund geben, eine Klasse zu ändern.
– Robert C. Martin: SRP: The Single Responsibility Principle
Nehmen wir z.B. eine Anwendung, die eine Sammlung von Formen – Kreise und Quadrate – nimmt und die Summe der Umfänge aller Formen der Sammlung berechnet.
Erstellt zunächst die Form-Klassen mit den notwendigen Parametern. Für
Quadrate ist dies die Kantenlänge und für Kreise der Durchmesser:
class Form:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
class Square(Form):
def __init__(self, length=1, x=0, y=0):
super().__init__(x, y)
self.length = length
class Circle(Form):
def __init__(self, diameter=1, x=0, y=0):
super().__init__(x, y)
self.diameter = diameter
Nun könnt ihr eine Klasse SquaresAndCircles erstellen mit der Logik zur
Berechnung aller Umfänge von Quadraten und Kreisen:
import gc
class SquaresAndCircles:
pi = 3.14159
@classmethod
def circumferences(cls):
csum = 0
for obj in gc.get_objects():
if isinstance(obj, Square):
csum = csum + 4 * obj.length
if isinstance(obj, Circle):
csum = csum + obj.diameter * cls.pi
return csum
Die Klasse SquaresAndCircles übernimmt die Logik, die zur Berechnung
aller Umfänge von Quadraten und Kreisen erforderlich ist. Damit ist das Prinzip
der Einzelverantwortung erfüllt.
Open-Closed-Prinzip¶
Das OCP besagt:
Objekte oder Entitäten sollten offen für Erweiterungen, aber geschlossen für Änderungen sein.
Das bedeutet, dass eine Klasse erweiterbar sein sollte, ohne die Klasse selbst zu verändern.
Schauen wir uns die Klasse SquaresAndCircles an und konzentrieren uns
auf die circumferences()-Methode. Stellt euch ein Szenario vor, in dem die
Summe zusätzlicher Formen wie Dreiecke, Fünfecke, Sechsecke usw. berechnet werden sollen. Ihr müsstet diese Klasse ständig bearbeiten
und weitere if-Blöcke hinzufügen. Das würde gegen das Open-Closed-Prinzip
verstoßen. Eine Möglichkeit, diese Methode zu verbessern, besteht darin, die
Logik zur Berechnung des Umfangs jeder Form aus der Klasse
SquaresAndCircles zu entfernen und sie an die Klassen der speziellen
Formen anzuhängen. Hier sind die Umfangsberechnungen in den Klassen
Square und Circle definiert:
class Square(Form):
def __init__(self, length=1, x=0, y=0):
super().__init__(x, y)
self.length = length
def circumference(self):
return 4 * self.length
class Circle(Form):
pi = 3.14159
def __init__(self, diameter=1, x=0, y=0):
super().__init__(x, y)
self.diameter = diameter
def circumference(self):
return self.diameter * Circle.pi
Die Summenmethode circumferences() in der Klasse
CircumferenceFormInstances kann dann wie folgt umgeschrieben werden:
class CircumferenceFormInstances:
def circumferences():
csum = 0
for obj in gc.get_objects():
if isinstance(obj, Form) and hasattr(obj, "circumference"):
csum = csum + obj.circumference()
return csum
Damit ist das Open-Closed-Prinzip erfüllt.
Tipp
Wenn euer Code noch nicht offen für neue Anforderungen ist, solltet ihr zunächst den vorhandenen Code so umordnen (refaktorieren), dass er für die neue Funktion offen ist. Erst dann solltet ihr neuen Code hinzufügen.
Unter Refaktorierung versteht man den Prozess, ein Softwaresystem so zu verändern, dass das äußere Verhalten des Codes nicht verändert, aber seine innere Struktur verbessert wird.
Bemerkung
Sicheres Refactoring ist auf Tests angewiesen. Wenn ihr den Code wirklich umgestaltet, ohne das Verhalten zu ändern, sollten die vorhandenen Tests bei jedem Schritt weiterhin erfolgreich sein. Die Tests sind ein Sicherheitsnetz, das das Vertrauen in die neue Anordnung des Codes rechtfertigt. Wenn sie versagen,
habt ihr den Code versehentlich beschädigt,
oder die vorhandenen Tests sind fehlerhaft.
Liskovsches Substitutionsprinzip¶
Das Liskovsche Substitutionsprinzip besagt, dass ein Programm, das Objekte der Basisklasse verwendet, auch mit Objekten der Unterklasse korrekt funktionieren muss.
Erweitern wir die Klasse Form, so dass die daraus abgeleiteten Klassen
in der x- und y-Richtung verschoben werden können:
class Form:
def __init__(self, x=0, y=0):
self.x = x
self.y = y
def move(self, delta_x, delta_y):
self.x = self.x + delta_x
self.y = self.y + delta_y
Anschließend könnt ihr sowohl Quadrate wie auch Kreise auf der x- und y-Achse verschieben:
>>> import forms
>>> s1 = forms.Square()
>>> c1 = forms.Circle()
>>> s1.x, s1.y, c1.x, c1.y
(0, 0, 0, 0)
>>> s1.move(4, 5)
>>> c1.move(2, 3)
>>> s1.x, s1.y, c1.x, c1.y
(4, 5, 2, 3)
Bemerkung
Das Liskovsche Substitutionsprinzip gilt auch für Duck-Typing: jedes Objekt, das behauptet, eine Ente zu sein, muss die API der Ente vollständig implementieren. Duck-Types sollten gegeneinander austauschbar sein. Die Logik über verschiedene Datentypen von Objekten hinweg anzuwenden, nennt sich Polymorphie.
Interface-Segregation-Prinzip¶
Das Interface-Segregation-Prinzip wendet das Single-Responsibility-Prinzip auf Schnittstellen (engl.: Interfaces) an um ein bestimmtes Verhalten zu isolieren. Wenn eine Änderung an einem Teil eures Codes erforderlich ist, eröffnet die Extraktion eines Objekts, das eine Rolle spielt, die Möglichkeit, das neue Verhalten zu unterstützen, ohne dass der bestehende Code geändert werden muss. Dies ist kodierten Konkretisierungen vorzuziehen.
So haben wir im vorigen Beispiel überprüft, ob unser Form-Objekt auch
tatsächlich eine circumference()-Methode bereitstellt. Dies ist notwendig,
wenn später Formen wie Point oder Line hinzukommen sollten,
die keinen Umfang aufweisen.
Bemerkung
In diesem Zusammenhang ist auch das Gesetz von Demeter interessant, das besagt, dass Objekte nur mit Objekten in ihrer unmittelbaren Umgebung kommunizieren sollen. Damit wird die Liste der anderen Objekte wirksam eingeschränkt, an die ein Objekt eine Nachricht senden kann und die Kopplung zwischen Objekten verringert: ein Objekt kann nur mit seinen Nachbarn sprechen, nicht aber mit den Nachbarn seiner Nachbarn; Objekte können nur Nachrichten an direkt Beteiligte senden.
Dependency-Inversion-Prinzip¶
Das Dependency-Inversion-Prinzip kann definiert werden als
Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.
– Robert C. Martin: The Dependency Inversion Principle
circumferences() sollte nicht bereits in der Form-Klasse
definiert werden, da es auch Formen ohne Umfang gibt.