SOLID: Objectgeoriënteerd ontwerp beheersen voor schone, onderhoudbare code
SOLID is een mnemonische acroniem voor klassedesign in objectgeoriënteerd programmeren. De principes stellen praktijken in die helpen goede programmeergewoontes en onderhoudbare code te ontwikkelen.
Door rekening te houden met code-onderhoud en uitbreidbaarheid op de lange termijn, verrijken SOLID-principes de Agile-ontwikkelomgeving. Rekening houden met en optimaliseren van codedependenties helpt een eenvoudigere en georganiseerdere softwareontwikkelcyclus te creëren.
Wat zijn SOLID-principes?
SOLID vertegenwoordigt een set principes voor het ontwerpen van klassen. Robert C. Martin (Uncle Bob) introduceerde de meeste ontwerpprincipes en bedacht het acroniem.
SOLID staat voor:
- Single-Responsibility Principle (Enkele-verantwoordelijkheid-principe)
- Open-Closed Principle (Open-gesloten-principe)
- Liskov Substitution Principle (Liskov-substitutieprincipe)
- Interface Segregation Principle (Interface-segregatieprincipe)
- Dependency Inversion Principle (Afhankelijkheidsinversie-principe)
SOLID-principes vertegenwoordigen een verzameling best practices voor softwareontwerp. Elk idee vertegenwoordigt een ontwerpframework, wat leidt tot betere programmeergewoontes, verbeterd code-ontwerp en minder fouten.
SOLID: 5 principes uitgelegd
De beste manier om te begrijpen hoe SOLID-principes werken, is door voorbeelden. Alle principes zijn complementair en van toepassing op individuele use cases. De volgorde waarin de principes worden toegepast, is onbelangrijk, en niet alle principes zijn in elke situatie toepasbaar.
Elke sectie hieronder biedt een overzicht van elk SOLID-principe in de Python-programmeertaal. De algemene ideeën van SOLID gelden voor elke objectgeoriënteerde taal, zoals PHP, Java of C#. Het generaliseren van de regels maakt ze toepasbaar op moderne programmeerbenaderingen, zoals microservices.
Single-Responsibility Principle (SRP)
Het single-responsibility-principe (SRP) stelt: "Er zou nooit meer dan één reden mogen zijn voor een klasse om te veranderen."
Bij het wijzigen van een klasse, zouden we slechts een enkele functionaliteit moeten wijzigen, wat impliceert dat elk object slechts één taak zou moeten hebben.
Als voorbeeld, bekijk de volgende klasse:
# Een klasse met meerdere verantwoordelijkheden class Animal: # Eigenschap constructor def __init__(self, name): self.name = name # Eigenschap representatie def __repr__(self): return f'Animal(name="{self.name}")' # Databasebeheer def save(animal): print(f'Saved {animal} to the database') if __name__ == '__main__': # Eigenschap instantie a = Animal('Cat') # Eigenschap opslaan in database Animal.save(a)
Bij het aanbrengen van wijzigingen in de save() methode, gebeurt de wijziging in de Animal klasse. Bij het aanbrengen van eigenschapswijzigingen, vinden de wijzigingen ook plaats in de Animal klasse.
De klasse heeft twee redenen om te veranderen en schendt het single-responsibility-principe. Hoewel de code werkt zoals verwacht, maakt het niet respecteren van het ontwerpprincipe de code op de lange termijn moeilijker te beheren.
Om het single-responsibility-principe te implementeren, let op dat de voorbeeldklasse twee distincte taken heeft:
- Eigenschapsbeheer (de constructor en
get_name()). - Databasebeheer
(save()).
Daarom is de beste manier om het probleem aan te pakken het scheiden van de databasebeheermethode in een nieuwe klasse. Bijvoorbeeld:
# Een klasse verantwoordelijk voor eigenschapsbeheer class Animal: def __init__(self, name): self.name = name def __repr__(self): return f'Animal(name="{self.name}")' # Een klasse verantwoordelijk voor databasebeheer class AnimalDB: def save(self, animal): print(f'Saved {animal} to the database') if __name__ == '__main__': # Eigenschap instantie a = Animal('Cat') # Database instantie db = AnimalDB() # Eigenschap opslaan in database db.save(a)
Het wijzigen van de AnimalDB klasse beïnvloedt de Animal klasse niet meer met het single-responsibility-principe toegepast. De code is intuïtief en gemakkelijk te wijzigen.
Open-Closed Principle (OCP)
Het open-closed-principe (OCP) stelt: "Software-entiteiten moeten open zijn voor uitbreiding maar gesloten voor wijziging."
Functionaliteiten en use-cases toevoegen aan het systeem zou niet moeten vereisen dat bestaande entiteiten worden gewijzigd. De formulering lijkt contradictorisch – nieuwe functionaliteiten toevoegen vereist het wijzigen van bestaande code.
Het idee is eenvoudig te begrijpen via het volgende voorbeeld:
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')
De Storage klasse slaat de informatie van een Animal instantie op in een database. Nieuwe functionaliteiten toevoegen, zoals opslaan in een CSV-bestand, vereist code toe te voegen aan de 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')
De save_to_csv methode wijzigt een bestaande Storage klasse om de functionaliteit toe te voegen. Deze benadering schendt het open-closed-principe door een bestaand element te wijzigen wanneer een nieuwe functionaliteit verschijnt.
De code vereist het verwijderen van de algemeen-doel Storage klasse en het creëren van individuele klassen voor het opslaan in specifieke bestandsformaten.
De volgende code demonstreert de toepassing van het open-closed-principe:
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')
De code voldoet aan het open-closed-principe. De volledige code ziet er nu zo uit:
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)
Uitbreiden met additionele functionaliteiten (zoals opslaan in een XML-bestand) wijzigt geen bestaande klassen.
Liskov Substitution Principle (LSP)
Het Liskov-substitutieprincipe (LSP) stelt: "Functies die pointers of referenties naar basisklassen gebruiken, moeten in staat zijn om objecten van afgeleide klassen te gebruiken zonder het te weten."
Het principe stelt dat een parent-klasse een child-klasse kan substitueren zonder merkbare wijzigingen in functionaliteit.
Bekijk het bestandsschrijf-voorbeeld hieronder:
# Parent-klasse class FileHandling(): def write_db(self): return f'Handling DB' def write_csv(self): return f'Handling CSV' # Child-klassen 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-klasse instantie en functie-aanroepen db = FileHandling() csv = FileHandling() print(db.write_db()) print(db.write_csv()) # Kinderen klassen instanties en functie-aanroepen db = WriteDB() csv = WriteCSV() print(db.write_db()) print(db.write_csv()) print(csv.write_db()) print(csv.write_csv())
De parent-klasse (FileHandling) bestaat uit twee methoden voor het schrijven naar een database en een CSV-bestand. De klasse behandelt beide functies en retourneert een bericht.
De twee child-klassen (WriteDB en WriteCSV) erven eigenschappen van de parent-klasse (FileHandling). Echter, beide kinderen gooien een fout bij het proberen de ongeschikte schrijffunctie te gebruiken, wat het Liskov-substitutieprincipe schendt omdat de overschrijvende functies niet overeenkomen met de parent-functies.
De volgende code lost de problemen op:
# Parent-klasse class FileHandling(): def write(self): return f'Handling file' # Child-klassen 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-klasse instantie en functie-aanroepen db = FileHandling() csv = FileHandling() print(db.write()) print(csv.write()) # Kinderen klassen instanties en functie-aanroepen db = WriteDB() csv = WriteCSV() print(db.write()) print(csv.write())
De child-klassen corresponderen nu correct met de parent-functie.
Interface Segregation Principle (ISP)
Het interface-segregatieprincipe (ISP) stelt: "Veel client-specifieke interfaces zijn beter dan één algemeen-doel interface."
Met andere woorden, uitgebreide interactie-interfaces worden opgesplitst in kleinere. Het principe zorgt ervoor dat klassen alleen de methoden gebruiken die ze nodig hebben, wat de algehele redundantie vermindert.
Het volgende voorbeeld demonstreert een algemeen-doel interface:
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")
De child-klassen erven van de parent Animal klasse, die walk en fly methoden bevat. Hoewel beide functies acceptabel zijn voor bepaalde dieren, hebben sommige dieren redundante functionaliteiten.
Om de situatie te hanteren, split de interface in kleinere secties. Bijvoorbeeld:
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")
De Fly klasse erft van de Walk, wat additionele functionaliteit biedt aan geschikte child-klassen. Het voorbeeld voldoet aan het interface-segregatieprincipe.
Het toevoegen van een ander dier, zoals een vis, vereist het interface verder te atomiseren aangezien vissen niet kunnen lopen.
Dependency Inversion Principle (DIP)
Het dependency-inversie-principe stelt: "Afhankelijk van abstracties, niet van concrete implementaties."
Het principe is gericht op het verminderen van koppelingen tussen klassen door een abstractielaag toe te voegen. Afhankelijkheden naar abstracties verplaatsen maakt de code robuust.
Het volgende voorbeeld demonstreert klasse-afhankelijkheid zonder een abstractielaag:
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()
Het voorbeeld heeft twee klassen:
LatinConvertergebruikt een imaginaire API om de Latijnse naam voor een dier op te halen (hardcoded "Felis catus" voor simplicitieit).Converteris een high-level module die een instantie vanLatinConverteren zijn functie gebruikt om de geleverde naam te converteren. DeConverteris zwaar afhankelijk van deLatinConverterklasse, die afhankelijk is van de API. Deze benadering schendt het principe.
Het dependency-inversie-principe vereist het toevoegen van een abstractie-interface-laag tussen de twee klassen.
Een voorbeeldoplossing ziet er als volgt uit:
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()
De Converter klasse is nu afhankelijk van de NameConverter interface in plaats van direct van de LatinConverter. Toekomstige updates staan toe om naamconversies te definiëren met een andere taal en API via de NameConverter interface.
Waarom is er behoefte aan SOLID-principes?
SOLID-principes helpen vechten tegen ontpatroonproblemen. Het algehele doel van SOLID-principes is het verminderen van codedependenties, en het toevoegen van een nieuwe functie of het wijzigen van een deel van de code breekt niet de hele build.
Als resultaat van het toepassen van SOLID-principes op objectgeoriënteerd ontwerp, wordt de code makkelijker te begrijpen, beheren, onderhouden en wijzigen. Aangezien de regels beter geschikt zijn voor grote projecten, verhoogt het toepassen van de SOLID-principes de algehele ontwikkelcyclus-snelheid en -efficiëntie.
Zijn SOLID-principes nog steeds relevant?
Hoewel SOLID-principes meer dan 20 jaar oud zijn, bieden ze nog steeds een goede basis voor softwarearchitectuurontwerp. SOLID biedt geldige ontwerpprincipes toepasbaar op moderne programma's en omgevingen, niet alleen objectgeoriënteerd programmeren.
SOLID-principes zijn van toepassing in situaties waar code wordt geschreven en gewijzigd door mensen, georganiseerd in modules, en interne of externe elementen bevat.
Conclusie
SOLID-principes helpen een goed framework en gids te bieden voor softwarearchitectuurontwerp. De voorbeelden uit deze gids tonen aan dat zelfs een dynamisch getypte taal zoals Python profiteert van het toepassen van de principes op code-ontwerp.
Lees vervolgens over de 9 DevOps-principes die uw team helpen het meest uit DevOps te halen.
Cloud computing
- Wrijving Beheersen: Sleutelprincipes Voor Lagerontwerp En Prestaties
- 5 Ontwerpprincipes voor Robuuste Interconnects in High-Speed Data-Intensieve Applicaties
- C# Static Keyword: Static Variabelen, Methodes en Classes Beheersen
- C# Geneste Classes: Definitie, Gebruik en Inheritance
- C++ Class Templates Beheersen: Een Praktische Gids
- C# Erfelijkheid Beheersen: Herbruikbare, Onderhoudbare Code Bouwen
- C# Polymorfisme: Statische vs Dynamische Binding Uitgelegd
- Java anonieme inner classes meesteren: definitie, syntax en praktische voorbeelden
- Java ObjectOutputStream: Serialisatie, methoden en praktische voorbeelden
- Java Generics: Herbruikbare, type-veilige code bouwen
- Java-bestandsbewerkingen onder de knie krijgen met java.io – Aanmaken, lezen, schrijven en verwijderen