Industrielle Fertigung
Industrielles Internet der Dinge | Industrielle Materialien | Gerätewartung und Reparatur | Industrielle Programmierung |
home  MfgRobots >> Industrielle Fertigung >  >> Industrial Internet of Things >> Cloud Computing

SOLID: Objektorientiertes Design meistern für sauberen, wartbaren Code

SOLID ist ein mnemonisches Akronym für Klassendesign in der objektorientierten Programmierung. Die Prinzipien etablieren Praktiken, die helfen, gute Programmiergewohnheiten und wartbaren Code zu entwickeln.

Indem sie Code-Wartbarkeit und Erweiterbarkeit langfristig berücksichtigen, bereichern SOLID-Prinzipien die agile Code-Entwicklungsumgebung. Die Berücksichtigung und Optimierung von Code-Abhängigkeiten hilft, einen unkomplizierteren und organisierteren Softwareentwicklungszyklus zu schaffen.

Was sind SOLID-Prinzipien?

SOLID repräsentiert eine Reihe von Prinzipien für das Design von Klassen. Robert C. Martin (Onkel Bob) führte die meisten Designprinzipien ein und prägte das Akronym.
SOLID steht für:

SOLID-Prinzipien repräsentieren eine Sammlung von Best Practices für Software-Design. Jede Idee repräsentiert ein Design-Framework, das zu besseren Programmiergewohnheiten, verbesserter Code-Struktur und weniger Fehlern führt.

SOLID: 5 Prinzipien erklärt

Der beste Weg, zu verstehen, wie SOLID-Prinzipien funktionieren, ist durch Beispiele. Alle Prinzipien sind komplementär und wenden sich auf individuelle Use Cases an. 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 jede objektorientierte Sprache wie PHP, Java oder C#. Die Verallgemeinerung der Regeln macht sie auf moderne Programmieransätze wie Microservices anwendbar.

Single-Responsibility Principle (SRP)

Das Einzelverantwortungsprinzip (SRP) besagt: „Es sollte nie mehr als einen Grund geben, warum eine Klasse geändert werden muss.“

Bei der Änderung einer Klasse sollten wir nur eine einzelne Funktionalität ändern, was impliziert, dass jedes Objekt nur einen Job haben sollte.

Als Beispiel betrachten Sie die folgende Klasse:

# Eine Klasse mit mehreren Verantwortlichkeiten
class Animal:
    # Property Konstruktor
    def __init__(self, name):
        self.name = name
    # Property Repräsentation
    def __repr__(self):
        return f'Animal(name="{self.name}")'
    # Datenbankmanagement
    def save(animal):
        print(f'Saved {animal} to the database')

if __name__ == '__main__':
    # Property Instanziierung
    a = Animal('Cat')
    # Speichern der Property in einer Datenbank
    Animal.save(a)

Bei Änderungen an der save()-Methode erfolgt die Änderung in der Animal-Klasse. Bei Property-Änderungen erfolgen die Modifikationen ebenfalls in der Animal-Klasse.

Die Klasse hat zwei Gründe für Änderungen und verletzt das Einzelverantwortungsprinzip. Auch wenn der Code wie erwartet funktioniert, macht die Nichtbeachtung des Designprinzips den Code langfristig schwerer zu verwalten.

Um das Einzelverantwortungsprinzip umzusetzen, beachten Sie, dass die Beispielklasse zwei unterschiedliche Jobs hat:

Der beste Weg, das Problem zu lösen, ist, die Datenbankmanagement-Methode in eine neue Klasse auszulagern. Zum Beispiel:

# Eine Klasse, die für Property-Management verantwortlich ist
class Animal:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f'Animal(name="{self.name}")'

# Eine Klasse, die für Datenbankmanagement verantwortlich ist
class AnimalDB:
    def save(self, animal):
        print(f'Saved {animal} to the database')

if __name__ == '__main__':
    # Property Instanziierung
    a = Animal('Cat')
    # Datenbank Instanziierung
    db = AnimalDB()
    # Speichern der Property in einer Datenbank
    db.save(a)

