Een solide gids voor SOLID-principes

1. Inleiding

In deze tutorial bespreken we de SOLID-principes van Object-Oriented Design.

Eerst beginnen we met het onderzoeken van de redenen waarom ze tot stand kwamen en waarom we ze zouden moeten overwegen bij het ontwerpen van software. Vervolgens schetsen we elk principe samen met een voorbeeldcode om het punt te benadrukken.

2. De reden voor SOLID-principes

De SOLID-principes werden voor het eerst geconceptualiseerd door Robert C.Martin in zijn paper uit 2000, Ontwerpprincipes en ontwerppatronen. Op deze concepten werd later voortgebouwd door Michael Feathers, die ons kennis liet maken met de afkorting SOLID. En in de afgelopen 20 jaar hebben deze 5 principes een revolutie teweeggebracht in de wereld van objectgeoriënteerd programmeren, door de manier waarop we software schrijven te veranderen.

Dus, wat is SOLID en hoe helpt het ons om betere code te schrijven? Simpel gezegd, Martin's and Feathers ' ontwerpprincipes moedigen ons aan om meer onderhoudbare, begrijpelijke en flexibele software te maken. Bijgevolg, naarmate onze applicaties groter worden, kunnen we hun complexiteit verminderen en bespaar onszelf een hoop kopzorgen verderop!

De volgende 5 concepten vormen onze SOLID-principes:

  1. Single Verantwoordelijkheid
  2. Open / gesloten
  3. L.iskov Wissel
  4. iknterface-segregatie
  5. Dafhankelijkheidsinversie

Hoewel sommige van deze woorden misschien ontmoedigend klinken, kunnen ze gemakkelijk worden begrepen met enkele eenvoudige codevoorbeelden. In de volgende secties zullen we dieper ingaan op wat elk van deze principes betekent, samen met een snel Java-voorbeeld om ze allemaal te illustreren.

3. Enkele verantwoordelijkheid

Laten we beginnen met het beginsel van enkele verantwoordelijkheid. Zoals we zouden verwachten, stelt dit principe dat een klas mag maar één verantwoordelijkheid hebben. Bovendien zou het maar één reden moeten hebben om te veranderen.

Hoe helpt dit principe ons om betere software te bouwen? Laten we een paar voordelen bekijken:

  1. Testen - Een klas met één verantwoordelijkheid zal veel minder testcases hebben
  2. Lagere koppeling - Minder functionaliteit in een enkele klasse heeft minder afhankelijkheden
  3. Organisatie - Kleinere, goed georganiseerde klassen zijn gemakkelijker te doorzoeken dan monolithische klassen

Neem bijvoorbeeld een klas die een eenvoudig boek vertegenwoordigt:

