Terug naar blog

SOLID: De eerste 5 principes van objectgeoriënteerd ontwerp?

SOLID: De eerste 5 principes van objectgeoriënteerd ontwerp?

Introductie

SOLID is een mnemonisch acroniem voor vijf principes van objectgeoriënteerd ontwerp die zijn geïntroduceerd door Robert C. Martin, die in de volksmond ook wel wordt aangeduid als Uncle Bob. Deze principes zijn bedoeld om softwareontwerpers, architecten, engineers en ontwikkelaars te helpen flexibelere, onderhoudbaardere en schaalbaardere software te maken. Door deze principes te volgen, kun je klassen ontwerpen die eenvoudiger te testen, te refactoren, te hergebruiken en uit te breiden zijn.

Het SOLID-acroniem staat voor:

S – Single Responsibility Principle

O – Open-Closed Principle

L – Liskov Substitution Principle

I – Interface Segregation Principle

D – Dependency Inversion Principle

In dit artikel leggen we elk principe afzonderlijk uit om te begrijpen hoe het je kan helpen betere code te schrijven. Daarnaast voegen we codefragmenten toe voor elk principe om te laten zien hoe je ze kunt toepassen in je programmeertraject, en wat je moet vermijden in een clean code-architectuur. Om de concepten te demonstreren, maken we gebruik van de Kotlin-programmeertaal ontwikkeld door JetBrains en open-source bijdragers.

Single Responsibility Principle

Het Single Responsibility Principle (SRP) is een softwareontwerpprincipe dat stelt dat elke klasse of module in een programma een enkele, goed gedefinieerde verantwoordelijkheid moet hebben. Dit betekent dat een klasse slechts één reden mag hebben om te veranderen, en dat deze verantwoordelijk moet zijn voor slechts een enkel deel van de functionaliteit van het programma.

Beschouw het volgende codefragment als een voorbeeld van hoe dit principe kan worden toegepast in Kotlin:

Het data-sleutelwoord in Kotlin geeft aan dat deze klasse een data-klasse is, wat betekent dat deze bedoeld is om gegevens te bevatten en geen complex gedrag vertoont. Met behulp van deze data-klasse kunnen we een UserService -klasse maken voor het beheren van gebruikers, zoals hieronder weergegeven:

In dit voorbeeld heeft de UserService-klasse een enkele verantwoordelijkheid: het beheren van gebruikers in een database. Elke gebruiker wordt vertegenwoordigd door de data class User -code die eerder is gedeeld. Alle methoden in de UserService-klasse zijn gerelateerd aan deze verantwoordelijkheid en zijn daarom coherent. Dit maakt de klasse gemakkelijker te begrijpen en te onderhouden, omdat het duidelijk is dat alle methoden in de klasse gerelateerd zijn aan een enkele, goed gedefinieerde taak.

Een methode die het Single Responsibility Principle (SRP) schendt in de UserService-klasse zou een methode zijn die niet gerelateerd is aan de primaire verantwoordelijkheid van de klasse, namelijk het beheren van gebruikers in een database. Neem bijvoorbeeld de volgende variatie van de UserService-klasse:

In dit voorbeeld heeft de sendEmail  methode geen betrekking op de primaire verantwoordelijkheid van de UserService class, namelijk het beheren van gebruikers in een database. Deze methode is verantwoordelijk voor het verzenden van e-mails, wat een afzonderlijke verantwoordelijkheid is van het beheren van gebruikers. Als gevolg hiervan schendt deze methode het SRP, omdat het een tweede reden introduceert voor de UserService-klasse om te veranderen.

Om te voldoen aan het Single Responsibility Principle, zou het beter zijn om de functionaliteit voor het verzenden van e-mails te scheiden in een aparte klasse, zoals een EmailService  klasse. Dit zou de UserService klasse in staat stellen zich te concentreren op haar primaire verantwoordelijkheid van het beheren van gebruikers, en de EmailService klasse om zich te concentreren op haar verantwoordelijkheid voor het verzenden van e-mails.

Merk op dat het Single Responsibility Principle niet gaat over het aantal methoden dat een klasse heeft, maar eerder over de samenhang van de methoden en de duidelijke scheiding van verantwoordelijkheden binnen een klasse.

Open-Closed Principle

