Een paar Object georiënteerde concepten

Leestijd: 15 Minuten

Object-georiënteerd programmeren (OOP) werd populair vanwege de belofte van code organisatie en het hergebruik. We gebruiken OOP concepten voor vele jaren, maar toch blijven we herhaaldelijk uitvoeren van dezelfde logica in onze projecten. OOP introduceerde een reeks goede uitgangspunten die, indien goed gebruikt kan leiden tot een betere, schonere code.

Een tijd geleden kwam ik op het idee om een eigen artikel te schrijven over OOP, het heeft mij geholpen om mijn kennis te verfrissen en ik hoop dat ik de lezer van dit document ook heeft.

Cohesion

Cohesion betekent samenhang, oftewel dingen die bij elkaar horen moeten samen worden gehouden, anders moet je ze ergens anders onder brengen. Het beste voorbeeld van samenhang/cohesion kan worden aangetoond met een klasse.

In dit voorbeeld wordt een klasse gedefinieerd met een technologie naam en een tool naam, deze kan je gekoppeld aan elkaar toevoegen. Deze eigenschappen, alleen beoordeeld op hun namen, horen niet bij elkaar. Het is duidelijk dat deze klasse is verantwoordelijk voor afzonderlijke groepen van informatie. Het heeft een zeer lage samenhang. Laten we het gaan herschrijven.

Dit is voorbeeld van een samenhangend klasse structuur. Waarom? Omdat de technologie en de tools een eigen klasse zijn, elke onderdeel hoort bij elkaar. Je kunt deze structuur nog verder verbeteren door interfaces toe te voegen. Je moet streven naar cohesie, maar pas op, het is soms moeilijk te bereiken.

Orthogonality

In eenvoudige termen, orthogonaal betekent het isoleren of verwijderen van bijwerkingen. Een methode, klasse of module die de status wijzigt van een niet verwante klasse of module is niet orthogonaal. Bijvoorbeeld:

Een blackbox in een vliegtuig is orthogonaal. Het heeft zijn eigen functionaliteit, zijn eigen krachtbron, microfoons en sensoren. Het heeft geen enkel effect op het vliegtuig of de buitenwereld waar hij zich in bevindt. Het enige wat die doet is het opnemen en ophalen van vluchtgegevens.

Een voorbeeld van een niet orthogonaal doel. Wanneer de snelheid van een auto verhoogd heeft het verschillende bijwerkingen. Zoals het verhogen van het radio volume (Sommige Seats). Dit effect verwacht je niet bij het intrappen van het gas.

En nu naar de code:

In dit voorbeeld van de Calculator class vertoont de functie add() onverwacht gedrag. Er wordt een tekst op het scherm getoond. De personen die deze klasse gebruiken zullen nooit verwachten dat er een bericht naar het scherm wordt geschreven. Ze verwachten dat ze de som van de getallen ontvangen.

Dit is beter. De klasse AlertMechanism heeft geen enkel effect op de klasse Calculator. In plaats daarvan, de klasse AlertMechanism heeft zijn eigen functies om te kijken of er een bericht naar het scherm moet worden geschreven.

 

Dependency and Coupling

Wat betekent dependency (afhankelijk)? Wanneer object A gebruik moet maken van object B om zijn taken uit te kunnen voeren. We zeggen dan Object A is afhankelijk van object B. In OOP komen afhankelijkheden zeer vaak voor. Terwijl het voorkomen van afhankelijkheden een nobel streven is, is het bijna onmogelijk. Het controleren van deze koppelingen (coupling) heeft de voorkeur.

Directe verwijzingen

Deze code is gebruikelijk om te zien, een klasse TechnologyToolPair in dit geval. Is afhankelijk van de Technology en Tool klasse door direct te verwijzen naar deze klasse. 

Verwijzingen via een functie

Zie het voorbeeld van een koppeling door toegang tot deze klasse door andere methoden te laten doen.

De afhankelijke klasse wordt geïnitialiseerd in de constructor, maar door middel van een andere functie gevuld. Dit is een andere vorm van een koppeling.

Verwijzingen via Polymorphism

Overerving is misschien wel de sterkte afhankelijkheid die je kunt bedenken.

Zonder de klasse Technology kan de klasse PHP zijn werk niet doen, hij kan niet eens bestaan.

Het verminderen van verwijzingen door dependency injection