Die Änderung der AnimalDB-Klasse beeinflusst die Animal-Klasse nicht mehr, wenn das Einzelverantwortungsprinzip angewendet wird. Der Code ist intuitiv und leicht zu modifizieren.

Open-Closed Principle (OCP)

Das Offen-Geschlossen-Prinzip (OCP) besagt: „Software-Entitäten sollten für Erweiterungen offen, aber für Modifikationen geschlossen sein.“

Das Hinzufügen von Funktionalitäten und Use Cases zum System sollte nicht die Modifikation bestehender Entitäten erfordern. Die Formulierung wirkt widersprüchlich – das Hinzufügen neuer Funktionalitäten erfordert das Ändern bestehenden Codes.

Die Idee ist durch das folgende Beispiel 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 einer Animal-Instanz in einer Datenbank. Das Hinzufügen neuer Funktionalitäten, wie dem Speichern in einer CSV-Datei, erfordert das Hinzufügen von Code zur 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):
        print(f'Saved {animal} to the CSV file')

Die save_to_csv-Methode modifiziert eine bestehende Storage-Klasse, um die Funktionalität hinzuzufügen. Dieser Ansatz verletzt das Offen-Geschlossen-Prinzip, indem er ein existierendes Element ändert, wenn eine neue Funktionalität erscheint.

Der Code erfordert das Entfernen der allgemeinen Storage-Klasse und das Erstellen individueller Klassen für das Speichern in spezifischen Dateiformaten.

Der folgende Code demonstriert die Anwendung des Offen-Geschlossen-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 entspricht dem Offen-Geschlossen-Prinzip. Der vollständige Code sieht jetzt 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)

Die Erweiterung um zusätzliche Funktionalitäten (wie Speichern in einer XML-Datei) modifiziert keine bestehenden Klassen.

Liskov Substitution Principle (LSP)

Das Liskovsche Substitutionsprinzip (LSP) besagt: „Funktionen, die Zeiger oder Referenzen 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 es merkliche Änderungen in der Funktionalität gibt.

Schauen Sie sich das Dateischreib-Beispiel unten an:

# Elternklasse
class FileHandling():
    def write_db(self):
        return f'Handling DB'
    def write_csv(self):
        return f'Handling CSV'

# Kindklassen
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__":
    # Elternklasse Instanziierung und Funktionsaufrufe
    db = FileHandling()
    csv = FileHandling()
    print(db.write_db())
    print(db.write_csv())

    # Kinderklassen Instanziierungen und Funktionsaufrufe
    db = WriteDB()
    csv = WriteCSV()
    print(db.write_db())
    print(db.write_csv())
    print(csv.write_db())
    print(csv.write_csv())

Die Elternklasse (FileHandling) besteht aus zwei Methoden für das Schreiben in eine Datenbank und eine CSV-Datei. Die Klasse handhabt beide Funktionen und gibt eine Nachricht zurück.

Die beiden Kindklassen (WriteDB und WriteCSV) erben Eigenschaften von der Elternklasse (FileHandling). Allerdings werfen beide Kinder einen Fehler, wenn sie die unpassende Schreibfunktion verwenden wollen, was das Liskovsche Substitutionsprinzip verletzt, da die überschreibenden Funktionen nicht den Elternfunktionen entsprechen.

Der folgende Code löst die Probleme:

# Elternklasse
class FileHandling():
    def write(self):
        return f'Handling file'

# Kindklassen
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__":
    # Elternklasse Instanziierung und Funktionsaufrufe
    db = FileHandling()
    csv = FileHandling()
    print(db.write())
    print(csv.write())

    # Kinderklassen Instanziierungen und Funktionsaufrufe
    db = WriteDB()
    csv = WriteCSV()
    print(db.write())
    print(csv.write())

Die Kindklassen entsprechen nun korrekt der Elternfunktion.

Interface Segregation Principle (ISP)

Das Schnittstellentrennungsprinzip (ISP) besagt: „Viele klientspezifische Schnittstellen sind besser als eine allgemeine Schnittstelle.“

