SOLID Deel 1 - Het beginsel van één enkele verantwoordelijkheid

Single Responsibility (SRP), Open / Close, Liskov's Substitution, Interface Segregation en Dependency Inversion. Vijf behendige principes die je elke keer moeten begeleiden als je code schrijft.

De definitie

Een klasse zou maar één reden moeten hebben om te veranderen.

Gedefinieerd door Robert C. Martin in zijn boek Agile Software Development, Principles, Patterns and Practices en later opnieuw gepubliceerd in de C # -versie van het boek Agile Principles, Patterns and Practices in C #, is het een van de vijf SOLID-agile-principes. Wat het zegt is heel simpel, maar die eenvoud bereiken kan heel lastig zijn. Een klasse zou maar één reden moeten hebben om te veranderen.

Maar waarom? Waarom is het zo belangrijk om maar één reden voor verandering te hebben?

In statisch getypeerde en gecompileerde talen kunnen verschillende redenen leiden tot verschillende, ongewenste herplaatsingen. Als er twee verschillende redenen zijn om te veranderen, is het denkbaar dat twee verschillende teams om twee verschillende redenen aan dezelfde code kunnen werken. Elk zal zijn oplossing moeten inzetten, wat in het geval van een gecompileerde taal (zoals C ++, C # of Java) kan leiden tot incompatibele modules met andere teams of andere delen van de applicatie..

Ook als u geen gecompileerde taal gebruikt, moet u om verschillende redenen dezelfde klasse of module opnieuw testen. Dit betekent meer QA-werk, tijd en moeite.

Het publiek

Het bepalen van de enige verantwoordelijkheid die een klasse of module moet hebben, is veel complexer dan alleen kijken naar een checklist. Een aanwijzing om onze redenen voor verandering te vinden, is bijvoorbeeld om het publiek te analyseren voor onze klas. De gebruikers van de applicatie of het systeem dat wij ontwikkelen en die bediend worden door een bepaalde module, zullen degene zijn die om wijzigingen verzoekt. De dienaren zullen om verandering vragen. Hier zijn een aantal modules en hun mogelijke doelgroepen.

  • Persistence Module - Het publiek omvat DBA's en software-architecten.
  • Rapporteringsmodule - Publiek omvat griffiers, boekhouders en bewerkingen.
  • Betalingsberekeningsmodule voor een loonlijstensysteem - Tot het publiek kunnen advocaten, managers en accountants behoren.
  • Boekzoekmodule voor een bibliotheekbeheersysteem - Publiek kan de bibliothecaris en / of de klanten zelf zijn.

Rollen en acteurs

Het kan moeilijk zijn om concrete personen te koppelen aan al deze rollen. In een klein bedrijf moet een persoon misschien verschillende rollen vervullen, terwijl in een groot bedrijf meerdere personen aan een enkele rol kunnen worden toegewezen. Het lijkt dus veel redelijker om na te denken over de rollen. Maar rollen op zich zijn vrij moeilijk te definiëren. Wat is een rol? Hoe vinden we het? Het is veel gemakkelijker om je voor te stellen dat acteurs die rollen spelen en ons publiek met die acteurs associëren.

Dus als ons publiek redenen voor verandering definieert, definiëren de acteurs het publiek. Dit helpt ons enorm om het concept van concrete personen zoals "John the architect" naar Architecture, of "Mary the referent" naar Operations te verminderen.

Een verantwoordelijkheid is dus een familie van functies die een specifieke actor dient. (Robert C. Martin)

Bron van verandering

In de zin van deze redenering worden actoren een bron van verandering voor de familie van functies die hen dient. Naarmate hun behoeften veranderen, moet die specifieke familie van functies ook worden aangepast aan hun behoeften.

Een acteur voor verantwoordelijkheid is de enige bron van verandering voor die verantwoordelijkheid. (Robert C. Martin)

Klassieke voorbeelden

Objecten die zichzelf kunnen "printen"

Laten we zeggen dat we een hebben Boek klasse die het concept van een boek en zijn functionaliteiten inkapselt.

class Book function getTitle () return "A Great Book";  functie getAuthor () terug "John Doe";  functie turnPage () functie // pointer naar volgende pagina printCurrentPage () echo "huidige pagina-inhoud"; 

Dit kan een redelijke klasse lijken. We hebben een boek, het kan de titel ervan geven, de auteur en het kan de pagina omslaan. Ten slotte is het ook mogelijk om de huidige pagina op het scherm af te drukken. Maar er is een klein probleempje. Als we nadenken over de actoren die betrokken zijn bij het opereren van de Boek object, wie zouden ze kunnen zijn? We kunnen hier gemakkelijk twee verschillende actoren bedenken: boekbeheer (zoals de bibliothecaris) en het mechanisme voor gegevenspresentatie (zoals de manier waarop we de inhoud aan de gebruiker willen leveren - op het scherm, grafische gebruikersinterface, tekstuele gebruikersinterface, misschien afdrukken) . Dit zijn twee heel verschillende acteurs.

Het combineren van bedrijfslogica en presentatie is slecht omdat het tegen het Single Responsibility Principle (SRP) is. Bekijk de volgende code:

class Book function getTitle () return "A Great Book";  functie getAuthor () terug "John Doe";  functie turnPage () // pointer naar volgende pagina functie getCurrentPage () terug "huidige pagina-inhoud";  interface Printer function printPage ($ page);  class PlainTextPrinter implementeert Printer function printPage ($ page) echo $ page;  klasse HtmlPrinter implementeert Printer function printPage ($ page) echo '
'. $ pagina. '
';

Zelfs dit zeer eenvoudige voorbeeld laat zien hoe het scheiden van presentatie van bedrijfslogica en het naleven van SRP grote voordelen biedt in de flexibiliteit van ons ontwerp.

Objecten die zichzelf kunnen "redden"

Een vergelijkbaar voorbeeld als hierboven is wanneer een object zichzelf kan opslaan en ophalen uit de presentatie.

class Book function getTitle () return "A Great Book";  functie getAuthor () terug "John Doe";  functie turnPage () // pointer naar volgende pagina functie getCurrentPage () terug "huidige pagina-inhoud";  function save () $ filename = '/ documents /'. $ This-> getTitle (). '-'. $ This-> getAuthor (); file_put_contents ($ filename, serialize ($ this)); 

We kunnen opnieuw verschillende actoren identificeren, zoals Book Management System en Persistence. Wanneer we doorzettingsvermogen willen veranderen, moeten we deze klasse wijzigen. Wanneer we willen veranderen hoe we van de ene pagina naar de volgende gaan, moeten we deze klasse aanpassen. Er zijn verschillende veranderingsassen hier.

class Book function getTitle () return "A Great Book";  functie getAuthor () terug "John Doe";  functie turnPage () // pointer naar volgende pagina functie getCurrentPage () terug "huidige pagina-inhoud";  class SimpleFilePersistence functie opslaan (boek $ boek) $ bestandsnaam = '/ documenten /'. $ book-> getTitle (). '-'. $ Boeken-> getAuthor (); file_put_contents ($ filename, serialize ($ book)); 

Het verplaatsen van de persistentie naar een andere klasse zal duidelijk de verantwoordelijkheden scheiden en we zullen vrij zijn om persistentiemethoden uit te wisselen zonder onze Boek klasse. Bijvoorbeeld het implementeren van een DatabasePersistence klasse zou triviaal zijn en onze bedrijfslogica die is opgebouwd rond bewerkingen met boeken zal niet veranderen.

Een hoger niveauoverzicht

In mijn vorige artikelen noemde ik vaak het architecturale schema op hoog niveau dat hieronder te zien is.


Als we dit schema analyseren, kunt u zien hoe het beginsel van één verantwoordelijkheid wordt gerespecteerd. Het creëren van objecten is rechts gescheiden in fabrieken en het belangrijkste punt van binnenkomst van onze applicatie, één actor één verantwoordelijkheid. Persistentie wordt ook onderaan verzorgd. Een aparte module voor de afzonderlijke verantwoordelijkheid. Eindelijk, aan de linkerkant, hebben we presentatie of het leveringsmechanisme als u dat wenst, in de vorm van een MVC of een ander type gebruikersinterface. SRP gerespecteerd opnieuw. Het enige dat overblijft is om erachter te komen wat te doen in onze bedrijfslogica.

Overwegingen bij het ontwerpen van software

Wanneer we nadenken over de software die we moeten schrijven, kunnen we veel verschillende aspecten analyseren. Verschillende vereisten die van invloed zijn op dezelfde klasse, kunnen bijvoorbeeld een veranderingsas vertegenwoordigen. Deze assen van verandering kunnen een aanwijzing zijn voor een enkele verantwoordelijkheid. Het is zeer waarschijnlijk dat groepen vereisten die dezelfde groep functies beïnvloeden, redenen hebben om te veranderen of om op de eerste plaats te worden gespecificeerd.

De primaire waarde van software is gemak van verandering. Het secundaire is functionaliteit, in de zin van voldoen aan zo veel mogelijk vereisten, voldoen aan de behoeften van de gebruiker. Om echter een hoge secundaire waarde te bereiken, is een primaire waarde verplicht. Om onze primaire waarde hoog te houden, moeten we een ontwerp hebben dat gemakkelijk kan worden gewijzigd, uitgebreid, aangepast aan nieuwe functies en om ervoor te zorgen dat SRP wordt gerespecteerd.

We kunnen stap voor stap redeneren:

  1. Hoge primaire waarde leidt in de tijd tot hoge secundaire waarde.
  2. Secundaire waarde betekent behoeften van de gebruikers.
  3. De behoeften van de gebruikers zijn de behoeften van de actoren.
  4. De behoeften van de actoren bepalen de behoeften van veranderingen van deze actoren.
  5. De behoefte aan verandering van actoren bepaalt onze verantwoordelijkheden.

Dus wanneer we onze software ontwerpen, moeten we:

  1. Zoek en definieer de acteurs.
  2. Identificeer de verantwoordelijkheden die deze actoren dienen.
  3. Groepeer onze functies en klassen, zodat elk slechts één toegewezen verantwoordelijkheid heeft.

Een minder voor de hand liggend voorbeeld

class Book function getTitle () return "A Great Book";  functie getAuthor () terug "John Doe";  functie turnPage () // pointer naar volgende pagina functie getCurrentPage () terug "huidige pagina-inhoud";  functie getLocation () // retourneert de positie in de bibliotheek // ie. planknummer en kamernummer

Dit lijkt misschien heel redelijk. We hebben geen methode die te maken heeft met persistentie of presentatie. We hebben onze bladzijde omslaan() functionaliteit en een paar methoden om verschillende informatie over het boek te bieden. We hebben echter mogelijk een probleem. Om dit te achterhalen, willen we misschien onze applicatie analyseren. De functie getLocation () kan het probleem zijn.

Alle methoden van de Boek klasse gaat over bedrijfslogica. Ons perspectief moet dus vanuit het oogpunt van het bedrijf zijn. Als onze applicatie is geschreven voor gebruik door echte bibliothecarissen die op zoek zijn naar boeken en ons een fysiek boek geven, kan SRP worden geschonden.

We kunnen redeneren dat de actor-operaties degenen zijn die geïnteresseerd zijn in de methoden getTitle (), getAuthor () en getLocation (). De clients hebben mogelijk ook toegang tot de applicatie om een ​​boek te selecteren en de eerste paar pagina's te lezen om een ​​idee te krijgen over het boek en te beslissen of ze het willen of niet. Dus de acteurlezers zijn wellicht geïnteresseerd in alle methoden behalve getLocations (). Een gewone klant maakt het niet uit waar het boek in de bibliotheek wordt bewaard. Het boek wordt door de bibliothecaris aan de klant overgedragen. Dus we hebben inderdaad een schending van SRP.

class Book function getTitle () return "A Great Book";  functie getAuthor () terug "John Doe";  functie turnPage () // pointer naar volgende pagina functie getCurrentPage () terug "huidige pagina-inhoud";  class BookLocator functie lokaliseren (boek $ boek) // geeft de positie in de bibliotheek weer // ie. planknummer & kamernummer $ libraryMap-> findBookBy ($ book-> getTitle (), $ book-> getAuthor ()); 

Introductie van de BookLocator, de bibliothecaris zal geïnteresseerd zijn in de BookLocator. De klant is geïnteresseerd in de Boek enkel en alleen. Natuurlijk zijn er verschillende manieren om a te implementeren BookLocator. Het kan de auteur en de titel of een boekobject gebruiken en de vereiste informatie van de Boek. Het hangt altijd af van ons bedrijf. Wat belangrijk is, is dat als de bibliotheek wordt gewijzigd en de bibliothecaris boeken in een anders georganiseerde bibliotheek moet vinden, de Boek object wordt niet beïnvloed. Op dezelfde manier, als we besluiten om een ​​voorgecompileerde samenvatting aan de lezers te geven in plaats van ze door de pagina's te laten bladeren, zal dit niet van invloed zijn op de bibliothecaris noch op het proces van het vinden van de plank waar de boeken op zitten.

Als het echter onze taak is om de bibliothecaris uit te schakelen en een zelfbedieningsmechanisme in onze bibliotheek te maken, dan kunnen we ervan uitgaan dat SRP in ons eerste voorbeeld wordt gerespecteerd. De lezers zijn ook onze bibliothecarissen, ze moeten het boek zelf gaan zoeken en het vervolgens in het geautomatiseerde systeem bekijken. Dit is ook een mogelijkheid. Wat hier belangrijk is om te onthouden, is dat u uw bedrijf altijd zorgvuldig dient te overwegen.

Laatste gedachten

Het Single Responsibility Principle moet altijd worden overwogen wanneer we code schrijven. Klasse- en moduleontwerp wordt hierdoor sterk beïnvloed en leidt tot een laag gekoppeld ontwerp met minder en lichtere afhankelijkheden. Maar zoals elke munt heeft het twee gezichten. Het is verleidelijk om vanaf het begin van onze applicatie te ontwerpen met SRP in gedachten. Het is ook verleidelijk om zoveel actoren te identificeren als we willen of nodig hebben. Maar dit is eigenlijk gevaarlijk - vanuit een ontwerp-oogpunt - om vanaf het allereerste begin aan alle partijen te denken. Overmatige SRP-afweging kan gemakkelijk leiden tot voortijdige optimalisatie en in plaats van een beter ontwerp, kan dit leiden tot een verstrooiing waarbij de duidelijke verantwoordelijkheden van klassen of modules moeilijk te begrijpen zijn..

Dus, wanneer u constateert dat een klasse of module om verschillende redenen begint te veranderen, aarzel dan niet om de noodzakelijke stappen te nemen om SRP te respecteren, maar wacht niet te laat, want voortijdige optimalisatie kan u gemakkelijk misleiden.