Enums, switch statements en SOLID - deel 3

Het Dependency inversion principe

Vorige week refactorde ik een stuk code om meer in lijn te zijn met het Single-Responsiblity principe.

Ik abstraheerde logica die oorspronkelijk in onderstaande switch-statement zat, naar aparte classes. Dat zorgde ervoor dat de oorspronkelijke class een reden minder had om gewijzigd te worden (of drie drie redenen minder, afhankelijk van hoe je het bekijkt). Anders gezegd: door code te verhuizen, bracht ik het aantal verantwoordelijkheden van de oorspronkelijke class terug.

Het resultaat was een stapje in de richting van beter leesbare en onderhoudbare code.

De logica in GetClaimsForUser() bevat echter nog veel repetitie en is afhankelijk van de concrete implementatie van de classes die we aanroepen. Om dat probleem aan te pakken, kunnen we gebruik maken van een ander SOLID-principe: het Dependency inversion principe.

Het kan me niet schelen hoe je het doet, als je het maar doet

Dit principe stelt twee dingen.

  1. Modules op een hoger abstractieniveau moeten onafhankelijk zijn van modules op een lager abstractieniveau. Beide moeten afhankelijk zijn van abstracties, zoals interfaces of baseclasses.

  2. Abstracties moeten onafhankelijk zijn van details. Het is juist andersom: details, in de vorm van concrete implementaties, moeten afhankelijk zijn van abstracties.

(Deze karakterisering van het Dependency inversion principe is afkomstig van softwareguru Robert “Uncle Bob” Martin, wiens boeken ik van harte aanraad. Maar ik zal niet liegen: voor deze blog baseer ik me gewoon op de Wikipediapagina.)

Dit zijn veel woorden om te zeggen: programmeer tegen een interface aan en niet tegen een concrete class. De reden daarvoor is simpel: als je tegen een interface aan programmeert, kun je de concrete implementatie aanpassen zonder de aanroepende code te hoeven wijzigen.

Hoe verhoudt dit principe zich tot de code hierboven? We kunnen de ClaimsHelper zien als een module op hoger abstractieniveau, en de Read-, Write- en DeleteClaimProvider als modules op een lager abstractieniveau. Het probleem wordt dan meteen duidelijk: de eerste heeft nu een afhankelijkheid van de laatste drie.

Drie classes, één abstractie

Als je de code snippet bekijkt waar deze blog-post mee begon, valt je dan iets op aan de drie methods die we aan hebben gemaakt?

In wezen doen ze alle drie hetzelfde, namelijk: de claim teruggeven die bij de permission hoort. Dit is een functie van alle drie de methods die de concrete implementatie overstijgt. We zijn op een abstractie gestuit. Dat gedrag kunnen we vangen in een interface:

Onze eerder geschetste ReadClaimProvider hoeven we maar minimaal aan te passen om aan deze interface te kunnen voldoen:

Nu we tegen een interface aan kunnen programmeren, hoeft de ClaimsHelper niet meer te weten van concrete methods die de claim van elke individuele permission teruggaven. Hij hoeft alleen maar te weten dat hij met een class te maken heeft die IProvideClaims implementeert en daar de method GetClaim() van aan te roepen.

De ClaimsHelper komt er nu als volgt uit te zien:

Zoals je ziet, is GetClaimsForUser() een stuk opgeschoond. Omdat we niet meer afhankelijk zijn van concrete implementaties, hebben we het oorspronkelijke switch-statement uit de method kunnen abstraheren naar zijn eigen helpermethod met een switch-expressie.

De oorspronkelijke method hoeft zich alleen nog maar te bekommeren om het verkrijgen van de claim, niet meer om alle concrete manieren waarop die claims verkregen worden. Ook vanuit het perspectief van het Single-Responsibility principe is er vooruitgang geboekt: alweer een verantwoordelijkheid minder!

What’s next?

Maar helemaal zijn we er nog niet. De ClaimsHelper heeft via GetClaimProvider() op dit moment nog weet van de concrete implementaties die er van IGetClaim bestaan. Hij moet ook wel, lijkt het, want hoe kan GetClaimsForUser() ooit de juiste claims aan de rechten van de gebruiker koppelen? Dat bekijken we volgende week.

Wie tot die tijd graag zelf wil experimenteren, kan de code via GitHub binnenhalen.

Meer in deze reeks

  1. De casus
  2. Het Single-Responsibility principe
  3. Het Dependency inversion principe
  4. Het Open-closed principe
  5. SOLID en performance
  6. Conclusie
  7. Addendum: Slimmere Enums

clean code · dependency inversion principe · enums · refactoren · single-responsibility principe · solid · switch statements