SOLID
test:
Important tip: this note needs to be highlighted
Important danger: this note needs to be highlighted
Bron: https://github.com/GitbookIO/plugin-codetabs
msg = "Hello World" print msg
var msg = "Hello World"; console.log(msg);
Hello World
https://github.com/GitbookIO/plugin-codetabs
Dit hoofdstuk werd origineel geschreven door Tom Peeters. Bron. maar moet nog langs editor Tim ;)
Inleiding
Software ontwikkelaars worden geconfronteerd met ontwerpproblemen. Professionals zullen echter merken dat bepaalde soorten van ontwerpproblemen steeds terugkomen. Eénmaal je een probleem herkent als een variant van een probleem dat je vroeger al eens hebt opgelost, kan je gebruik maken van de inzichten die je al verworven hebt. Je ziet bepaalde patronen terugkeren.
Wat is nu precies een ontwerppatroon of design pattern
Een ontwerppatroon is een standaardoplossing voor een vaak voorkomend ontwerpprobleem. Deze patronen zijn belangrijk omdat ze je de moeite kunnen besparen om telkens opnieuw het warm water uit te vinden. Bovendien heeft elk patroon een eigen naam, wat ervoor zorgt dat het heel eenvoudig wordt om bepaalde complexe ideeën in een oogwenk te communiceren aan een andere programmeur.
GESCHIEDENIS VAN ONTWERPEN
Sinds het begin van het computertijdperk is probleem-oplossend denken ingrijpend veranderd.
PROGRAMMEREN: THE SEQUEL
In het begin programmeerden we met assembly, en was elk programma beperkt tot een honderdtal lijnen. Elke programmeur had zijn eigen stijl volgens intuïtie.
PROGRAMMEREN: FLOW BASED DESIGN
Toen de complexiteit toenam, gingen meerdere programmeurs code reviews verrichten bij elkaar en merkte men al dat onderhoud en begrijpen van code niet voor de hand lag. Men trachtte normen op te leggen en ging flowcharts maken om programmeurs een goed design te laten maken. Flowcharts bleken ook nuttig om programma’s eenvoudiger te begrijpen.
PROGRAMMEREN: GESTRUCTUREERD PROGRAMMEREN
Gestructureerd programmeren volgde in de jaren ‘70. Een gestructureerde code bestaat uit één enkel begin en afsluitpunt en daar tussen een set van modules. Gestructureerde programma’s zijn makkelijker te lezen en te begrijpen, te onderhouden en vereisen minder ontwikkel-tijd.
PROGRAMMEREN: OBJECT-GEORIENTEERD DESIGN
Object-georiënteerd programmeren gebeurt intuïtief en identificeert natuurlijke objecten ( Hero, vijand, ...) die voorkomen in je probleem. Daarnaast worden relaties zoals composities, referenties, overerving bepaald. Dit resulteert in herbruikbaarheid van code, en overzichtelijkere en makkelijk te onderhouden code.
VANDAAG
Door de toenemende concurrentie moet je als programmeur tegenwoordig zeer dynamisch (Agile Principe) zijn. Ook is de gemiddelde levensduur van een product drastisch verlaagd. Organisaties moeten snel op marktveranderingen kunnen antwoorden. Ook worden business strategieën snel aangepast wat wil zeggen dat bijvoorbeeld een goed software design zeer belangrijk is om snel op deze veranderingen in te kunnen inspelen. Software moet snel ontwikkeld kunnen worden en staat dicht bij de klant ( deze kan al vaak worden betrokken bij de ontwikkeling van gepersonaliseerde software).
Object georienteerd programmeren Intro
De basisgedachte achter object georiënteerd programmeren is dat mensen een beetje van de realiteit proberen te modelleren zodat het model in de vorm van een werkend programma kan worden gegoten. Je kan je object model beschouwen als een blackbox. Bijvoorbeeld een auto als blackbox betekent dat je een handvol pedalen, schakelaars hebt die fungeren als interface. Duwen op de rem betekent dat je auto mindert, maar je hoeft niet te weten hoe dat gebeurt, enkel maar wat er gebeurt. Dit principe heet encapsulatie.
Encapsulatie: je probeert zoveel mogelijk zaken af te schermen van de rest.
Bijvoorbeeld een auto kan starten, maar je weet niet wat er allemaal moet gebeuren om de auto te starten. Dit noemen we een interface.
Klassen en objecten
Klasse: een beschrijving en verzameling van dingen (objecten) met soortgelijke eigenschappen
Object: een instantie van een klasse
Als voorbeeld kunnen we een auto nemen.Een auto catalogeren we als een klasse, want bestaat uit een aantal eigenschappen, zoals de kleur van de auto, het aantal pk, benzine of diesel motor, enzovoort. Maar ook het starten, stoppen, schakelen van de wagen worden als eigenschappen bezien.
Een object betekent bijvoorbeeld een nieuwe renault met een rode kleur, 100pk en dieselmotor.
De auto is de klasse die beschrijft hoe een auto er voor onze probleemstelling moet uit zien, terwijl de renault een instantie van de klasse is, of ook wel object genoemd. Wat betekent dat dit een effectieve auto is die je kan gebruiken.
Hoe maak je een klasse
Een klasse kan bestaan uit:
private member variabelen: bepalen de toestand van de klasse
constructor
public methoden: aanspreekpunten voor de buitenwereld, of interfaces genoemd
properties: een gecontroleerde toegang tot de toestand
private methoden: hulpmethoden die enkel beschikbaar zijn binnen het object
SOLID
S.O.L.I.D. zijn 5 principes die ons helpen om een goede software architectuur te schrijven (door Robert C. Martin)
S : SRP (Single responsibility principle)
O : OCP (Open closed principle)
L : LSP (Liskov substitution principle)
I : ISP (Interface segregation principle)
D : DIP (Dependency inversion principle)
Single Responsibility Principle
Een klasse heeft slechts 1 bestaansreden en kan maar 1 reden hebben om te veranderen
Eigenschappen van SRP zijn:
coupling
cohesion
Cohesion: wat een klasse zou moeten doen. Lage cohesie betekent dat een klasse verschillende zaken doet, en niet gefocust is op de taak die hij zou moeten doen. Terwijl hoge cohesie betekent dat een klasse doet wat hij moet doen, en maar 1 taak uitvoert. Probeer er voor te zorgen dat alle methoden in een klasse betrekking hebben tot 1 doel, maw er een hoge cohesie heerst.
Coupling: Hangt een klasse van nog andere klassen af. Of hoeveel weet een klasse over de werking (inner working) van een andere klasse af.
Men streeft naar "low coupling" en "high cohesion"
Waarop letten?
Klassen mogen maar een beperkt aantal instantievariabelen hebben. De methoden van deze klasse moeten één of meerdere van deze variabelen manipuleren.
Wat bedoelen we met verantwoordelijkheid?
Een reden tot verandering!
Een voorbeeld van cohesie
Een klasse met hoge cohesie:
Een voorbeeld van lage cohesie :
Lage Cohesie
Hoge Cohesie
De Login methode and username klasse variabele heeft niets te maken met de EmailMessage klassen hoofddoel. Daarom zeggen we dat er een lage cohesie is.
Een voorbeeld van high coupling
Bijvoorbeeld iPods. Eens de batterij kapot is moet je een nieuwe iPod kopen, want de batterij is gesoldeerd in het apparaat, en kan dus niet loskomen. Bij lage koppeling (of loosly coupled) zou je de batterij moeten kunnen vervangen. Deze zelfde 1:1 relatie gaat op in software engineering.
Een voorbeeld van high coupling:
Waarom high coupling? Klasse A instantiëert objecten van klasse B, en heeft toegang tot variabelen (elementB). Op deze manier is klasse A erg afhankelijk van klasse B. Waarom afhankelijk? Als we beslissen om een extra parameter toe te voegen in de constructor van B en de default constructor private te maken. Dan moeten we elk gebruik van klasse B aanpassen (dus aanpassingen in klasse A!).
Wat is de oplossing?
We kunnen tight coupling oplossen door de dependencies te inverteren. Dit is het toevoegen van een extra laag. Bijvoorbeeld een interface toevoegen. Op deze manier zal klasseA enkel afhankelijk zijn van de interface en niet van de actuele implementatie van klasse B.
SRP voorbeeld
De werknemer klasse is nu verantwoordelijk voor CRUD operaties, maar ook voor het loggen van errors. Dus meer dan 1 verantwoordelijkeheid. Indien we beslissen om niet meer naar een bestand te loggen, moeten we de klasse aanpassen.
Daarom is het beter om dit als volgt te coderen:
De klasse FileLogger
:
Met deze FileLogger
verhoog je de "coupling" graad, en moet je een extra laag toevoegen, bijvoorbeeld een interface.
Single responsibility is niet enkel op klasse maar ook op method niveau.
Single Responsibility op method niveau
Probleemstelling
Er is je gevraagd om software te schrijven voor een online video shop. Het programma berekent en print de rekening van een klant bij onze online shop. Onderstaande paragraaf geeft ons de voorbeeldcode van het programma. We zullen deze oplossing grondig analyseren en bekijken hoe we de code kunnen verbeteren. Aan het programma wordt meegegeven welke film de klant heeft gehuurd, en voor hoe lang. Daarna wordt de rekening gemaakt – afhankelijk van hoe lang de film gehuurd geweest is, en welk type film (nieuwe release, kinder, gewone). UML notatie:
Voorbeeld van de MAIN functie
(altijd goed om je architectuur uit te testen door in je main een voorbeeld applicatie te laten draaien)
Movie klasse .. een simpele klasse
Rental klasse
Deze klasse stelt voor hoe lang een klant een bepaalde film gehuurd heeft.
Customer klasse
Deze klasse stelt de klant van de winkel voor
Analyse van onze architectuur
Voor een dergelijke (simpele) applicatie is design/architectuur niet zo belangrijk. We zien echter dat dit niet echt object georiënteerde code is, wat een invloed heeft op het gemak waarmee de toepassing kan uitgebreid en veranderd worden.
Enkele bemerkingen: de statement functie in onze Customer klasse is te lang en doet te veel. Veel zaken die we hier in doen, zouden naar andere klasses overgedragen moeten worden.
Ook al werkt ons programma (mooi geschreven code of lelijke code speelt echt geen rol voor een compiler), we moeten ons steeds het volgende afvragen: als in onze applicatie toevoegingen of veranderingen moeten aangebracht worden, moet er iemand zijn die dit kan klaar spelen, en een zwak gedesigned systeem is moeilijk te veranderen. Het vergt dan heel wat analysetijd van de programmeur om je programma te doorgronden.
Een voorbeeld van verandering: stel dat je klant vraagt om je rekening ook op een webpagina in HTML af te drukken. Welke impact heeft dit op je programma? Als we naar onze code kijken, merken we dat voor dergelijke vraagstelling het niet mogelijk is code te hergebruiken. Dus moeten we een nieuwe functie maken, die veel gedrag van de reeds bestaande statement functie kopieert. Op zich nog niet echt een probleem, want met wat copy-paste werk kan je de statement functie dupliceren en hernoemen naar htmlstatement() en de result string aanpassen met bijvoorbeeld: result+=”<b>”blabla</b>”
.
Maar bedenk eens wat je allemaal moet doen als één regel in het rekening maken verandert? Je moet zowel aanpassingen maken in de statement als de htmlstatement functie, wat gegarandeerd fouten (bugs) zal introduceren!
Nog een andere opmerking. Als de winkel beslist om de classificatie (gewone film, kinder, nieuwe release) te veranderen, maar nog niet zeker is hoe, kan het zijn dat ze je vragen de mogelijke ideeën uit te testen. Dat heeft dan ook een invloed op hoe kosten voor films en huurpunten worden berekend. Als professionele software ontwikkelaar in spe ga ik je reeds verwittigen dat dergelijke veranderingen heel regelmatig voorkomen!
De statement()
functie is de plaats waar de veranderingen in classificatie en berekeningen gebeuren. Dus ook niet te vergeten consistente veranderingen te maken in de htmlstatement()
functie. Als de berekeningsmethodes steeds complexer worden, zal het met ons design ook steeds moeilijker worden om deze veranderingen door te voeren.
Wat nu volgt zijn voorstellen om onze software architectuur stap voor stap te veranderen totdat we object georiënteerde code hebben geschreven die ons in staat stelt dergelijke veranderingen op een makkelijke manier te realiseren.
Analyseren van de statement functie
Tracht steeds korte functies/methodes te schrijven. Tracht lange functies onder te verdelen in kleinere delen. Kleinere stukken code zijn veel eenvoudiger te onderhouden! Om een functie te verdelen tracht je bij elkaar horende blokken te vinden. Een goede manier is om naar lokale scope variabelen te zoeken. Bijvoorbeeld thisAmount en Rental r, waarbij r niet wordt veranderd, terwijl thisAmount wel. Elke variabele die niet wordt veranderd, kunnen we als argument doorgeven. Indien er variabelen zijn die wel worden veranderd kunnen we, indien er maar 1 is, deze terug retourneren.
We zoeken in onze statement() functie naar deze lijnen code:
En maken hiervoor een aparte functie:
Terwijl we in de statement functie deze verandering maken:
(zie volledige C# code - project SoftwareArchitectuur2) [TODO]
Analyse van AmountFor functie
Als we naar onze nieuwe AmountFor(Rental r) functie kijken, valt het op dat we hier met Rental data werken, en eigenlijk geen data van de customer klasse gebruiken. In de meeste gevallen moeten functies/methodes in die klasse staan vanwaar ze data gebruiken, dus in dit geval van de Rental klasse.
Bij deze heb ik ook de naam van de functie veranderd in GetCharge(), omwille van de duidelijkheid. Tracht altijd naamgevingen te gebruiken die direct duidelijk maken wat je programmeert. Dus in de Customer klasse staat nu
Het klasse diagramma is nu veranderd naar:
Als we terug naar de statement() functie kijken dan is de variabele thisAmount redundant, en veranderen we naar:
Best is om tijdelijke variabelen te verwijderen, omdat je makkelijk vergeet waarvoor ze dienen. Je zou in bovenstaand geval toch kunnen kiezen voor een temporary variabele thisAmount, omdat de getCharge() tweemaal wordt opgeroepen dus tweemaal een berekening maakt, als we dan naar performantie kijken.
In de Customer klasse:
Met de statement functie als:
HTMLStatement() functie
In plaats van tekst te loggen wil ik mijn prijsberekening naar een HTML pagina schrijven. Dit is nu vrij simpel, en bij veranderingen in de prijsberekening moet ik de customer klasse niet meer aanpassen!
Bij een verandering aan de berekening, of toevoeging van nieuwe types films worden de statement functies niet meer gewijzigd, waardoor we duidelijk meer onderhoudvriendelijke code hebben geschreven.
Het valt hier op dat we in de Rental klasse met een Movie object werken. Logischerwijze zou deze functie beter in de movie klasse staan. Het is een slecht idee om een switch te doen op een attribuut van een ander object!
We moeten dan wel het aantal huurdagen meegeven als parameter van deze nieuwe functie. Dus eigenlijk gebruikt deze functie 2 stukken data – type film, en aantal huurdagen. Waarom dan toch naar Movie klasse brengen, en daysRented meegeven als argument? Wel , de voorgestelde veranderingen gingen om type film (wat te doen als nieuw type wordt geïntroduceerd ), daarom is het logisch om de type informatie zo compact mogelijk te bundelen (in 1 functie ipv 2 functies (als je het type zou doorgeven als parameter)).
De Rental klasse:
In de klasse Movie zit nu:
SRP, the law of demeter
Dit is het principe van "least knowledge", is een object-oriented software design principe. Een methode van een object mag enkel wie oproepen:
het object zelf
een argument van de methode
elk object dat in de methode gecreerd is
alle properties, variabelen van het object zelf
Open-Closed Principe (OCP)
Het open/closed principe stelt dat klasses of functies open moeten zijn voor uitbreiding, maar gesloten voor wijziging!
Open for extension, closed for modification
Gesloten voor wijziging betekent dat het gedrag mag veranderd worden zonder de broncode aan te passen..
Een typisch voorbeeld:
Nu bouwen we een applicatie die de oppervlakte van een collectie rechthoeken zal berekenen.
En we schrijven ons testprogramma:
De volgende vraag komt op: kunnen we het programma uitbreiden zodat we ook de oppervlakte van een cirkel kunnen berekenen?
We passsen de code als volgt aan:
Wat later krijgen we de vraag om de OppBereken klasse uit te breiden zodat we ook de oppervlakte van driehoeken kunnen opnemen. Dit druist in tegen het principe "gesloten voor wijziging!"
OPC oplossing
Maak gebruik van abstractie.
In .NET betekent abstractie : gebruik maken van interfaces, of abstracte klassen.
Wat is een interface?
Je hebt geleerd dat een klasse slechts van één klasse kan erven. Een klasse kan echter ook nog interfaces implementeren. Wanneer een klasse een interface implementeert sluit de klasse een contract met de compiler dat de klasse zich zal gedragen volgens de interface. Concreet betekent dit dat in de klasse alle eigenschappen (properties) en methoden van de interface moet implementeren. Een interface bevat dus eigenlijk enkel een lijst van eigenschappen en methoden die nog geen concrete invulling hebben.
Volgens WIKIPEDIA: Een interface in de programmeertaal als Java of C# is een soort abstracte klasse die een interface aanduidt die klassen kunnen implementeren. Een interface wordt aangeduid met het sleutelwoord interface en bevat alleen ongedefinieerde methoden.
Wat is een abstracte klasse?
In de informatica is een abstracte klasse een klasse die ongedefinieerde methoden kan bevatten. Deze methoden worden geïmplementeerd in een subklasse van de abstracte klasse. Het is niet mogelijk om een object te maken van abstracte klassen maar wel van niet-abstracte subklassen. Door middel van overerving is het wel mogelijk om de methoden die wel gedefinieerd zijn in de abstracte klasse te erven en in de subklassen te gebruiken.
Een klasse kan meerdere interfaces implementeren maar alleen van één klasse (rechtstreeks) overerven. Een verschil met abstracte klassen is dat een abstracte klasse wel gedefinieerde methoden kan bevatten maar een interface bevat alleen ongedefinieerde methoden.
Om aan het OPC principe te voldoen moeten we als volgt te werk gaan:
We maken een basis klasse voor rechthoeken, cirkels, driehoeken, andere vormen, en deze definieert een abstracte methode om de oppervlakte te berekenen.
De andere klassen leiden af van vorm:
De berekening gebeurt nu als volgt:
Op deze manier is de OppBerekenaar klasse gesloten voor wijziging, maar toch open voor uitbreiding!
In de praktijk
OPC zal je als ervaren programmeur sneller toepassen. Van bij de start van je ontwikkeling zal je niet altijd OPC toepassen, en accepteer dat een klasse veranderd moet worden. Maar bij nog verandering, zorg je ervoor dat je naar het OPC principe refactort.
Liskov Substitution Design Principle
TOPROCESS:
!!!!! bron
Subtypes moeten vervangbaar zijn door hun super types (parent class).
de IS-A relatie zou vervangen moeten worden door IS-VERVANGBAAR DOOR
Als voorbeeld werken we met een klasse vierkant die overerft van Rechthoek. De klasse Rechthoek heeft eigenschappen als "width" en "height", en vierkant erft deze over. Maar als voor de klasse vierkant de width OF height gekend is, ken je de waarde van de andere ook. En dit is tegen het principe van Liskov.
De klasse Vierkant erft over van Rechthoek (maar is in programmeren een vierkant wel een rechthoek?) Een vierkant is een rechthoek met gelijke breedte en hoogte, en we kunnen de properties virtual maken in de klasse Rechthoek om dit te realiseren. Rare implementatie, niet? Maar kijk nu naar de client code..
Client code:
De gebruiker weet dat r een Rechthoek is dus is hij in de veronderstelling dat hij de width en height kan aanpassen zoals in de parent klasse. Dit in acht genomen zal de gebruiker verrast zijn om 100 te zien ipv 50.
Oplossen van het LSP probleem
Code dat niet vervangbaar is zorgt ervoor dat polymorfisme niet werkt
Client code (en dit geval de Main) veronderstelt dat basis klassen kunnen vervangen worden door hun afgeleide klassen (Rechthoek r = new Vierkant())
Het oplossen van LSP door switch cases zorgt voor een onderhoudsnachtmerrie!
Een ander voorbeeld:
Wat gebeurt er als we een Formule 1 auto hebben:
De interface dient als het contract, en moet je veronderstellen dat alle auto's dit gedrag hebben.
Dit is de essentie van het Liskov Substitution Principle.
Waarom is het schenden van LSP niet goed?
Gebruik van abstracte klassen betekent dat je in de toekomst makkelijk een subklasse kan toevoegen in de werkende, geteste code. Dit is de essentie van het open closed principe. Maar wanneer je subklassen gebruikt die niet volledig de interface (abstracte klasse) supporteren moet je in de bestaande code speciale gevallen gaan definiëren.
Bijvoorbeeld:
Interface Segregation Principle (ISP)
Clients mogen niet afhankelijk zijn over interfaces die ze niet gebruiken
Clients mogen niet gedwongen worden om interfaces te implementeren die operaties bevatten die ze niet nodig hebben, of nooit zullen gebruiken. Deze interfaces noemen we "fat" interfaces.
In plaats van een fat interface is het beter om deze verder op te delen in meer specifieke interfaces.
Hoe ISP oplossen:
Er zijn verschillende redenen waarom interface moeten gesegregeerd zijn. Om functionaliteit naar de clients te verstoppen, zelf documenterend voor andere ontwikkelaars.
Hoe ISP detecteren?
Implementeer je sommige methodes van een interface niet, dan voldoe je niet aan ISP.
Als je klasse refereert naar een andere klasse, maar gebruikt in beperkte mate deze gerefereerde klasse, dan voldoe je niet aan ISP.
Kom je code tegen zoals bovenstaande: if( b is Pinguin) ... else , is dit een teken dat je niet aan ISP voldoet, en bijgevolg je code flexibeler moet schrijven.
Kom je code tegen waarbij methoden afgeleid van een basis klasse niet geïmplementeerd worden: vb.
Dependency inversion principle
High level modules mogen niet afhankelijk zijn van low level modules. Ze mogen enkel afhankelijk zijn van abstractie.
Wat zijn dependencies?
Een dependency is iets dat misschien kan veranderen tijdens de levenscyclus van je code.
Stel je voor dat je muis, keyboard, monitor door de fabrikant aan je moederbord zijn gesoldeerd zijn. Met andere woorden, wanneer je een moederbord koopt heb je ook een muis, keyboard en monitor. Stel je voor je met één van de devices vervangen. Je beschadigt misschien het moederboard.
In programmeren is het net hetzelfde. Het dependency Inversion principe is een manier om plugs aan je code toe te voegen zodat de high level modules (moederbord) onafhankelijk is van de low level modules (muis). De low level modules kunnen later ontwikkeld worden en zouden makkelijk vervangbaar moeten zijn.
Een indicatie die veranderingen te weeg brengen is het "new" keywoord.
Een praktische oplijsting van veel voorkomende dependencies:
Third party library
Database
File system
Web Service
New keyword
Je zou er voor moeten zorgen dat constructors alle dependencies bevat die een klasse nodig heeft. Dit noemen we EXPLICIT DEPENDENCIES. In het andere geval noemen we het hidden dependencies.
Dependency injection is een techniek om een dependency (afhankelijkheid) te injecteren in een klasse wanneer deze klasse ze nodig heeft.
Een voorbeeld:
Op het eerste zicht is er niets mis met bovenstaande code. Maar eigenlijk schenden we DIP. De high level module 'Printer' is afhankelijk van de klasse EventLogWriter. Deze klasse noemen we een concrete klasse, en is dus geen abstracte klasse.
Het wordt duidelijker als we ook een email naar de IT admin willen sturen bij een bepaald probleem en niet enkel een log neerschrijven.
Als we een klasse schrijven voor het versturen van emails, moet de Printer klasse het juiste kunnen afhandelen, zonder dat wij concrete code implementeren. Dus nu is de Printer klasse afhankelijk van de EventLogWriter klasse, en hiervan willen we vanaf.
Onderstaande code laat zien hoe je verandering op een ondynamische manier injecteert:
Dus onze printer klasse moet een instantie van al onze loggers bijhouden. Volgens het dependency inversion principe moeten we ons systeem ontkoppelen zodat de higher level modules, dus de Printer module afhankelijk is van een abstracte klasse of interface. Deze abstractie zal gemapt worden (polymorf gedrag) naar een concrete klasse die de juiste actie zal ondernemen.
Op deze manier maken we de Printer klasse (High level module) onafhankelijk van de concrete log klassen.
Hoe kunnen we deze verder ontkoppelen zodat we bij het toevoegen van andere log klassen (vb. SMS logger), de printer klasse niet meer aangepast hoeft te worden?
Dit kan je implementeren door Dependency injectie
Dependency Injection
Dependency Injection betekent dat we een concrete implementatie in een klasse injecteren, met als doel om de koppeling tussen klassen te verminderen.
3 manieren voor Dependency injection:
Constructor injection
Method injection
Property injection
Contructor injection
Constructor Injection
We geven het object van de concrete klasse mee met de constructor van de afhankelijke klasse.
Method Injection
In constructor injection wordt de concrete klasse gedurende de volledige levenscyclus van de Printer gebruikt. Als je verschillende concrete klassen moet aanroepen, moet je deze in de methode zelf injecteren.
Property Injection
We geven het object van de concrete klasse mee via een set property.
Een tweede voorbeeld
Bepaal de dependencies:
De dependencies opgelijst:
MailMessage
SmtpClient
Inventory
PaymentGateway
Hoe moeten we dit refactoren?
Dependencies in interfaces stoppen
Injecteer de implementatie van deze interface in de order klasse
Zorg voor Single Responsible principle
Het toepassen van dependency injection zorgt typisch voor heel wat interfaces die ergens moeten geinstantierd worden. Dit doen we meestal in de default constructor of in de Main (de startup routine van je applicatie)
De MailMessage en SmtpClient zorgt voor een eventuele verandering. Stel je wil later de klant niet via email een notificatie sturen, maar via facebook messenger, dan zal je deze code moeten aanpassen. Door dit in interface te duwen, zal je veel flexibelere code schrijven:
In de order klasse zie je de flexibiliteit terugkomen:
Op deze manier moet de Order klasse niet weten of we een email notificatie sturen, en push notification voor mobile phone, een facebook message, ...
Strategy patroon
We gaan verder met het voorbeeld van Single Responsibility op method niveau.
De conditionele logica van PrijsCode veranderen met behulp van Polymorfisme
Overerving
Omdat we verschillende types films hebben, die elk een verschillende GetCharge() implementatie hebben, kan je beter gebruik maken van subclasses. Op die manier is het zeer onderhoudbaar om nieuwe types toe te voegen (OPC Principe). Indien je niet met subclasses werkt, zal je in verschillende functies aanpassingen moeten doen: dit leidt tot bugs en inefficiëntie..
Klassediagramma:
Op deze manier heb je het switch statement (getCharge() functie) in de klasse Movie niet meer nodig!
Maar elke subklasse zal een implementatie maken van getCharge(). Voorbeeldcode:
Aan deze implementatie zit wel een zeer groot nadeel. Het is niet mogelijk om een film van classificatie te wisselen. Dus stel dat je de film “Rundskop” maakt als een NewReleaseMovie, dan is het later niet mogelijk om van deze film een RegularMovie te maken.
Indien je nu van m2 een RegularMovie wil maken, zal je de variabele opnieuw moeten instantiëren, met als gevolg dat je alle originele data kwijt bent.
Met andere woorden, onze objecten kunnen niet van klasse veranderen gedurende hun levenstijd. Er is wel een oplossing voor, namelijk gebruik maken van het strategy Patroon.
In plaats van te subklassen met Movie (RegularMovie, NewReleaseMovie, ChildrensMovie,) gaan we dit op een indirecte manier doen en van price subklassen maken. (zie klassediagramma hierboven). Op die manier kunnen we van een movie object steeds de prijs veranderen, zonder dat we het ganse movie object opnieuw moeten instantiëren.
Het strategy pattern is dus in staat om een status/ of om in realtime algoritme te veranderen ( bijvoorbeeld prijs van een film veranderen na een week van New Release naar Regular) bij te houden. Door vele programmeurs wordt dit initieel via enum en switch/case structuur opgezet. Het state patroon wordt aangemaakt door de type code naar state klassen over te brengen. Daarna gaan we de conditionele logica (switch/case) naar de price klassen zetten, om tenslotte deze switch case te verwijderen. De strategy klassen
We maken nu een abstracte klasse price met zijn subklassen. De getCharge() functie in de superklasse is abstract, zodat de afgeleide klassen verplicht zijn een eigen implementatie te geven aan deze functie. In de Movie klasse zal je in plaats van de int _priceCode een member van het type Price moeten definiëren. In runtime beslis je dan of deze member de constructor van NewReleasePrice, ChildrensPrice of RegularPrice oproept.
De volgende stap is om de getCharge() functie in de klasse Movie over te dragen naar de juiste Price klasse. (de implementatie van getCharge() in de desbetreffende klassen kan je reeds zien op de vorige pagina). De getCharge() functie van de Movie klasse wordt dan:
Hierbij is ook de conditionele logica in de functie verdwenen (want deze is overgedragen naar de Price klassen).
We kunnen dit nu ook doen voor de getFrequentRenterPoints(int daysRented). We moeten deze functie in de superclasse ‘Price’ niet abstract maken maar geven er hier reeds een implementatie aan. We maken deze wel virtual zodat de afgeleide kunnen bepalen of ze er een eigen definitie aan geven.
Enkel de NewReleasePrice geeft een eigen implementatie aan de getFrequentRenterPoints(int daysRented) functie:
Dit strategy patroon invoegen gaf ons heel wat werk, maar de winst is dat ik prijzen gemakkelijk kan veranderen en nieuwe prijsklassen kan aanmaken zonder andere code te wijzigen. Want de rest van de implementatie weet niets van dit state pattern! Voor grote complexe projecten betekent dat heel wat winst.
Alle bovenstaande veranderingen moeten leiden tot makkelijker te onderhouden code. Een heel verschil met proceduraal programmeren, maar wanneer je dit onder knie hebt, zal je veel gemakkelijker tests kunnen schrijven en veranderingen implementeren!
Eigenschap van het strategy patroon: Het strategy patroon laat toe om objecten in real time van status te veranderen.
Last updated