SOLID:Objektorientierte Designprinzipien
SOLID ist ein mnemonisches Akronym für Klassenentwurf in der objektorientierten Programmierung. Die Prinzipien führen Praktiken ein, die dabei helfen, gute Programmiergewohnheiten und wartbaren Code zu entwickeln.
Durch die Berücksichtigung der Codewartung und -erweiterbarkeit auf lange Sicht bereichern die SOLID-Prinzipien die agile Codeentwicklungsumgebung. Die Berücksichtigung und Optimierung von Codeabhängigkeiten trägt dazu bei, einen einfacheren und organisierteren Softwareentwicklungslebenszyklus zu schaffen.
Was sind solide Prinzipien?
SOLID repräsentiert eine Reihe von Prinzipien zum Entwerfen von Klassen. Robert C. Martin (Onkel Bob) führte die meisten Designprinzipien ein und prägte das Akronym.
SOLID steht für:
- Prinzip der alleinigen Verantwortung
- Open-Closed-Prinzip
- Liskov-Substitutionsprinzip
- Grundsatz der Schnittstellentrennung
- Prinzip der Abhängigkeitsinversion
SOLID-Prinzipien stellen eine Sammlung von Best Practices für das Softwaredesign dar. Jede Idee stellt ein Design-Framework dar, das zu besseren Programmiergewohnheiten, verbessertem Code-Design und weniger Fehlern führt.
SOLID:5 Prinzipien erklärt
Der beste Weg, um zu verstehen, wie die SOLID-Prinzipien funktionieren, sind Beispiele. Alle Prinzipien sind komplementär und gelten für individuelle Anwendungsfälle. Die Reihenfolge, in der die Prinzipien angewendet werden, ist unwichtig, und nicht alle Prinzipien sind in jeder Situation anwendbar.
Jeder Abschnitt unten bietet einen Überblick über jedes SOLID-Prinzip in der Programmiersprache Python. Die allgemeinen Ideen von SOLID gelten für alle objektorientierten Sprachen wie PHP, Java oder C#. Die Verallgemeinerung der Regeln macht sie auf moderne Programmieransätze wie Microservices anwendbar.
Single-Responsibility-Prinzip (SRP)
Das Single-Responsibility-Prinzip (SRP) besagt:„Es sollte nie mehr als einen Grund für einen Klassenwechsel geben.“
Wenn wir eine Klasse ändern, sollten wir nur eine einzige Funktionalität ändern, was bedeutet, dass jedes Objekt nur einen Job haben sollte.
Betrachten Sie als Beispiel die folgende Klasse:
# A class with multiple responsibilities
class Animal:
# Property constructor
def __init__(self, name):
self.name = name
# Property representation
def __repr__(self):
return f'Animal(name="{self.name}")'
# Database management
def save(animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# Property instantiation
a = Animal('Cat')
# Saving property to a database
Animal.save(a)
Bei Änderungen am save()
-Methode erfolgt die Änderung im Animal
Klasse. Wenn Eigenschaftsänderungen vorgenommen werden, treten die Änderungen auch in Animal
auf Klasse.
Die Klasse hat zwei Gründe für einen Wechsel und verstößt gegen das Prinzip der Einzelverantwortung. Auch wenn der Code wie erwartet funktioniert, erschwert die Nichteinhaltung des Designprinzips die Verwaltung des Codes auf lange Sicht.
Um das Prinzip der Einzelverantwortung zu implementieren, beachten Sie, dass die Beispielklasse zwei unterschiedliche Aufgaben hat:
- Eigenschaftsverwaltung (der Konstruktor und
get_name()
). - Datenbankverwaltung
(save()
).
Daher besteht die beste Möglichkeit zur Behebung des Problems darin, die Datenbankverwaltungsmethode in eine neue Klasse aufzuteilen. Zum Beispiel:
# A class responsible for property management
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
# A class responsible for database management
class AnimalDB:
def save(self, animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# Property instantiation
a = Animal('Cat')
# Database instantiation
db = AnimalDB()
# Saving property to a database
db.save(a)
Ändern von AnimalDB
-Klasse wirkt sich nicht auf Animal
aus Klasse unter Anwendung des Eigenverantwortungsprinzips. Der Code ist intuitiv und einfach zu ändern.
Auf-Zu-Prinzip (OCP)
Das Open-Closed-Prinzip (OCP) besagt:„Softwareentitäten sollten für Erweiterungen offen, aber für Änderungen geschlossen sein.“
Das Hinzufügen von Funktionalitäten und Anwendungsfällen zum System sollte keine Änderung bestehender Entitäten erfordern. Der Wortlaut erscheint widersprüchlich – das Hinzufügen neuer Funktionalitäten erfordert das Ändern des bestehenden Codes.
Die Idee ist anhand des folgenden Beispiels einfach zu verstehen:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
class Storage:
def save_to_db(self, animal):
print(f'Saved {animal} to the database')
Die Storage
Klasse speichert die Informationen von einem Animal
Instanz zu einer Datenbank. Das Hinzufügen neuer Funktionen, wie das Speichern in einer CSV-Datei, erfordert das Hinzufügen von Code zu Storage
Klasse:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
class Storage:
def save_to_db(self, animal):
print(f'Saved {animal} to the database')
def save_to_csv(self,animal):
printf(f’Saved {animal} to the CSV file’)
Die save_to_csv
-Methode ändert einen vorhandenen Storage
Klasse, um die Funktionalität hinzuzufügen. Dieser Ansatz verstößt gegen das Open-Closed-Prinzip, indem ein vorhandenes Element geändert wird, wenn eine neue Funktionalität erscheint.
Der Code erfordert das Entfernen des allgemeinen Storage
Klasse und das Erstellen individueller Klassen zum Speichern in bestimmten Dateiformaten.
Der folgende Code demonstriert die Anwendung des Open-Closed-Prinzips:
class DB():
def save(self, animal):
print(f'Saved {animal} to the database')
class CSV():
def save(self, animal):
print(f'Saved {animal} to a CSV file')
Der Code folgt dem Open-Closed-Prinzip. Der vollständige Code sieht nun so aus:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'"{self.name}"'
class DB():
def save(self, animal):
print(f'Saved {animal} to the database')
class CSV():
def save(self, animal):
print(f'Saved {animal} to a CSV file')
if __name__ == '__main__':
a = Animal('Cat')
db = DB()
csv = CSV()
db.save(a)
csv.save(a)
Das Erweitern um zusätzliche Funktionalitäten (wie das Speichern in einer XML-Datei) ändert keine bestehenden Klassen.
Liskov-Substitutionsprinzip (LSP)
Das Liskov-Substitutionsprinzip (LSP) besagt:„Funktionen, die Zeiger oder Verweise auf Basisklassen verwenden, müssen in der Lage sein, Objekte abgeleiteter Klassen zu verwenden, ohne es zu wissen.“
Das Prinzip besagt, dass eine Elternklasse eine Kindklasse ersetzen kann, ohne dass sich die Funktionalität merklich ändert.
Sehen Sie sich das folgende Beispiel zum Schreiben von Dateien an:
# Parent class
class FileHandling():
def write_db(self):
return f'Handling DB'
def write_csv(self):
return f'Handling CSV'
# Child classes
class WriteDB(FileHandling):
def write_db(self):
return f'Writing to a DB'
def write_csv(self):
return f"Error: Can't write to CSV, wrong file type."
class WriteCSV(FileHandling):
def write_csv(self):
return f'Writing to a CSV file'
def write_db(self):
return f"Error: Can't write to DB, wrong file type."
if __name__ == "__main__":
# Parent class instantiation and function calls
db = FileHandling()
csv = FileHandling()
print(db.write_db())
print(db.write_csv())
# Children classes instantiations and function calls
db = WriteDB()
csv = WriteCSV()
print(db.write_db())
print(db.write_csv())
print(csv.write_db())
print(csv.write_csv())
Die übergeordnete Klasse (FileHandling
) besteht aus zwei Methoden zum Schreiben in eine Datenbank und eine CSV-Datei. Die Klasse verarbeitet beide Funktionen und gibt eine Nachricht zurück.
Die beiden untergeordneten Klassen (WriteDB
und WriteCSV
) erben Eigenschaften von der übergeordneten Klasse (FileHandling
). Beide Kinder werfen jedoch einen Fehler, wenn sie versuchen, die unangemessene Write-Funktion zu verwenden, was gegen das Liskov-Substitutionsprinzip verstößt, da die überschreibenden Funktionen nicht den übergeordneten Funktionen entsprechen.
Der folgende Code behebt die Probleme:
# Parent class
class FileHandling():
def write(self):
return f'Handling file'
# Child classes
class WriteDB(FileHandling):
def write(self):
return f'Writing to a DB'
class WriteCSV(FileHandling):
def write(self):
return f'Writing to a CSV file'
if __name__ == "__main__":
# Parent class instantiation and function calls
db = FileHandling()
csv = FileHandling()
print(db.write())
print(csv.write())
# Children classes instantiations and function calls
db = WriteDB()
csv = WriteCSV()
print(db.write())
print(csv.write())
Die untergeordneten Klassen entsprechen korrekt der übergeordneten Funktion.
Prinzip der Schnittstellentrennung (ISP)
Das Prinzip der Schnittstellentrennung (ISP) besagt:„Viele kundenspezifische Schnittstellen sind besser als eine universelle Schnittstelle.“
Das heißt, umfangreichere Interaktionsschnittstellen werden in kleinere aufgeteilt. Das Prinzip stellt sicher, dass Klassen nur die Methoden verwenden, die sie benötigen, wodurch die Gesamtredundanz reduziert wird.
Das folgende Beispiel demonstriert eine universelle Schnittstelle:
class Animal():
def walk(self):
pass
def swim(self):
pass
class Cat(Animal):
def walk(self):
print("Struts")
def fly(self):
raise Exception("Cats don't swim")
class Duck(Animal):
def walk(self):
print("Waddles")
def swim(self):
print("Floats")
Die untergeordneten Klassen erben von den übergeordneten Animal
Klasse, die walk
enthält und fly
Methoden. Obwohl beide Funktionen für bestimmte Tiere akzeptabel sind, haben einige Tiere redundante Funktionen.
Um die Situation zu bewältigen, teilen Sie die Schnittstelle in kleinere Abschnitte auf. Zum Beispiel:
class Walk():
def walk(self):
pass
class Swim(Walk):
def swim(self):
pass
class Cat(Walk):
def walk(self):
print("Struts")
class Duck(Swim):
def walk(self):
print("Waddles")
def swim(self):
print("Floats")
Die Fly
Klasse erbt von Walk
, wodurch zusätzliche Funktionalität für geeignete untergeordnete Klassen bereitgestellt wird. Das Beispiel erfüllt das Prinzip der Schnittstellentrennung.
Das Hinzufügen eines weiteren Tieres, z. B. eines Fisches, erfordert eine weitere Atomisierung der Schnittstelle, da Fische nicht laufen können.
Prinzip der Abhängigkeitsinversion (DIP)
Das Prinzip der Abhängigkeitsinversion besagt:„Abhängig von Abstraktionen, nicht von Konkretionen.“
Das Prinzip zielt darauf ab, Verbindungen zwischen Klassen zu reduzieren, indem eine Abstraktionsschicht hinzugefügt wird. Das Verschieben von Abhängigkeiten in Abstraktionen macht den Code robust.
Das folgende Beispiel demonstriert die Klassenabhängigkeit ohne Abstraktionsschicht:
class LatinConverter:
def latin(self, name):
print(f'{name} = "Felis catus"')
return "Felis catus"
class Converter:
def start(self):
converter = LatinConverter()
converter.latin('Cat')
if __name__ == '__main__':
converter = Converter()
converter.start()
Das Beispiel hat zwei Klassen:
LatinConverter
verwendet eine imaginäre API, um den lateinischen Namen für ein Tier abzurufen (fest codiert „Felis catus
” der Einfachheit halber).Converter
ist ein übergeordnetes Modul, das eine Instanz vonLatinConverter
verwendet und seine Funktion zum Konvertieren des bereitgestellten Namens. DerConverter
hängt stark vonLatinConverter
ab Klasse, die von der API abhängt. Diese Vorgehensweise verstößt gegen das Prinzip.
Das Prinzip der Abhängigkeitsumkehr erfordert das Hinzufügen einer Abstraktionsschnittstellenschicht zwischen den beiden Klassen.
Eine Beispiellösung sieht wie folgt aus:
from abc import ABC
class NameConverter(ABC):
def convert(self,name):
pass
class LatinConverter(NameConverter):
def convert(self, name):
print('Converting using Latin API')
print(f'{name} = "Felis catus"')
return "Felis catus"
class Converter:
def __init__(self, converter: NameConverter):
self.converter = converter
def start(self):
self.converter.convert('Cat')
if __name__ == '__main__':
latin = LatinConverter()
converter = Converter(latin)
converter.start()
Der Converter
Klasse hängt jetzt von NameConverter
ab Schnittstelle statt auf LatinConverter
direkt. Zukünftige Updates ermöglichen die Definition von Namensumwandlungen mit einer anderen Sprache und API über NameConverter
Schnittstelle.
Warum gibt es eine Notwendigkeit für solide Prinzipien?
SOLID-Prinzipien helfen bei der Bekämpfung von Entwurfsmusterproblemen. Das übergeordnete Ziel der SOLID-Prinzipien besteht darin, Codeabhängigkeiten zu reduzieren, und das Hinzufügen einer neuen Funktion oder das Ändern eines Teils des Codes unterbricht nicht den gesamten Build.
Als Ergebnis der Anwendung von SOLID-Prinzipien auf objektorientiertes Design wird der Code einfacher zu verstehen, zu verwalten, zu warten und zu ändern. Da die Regeln besser für große Projekte geeignet sind, erhöht die Anwendung der SOLID-Prinzipien die Gesamtgeschwindigkeit und Effizienz des Entwicklungslebenszyklus.
Sind SOLID-Prinzipien noch relevant?
Obwohl die SOLID-Prinzipien über 20 Jahre alt sind, bieten sie immer noch eine gute Grundlage für das Design von Softwarearchitekturen. SOLID bietet solide Designprinzipien, die auf moderne Programme und Umgebungen anwendbar sind, nicht nur auf objektorientierte Programmierung.
SOLID-Prinzipien gelten in Situationen, in denen Code von Menschen geschrieben und modifiziert wird, in Modulen organisiert ist und interne oder externe Elemente enthält.
Schlussfolgerung
SOLID-Prinzipien helfen dabei, einen guten Rahmen und Leitfaden für das Design von Softwarearchitekturen bereitzustellen. Die Beispiele aus diesem Leitfaden zeigen, dass selbst eine dynamisch typisierte Sprache wie Python von der Anwendung der Prinzipien auf das Codedesign profitiert.
Lesen Sie als Nächstes mehr über die 9 DevOps-Prinzipien, die Ihrem Team helfen, das Beste aus DevOps herauszuholen.
Cloud Computing