public class Book {private String naam; private String-auteur; private String-tekst; // constructeur, getters en setters}

In deze code slaan we de naam, auteur en tekst op die zijn gekoppeld aan een instantie van een Boek.

Laten we nu een aantal methoden toevoegen om de tekst te doorzoeken:

public class Book {private String naam; private String-auteur; private String-tekst; // constructor, getters en setters // methoden die rechtstreeks verband houden met de boekeigenschappen public String replaceWordInText (String-woord) {return text.replaceAll (woord, tekst); } openbare boolean isWordInText (String-woord) {return text.contains (woord); }}

Nu, onze Boek class werkt goed, en we kunnen zoveel boeken opslaan als we willen in onze applicatie. Maar wat heb je eraan om de informatie op te slaan als we de tekst niet naar onze console kunnen sturen en deze niet kunnen lezen?

Laten we voorzichtig zijn met de wind en een afdrukmethode toevoegen:

public class Book {// ... void printTextToConsole () {// onze code voor het opmaken en afdrukken van de tekst}}

Deze code is echter in strijd met het beginsel van één enkele verantwoordelijkheid dat we eerder hebben uiteengezet. Om onze rotzooi op te lossen, moeten we een aparte klasse implementeren die zich alleen bezighoudt met het afdrukken van onze teksten:

public class BookPrinter {// methoden voor het uitvoeren van tekst void printTextToConsole (String-tekst) {// onze code voor het opmaken en afdrukken van de tekst} void printTextToAnotherMedium (String-tekst) {// code om naar een andere locatie te schrijven ..}}

Geweldig. We hebben niet alleen een klasse ontwikkeld die de Boek van zijn afdruktaken, maar we kunnen ook gebruikmaken van onze BookPrinter klas om onze tekst naar andere media te sturen.

Of het nu gaat om e-mail, logboekregistratie of iets anders, we hebben een aparte klasse gewijd aan dit ene probleem.

4. Open voor extensie, gesloten voor wijziging

Nu is het tijd voor de 'O' - formeler bekend als de open-gesloten principe. Simpel gezegd, klassen zouden open moeten staan ​​voor uitbreiding, maar gesloten voor wijziging.Daarbij doen wevoorkomen dat we bestaande code aanpassen en mogelijke nieuwe bugs veroorzaken in een verder gelukkige applicatie.

Natuurlijk is de een uitzondering op de regel is bij het oplossen van bugs in bestaande code.

Laten we het concept verder verkennen met een snel codevoorbeeld. Stel je voor dat we als onderdeel van een nieuw project een Gitaar klasse.

Het is volwaardig en heeft zelfs een volumeknop:

openbare klasse Guitar {private String make; privé String-model; privé int volume; // Constructeurs, getters & setters}

We starten de applicatie en iedereen vindt het geweldig. Na een paar maanden besluiten we echter de Gitaar is een beetje saai en zou een geweldig vlammenpatroon kunnen gebruiken om het er wat meer ‘rock and roll 'uit te laten zien.

Op dit punt kan het verleidelijk zijn om gewoon het Gitaar class en voeg een vlampatroon toe - maar wie weet welke fouten dat in onze applicatie kan veroorzaken.

Laten we in plaats daarvan blijf bij het open-gesloten principe en verleng gewoon onze Gitaar klasse:

openbare klasse SuperCoolGuitarWithFlames breidt Guitar {private String flameColor; // constructor, getters + setters}

Door het Gitaar class kunnen we er zeker van zijn dat onze bestaande applicatie niet wordt beïnvloed.

5. Liskov Wissel

De volgende op onze lijst is de vervanging van Liskov, wat misschien wel de meest complexe van de 5 principes is. Simpel gezegd, als klasse EEN is een subtype van klasse B, dan zouden we moeten kunnen vervangen B met EEN zonder het gedrag van ons programma te verstoren.

Laten we meteen naar de code springen om ons te helpen ons hoofd rond dit concept te wikkelen:

openbare interface Car {void turnOnEngine (); leegte versnellen (); }

Hierboven definiëren we een eenvoudig Auto interface met een aantal methoden die alle auto's zouden moeten kunnen vervullen - de motor aanzetten en vooruit accelereren.

Laten we onze interface implementeren en wat code geven voor de methoden:

openbare klasse MotorCar implementeert Car {private Engine engine; // Constructeurs, getters + setters public void turnOnEngine () {// zet de motor aan! engine.on (); } public void accelerate () {// ga vooruit! engine.powerOn (1000); }}

Zoals onze code beschrijft, hebben we een motor die we kunnen inschakelen en kunnen we het vermogen vergroten. Maar wacht, het is 2019, en Elon Musk heeft het druk gehad.

We leven nu in het tijdperk van elektrische auto's:

openbare klasse ElectricCar implementeert Car {public void turnOnEngine () {gooi nieuwe AssertionError ("Ik heb geen motor!"); } public void accelerate () {// deze versnelling is waanzinnig! }}

Door een auto zonder motor in de mix te gooien, veranderen we inherent het gedrag van ons programma. Dit is een flagrante schending van Liskov-vervanging en is een beetje moeilijker op te lossen dan onze vorige 2 principes.

Een mogelijke oplossing zou zijn om ons model om te bouwen tot interfaces die rekening houden met de motorloze toestand van onze Auto.

6. Interfacescheiding

De ‘I’ in SOLID staat voor scheiding van interfaces, en dat betekent simpelweg dat grotere interfaces moeten worden opgesplitst in kleinere. Door dit te doen, kunnen we ervoor zorgen dat implementatieklassen zich alleen zorgen hoeven te maken over de methoden die voor hen van belang zijn.

Voor dit voorbeeld gaan we onze handen uitproberen als dierenverzorgers. En meer specifiek, we werken in het berenverblijf.

Laten we beginnen met een interface die onze rollen als berenhouder schetst:

openbare interface BearKeeper {void washTheBear (); leegte feedTheBear (); leegte petTheBear (); }

Als enthousiaste dierenverzorgers wassen en voeren we onze geliefde beren graag. We zijn ons echter maar al te goed bewust van de gevaren van aaien. Helaas is onze interface vrij groot en hebben we geen andere keuze dan de code te implementeren om de beer te aaien.

Laten we los dit op door onze grote interface op te splitsen in 3 aparte:

openbare interface BearCleaner {void washTheBear (); } openbare interface BearFeeder {void feedTheBear (); } openbare interface BearPetter {void petTheBear (); }

Dankzij interfacescheiding zijn we nu vrij om alleen de methoden te implementeren die voor ons van belang zijn:

public class BearCarer implementeert BearCleaner, BearFeeder {public void washTheBear () {// Ik denk dat we een plekje hebben gemist ...} public void feedTheBear () {// Tuna Tuesday ...}}

En tot slot kunnen we de gevaarlijke dingen aan de gekke mensen overlaten:

public class CrazyPerson implementeert BearPetter {public void petTheBear () {// Veel succes daarmee! }}

Als we verder gaan, kunnen we zelfs onze BookPrinter class uit ons voorbeeld eerder om interfacescheiding op dezelfde manier te gebruiken. Door een Printer interface met een enkele afdrukken methode, kunnen we afzonderlijk instantiëren ConsoleBookPrinter en OtherMediaBookPrinter klassen.

7. Omkering van afhankelijkheid

Het principe van Dependency Inversion verwijst naar de ontkoppeling van softwaremodules. Op deze manier zijn beide afhankelijk van abstracties in plaats van modules op hoog niveau die afhankelijk zijn van modules op laag niveau.

Om dit te demonstreren, gaan we naar de oude school en brengen we een Windows 98-computer met code tot leven:

openbare klasse Windows98Machine {}

Maar wat heb je aan een computer zonder monitor en toetsenbord? Laten we een van elk toevoegen aan onze constructor zodat elk Windows98Computer we instantiëren komt voorverpakt met een Monitor en een Standaard toetsenbord:

openbare klasse Windows98Machine {privé definitief StandardKeyboard-toetsenbord; particuliere eindmonitor; openbare Windows98Machine () {monitor = nieuwe Monitor (); keyboard = nieuw StandardKeyboard (); }}

Deze code werkt en we kunnen de Standaard toetsenbord en Monitor vrij binnen onze Windows98Computer klasse. Probleem opgelost? Niet helemaal. Door het Standaard toetsenbord en Monitor met de nieuw trefwoord, hebben we deze drie klassen nauw met elkaar verbonden.

Dit maakt niet alleen onze Windows98Computer moeilijk te testen, maar we hebben ook de mogelijkheid verloren om onze Standaard toetsenbord klasse met een andere als dat nodig mocht zijn. En we zitten opgescheept met onze Monitor klasse ook.

Laten we onze machine loskoppelen van de Standaard toetsenbord door een meer algemeen Toetsenbord interface en dit gebruiken in onze klas:

openbare interface Toetsenbord {}
openbare klasse Windows98Machine {privé definitief toetsenbordtoetsenbord; particuliere eindmonitor; openbare Windows98Machine (toetsenbordtoetsenbord, monitormonitor) {this.keyboard = toetsenbord; this.monitor = monitor; }}

Hier gebruiken we het afhankelijkheidsinjectiepatroon hier om het toevoegen van het Toetsenbord afhankelijkheid in de Windows98Machine klasse.

Laten we ook onze Standaard toetsenbord klasse om het Toetsenbord interface zodat deze geschikt is om te injecteren in de Windows98Machine klasse:

openbare klasse StandardKeyboard implementeert Keyboard {}

Nu zijn onze klassen ontkoppeld en communiceren ze via de Toetsenbord abstractie. Als we willen, kunnen we eenvoudig het type toetsenbord in onze machine wisselen met een andere implementatie van de interface. We kunnen hetzelfde principe volgen voor de Monitor klasse.

Uitstekend! We hebben de afhankelijkheden losgekoppeld en zijn vrij om onze Windows98Machine met welk testkader we ook kiezen.

8. Conclusie

In deze tutorial hebben we een duik diep in de SOLID-principes van objectgeoriënteerd ontwerp.

Wij begon met een kort stukje SOLID-geschiedenis en de redenen waarom deze principes bestaan.

Letter voor letter hebben we de betekenis van elk principe uitgesplitst met een snel codevoorbeeld dat het schendt. We hebben toen gezien hoe we onze code konden repareren en zorg ervoor dat het zich houdt aan de SOLID-principes.

Zoals altijd is de code beschikbaar op GitHub.


$config[zx-auto] not found$config[zx-overlay] not found