Het Open-Closed Principle (OCP) is een ontwerpprincipe voor software dat stelt dat software-entiteiten (zoals klassen, modules of functies) open moeten staan voor uitbreiding, maar gesloten moeten zijn voor aanpassing. Dit betekent dat het mogelijk moet zijn om nieuwe functionaliteit aan een klasse of module toe te voegen zonder de bestaande code te wijzigen.

Neem de UserService klasse uit het vorige voorbeeld. Stel dat we een nieuwe functie willen toevoegen aan de UserService klasse waarmee we naar gebruikers kunnen zoeken op e-mailadres. Een manier om dit te doen is door een nieuwe methode toe te voegen aan de UserService klasse, zoals gemarkeerd in de onderstaande code:

Deze aanpak werkt, maar schendt het Open-Closed Principle, omdat we de bestaande UserService klasse moesten aanpassen om de nieuwe functie toe te voegen. Een betere aanpak zou zijn om overerving of compositie te gebruiken om de functionaliteit van de UserService klasse uit te breiden zonder de code ervan te wijzigen.

Om dit te bereiken, zouden we een nieuwe klasse kunnen maken genaamd UserSearchService  die de UserService klasse uitbreidt en de e-mailzoekfunctionaliteit toevoegt:

In dit voorbeeld staat de UserSearchService klasse open voor uitbreiding, omdat deze extra functionaliteit biedt die verder gaat dan wat wordt aangeboden door de UserService class. Tegelijkertijd blijft de UserService class gesloten voor wijziging, omdat we de code ervan niet hoefden aan te passen om de e-mailzoekfunctie toe te voegen.

Liskov-substitutieprincipe

Het Liskov-substitutieprincipe (LSP) is een softwareontwerpprincipe dat stelt dat objecten van een superklasse vervangen moeten kunnen worden door objecten van een subklasse zonder de correctheid van het programma te beïnvloeden. Dit betekent dat een subklasse een geldige vervanger moet zijn voor zijn superklasse, en zich op dezelfde manier moet gedragen als de superklasse wanneer deze in dezelfde context wordt gebruikt.

We vervolgen de demonstratie met de User en de UserService klassen uit de vorige voorbeelden. Om de User-klasse uitbreidbaar te maken, gebruiken we het open  sleutelwoord in Kotlin:

Hier is de originele UserService klasse die de bovenstaande User data-klasse gebruikt:

Stel dat we een subklasse van User willen maken genaamd AdminUser die gebruikers met administratieve rechten vertegenwoordigt. Dit zouden we als volgt kunnen doen:

In dit voorbeeld is de AdminUser klasse een geldige vervanger voor de User klasse, aangezien deze zich op dezelfde manier gedraagt als de User klasse en overal kan worden gebruikt waar een User object wordt verwacht. We kunnen bijvoorbeeld de AdminUser klasse als volgt gebruiken met de UserService  klasse:

Deze code is correct, aangezien de AdminUser klasse een geldige vervanger is voor de User klasse en op dezelfde manier kan worden gebruikt als een User object.

Het is belangrijk om op te merken dat het Liskov-substitutieprincipe over meer gaat dan alleen overerving. Het gaat erom ervoor te zorgen dat objecten van een subklasse zich op dezelfde manier gedragen als objecten van de superklasse, ongeacht hoe de subklasse is geïmplementeerd. Bijvoorbeeld, als de AdminUser klasse zich op een of andere manier anders zou gedragen dan de User klasse, zou dit het Liskov-substitutieprincipe schenden, aangezien het geen geldige vervanger zou zijn voor de User klasse.

Interface-segregatieprincipe

Het Interface-segregatieprincipe (ISP) is een softwareontwerpprincipe dat stelt dat clients niet gedwongen moeten worden om afhankelijk te zijn van interfaces die ze niet gebruiken. Dit betekent dat het over het algemeen een goed idee is om kleine, gerichte interfaces te maken die één ding goed doen, in plaats van het maken van grote, algemene interfaces die veel dingen proberen te doen.

Hier is een voorbeeld van hoe dit principe kan worden toegepast door het herschrijven van de User  en UserService  klassen uit de vorige voorbeelden:

In dit voorbeeld definieert de UserService interface vier methoden die gerelateerd zijn aan het beheren van gebruikers in een database. De DatabaseUserService  klasse implementeert deze interface en biedt concrete implementaties voor deze methoden.

Suppose we want to add a new feature to the UserService interface that allows us to search for users by email address. One way to do this would be to add a new method to the UserService interface:

Je code zal niet werken, tenzij je deze methode ook implementeert in alle klassen die de UserService interface implementeren:

Hoewel deze aanpak werkt, schendt het het Interface Segregation Principle, omdat het de DatabaseUserService  klasse dwingt om een methode te implementeren ( searchUsersByEmail ) die het misschien niet nodig heeft of gebruikt.

Een betere aanpak zou zijn om een aparte interface  te maken voor de e-mailzoekfunctionaliteit:

Nu hebben we aparte, kleine en gerichte interfaces, d.w.z. UserService  en UserSearchServicedie een enkele verantwoordelijkheid hebben. Een klasse die alle functionaliteiten van deze interfaces vereist, kan deze implementeren zoals getoond in het onderstaande codefragment:

Dit voldoet aan het Interface Segregation Principle, omdat het ervoor zorgt dat clients (zoals de DatabaseUserService klasse) niet worden gedwongen om afhankelijk te zijn van interfaces die ze niet gebruiken.

Om dit concept beter te begrijpen, stel dat we een andere klasse hebben genaamd MemoryUserService  die de UserService  interface implementeert maar de e-mailzoekfunctionaliteit niet nodig heeft, dan kunnen we de code als volgt schrijven:

In dit voorbeeld hoeft de MemoryUserService klasse alleen de methoden te implementeren die zijn gedefinieerd in de UserService interface, en hoeft zich geen zorgen te maken over de e-mailzoekfunctionaliteit. Dit stelt de MemoryUserService klasse in staat om zich te concentreren op haar primaire verantwoordelijkheid van het beheren van gebruikers in het geheugen, in plaats van gedwongen te worden om niet-gerelateerde functionaliteit te implementeren.

Dependency Inversion Principle

Het Dependency Inversion Principle (DIP) is een softwareontwerpprincipe dat stelt dat high-level modules niet afhankelijk moeten zijn van low-level modules, maar dat beide afhankelijk moeten zijn van abstracties. Dit betekent dat het over het algemeen een goed idee is om uw software zo te ontwerpen dat high-level componenten niet gebonden zijn aan specifieke implementaties van low-level componenten, maar in plaats daarvan afhankelijk zijn van abstracties (zoals interfaces of abstracte klassen) die op verschillende manieren kunnen worden geïmplementeerd.

Laten we eens kijken naar een voorbeeld van hoe dit principe kan worden toegepast op de User en UserService klassen die in eerdere codefragmenten zijn gebruikt:

In dit voorbeeld is de UserService klasse afhankelijk van een abstractie genaamd de UserRepository interface, in plaats van afhankelijk te zijn van een specifieke implementatie van een user repository. Dit stelt ons in staat om de UserRepository interface op verschillende manieren te implementeren, zoals met een database of in-memory opslag, zonder dat dit invloed heeft op de UserService klasse.

Hier is bijvoorbeeld een implementatie van de UserRepository interface die een database gebruikt:

Hier is een andere implementatie van de UserRepository interface die in-memory opslag gebruikt:

Dit maakt het systeem flexibeler en beter te onderhouden, omdat het ons in staat stelt de repository-implementatie te wijzigen zonder de rest van het systeem te beïnvloeden. Het maakt het ook gemakkelijker om de UserService-klasse te testen, omdat we de UserRepository-afhankelijkheid in onze tests kunnen mocken.

Conclusie

In dit artikel hebben we de vijf principes besproken van SOLID  code en gedeelde codefragmenten die aan elk principe voldoen. Het naleven van deze principes kan je helpen softwaresystemen te ontwerpen die flexibeler, onderhoudbaarder en schaalbaarder zijn. Het is echter belangrijk om in gedachten te houden dat deze principes richtlijnen zijn, in plaats van harde regels, en het is aan de ontwikkelaar om te beslissen wanneer en hoe deze toe te passen in de context van hun specifieke project. Blijf leren door onze blog te bekijken voor meer diepgaande en actuele artikelen en tutorials over cloud computing en DevOps, softwareontwerp en -ontwikkeling, technologische trends om in de gaten te houden, en meer.

Veel codeerplezier!

author

Preslav Dobrev

Auteur · CloudSigma

Preslav Dobrev is een creatief ontwerper bij CloudSigma, met de nadruk op een consistente bedrijfsidentiteit door middel van traditionele en innovatieve marketingkanalen. Hij is bedreven in het samenvoegen van artistieke visie met strategische marketing om impactvolle merkverhalen te creëren.

Reacties

Nog geen reacties. Wees de eerste.