Eine der besten Möglichkeiten, Ihren Code zu verbessern, besteht darin, einige Prinzipien des Programmierentwurfs zu erlernen. Sie können sich Programmierprinzipien als einen allgemeinen Leitfaden vorstellen, um ein besserer Programmierer zu werden – die rohen Philosophien des „Clean Code Python“, könnte man sagen. Nun, es gibt eine ganze Reihe von Prinzipien (man könnte sogar argumentieren, dass es einen Überfluss geben könnte), aber ich werde fünf wesentliche behandeln, die unter dem Akronym SOLID zusammengefasst sind.
Hinweis: Ich werde in meinen Beispielen Python verwenden, aber diese Konzepte sind leicht auf andere Sprachen wie Java übertragbar. Also auf zu Clean Code Python.
1. Zunächst einmal… „S“ in SOLID steht für Single Responsibility
Dieses Prinzip lehrt uns:
Teilen Sie unseren Code in Module mit jeweils einer Verantwortung auf.
Werfen wir einen Blick auf diese Klasse, die nicht verwandte Aufgaben wie das Versenden von E-Mails und das Berechnen von Steuern ausführt.Person
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def send_email(self, message):
# Code to send an email to the person
print(f"Sending email to {self.name}: {message}")
def calculate_tax(self):
# Code to calculate tax for the person
tax = self.age * 100
print(f"{self.name}'s tax: {tax}")
Nach dem Prinzip der einzigen Verantwortung sollten wir die Klasse in mehrere kleinere Klassen aufteilen, um eine Verletzung des Prinzips zu vermeiden.Person
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
class EmailSender:
def send_email(person, message):
# Code to send an email to the person
print(f"Sending email to {person.name}: {message}")
class TaxCalculator:
def calculate_tax(person):
# Code to calculate tax for the person
tax = person.age * 100
print(f"{person.name}'s tax: {tax}")
Es sind mehr Zeilen – sicher – aber jetzt können wir leichter identifizieren, was jeder Abschnitt des Codes zu erreichen versucht, ihn sauberer testen und Teile davon an anderer Stelle wiederverwenden (ohne uns um irrelevante Methoden kümmern zu müssen).
2. Als nächstes kommt „O“… oder das Open/Closed-Prinzip
Dieses Prinzip legt nahe, dass wir unsere Module so gestalten, dass sie:
Fügen Sie in Zukunft neue Funktionen hinzu, ohne unseren vorhandenen Code direkt zu ändern
Sobald ein Modul verwendet wird, ist es im Wesentlichen gesperrt, und dies verringert die Wahrscheinlichkeit, dass neue Ergänzungen Ihren Code beschädigen.
Dies ist eines der am schwersten zu verstehenden der 5 Prinzipien, da es widersprüchlich ist, also werfen wir einen Blick auf ein Beispiel:
class Shape:
def __init__(self, shape_type, width, height):
self.shape_type = shape_type
self.width = width
self.height = height
def calculate_area(self):
if self.shape_type == "rectangle":
# Calculate and return the area of a rectangle
elif self.shape_type == "triangle":
# Calculate and return the area of a triangle
Im obigen Beispiel behandelt die Klasse verschiedene Formtypen direkt in ihrer Methode. Dies verstößt gegen das Open/Closed-Prinzip, da wir den vorhandenen Code ändern, anstatt ihn zu erweitern.Shape
calculate_area()
Dieser Entwurf ist problematisch, da die Methode komplexer und schwieriger zu verwalten ist, je mehr Formtypen hinzugefügt werden. Es verstößt gegen das Prinzip der Trennung von Verantwortlichkeiten und macht den Kodex weniger flexibel und erweiterbar. Werfen wir einen Blick auf eine Möglichkeit, wie wir dieses Problem lösen können.calculate_area()
class Shape:
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
pass
class Rectangle(Shape):
def calculate_area(self):
# Implement the calculate_area() method for Rectangle
class Triangle(Shape):
def calculate_area(self):
# Implement the calculate_area() method for Triangle
Im obigen Beispiel definieren wir die Basisklasse , deren einziger Zweck darin besteht, spezifischeren Formklassen zu ermöglichen, ihre Eigenschaften zu erben. Die Klasse wird z. B. auf die Methode erweitert, um die Fläche eines Dreiecks zu berechnen und zurückzugeben.Shape
Triangle
calculate_area()
Indem wir dem Open/Closed-Prinzip folgen, können wir neue Formen hinzufügen, ohne die vorhandene Klasse zu ändern. Dies ermöglicht es uns, die Funktionalität des Codes zu erweitern, ohne die Kernimplementierung ändern zu müssen.Shape
3. Nun zu ‚L’… das Liskow-Substitutionsprinzip (LSP)
In diesem Prinzip versucht Liskov im Grunde, uns folgendes zu sagen:
Unterklassen sollten in der Lage sein, austauschbar mit ihren Oberklassen verwendet zu werden, ohne die Funktionalität des Programms zu beeinträchtigen.
Was bedeutet das eigentlich? Betrachten wir eine Klasse mit einer Methode namens .Vehicle
start_engine()
class Vehicle:
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
# Start the car engine
print("Car engine started.")
class Motorcycle(Vehicle):
def start_engine(self):
# Start the motorcycle engine
print("Motorcycle engine started.")
Nach dem Liskov-Substitutionsprinzip sollte jede Unterklasse von auch in der Lage sein, den Motor ohne Probleme zu starten.Vehicle
Aber wenn wir zum Beispiel eine Klasse hinzugefügt haben. Wir könnten den Motor natürlich nicht mehr starten, weil Fahrräder keinen Motor haben. Im Folgenden wird die falsche Methode zur Behebung dieses Problems gezeigt.Bicycle
class Bicycle(Vehicle):
def ride(self):
# Rides the bike
print("Riding the bike.")
def start_engine(self):
# Raises an error
raise NotImplementedError("Bicycle does not have an engine.")
Um uns richtig an den LSP zu halten, könnten wir zwei Wege einschlagen. Werfen wir einen Blick auf die erste.
Lösung 1: wird zu einer eigenen Klasse (ohne Vererbung), um sicherzustellen, dass sich alle Unterklassen konsistent mit ihrer Oberklasse verhalten.Bicycle
Vehicle
class Vehicle:
def start_engine(self):
pass
class Car(Vehicle):
def start_engine(self):
# Start the car engine
print("Car engine started.")
class Motorcycle(Vehicle):
def start_engine(self):
# Start the motorcycle engine
print("Motorcycle engine started.")
class Bicycle():
def ride(self):
# Rides the bike
print("Riding the bike.")
Lösung 2: Die Superklasse ist in zwei Teile unterteilt, eine für Fahrzeuge mit Motor und eine für Letztere. Alle Unterklassen können dann austauschbar mit ihrer Oberklasse verwendet werden, ohne das erwartete Verhalten zu ändern oder Ausnahmen einzuführen.Vehicle
class VehicleWithEngines:
def start_engine(self):
pass
class VehicleWithoutEngines:
def ride(self):
pass
class Car(VehicleWithEngines):
def start_engine(self):
# Start the car engine
print("Car engine started.")
class Motorcycle(VehicleWithEngines):
def start_engine(self):
# Start the motorcycle engine
print("Motorcycle engine started.")
class Bicycle(VehicleWithoutEngines):
def ride(self):
# Rides the bike
print("Riding the bike.")
4. Der Nächste in der Reihe… „I“ für Schnittstellentrennung
Die allgemeine Definition besagt, dass unsere Module nicht gezwungen sein sollten, sich um Funktionalitäten zu kümmern, die sie nicht nutzen. Das ist allerdings etwas unklar. Lassen Sie uns diesen obskuren Satz in eine konkretere Anleitung umwandeln:
Client-spezifische Schnittstellen sind besser als universelle Schnittstellen. Das bedeutet, dass Klassen nicht gezwungen werden sollten, von Schnittstellen abhängig zu sein, die sie nicht verwenden. Stattdessen sollten sie auf kleinere, spezifischere Schnittstellen setzen.
Nehmen wir an, wir haben eine Tierschnittstelle
mit Methoden wie walk()
, swim()
und fly().
class Animal:
def walk(self):
pass
def swim(self):
pass
def fly(self):
pass
Die Sache ist die, dass nicht alle Tiere all diese Aktionen ausführen können.
Zum Beispiel: Hunde können weder schwimmen noch fliegen, und daher sind diese beiden Methoden, die von der Schnittstelle geerbt werden, überflüssig.Animal
class Dog(Animal):
# Dogs can only walk
def walk(self):
print("Dog is walking.")
class Fish(Animal):
# Fishes can only swim
def swim(self):
print("Fish is swimming.")
class Bird(Animal):
# Birds cannot swim
def walk(self):
print("Bird is walking.")
def fly(self):
print("Bird is flying.")
Wir müssen unsere Benutzeroberfläche in kleinere, spezifischere Unterkategorien unterteilen, die wir dann verwenden können, um einen genauen Satz von Funktionen zusammenzustellen, die jedes Tier benötigt.Animal
class Walkable:
def walk(self):
pass
class Swimmable:
def swim(self):
pass
class Flyable:
def fly(self):
pass
class Dog(Walkable):
def walk(self):
print("Dog is walking.")
class Fish(Swimmable):
def swim(self):
print("Fish is swimming.")
class Bird(Walkable, Flyable):
def walk(self):
print("Bird is walking.")
def fly(self):
print("Bird is flying.")
Auf diese Weise erreichen wir ein Design, bei dem sich die Klassen nur auf die Schnittstellen verlassen, die sie benötigen, wodurch unnötige Abhängigkeiten reduziert werden. Dies ist besonders nützlich beim Testen, da es uns ermöglicht, nur die Funktionalität zu simulieren, die jedes Modul benötigt.
5. Womit wir wären… ‚D‘ für Abhängigkeitsumkehr
Dies ist dann auch der letzte Punkt im Clean Code Python und er ist ziemlich einfach zu erklären, es besagt:
High-Level-Module sollten sich nicht direkt auf Low-Level-Module verlassen. Stattdessen sollten beide auf Abstraktionen (Interfaces oder abstrakte Klassen) setzen.
Schauen wir uns noch einmal ein Beispiel an. Angenommen, wir haben eine Klasse, die auf natürliche Weise Berichte generiert. Um diese Aktion auszuführen, müssen zuerst Daten aus einer Datenbank abgerufen werden.ReportGenerator
class SQLDatabase:
def fetch_data(self):
# Fetch data from a SQL database
print("Fetching data from SQL database...")
class ReportGenerator:
def __init__(self, database: SQLDatabase):
self.database = database
def generate_report(self):
data = self.database.fetch_data()
# Generate report using the fetched data
print("Generating report...")
In diesem Beispiel hängt die Klasse direkt von der konkreten Klasse ab.ReportGenerator
SQLDatabase
Das funktioniert im Moment gut, aber was wäre, wenn wir zu einer anderen Datenbank wie MongoDB wechseln wollten? Diese enge Kopplung würde es schwierig machen, die Datenbankimplementierung auszutauschen, ohne die Klasse zu ändern.ReportGenerator
Um dem Dependency Inversion Principle zu entsprechen, würden wir eine Abstraktion (oder Schnittstelle) einführen, von der sowohl die als auch die Klassen abhängen können.SQLDatabase
MongoDatabase
class Database():
def fetch_data(self):
pass
class SQLDatabase(Database):
def fetch_data(self):
# Fetch data from a SQL database
print("Fetching data from SQL database...")
class MongoDatabase(Database):
def fetch_data(self):
# Fetch data from a Mongo database
print("Fetching data from Mongo database...")
Beachten Sie, dass die Klasse nun auch über ihren Konstruktor von der neuen Schnittstelle abhängt.ReportGenerator
Database
class ReportGenerator:
def __init__(self, database: Database):
self.database = database
def generate_report(self):
data = self.database.fetch_data()
# Generate report using the fetched data
print("Generating report...")
Das High-Level-Modul () ist nun nicht mehr direkt von Low-Level-Modulen ( oder ) abhängig. Stattdessen hängen beide von der Schnittstelle () ab.ReportGenerator
SQLDatabase
MongoDatabase
Database
Abhängigkeitsumkehr bedeutet, dass unsere Module nicht wissen müssen, welche Implementierung sie erhalten – nur, dass sie bestimmte Eingaben erhalten und bestimmte Ausgaben zurückgeben.
Fazit
Heutzutage sehe ich online viele Diskussionen über die SOLID-Designprinzipien und darüber, ob sie den Test der Zeit bestanden haben. In dieser modernen Welt der Multi-Paradigmen-Programmierung, des Cloud Computing und des maschinellen Lernens… Ist SOLID noch relevant?
Persönlich glaube ich, dass die SOLID-Prinzipien immer die Grundlage für gutes Code-Design sein werden. Manchmal sind die Vorteile dieser Prinzipien bei der Arbeit mit kleinen Anwendungen nicht offensichtlich, aber sobald Sie anfangen, an größeren Projekten zu arbeiten, ist der Unterschied in der Codequalität die Mühe wert, sie zu erlernen. Die Modularität, die SOLID fördert, macht diese Prinzipien immer noch zur Grundlage für eine moderne Softwarearchitektur, und ich persönlich glaube nicht, dass sich das in absehbarer Zeit ändern wird. Dazu sollte dieser Beitrag Clean Code Python beitragen.
Weiterer lesenswerter Beitrag: „switch“ vermeiden und Polymorphismus überlegt einsetzen