Je kunt verwijzingen verminderen door een klasse te injecteren in een andere klasse. Bijvoorbeeld:

Door de klasse TechnologyTool te injecteren in de constructor van de display klasse verlagen we de afhankelijkheid van de TechnologyTool klasse. Dit is echter niet de beste oplossing. 

Het verminderen van verwijzingen via interfaces

We kunnen de afhankelijkheid van een klasse nog verder verlagen door het gebruik van interfaces, zie het voorbeeld.

Deze code maakt een interface met de naam ITechnology, een interface is heel abstract. Een interface definieert alleen maar welke functies en attributen een lid moet implementeren. Het voordeel hiervan is dat je er van uit kan gaan dat wanneer een klasse een specifieke interface heeft ook die functies of attributen heeft.

Zoals in het voorbeeld beschreven de functie __toString() van de klasse Technology kan er vanuit gaan dat een lid van $this->array de functie display() heeft. Omdat bij het toevoegen verplicht wordt dat een klasse wordt toegevoegd van de interface ITechnology. 

SOLID

SOLID is een set van richtlijnen voor het schrijven van schone code. Het SOLID principe maakt het ook makkelijk om te veranderen te maken, code te onderhouden en in de toekomst uit te breiden. Het zijn richtlijnen geen regels, wanneer toegepast op de code een positief effect hebben op de onder houdbaarheid.

Waar komt SOLID vandaan

SOLID staat voor

  1. Single responsibility principle
  2. Open/closed principle
  3. Liskov substitution principle
  4. Interface segregation principle
  5. Dependency inversion principle

Deze richtlijnen zijn voor het eerste definieert door Robert C. Martin. Hij heeft ze niet allemaal bedacht maar wel samengevoegd tot SOLID. SOLID is een onderdeel van het Agile. 

Single responsibility principle

Een object / klasse heeft maar één verantwoordelijkheid. Dat wil zeggen; een klasse heeft maar betrekking op één taak die ze uitvoert. Bijvoorbeeld.

Het is een heel abstract voorbeeld maar het idee wordt naar mijn menig goed overgedragen. Dit is een klasse voor een administratieve taak, echter andere rollen kunnen ook dezelfde functies gebruiken die beschikbaar zijn in deze klasse. Een financiële rol kan credit en debet rapporten maken, en zelf de balans opmaken. Een archief rol kan een rapport uitprinten.

Wat beter is

Nu heeft elke klasse zijn eigen functionaliteit en zit andere functionaliteit niet in de weg. Enkele andere functionele keuzes kunnen zijn dat een andere rol een specifieke implementatie wil hebben van een functie, een financiële rol heeft meer interesse in geld en een archief rol heeft meer interesse in het daadwerkelijke rapport dan de inhoud.

Open/closed principle

Een klasse moet open zijn voor uitbreiding maar gesloten voor veranderingen. Neem het voorbeeld van een lamp.

Dit voorbeeld definieert een klasse met de naam Button en kan een lamp aan of uit schakelen. Een consultant verzint dat met dezelfde knop (Button) ook een stereo toren aan moet gaan. Dit is een probleem want je moet de klasse Button aanpassen. Elke wijziging aan een bestaande klasse is een potentiele fout. Het aanpassen van bestaande functionaliteit kan tot ongewenst gedrag leiden in andere modules. Zoals je al hebt gelezen in “Dependency and coupling” weet je dat de klasse Button een sterke verwijzing heeft naar de klasse Lamp. Dit is waar de oplossing ligt, laat het me toelichten met een voorbeeld.

De interface IButton beschrijft een knop, een knop kan aan of uit. Aan de klasse Button geef je in de constructor een object mee die de interface IButton heeft geïmplementeerd. Oftewel; een object wat aan en uit kan. De knop weet niet wat hij aan of uit zet, en dat maakt hem ook niet uit. Hij is verantwoordelijk voor zichzelf. Zie: “Single responsibility principle”. Hoe zet je nu de lamp aan, simpel:

In de applicatie geef je de objecten mee aan de Button klasse welke je wilt aanzetten/uitzetten. Een verandering in het aanzetten van een lamp heeft alleen maar betrekking op een lamp. Wanneer je ook een radio wilt aanzetten geef je dat aan op de plek waar je de radio wilt aanzetten.

Liskov substitution principle