Mit anderen Worten: Umfangreiche Interaktionsschnittstellen werden in kleinere aufgeteilt. Das Prinzip stellt sicher, dass Klassen nur die Methoden nutzen, die sie benötigen, was die Gesamtredundanz reduziert.

Das folgende Beispiel demonstriert eine allgemeine 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 Kindklassen erben von der Elternklasse Animal, die walk und fly Methoden enthält. Obwohl beide Funktionen für bestimmte Tiere akzeptabel sind, haben einige Tiere redundante Funktionalitäten.

Um die Situation zu handhaben, 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 und bietet zusätzliche Funktionalität für passende Kindklassen. Das Beispiel erfüllt das Schnittstellentrennungsprinzip.
Das Hinzufügen eines weiteren Tieres, wie eines Fisches, erfordert eine weitere Atomisierung der Schnittstelle, da Fische nicht laufen können.

Dependency Inversion Principle (DIP)

Das Abhängigkeitsumkehrprinzip besagt: „Abhängig von Abstraktionen, nicht von Konkretisierungen.“

Das Prinzip zielt darauf ab, Verbindungen zwischen Klassen zu reduzieren, indem eine Abstraktionsschicht hinzugefügt wird. Das Verschieben von Abhängigkeiten zu Abstraktionen macht den Code robust.

Das folgende Beispiel demonstriert 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:

Das Abhängigkeitsumkehrprinzip erfordert das Hinzufügen einer Abstraktionsschnittstelle zwischen den beiden Klassen.

Eine Beispiel-Lö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()

Die Converter-Klasse hängt nun vom NameConverter-Interface ab, statt direkt vom LatinConverter. Zukünftige Updates ermöglichen die Definition von Namenskonvertierungen mit einer anderen Sprache und API über das NameConverter-Interface.

Warum gibt es Bedarf an SOLID-Prinzipien?

SOLID-Prinzipien helfen, Design-Pattern-Probleme zu bekämpfen. Das übergeordnete Ziel der SOLID-Prinzipien ist die Reduzierung von Code-Abhängigkeiten, sodass das Hinzufügen eines neuen Features oder das Ändern eines Code-Teils den gesamten Build nicht bricht.

Durch die Anwendung der SOLID-Prinzipien auf objektorientiertes Design wird der Code leichter verständlich, verwaltbar, wartbar und änderbar. Da die Regeln besser für große Projekte geeignet sind, erhöht die Anwendung der SOLID-Prinzipien die Gesamtgeschwindigkeit und Effizienz des Entwicklungszyklus.

Sind SOLID-Prinzipien noch relevant?

Obwohl SOLID-Prinzipien über 20 Jahre alt sind, bieten sie immer noch ein gutes Fundament für Softwarearchitektur-Design. SOLID liefert 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 Module organisiert ist und interne oder externe Elemente enthält.

Fazit

SOLID-Prinzipien helfen, ein gutes Framework und eine Anleitung für Softwarearchitektur-Design zu bieten. Die Beispiele aus diesem Leitfaden zeigen, dass sogar eine dynamisch typisierte Sprache wie Python von der Anwendung der Prinzipien auf das Code-Design profitiert.

Als Nächstes lesen Sie über die 9 DevOps-Prinzipien, die Ihrem Team helfen, das Beste aus DevOps herauszuholen.

Cloud Computing

  1. C# Static Keyword: Statische Variablen, Methoden und Klassen meistern
  2. C# Geschachtelte Klassen verstehen: Definition, Verwendung und Vererbung
  3. C++ Klassentemplates meistern: Ein praktischer Leitfaden
  4. Java anonyme innere Klassen meistern: Definition, Syntax und praktische Beispiele
  5. Java ObjectOutputStream meistern: Serialisierung, Methoden und praktische Beispiele
  6. Java Generics meistern – Wiederverwendbaren, typsicheren Code erstellen
  7. Java-Dateioperationen mit java.io meistern – Erstellen, Lesen, Schreiben & Löschen
  8. 5 Designprinzipien für robuste Interconnects in High-Speed Data-Intensive Applikationen
  9. C# Vererbung: Wiederverwendbaren & wartbaren Code erstellen
  10. C# Polymorphie: Statische vs. Dynamische Bindung