Wenn man mit der Arbeit an größeren Python-Projekten beginnt, fällt einem als erstes auf, dass Der Code wird dadurch schwer verständlich, testbar und erweiterbar. Wenn man einige grundlegende Konstruktionsregeln nicht beachtet, kommen die bekannten SOLID-Prinzipien ins Spiel: eine Sammlung bewährter Methoden, die die Arbeit des Teams deutlich erleichtern sollen.
Diese Prinzipien haben ihren Ursprung im Bereich der klassische objektorientierte Programmierung (Java, C++, C#, etc.)Aber sie passen perfekt zu Python, solange man Klassen und Objekte mehr oder weniger ernst nimmt. Schauen wir uns genauer an, was sie sind, woher sie kommen, warum sie wichtig sind und vor allem, wie man sie verwendet. Wenden Sie SOLID anhand klarer Beispiele in Python an. um Ihren Code wartungsfreundlicher, skalierbarer und angenehmer in der Arbeit zu gestalten.
Was ist SOLID und woher kommt es?
Der Begriff SOLID ist ein Akronym, das von Michael Feathers populär gemacht wurde. Diese Gruppe fasst fünf Designprinzipien zusammen, die ursprünglich von Robert C. Martin, besser bekannt als Uncle Bob, vorgeschlagen wurden. Dieser amerikanische Softwareingenieur, einer der Unterzeichner des Agilen Manifests, veröffentlichte Mitte der 90er Jahre den Artikel „The Principles of OOD“ und später „Design Principles and Design Patterns“, in denen er viele der Grundlagen des modernen objektorientierten Designs legte.
Im Laufe der Zeit kamen weitere Autoren wie zum Beispiel Barbara Liskov und Bertrand Meyer Sie steuerten außerdem Ideen bei, die in diese Prinzipien integriert wurden. Michael Feathers hatte die (sehr kluge) Idee, die Initialen so anzuordnen, dass sie das Wort SOLID bildeten, was dazu beitrug, dass sie sich in der Entwicklergemeinschaft rasant verbreiteten.
Die fünf Buchstaben von SOLID entsprechen diesen objektorientierten Designprinzipien, die auch für Python gelten:
- S – Prinzip der Einzelverantwortung (Prinzip der Einzelverantwortung)
- O – Offen/Geschlossen-Prinzip (Offen/Geschlossen-Prinzip)
- L – Liskovsches Substitutionsprinzip (Liskovsches Substitutionsprinzip)
- I – Prinzip der Schnittstellentrennung (Prinzip der Schnittstellentrennung)
- D – Prinzip der Abhängigkeitsumkehrung (Prinzip der Abhängigkeitsumkehr)
Die Grundidee ist, dass diese fünf Prinzipien, wenn sie zusammen angewendet werden, Sie helfen Ihnen dabei, flexible, einfach zu testende und wartungsfreundliche Software zu schreiben.Dies führt zu schnelleren Bereitstellungen, weniger unerklärlichen Fehlern, besserer Code-Wiederverwendung und weniger Problemen, wenn das Projekt schon einige Jahre im Produktiveinsatz ist.
Wozu dienen die SOLID-Prinzipien in Python?
Die Anwendung der SOLID-Prinzipien in Python ist nicht nur eine akademische Übung, sondern hat direkte Auswirkungen auf die tägliche Arbeit des Teams. Wenn Sie sich an diese Prinzipien halten, Sie reduzieren Spaghetti-Code, mindern den Code-Geruch und verhindern, dass Ihre Codebasis „verkommen riecht“.Um es mit der bekannten Analogie zu sagen: „Wenn es schlecht riecht, ist etwas schlecht konstruiert.“ Viele Entwickler entscheiden sich unter Windows dafür, Installieren und Konfigurieren von WSL2 um eine Linux-Umgebung zu erhalten, die näher an der Produktionsumgebung liegt.
In kollaborativen Umgebungen (Backend-Entwicklungsteams, Datenverarbeitung, Produkte mit langen Zyklen usw.) sind diese Prinzipien von zentraler Bedeutung. Mehrere Personen können an derselben Codebasis arbeiten, ohne ihre Kompetenzen zu überschreiten oder bei der kleinsten Änderung alles kaputt zu machen.Darüber hinaus ermöglicht Python trotz seiner Flexibilität und Dynamik die nahtlose Anwendung typischer OOP-Abstraktionen: abstrakte Klassen, Vererbungshierarchien, Komposition und Schnittstellen. abc, usw.
Zusammenfassend lässt sich sagen, dass SOLID Ihnen dabei hilft, Folgendes zu erreichen:
- Saubererer, besser lesbarer Codeselbst Jahre nach seiner Entstehung.
- Verbesserte Testbarkeitweil die Verantwortlichkeiten klar getrennt sind.
- Hohe Wiederverwendbarkeit und Skalierbarkeit dank weniger starrer Abhängigkeiten zwischen den Modulen.
- Weniger KollateralschädenWenn Sie etwas in einem Modul ändern, machen Sie nicht versehentlich fünf andere Dinge kaputt.
S – Prinzip der Einzelverantwortung
Der erste Grundsatz besagt, dass Eine Klasse sollte nur einen Grund für eine Veränderung haben.Anders ausgedrückt: Es muss eine einzige, klar definierte Verantwortung übernehmen. Das bedeutet nicht, nur eine Methode zu haben, sondern vielmehr, dass seine gesamte Logik auf ein einziges, kohärentes Ziel ausgerichtet sein muss.
Stellen Sie sich eine Python-Klasse vor, die einen Benutzer repräsentiert und neben der Speicherung seiner Daten auch den Zugriff auf die Datenbank und die Generierung von Berichten übernimmt:
class User:
def __init__(self, name: str):
self.name = name
def get_user_from_database(self, user_id: int) -> dict:
# Recupera datos desde la base de datos
# ...
pass
def save_user_to_database(self) -> None:
# Persiste el usuario en la base de datos
# ...
pass
def generate_user_report(self) -> str:
# Genera un informe del usuario
# ...
pass
Hier ist die Klasse vereint drei unterschiedliche VerantwortlichkeitenDie Klasse repräsentiert den Benutzer, verwaltet die Persistenz und erstellt Berichte. Änderungen an der Datenbank, dem Berichtsformat oder den Benutzerattributen erfordern die Anpassung derselben Klasse, wodurch das Risiko übergreifender Fehler steigt.
Wenn wir diese Aspekte trennen, verbessert sich das Design deutlich:
class User:
def __init__(self, name: str):
self.name = name
class UserDB:
@staticmethod
def get_user(user_id: int) -> User:
# Lógica para obtener usuarios de la base de datos
# ...
return User("John Doe")
@staticmethod
def save_user(user: User) -> None:
# Lógica para guardar el usuario
# ...
pass
class UserReportGenerator:
@staticmethod
def generate_report(user: User) -> str:
# Lógica para generar informes de usuario
# ...
return f"Report for user: {user.name}"
Nun zur Klasse Der Benutzer repräsentiert lediglich den Benutzer als EntitätWenn sich die Art der Berichtserstellung ändert, tippen Sie einfach UserReportGeneratorWenn Sie die Datenbank ändern, müssen Sie nur die entsprechende Stelle berühren. UserDBJede Klasse hat einen einzigen Grund für eine Änderung, was das Debuggen und die Systementwicklung vereinfacht.
SRP angewendet auf ein realistischeres Beispiel: Enten und Kommunikation
Betrachten wir ein klassisches, angepasstes Szenario: eine Klasse Duck Zunächst werden nach und nach immer mehr Verantwortlichkeiten hinzugefügt, bis daraus ein schwer zu wartendes Monstrum wird. Stellen Sie sich eine naive Implementierung vor:
class Duck:
def __init__(self, name: str):
self.name = name
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
def greet(self, other_duck: "Duck") -> None:
print(f"{self.name}: {self.do_sound()}, hello {other_duck.name}")
Klasse Es sollte einfach als „eine Ente“ definiert werden.Es steuert aber auch, wie sie miteinander kommunizieren. Wenn Sie morgen die Logik der Konversation ändern (mehr Phrasen, andere Sprachen, andere Kanäle), müssen Sie die Entenklasse anpassen, die bereits als Entität gut funktioniert.
Die Lösung, die das SRP respektiert, besteht darin, diese zweite Verantwortung aus einer anderen Klasse auszulagern, die auf Kommunikation spezialisiert ist:
class Duck:
def __init__(self, name: str):
self.name = name
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
class Communicator:
def __init__(self, channel: str):
self.channel = channel
def communicate(self, duck1: Duck, duck2: Duck) -> None:
sentence1 = f"{duck1.name}: {duck1.do_sound()}, hello {duck2.name}"
sentence2 = f"{duck2.name}: {duck2.do_sound()}, hello {duck1.name}"
conversation =
print(*conversation, f"(via {self.channel})", sep="\n")
Dank dieser Trennung Man kann die Kommunikationslogik weiterentwickeln, ohne die Definition der Ente anzutasten.Außerdem ist der Code leichter zu testen: Man testet das Verhalten von Duck und andererseits diejenige von Communicatorohne Vermischung von Verantwortlichkeiten.
O – Offen/Geschlossen-Prinzip
Das OCP-Prinzip besagt, dass Software-Entitäten sollten offen für Erweiterungen ihres Verhaltens sein, jedoch für direkte Modifikationen verschlossen.Mit anderen Worten: Wenn Sie neue Funktionen hinzufügen möchten, sollten Sie idealerweise keine Klassen neu schreiben müssen, die bereits funktionieren und von anderen Modulen verwendet werden.
Ein klassisches Beispiel ist die Berechnung der Flächen geometrischer Figuren. Schauen wir uns zunächst eine Variante an, die respektiert OCP nicht:
class Rectangle:
def __init__(self, width: float, height: float):
self.width = width
self.height = height
class Circle:
def __init__(self, radius: float):
self.radius = radius
class AreaCalculator:
def calculate_area(self, shape) -> float:
if isinstance(shape, Rectangle):
return shape.width * shape.height
elif isinstance(shape, Circle):
return 3.14159 * shape.radius * shape.radius
else:
raise ValueError("Forma no soportada")
Wenn Sie morgen ein Dreieck hinzufügen möchten, werden Sie dazu gezwungen sein den Code von AreaCalculatoreinen weiteren hinzufügen elifDies verstößt gegen OCP, da die Klasse nicht mehr „geschlossen“ gegenüber Änderungen ist.
Die korrekte Version beinhaltet die Einführung einer Abstraktion. Shape mit einer Methode area() die jede Figur auf ihre eigene Weise umsetzt:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self) -> float:
pass
class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height
def area(self) -> float:
return self.width * self.height
class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius
def area(self) -> float:
return 3.14159 * self.radius * self.radius
class AreaCalculator:
def calculate_area(self, shape: Shape) -> float:
return shape.area()
Dank dieses Designs, für Füge ein Dreieck hinzu, das du nicht berührst. AreaCalculatorSie erstellen einfach eine neue Unterklasse:
class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height
def area(self) -> float:
return 0.5 * self.base * self.height
Das Offen/Geschlossen-Prinzip passt sehr gut zu der Idee von durch Abstraktionen klare Erweiterungspunkte definierenSchnittstellen, abstrakte Klassen, Hooks usw. In Python ist das Modul abc Es ermöglicht Ihnen, dies explizit auszudrücken, selbst wenn die Sprache dynamisch ist.
OCP angewendet auf das Kommunikationsbeispiel
Wenn wir auf das Beispiel von zurückkommen CommunicatorWir können noch einen Schritt weitergehen und das Design so gestalten, dass es verschiedene Gesprächsarten unterstützt, ohne den Kommunikator jedes Mal neu schreiben zu müssen. Dazu definieren wir eine Gesprächsabstraktion, die der Kommunikator ausschließlich verwendet.
from typing import final
from abc import ABC, abstractmethod
class AbstractConversation(ABC):
@abstractmethod
def do_conversation(self) -> list:
pass
class SimpleConversation(AbstractConversation):
def __init__(self, duck1: Duck, duck2: Duck):
self.duck1 = duck1
self.duck2 = duck2
def do_conversation(self) -> list:
sentence1 = f"{self.duck1.name}: {self.duck1.do_sound()}, hello {self.duck2.name}"
sentence2 = f"{self.duck2.name}: {self.duck2.do_sound()}, hello {self.duck1.name}"
return
class Communicator:
def __init__(self, channel: str):
self.channel = channel
@final
def communicate(self, conversation: AbstractConversation) -> None:
print(*conversation.do_conversation(), f"(via {self.channel})", sep="\n")
In dieser Version Wenn Sie eine neue Gesprächsart hinzufügen möchten (zum Beispiel eine aggressive Konversation, eine Konversation mit abwechselnden Gesprächsrunden usw.), erstellen Sie einfach eine weitere Unterklasse von AbstractConversation. Die Methode communicate() de Communicator Es ändert sich nichts, die OCP wird weiterhin buchstabengetreu eingehalten.
L – Liskovsches Substitutionsprinzip
Das von Barbara Liskov formulierte Liskov-Substitutionsprinzip besagt, dass Unterklassen sollten in der Lage sein, ihre Basisklassen zu ersetzen, ohne das erwartete Verhalten des Programms zu verändern.In der Praxis bedeutet dies, dass, wenn der Code mit einer Instanz der Basisklasse funktioniert, er genauso gut mit jeder Instanz einer Unterklasse funktionieren sollte.
Ein typisches Beispiel für eine Verletzung des LSP ist die Modellierung aller Vögel mit einer einzigen Methode. fly()einschließlich Strauße:
class Bird:
def fly(self) -> None:
pass
class Duck(Bird):
def fly(self) -> None:
print("¡El pato está volando!")
class Ostrich(Bird):
def fly(self) -> None:
# Las avestruces no vuelan
raise NotImplementedError("Las avestruces no pueden volar")
Jeder Code, der davon ausgeht, dass Jeder Vogel, der fliegen kann, wird scheitern, wenn er einen Strauß bekommt.. Ich meine Ostrich Es ist kein gültiger Ersatz für Birdund verstößt damit gegen die LSP.
Die Lösung besteht darin, die Hierarchie so anzupassen, dass sie die Realität besser widerspiegelt: Nicht alle Vögel fliegen, also Nur ein Teil der Vögel sollte diese Methode anwenden. fly():
class Bird:
pass
class FlyingBird(Bird):
def fly(self) -> None:
pass
class Duck(FlyingBird):
def fly(self) -> None:
print("¡El pato está volando!")
class Ostrich(Bird):
# No vuela, así que no implementa fly()
pass
Mit diesem Design Jede Funktion, die einen fliegenden Vogel benötigt, wird deklarieren, dass sie einen benötigt. FlyingBirdund wird niemals einen Strauß erhalten. Auf diese Weise wird das LSP eingehalten und unerwartete Laufzeitausnahmen werden vermieden.
LSP und Vogelgespräche
Um auf das Beispiel der Konversationen zurückzukommen: Häufig beginnt man mit dem Programmieren und denkt zunächst nur an Enten, möchte dann aber Krähen oder andere Vögel hinzufügen. Wenn die Konversationsklasse von … abhängt … Duck, Sie können es nicht für andere Vogelarten wiederverwenden. ohne den Code zu verändern:
class Crow:
# Implementación específica del cuervo
...
Si SimpleConversation Es ist nur für Enten typisiert; man kann nicht einfach eine Krähe ohne Anpassungen durchlaufen lassen. Der richtige Ansatz ist die Schaffung einer gemeinsamen Abstraktion. Bird und die Konversation von dieser Abstraktion abhängig machen:
from abc import ABC, abstractmethod
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def do_sound(self) -> str:
pass
class Crow(Bird):
def do_sound(self) -> str:
return "Caw"
class Duck(Bird):
def do_sound(self) -> str:
return "Quack"
class SimpleConversation(AbstractConversation):
def __init__(self, bird1: Bird, bird2: Bird):
self.bird1 = bird1
self.bird2 = bird2
def do_conversation(self) -> list:
sentence1 = f"{self.bird1.name}: {self.bird1.do_sound()}, hello {self.bird2.name}"
sentence2 = f"{self.bird2.name}: {self.bird2.do_sound()}, hello {self.bird1.name}"
return
Auf diese Weise kann jede Unterklasse von Bird die den Vertrag respektiert (do_sound()(Name usw.) ist ein gültiger Ersatz und wird das erwartete Verhalten nicht beeinträchtigen SimpleConversation.
I – Prinzip der Schnittstellentrennung
Das ISP-Prinzip besagt, dass Kein Kunde sollte gezwungen sein, auf Methoden zurückzugreifen, die er nicht anwendet.Übersetzt in abstrakte Klassen oder Schnittstellen bedeutet dies, dass es besser ist, mehrere kleine, spezifische Schnittstellen zu haben als eine riesige, generische Schnittstelle.
Betrachten Sie dieses Design, bei dem eine Schnittstelle Worker Es erfordert von allen, die es umsetzen, bestimmte Arbeits- und Ernährungsmethoden:
from abc import ABC, abstractmethod
class Worker(ABC):
@abstractmethod
def work(self) -> None:
pass
@abstractmethod
def eat(self) -> None:
pass
class Human(Worker):
def work(self) -> None:
print("El humano está trabajando")
def eat(self) -> None:
print("El humano está comiendo")
class Robot(Worker):
def work(self) -> None:
print("El robot está trabajando")
def eat(self) -> None:
# El robot no come, pero está obligado a declarar este método
pass
Klasse Der Roboter nutzt eine Methode eat() das nicht benötigtJede Veränderung im Zusammenhang mit Nahrung wirkt sich auf den Roboter aus, selbst wenn sie nichts mit diesem Verhalten zu tun hat.
Durch die Anwendung von ISP haben wir die Schnittstelle in zwei kleinere, spezifischere Schnittstellen unterteilt:
class Workable(ABC):
@abstractmethod
def work(self) -> None:
pass
class Eatable(ABC):
@abstractmethod
def eat(self) -> None:
pass
class Human(Workable, Eatable):
def work(self) -> None:
print("El humano está trabajando")
def eat(self) -> None:
print("El humano está comiendo")
class Robot(Workable):
def work(self) -> None:
print("El robot está trabajando")
Jetzt Jede Klasse implementiert nur die Methoden, die sie tatsächlich benötigt.Dadurch werden Kopplungen reduziert, die Weiterentwicklung des Designs erleichtert und der Code ausdrucksstärker: Es wird sehr deutlich, wer was tun kann.
ISP in der Vogelmodellierung: Fliegen und Schwimmen
Etwas Ähnliches geschieht bei der Modellierung von Vögeln, die fliegen und schwimmen. Wenn die grundlegende Abstraktion Bird Es erfordert die Umsetzung beider. fly() als swim()Am Ende wirst du Kurse wie diesen haben: Crow die so tun müssen, als könnten sie schwimmen:
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def fly(self) -> None:
pass
@abstractmethod
def swim(self) -> None:
pass
@abstractmethod
def do_sound(self) -> str:
pass
Die Lösung laut Internetanbieter lautet: die Schnittstelle in spezifischere Funktionen unterteilen:
class Bird(ABC):
def __init__(self, name: str):
self.name = name
@abstractmethod
def do_sound(self) -> str:
pass
class FlyingBird(Bird):
@abstractmethod
def fly(self) -> None:
pass
class SwimmingBird(Bird):
@abstractmethod
def swim(self) -> None:
pass
class Crow(FlyingBird):
def fly(self) -> None:
print(f"{self.name} is flying high and fast!")
def do_sound(self) -> str:
return "Caw"
class Duck(SwimmingBird, FlyingBird):
def fly(self) -> None:
print(f"{self.name} is flying not very high")
def swim(self) -> None:
print(f"{self.name} swims in the lake and quacks")
def do_sound(self) -> str:
return "Quack"
Falls Sie jemals beschließen sollten, einen Pinguin als Modell zu bauen, dann einfach Du lässt ihn erben von SwimmingBird aber nicht von FlyingBirdUnd Sie müssen keine leeren Methoden implementieren oder künstliche Ausnahmen auslösen.
D – Prinzip der Abhängigkeitsumkehrung
Das letzte Prinzip, DIP, lässt sich in zwei Kernideen zusammenfassen: Hochrangige Module sollten nicht von niedrigrangigen Modulen abhängen; beide sollten von Abstraktionen abhängen.Abstraktionen sollten nicht von Details abhängen, sondern Details sollten von Abstraktionen abhängen.
In der Praxis bedeutet dies, dass Ihre Geschäftslogik nicht an spezifische Details wie „Ich verwende MySQL“, „Ich schreibe in eine lokale Datei“ oder „Ich versende SMS-Nachrichten mit diesem Anbieter“ gebunden sein sollte. Stattdessen definieren Sie abstrakte Schnittstellen (zum Beispiel, Database, Channel, NotificationService) und Sie sorgen dafür, dass Ihr übergeordneter Code nur mit ihnen kommuniziert.
Ein Design, das DIP brechen Dies wäre ein Benutzer-Repository, das direkt eine MySQL-Datenbank instanziiert:
class MySQLDatabase:
def connect(self) -> None:
# Conectar a MySQL
pass
def query(self, sql: str) -> list:
# Ejecutar consulta
return []
class UserRepository:
def __init__(self) -> None:
self.database = MySQLDatabase() # Dependencia directa
def get_users(self) -> list:
return self.database.query("SELECT * FROM users")
Wenn Sie sich morgen für PostgreSQL entscheiden, müssen Sie die Klasse auf hohem Niveau modifizieren UserRepositorySie sind an ein bestimmtes Implementierungsdetail gebunden.
Durch die Anwendung von DIP definieren wir zunächst eine Datenbankabstraktion und lassen dann die konkreten Implementierungen davon erben:
from abc import ABC, abstractmethod
class Database(ABC):
@abstractmethod
def connect(self) -> None:
pass
@abstractmethod
def query(self, sql: str) -> list:
pass
class MySQLDatabase(Database):
def connect(self) -> None:
# Conexión a MySQL
pass
def query(self, sql: str) -> list:
# Consulta en MySQL
return []
class PostgreSQLDatabase(Database):
def connect(self) -> None:
# Conexión a PostgreSQL
pass
def query(self, sql: str) -> list:
# Consulta en PostgreSQL
return []
class UserRepository:
def __init__(self, database: Database) -> None:
self.database = database # Depende de una abstracción
def get_users(self) -> list:
return self.database.query("SELECT * FROM users")
Somit Sie können jede beliebige Implementierung von Database beim Erstellen des Repositorys, ohne dessen internen Code zu verändern:
mysql_db = MySQLDatabase()
user_repo = UserRepository(mysql_db)
postgres_db = PostgreSQLDatabase()
user_repo = UserRepository(postgres_db)
Dieses Muster ist bekannt als Abhängigkeitsinjektion Und dies ist die gebräuchlichste Art, DIP anzuwenden: Klassen erzeugen ihre Abhängigkeiten nicht selbst, sondern erhalten sie von außen (über den Konstruktor oder über spezifische Methoden), wobei immer Abstraktionen als Typ verwendet werden.
DIP angewendet auf Kanäle und Kommunikationsgeräte
Am Beispiel der Vogelkommunikation lässt sich das Kanalmanagement ebenfalls durch die Anwendung von DIP verbessern. Angenommen, man definiert eine Abstraktion für den Kanal und eine weitere für den Kommunikator:
class AbstractChannel(ABC):
@abstractmethod
def get_channel_message(self) -> str:
pass
class AbstractCommunicator(ABC):
@abstractmethod
def get_channel(self) -> AbstractChannel:
pass
@final
def communicate(self, conversation: AbstractConversation) -> None:
print(*conversation.do_conversation(),
self.get_channel().get_channel_message(),
sep="\n")
Eine erste, naive Implementierung könnte so aussehen:
class SMSChannel(AbstractChannel):
def get_channel_message(self) -> str:
return "(via SMS)"
class SMSCommunicator(AbstractCommunicator):
def __init__(self) -> None:
self._channel = SMSChannel() # Depende de detalle concreto
def get_channel(self) -> AbstractChannel:
return self._channel
Obwohl es richtig erscheint, Dieser Kommunikator ist weiterhin direkt gekoppelt mit SMSChannelWir haben das Design verbessert, indem wir den Kommunikator den Kanal von außen empfangen ließen (Dependency Injection) und er somit nur noch von der Abstraktion abhängig ist:
class SimpleCommunicator(AbstractCommunicator):
def __init__(self, channel: AbstractChannel) -> None:
self._channel = channel
def get_channel(self) -> AbstractChannel:
return self._channel
Mit diesem Ansatz implementiert jeder neue Kanal (E-Mail, Push-Benachrichtigungen usw.) AbstractChannel y Es kann verwendet werden, ohne den Kommunikatorcode zu ändern.Auch hier gilt: Hochrangige Klassen basieren auf Abstraktionen, nicht auf Details.
Was passiert, wenn man SOLID ignoriert?
Werden diese Prinzipien nicht berücksichtigt, neigt der Code zu Problemen wie beispielsweise Code-Geruch, Code-Verfall und unlösbare VerflechtungenDas heißt, riesige Klassen mit tausend Verantwortlichkeiten, Unterklassen, die Verträge brechen, zyklische Abhängigkeiten und Methoden, die sich ständig ändern, weil sie zu viele Dinge tun.
Die Konsequenzen sind klar und für jedes Team äußerst schmerzhaft: Mehr Sicherheitslücken, mehr Bugs, ständiges Refactoring und im schlimmsten Fall Code, der am Ende praktisch unbrauchbar ist.Es handelt sich um sogenannten „Spaghetti-Code“: schwer nachzuvollziehen, voller Flickstellen und fast unmöglich zu erweitern, ohne etwas Wichtiges zu zerstören.
Die SOLID-Prinzipien sind nicht in Stein gemeißelt, und es ist nicht immer sinnvoll, sie alle strikt anzuwenden, insbesondere bei Rapid Prototyping oder sehr kleinen Projekten. Trotzdem Behalte sie im Hinterkopf und wende sie bei den meisten deiner objektorientierten Designs in Python an. Das macht den Unterschied aus zwischen einem Projekt, das mit der Zeit wächst, und einem, das zusammenbricht, sobald es ein wenig größer wird.