Liskov substitution principle (LSP) beschrijft dat een child klasse nooit en te nimmer de functionaliteit van de parent mag veranderen. Dit is belangrijk omdat de gebruikers van de parent klasse verwachten dat bepaalde functies een bepaald gedrag vertonen. Oftewel een child klasse aanbieden geeft hetzelfde resultaat dan wanneer je de parent klasse aanbiedt.

Het is moeilijk om een goed voorbeeld te geven de letterlijke definitie is:

“What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.”

Veel mensen gebruiken een vierkant vs. Rechthoek voorbeeld om LSP uit te leggen maar persoonlijk kwam ik een beter voorbeeld tegen (http://www.objectmentor.com/resources/articles/lsp.pdf Uncle Bob)

Voorbeeld:

We hebben twee verschillende klasse die de interface ISetting implementeren, daarom zijn ze verplicht om de functies over te nemen en kunnen ze die op hun eigen manier implementeren.

Stel; ergens in de applicatie zijn de volgende twee functies beschikbaar.

Volkomen normaal en alles werkt, alle settings kunnen worden geladen en alles kan ook weer worden opgeslagen, totdat iemand bedenkt dat er een speciale setting klasse moet worden gemaakt.

De loadAll() functie moet worden aangepast zodat de nieuwe setting klasse ook wordt toegevoegd aan de array. Maar wat blijkt; deze speciale settings kunnen alleen worden ingeladen en niet worden opgeslagen. Of er nu een exception word gegooid of niet, de save functie doet niet wat er wordt verwacht.

 

Maar wanneer de saveAll() functie wordt aangeroepen wordt er een exception gegooid. We kunnen deze fout oplossen door in de saveAll() functie een if statement in te bouwen die speciale settings overslaat.

Maar dit is geen goede oplossing, het is een probleem proberen te verhelpen wat geen probleem hoeft te zijn. Wat is de korte definitie van LSP

An object should be substitutable by its base class (or interface).”

“Een object moet kunnen worden vervangen door haar parent (of interface).”

Maar wat is de oplossing. We zien dat de SpecialSetting klasse duidelijk geen onderdeel kan zijn van de interface ISavedSetting omdat deze niet kan opslaan. Het is een Setting geen op te slaan setting. Het probleem ligt dus bij de functie save van de klasse SpecialSetting. Iemand kan zeggen: “Laten we de throw new Exception eruit halen”. Een functie definiëren in een klasse die niet doet wat de naam impliceert, jammer, zeer jammer.

Een goede oplossing is gebruik maken van Interface segregation principle. 

Interface segregation principle

Veel interfaces met een specifiek doel zijn beter dan een globale interface.

Om het probleem van net op te lossen maken we twee interfaces ISaveSetting en ILoadSetting we passen de interface aan voor wat het stuk code wil. De loadAll() functie heeft alleen interesse in het laden van settings en de saveAll() functie heeft alleen interesse in het opslaan van settings. Dit is wat deze twee functies willen.

Nu komt ook direct één van de zwaktes van PHP aan bod, een array kan verschillende typen objecten bevatten. Er is dus nog een check nodig.

Klopt, het lijkt misschien omslachtiger dan een throw new Exception weghalen, maar het verdelen van een globale interface in twee specifieke interfaces zorgt dat een individuele klasse zelf kan kiezen wat die implementeert. Opslaan, laden en zelfs beide. Op deze manier heb je geen functies in een klasse staan die deze klasse niet aankan.

 

Dependency inversion principle

High level modules zijn afhankelijk van low level modules, maar deze modules moeten niet afhankelijk zijn van hun implementatie. Anders gezegd, je moet zoveel mogelijk afhankelijk zijn van een abstractie in plaats van concrete klasse. De truck met Dependency inversion principle (DIP) is dat je de afhankelijk moet zien om te draaien.

In een vorig voorbeeld hadden we een Button die een Lamp aan en uit kon zetten. Het eerste voorbeeld was dat we de Lamp direct konden schakelen met de Button.

Dependency inversion principle

Zoals je ziet is de klasse Button afhankelijk van de klasse Light en de informatie stroom gaat dezelfde kant op. Dat was niet wenselijk dus hebben we een interface geïntroduceerd.

Dependency inversion principle

Het is leuk om te zien dat door deze interface de code zowel DIP evenals OCP is geworden. Zoals je kunt zien is geen klasse afhankelijk van de klasse Light de afhankelijkheid is omgedraaid maar de informatie flow is hetzelfde gebleven.

Twee shift registers
Sonar