Hoe code te schrijven die verandering omvat

Het schrijven van code, die gemakkelijk te veranderen is, is de heilige graal van programmeren. Welkom bij het programmeren van nirvana! Maar dingen zijn in werkelijkheid veel moeilijker: broncode is moeilijk te begrijpen, afhankelijkheden wijzen in ontelbare richtingen, koppeling is vervelend en je voelt al snel de hitte van programmeerhel. In deze zelfstudie bespreken we enkele principes, technieken en ideeën die u helpen bij het schrijven van gemakkelijk te veranderen code.


Sommige objectgerichte concepten

Object-oriented programming (OOP) werd populair, vanwege de belofte van code-organisatie en hergebruik; het heeft volstrekt gefaald in dit streven. We gebruiken al jaren OOP-concepten, maar toch blijven we herhaaldelijk dezelfde logica implementeren in onze projecten. OOP introduceerde een reeks goede basisprincipes die, mits goed gebruikt, kunnen leiden tot betere, schonere code.

Samenhang

De dingen die bij elkaar horen, moeten bij elkaar worden gehouden; anders moeten ze elders worden verplaatst. Dit is waar de term cohesie naar verwijst. Het beste voorbeeld van cohesie kan worden aangetoond met een klasse:

class ANOTCohesiveClass private $ firstNumber; privé $ secondNumber; privé $ lengte; privé $ breedte; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  function setLength ($ length) $ this-> length = $ length;  function setHighight ($ height) $ this-> width = $ height;  functie add () return $ this-> firstNumber + $ this-> secondNumber;  functie aftrekken () return $ this-> firstNumber - $ this-> secondNumber;  functiegebied () return $ this-> length * $ this-> width; 

In dit voorbeeld wordt een klasse gedefinieerd met velden die cijfers en grootten voorstellen. Deze eigenschappen, alleen beoordeeld op basis van hun naam, horen niet bij elkaar. We hebben dan twee methoden, toevoegen() en aftrekken (), die alleen werken op de twee nummervariabelen. We hebben verder een Gebied() methode, die werkt op de lengte en breedte velden.

Het is duidelijk dat deze klasse verantwoordelijk is voor afzonderlijke groepen informatie. Het heeft een zeer lage cohesie. Laten we het refactiveren.

class ACohesiveClass private $ firstNumber; privé $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  functie add () return $ this-> firstNumber + $ this-> secondNumber;  functie aftrekken () return $ this-> firstNumber - $ this-> secondNumber; 

Dit is een zeer samenhangende klasse. Waarom? Omdat elke sectie van deze klasse bij elkaar hoort. Je moet naar cohesie streven, maar pas op, het kan moeilijk zijn om te bereiken.

orthogonaliteit

Simpel gezegd verwijst orthogonaliteit naar het isoleren of elimineren van bijwerkingen. Een methode, klasse of module die de status van andere niet-gerelateerde klassen of modules verandert, is niet orthogonaal. De zwarte doos van een vliegtuig is bijvoorbeeld orthogonaal. Het heeft zijn interne functionaliteit, innerlijke stroombron, microfoons en sensoren. Het heeft geen effect op het vliegtuig waarin het zich bevindt, of in de buitenwereld. Het biedt alleen een mechanisme om vluchtgegevens te registreren en op te halen.

Een voorbeeld van een dergelijk niet-orthogonaal systeem is de elektronica van uw auto. Het verhogen van de snelheid van uw voertuig heeft verschillende neveneffecten, zoals het verhogen van het radiovolume (onder andere). Snelheid is niet orthogonaal voor de auto.

klasse Calculator private $ firstNumber; privé $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  functie add () $ sum = $ this-> firstNumber + $ this-> secondNumber; if ($ sum> 100) (nieuw AlertMechanism ()) -> tooBigNumber ($ sum);  geeft $ bedrag terug;  functie aftrekken () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism function tooBigNumber ($ number) echo $ number. 'is te groot!'; 

In dit voorbeeld is de Rekenmachine klasse toevoegen() methode vertoont onverwacht gedrag: het maakt een AlertMechanism object en roept een van zijn methoden aan. Dit is onverwacht en ongewenst gedrag; bibliotheekconsumenten verwachten nooit dat een bericht op het scherm wordt afgedrukt. In plaats daarvan verwachten ze alleen de som van de verstrekte nummers.

klasse Calculator private $ firstNumber; privé $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  functie add () return $ this-> firstNumber + $ this-> secondNumber;  functie aftrekken () return $ this-> firstNumber - $ this-> secondNumber;  class AlertMechanism functie checkLimits ($ firstNumber, $ secondNumber) $ sum = (nieuwe Calculator ($ firstNumber, $ secondNumber)) -> add (); if ($ sum> 100) $ this-> tooBigNumber ($ sum);  function tooBigNumber ($ number) echo $ number. 'is te groot!'; 

Dit is beter. AlertMechanism heeft geen effect op Rekenmachine. In plaats daarvan, AlertMechanism gebruikt alles wat het nodig heeft om te bepalen of een waarschuwing moet worden uitgegeven.

Afhankelijkheid en koppeling

In de meeste gevallen zijn deze twee woorden uitwisselbaar; maar in sommige gevallen heeft de ene term de voorkeur boven de andere.

Dus wat is a afhankelijkheid? Wanneer object EEN moet object gebruiken B, om het voorgeschreven gedrag uit te voeren, zeggen we dat EEN hangt af van B. In OOP zijn afhankelijkheden heel gebruikelijk. Objecten werken vaak met en zijn afhankelijk van elkaar. Dus, terwijl het elimineren van afhankelijkheid een nobele achtervolging is, is het bijna onmogelijk om dit te doen. Het beheersen van afhankelijkheden en het verminderen hiervan heeft echter de voorkeur.

De voorwaarden, zware-koppeling en losse koppeling, meestal verwijzen naar hoeveel een object afhankelijk is van andere objecten.

In een losjes gekoppeld systeem hebben wijzigingen in het ene object een verminderd effect op de andere objecten die ervan afhankelijk zijn. In dergelijke systemen zijn klassen afhankelijk van interfaces in plaats van concrete implementaties (we zullen daar later meer over spreken). Dit is de reden waarom losjes gekoppelde systemen meer openstaan ​​voor wijzigingen.

Koppelen in een veld

Laten we een voorbeeld bekijken:

class private $ calculator weergeven; function __construct () $ this-> calculator = new Calculator (1,2); 

Het is gebruikelijk om dit type code te zien. Een klas, tonen in dit geval is afhankelijk van de Rekenmachine klasse door direct naar die klasse te verwijzen. In de bovenstaande code, tonen's $ calculator veld is van het type Rekenmachine. Het object dat het veld bevat, is het resultaat van rechtstreeks bellen Rekenmachine's constructeur.

Koppeling door toegang te krijgen tot de andere klassenmethoden

Bekijk de volgende code voor een demonstratie van dit soort koppeling:

class private $ calculator weergeven; function __construct () $ this-> calculator = new Calculator (1, 2);  function printSum () echo $ this-> calculator-> add (); 

De tonen klasse noemt het Rekenmachine voorwerpen toevoegen() methode. Dit is een andere vorm van koppeling, omdat de ene klasse toegang heeft tot de andere methode.

Koppeling door methodeferentie

Je kunt klassen ook koppelen aan methodeverwijzingen. Bijvoorbeeld:

 class private $ calculator weergeven; function __construct () $ this-> calculator = $ this-> makeCalculator ();  function printSum () echo $ this-> calculator-> add ();  function makeCalculator () retourneer nieuwe rekenmachine (1, 2); 

Het is belangrijk op te merken dat de makeCalculator () methode geeft a terug Rekenmachine voorwerp. Dit is een afhankelijkheid.

Koppeling door polymorfisme

Overerving is waarschijnlijk de sterkste vorm van afhankelijkheid:

class AdvancedCalculator breidt Calculator function sinus ($ value) return sin ($ value); 

Dat kan niet alleen AdvancedCalculator zijn werk niet zonder doen Rekenmachine, maar het zou zelfs niet kunnen bestaan ​​zonder het.

Koppeling verminderen door afhankelijkheid Injectie

Men kan de koppeling verminderen door een afhankelijkheid in te spuiten. Hier is een voorbeeld van:

class private $ calculator weergeven; function __construct (Calculator $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  // ... //

Door de Rekenmachine object door tonen's constructor, we hebben het gereduceerd tonenafhankelijk van de Rekenmachine klasse. Maar dit is slechts de helft van de oplossing.

Koppeling met interfaces verminderen

We kunnen de koppeling verder verminderen door interfaces te gebruiken. Bijvoorbeeld:

interface CanCompute function add (); functie aftrekken ();  klassencalculator implementeert CanCompute private $ firstNumber; privé $ secondNumber; function __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  functie add () return $ this-> firstNumber + $ this-> secondNumber;  functie aftrekken () return $ this-> firstNumber - $ this-> secondNumber;  class Geef private $ calculator; function __construct (CanCompute $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  function printSum () echo $ this-> calculator-> add ();  function makeCalculator () retourneer nieuwe rekenmachine (1, 2); 

U kunt ISP beschouwen als een cohesieprincipe van een hoger niveau.

Deze code introduceert de CanCompute interface. Een interface is zo abstract als je kunt krijgen in OOP; het definieert de leden die een klasse moet implementeren. In het geval van het bovenstaande voorbeeld, Rekenmachine implementeert de CanCompute interface.

tonenDe constructor verwacht een object dat werkt CanCompute. Op dit punt, tonenafhankelijkheid met Rekenmachine is effectief verbroken. Op elk moment kunnen we een andere klasse maken die wordt geïmplementeerd CanCompute en geef een object van die klasse door aan tonen's constructeur. tonen hangt nu alleen af ​​van de CanCompute interface, maar zelfs die afhankelijkheid is optioneel. Als we geen argumenten doorgeven tonen's constructor, het zal gewoon een klassieker creëren Rekenmachine object door te bellen makeCalculator (). Deze techniek wordt vaak gebruikt en is zeer nuttig voor testgestuurde ontwikkeling (TDD).


De SOLID-principes

SOLID is een set principes voor het schrijven van schone code, die het vervolgens gemakkelijker maakt om te veranderen, te onderhouden en uit te breiden in de toekomst. Het zijn aanbevelingen die, wanneer toegepast op broncode, een positief effect hebben op de onderhoudbaarheid.

Een beetje geschiedenis

De SOLID-principes, ook wel bekend als Agile-principes, werden aanvankelijk gedefinieerd door Robert C. Martin. Hoewel hij al deze principes niet uitvond, was hij degene die ze samenbracht. Je kunt er meer over lezen in zijn boek: Agile Software Development, Principles, Patterns and Practices. De principes van SOLID bestrijken een breed scala aan onderwerpen, maar ik zal ze presenteren op een eenvoudige manier als ik in staat ben. Aarzel niet om extra informatie in de opmerkingen te vragen, indien nodig.

Single Responsibility Principle (SRP)

Een klas heeft één verantwoordelijkheid. Dit klinkt misschien eenvoudig, maar het kan soms moeilijk te begrijpen en in praktijk te brengen zijn.

class Reporter functie generateIncomeReports (); function generatePaymentsReports (); function computeBalance (); functie printReport (); 

Wie denk je te profiteren van het gedrag van deze klasse? Welnu, een boekhoudafdeling is een optie (voor het saldo), de financiële afdeling kan een andere zijn (voor inkomsten- / betalingsrapporten), en zelfs de archiveringsafdeling kan de rapporten afdrukken en archiveren.

Er zijn vier redenen waarom u deze klasse zou moeten veranderen; elke afdeling wil mogelijk hun respectieve methodes aangepast aan hun behoeften.

De SRP beveelt aan om dergelijke klassen te splitsen in kleinere, beahvior-specifieke klassen, die elk maar één reden hebben om te veranderen. Dergelijke klassen hebben de neiging om sterk samenhangend te zijn en losjes gekoppeld. In zekere zin is SRP cohesie gedefinieerd vanuit het gezichtspunt van de gebruikers.

Open-gesloten principe (OCP)

Klassen (en modules) zouden de uitbreiding van hun functionaliteit moeten verwelkomen, evenals bestand zijn tegen wijzigingen van hun huidige functionaliteit. Laten we spelen met het klassieke voorbeeld van een elektrische ventilator. Je hebt een schakelaar en je wilt de ventilator besturen. Dus je zou iets kunnen schrijven in de trant van:

class Switch_ private $ fan; function __construct () $ this-> fan = new Fan ();  functie turnOn () $ this-> fan-> aan ();  functie turnOff () $ this-> fan-> off (); 

Overerving is waarschijnlijk de sterkste vorm van afhankelijkheid.

Deze code definieert een Schakelaar_ klasse die een a creëert en beheert Ventilator voorwerp. Let op het onderstrepingsteken na "Switch_". PHP staat niet toe dat je een klasse definieert met de naam "Switch".

Je baas besluit dat hij het licht met dezelfde schakelaar wil besturen. Dit is een probleem, omdat jij moeten veranderen Schakelaar_.

Alle wijzigingen in bestaande code vormen een risico; andere delen van het systeem kunnen worden beïnvloed en vereisen nog verdere aanpassingen. Het heeft altijd de voorkeur om bestaande functionaliteit alleen te laten, wanneer nieuwe functies worden toegevoegd.

In de OOP-terminologie, kun je dat zien Schakelaar_ heeft een sterke afhankelijkheid van Ventilator. Dit is waar ons probleem ligt en waar we onze veranderingen moeten aanbrengen.

interface Schakelbare functie aan (); function off ();  class Fan implementeert omschakelbaar public function on () // code om de fan te starten public function off () // code om de ventilator te stoppen class Switch_ privé $ schakelbaar; function __construct (schakelbaar $ schakelbaar) $ this-> schakelbaar = $ schakelbaar;  functie turnOn () $ this-> schakelbaar-> aan ();  functie turnOff () $ this-> schakelbaar-> off (); 

Deze oplossing introduceert de schakelbare interface. Het definieert de methoden die alle objecten met schakeloptie moeten implementeren. De Ventilator gereedschap schakelbare, en Schakelaar_ accepteert een verwijzing naar a schakelbare object binnen zijn constructor.

Hoe helpt dit ons??

Ten eerste breekt deze oplossing de afhankelijkheid tussen Schakelaar_ en Ventilator. Schakelaar_ heeft geen idee dat het een fan begint en ook niet om. Ten tweede, de introductie van een Licht klasse heeft geen invloed op Schakelaar_ of schakelbare. Wilt u controle over een Licht object met jouw Schakelaar_ klasse? Maak eenvoudig een Licht object en geef het door aan Schakelaar_, zoals dit:

klasse Lichtwerktuigen Schakelbaar openbare functie aan () // code om licht in te schakelen openbare functie uit () // code om licht uit te schakelen class SomeWhereInYourCode function controlLight () $ light = new Light (); $ switch = new Switch _ ($ light); $ Switch-> ZetAan (); $ Switch-> afslag (); 

Liskov Substitution Principle (LSP)

LSP stelt dat een onderliggende klasse nooit de functionaliteit van de bovenliggende klasse mag doorbreken. Dit is extreem belangrijk omdat consumenten van een ouderklasse verwachten dat de klas zich op een bepaalde manier gedraagt. Een kinderklasse doorgeven aan een consument moet gewoon werken en geen invloed hebben op de oorspronkelijke functionaliteit.

Dit is op het eerste gezicht verwarrend, dus laten we eens naar een ander klassiek voorbeeld kijken:

class Rectangle private $ width; privé $ hoogte; functie setWidth ($ width) $ this-> width = $ width;  function setHeigth ($ hoogte) $ this-> height = $ hoogte;  functiegebied () return $ this-> width * $ this-> height; 

Dit voorbeeld definieert een eenvoudig Rechthoek klasse. We kunnen de hoogte en breedte en de hoogte ervan instellen Gebied() methode biedt het gebied van de rechthoek. De ... gebruiken Rechthoek klas kan er als volgt uitzien:

klasse Geometry function rectArea (Rectangle $ rectangle) $ rectangle-> setWidth (10); $ Rectangle-> setHeigth (5); return $ rectangle-> area (); 

De rectArea () methode accepteert a Rechthoek object als argument, stelt de hoogte en breedte in en retourneert het gebied van de vorm.

Op school hebben we geleerd dat vierkantjes rechthoeken zijn. Dit geeft aan dat als we ons programma modelleren naar ons geometrische object, een Plein klasse moet uitbreiden a Rechthoek klasse. Hoe zou zo'n klas eruit zien?

class Square verlengt Rectangle // Welke code om hier te schrijven? 

Ik heb moeite om uit te zoeken wat ik moet schrijven in de Plein klasse. We hebben verschillende opties. We kunnen de Gebied() methode en retourneer het kwadraat van $ breedte:

klasse Rectangle protected $ width; beschermde $ hoogte; // ... // class Square breidt Rectangle uit function area () return $ this-> width ^ 2; 

Merk op dat ik ben veranderd Rechthoek's velden naar beschermde, geven Plein toegang tot die velden. Dit lijkt redelijk vanuit een geometrisch gezichtspunt. Een vierkant heeft gelijke zijden; het kwadraat van de breedte retourneren is redelijk.

We hebben echter een probleem vanuit een programmeerperspectief. Als Plein is een Rechthoek, we zouden er geen probleem mee moeten hebben om het in de Geometrie klasse. Maar door dat te doen, kun je dat zien GeometrieDe code heeft niet veel zin; het stelt twee verschillende waarden in voor hoogte en breedte. Dit is de reden waarom een ​​vierkant is niet een rechthoek in programmeren. LSP geschonden.

Interface Segregation Principle (ISP)

Unit tests moeten snel lopen - erg snel.

Dit principe concentreert zich op het doorbreken van grote interfaces in kleine, gespecialiseerde interfaces. Het basisidee is dat verschillende consumenten van dezelfde klasse niet over de verschillende interfaces moeten weten - alleen de interfaces die de consument nodig heeft. Zelfs als een consument niet direct alle openbare methoden op een object gebruikt, hangt het nog steeds van alle methoden af. Dus waarom geen interfaces bieden die alleen de methoden verklaren die elke gebruiker nodig heeft?

Dit is in overeenstemming dat interfaces moeten behoren tot de clients en niet tot de implementatie. Als u uw interfaces afstemt op de consumerende klassen, respecteren zij ISP. De implementatie zelf kan uniek zijn, omdat een klasse meerdere interfaces kan implementeren.

Laten we ons voorstellen dat we een beursapplicatie implementeren. We hebben een makelaar die aandelen koopt en verkoopt en kan de dagelijkse inkomsten en verliezen rapporteren. Een zeer eenvoudige implementatie zou iets als een bevatten Makelaar interface, a NYSEBroker klasse die implementeert Makelaar en een paar klassen van gebruikersinterfaces: een voor het maken van transacties (TransactionsUI) en één voor rapportage (DailyReporter). De code voor een dergelijk systeem kan vergelijkbaar zijn met het volgende:

interface Broker functie kopen ($ -symbool, $ volume); functie verkopen ($ symbool, $ volume); function dailyLoss ($ date); function dailyEarnings ($ date);  class NYSEBroker implementeert Broker openbare functie kopen ($ -symbool, $ volume) // implementatie gaat hier public function currentBalance () // implementatie gaat hier public function dailyEarnings ($ date) // implementaties gaan hier public function dailyLoss ($ date) // implementsation goes here public function sell ($ symbol, $ volume) // implementsation goes here class TransactionsUI private $ broker; function __construct (Broker $ broker) $ this-> broker = $ broker;  function buyStocks () // UI-logica hier om informatie uit een formulier te verkrijgen in $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']);  function sellStocks () // UI-logica hier om informatie uit een formulier te verkrijgen in $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  class DailyReporter private $ broker; function __construct (Broker $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Huidig ​​balace voor vandaag'. datum Tijd()) . "\ N"; echo 'Earnings:'. $ this-> broker-> dailyEarnings (time ()). "\ N"; echo 'Verliezen:'. $ this-> broker-> dailyLoss (time ()). "\ N"; 

Hoewel deze code mogelijk werkt, is deze in strijd met de ISP. Beide DailyReporter en TransactionUI afhankelijk van de Makelaar interface. Ze gebruiken echter slechts een fractie van de interface. TransactionUI gebruikt de kopen() en verkopen() methoden, terwijl DailyReporter gebruikt de dailyEarnings () en dailyLoss () methoden.

Je zou dat kunnen betogen Makelaar is niet samenhangend omdat het methoden heeft die geen verband houden en dus niet bij elkaar horen.

Dit kan waar zijn, maar het antwoord hangt af van de implementaties van Makelaar; verkopen en kopen kan sterk gerelateerd zijn aan de huidige verliezen en inkomsten. U mag bijvoorbeeld geen aandelen kopen als u geld verliest.

Je zou dat ook kunnen betogen Makelaar is ook in strijd met SRP. Omdat we twee klassen hebben die het op verschillende manieren gebruiken, kunnen er twee verschillende gebruikers zijn. Nou, ik zeg nee. De enige gebruiker is waarschijnlijk de eigenlijke makelaar. Hij / zij wil hun huidige geld kopen, verkopen en bekijken. Maar nogmaals, het eigenlijke antwoord hangt af van het hele systeem en bedrijf.

ISP is zeker geschonden. Beide gebruikersinterfaceklassen zijn afhankelijk van het geheel Makelaar. Dit is een veel voorkomend probleem, als u denkt dat interfaces bij hun implementaties horen. Het verschuiven van uw gezichtspunt kan echter het volgende ontwerp suggereren:

interface BrokerTransactions functie kopen ($ -symbool, $ volume); functie verkopen ($ symbool, $ volume);  interface BrokerStatistics function dailyLoss ($ date); function dailyEarnings ($ date);  klasse NYSEBroker implementeert BrokerTransactions, BrokerStatistics openbare functie kopen ($ -symbool, $ volume) // implementatie gaat hier public function currentBalance () // implementatie gaat hier public function dailyEarnings ($ date) // implementaties gaan hier naartoe  public function dailyLoss ($ date) // implementaties gaan hier public function sell ($ symbol, $ volume) // implementaties gaan hier class TransactionsUI private $ broker; function __construct (BrokerTransactions $ broker) $ this-> broker = $ broker;  function buyStocks () // UI-logica hier om informatie uit een formulier te verkrijgen in $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volume']);  function sellStocks () // UI-logica hier om informatie uit een formulier te verkrijgen in $ data $ this-> broker-> sell ($ data ['sybmol'], $ data ['volume']);  class DailyReporter private $ broker; function __construct (BrokerStatistics $ broker) $ this-> broker = $ broker;  function currentBalance () echo 'Huidig ​​balace voor vandaag'. datum Tijd()) . "\ N"; echo 'Earnings:'. $ this-> broker-> dailyEarnings (time ()). "\ N"; echo 'Verliezen:'. $ this-> broker-> dailyLoss (time ()). "\ N"; 

Dit is eigenlijk logisch en respecteert de ISP. DailyReporter hangt alleen af ​​van BrokerStatistics; het maakt niet uit en hoeft niets te weten over verkoop- en inkoopactiviteiten. TransactionsUI, aan de andere kant, weet alleen over kopen en verkopen. De NYSEBroker is identiek aan onze vorige klasse, behalve dat het nu de BrokerTransactions en BrokerStatistics interfaces.

U kunt ISP beschouwen als een cohesieprincipe van een hoger niveau.

Wanneer beide UI-klassen afhankelijk waren van de Makelaar interface, ze waren vergelijkbaar met twee klassen, elk met vier velden, waarvan er twee werden gebruikt in een methode en de andere twee in een andere methode. De klas zou niet erg samenhangend zijn geweest.

Een meer gecompliceerd voorbeeld van dit principe is te vinden in een van de eerste artikelen van Robert C. Martin over dit onderwerp: The Interface Segregation Principle.

Dependency Inversion Principle (DIP)

Volgens dit principe moeten modules op hoog niveau niet afhankelijk zijn van modules op een laag niveau; beide moeten afhangen van abstracties. Abstracties mogen niet afhankelijk zijn van details; details moeten afhangen van abstracties. Simpel gezegd, u moet zoveel mogelijk afhangen van abstracties en nooit van concrete implementaties.

De truc met DIP is dat je de afhankelijkheid wilt omkeren, maar altijd de controle wilt behouden. Laten we ons voorbeeld van de OCP (de Schakelaar en Licht klassen). In de oorspronkelijke implementatie hadden we een schakelaar die rechtstreeks een licht bestuurt.

Zoals je ziet, vloeien zowel afhankelijkheid als controle af Schakelaar in de richting van Licht. Hoewel dit is wat we willen, willen we niet direct afhankelijk zijn van Licht. Dus hebben we een interface geïntroduceerd.

Het is verbazingwekkend hoe eenvoudigweg de introductie van een interface ervoor zorgt dat onze code zowel DIP als OCP respecteert. Zoals je kunt zien, hangt de klasse af van de concrete implementatie van Licht, en beide Licht en Schakelaar afhankelijk van de schakelbare interface. We keerden de afhankelijkheid om en de controlestroom was onveranderd.


Hoogwaardig ontwerp

Een ander belangrijk aspect van uw code is uw ontwerp op hoog niveau en algemene architectuur. Een verstrengelde architectuur produceert code die moeilijk te wijzigen is. Een schone architectuur behouden is essentieel en de eerste stap is het begrijpen hoe de verschillende zorgen van uw code kunnen worden gescheiden.

In deze afbeelding probeerde ik de belangrijkste zorgen samen te vatten. Centraal in het schema staat onze bedrijfslogica. Het moet goed geïsoleerd zijn van de rest van de wereld en in staat zijn om te werken en zich te gedragen zoals verwacht zonder het bestaan ​​van een van de andere delen. Zie het als orthogonaliteit op een hoger niveau.

Vanaf de rechterkant heb je je "hoofd" - het beginpunt voor de toepassing - en de fabrieken die objecten maken. Een ideale oplossing zou zijn objecten van gespecialiseerde fabrieken halen, maar dat is meestal onmogelijk of onpraktisch. Toch moet u fabrieken gebruiken wanneer u daartoe in de gelegenheid bent en deze buiten uw bedrijfslogica houden.

Onderaan (oranje) hebben we persistentie (databases, toegang tot bestanden, netwerkcommunicatie) met het doel om informatie te bewaren. Geen enkel object in onze bedrijfslogica zou moeten weten hoe persistentie werkt.

Aan de linkerkant is het leveringsmechanisme.

Een MVC, zoals Laravel of CakePHP, zou alleen het leveringsmechanisme moeten zijn, niets meer.

Hiermee kunt u het ene mechanisme met het andere verwisselen zonder uw bedrijfslogica aan te raken. Dit kan voor sommigen van jullie misschien schandalig klinken. Er wordt ons verteld dat onze bedrijfslogica in onze modellen moet worden geplaatst. Nou, ik ben het daar niet mee eens. Onze modellen zouden "request models" moeten zijn, d.w.z. domme data-objecten die worden gebruikt voor het doorgeven van informatie van MVC aan de bedrijfslogica. Optioneel zie ik geen probleem inclusief invoervalidatie in de modellen, maar niets meer. Bedrijfslogica hoort niet in de modellen te zitten.

Wanneer u de architectuur of directorystructuur van uw toepassing bekijkt, zou u een structuur moeten zien die suggereert wat het programma doet in tegenstelling tot welk framework of welke database u gebruikte.

Zorg ten slotte dat alle afhankelijkheden naar onze bedrijfslogica wijzen. Gebruikersinterfaces, fabrieken, databases zijn zeer concrete implementaties en u moet er nooit van afhankelijk zijn. Het omkeren van de afhankelijkheden om naar onze bedrijfslogica te wijzen, modulariseert ons systeem, waardoor we de afhankelijkheden kunnen veranderen zonder de bedrijfslogica te wijzigen.


Sommige gedachten over ontwerppatronen

Ontwerppatronen spelen een belangrijke rol bij het eenvoudiger te wijzigen van code door een gemeenschappelijke ontwerpoplossing aan te bieden die elke programmeur kan begrijpen. Vanuit structureel oogpunt zijn ontwerppatronen duidelijk voordelig. Het zijn goed geteste en doordachte oplossingen.

Als je meer wilt weten over ontwerppatronen, heb ik een cursus Tuts + Premium voor ze gemaakt!


De kracht van testen

Test-driven ontwikkeling stimuleert het schrijven van code die eenvoudig te testen is. TDD dwingt je om de meeste van de bovenstaande principes te respecteren om je code eenvoudig te kunnen testen. Het injecteren van afhankelijkheden en het schrijven van orthogonale klassen is essentieel; anders krijg je uiteindelijk enorme testmethoden. Unit tests moeten snel lopen - heel snel, eigenlijk, en alles wat niet getest is moet bespot worden. Het bespotten van veel complexe klassen voor een eenvoudige test kan overweldigend zijn. Dus als je merkt dat je tien objecten bespot om een ​​enkele methode in een klas te testen, heb je mogelijk een probleem met je code ... niet je test.


Laatste gedachten

Aan het eind van de dag komt het allemaal neer op hoeveel u geeft om uw broncode. Technische kennis hebben is niet genoeg; je moet die kennis keer op keer toepassen, nooit 100% tevreden met je code. U moet uw code gemakkelijk kunnen onderhouden, opruimen en openen om te veranderen.

Bedankt voor het lezen en voel je vrij om je technieken bij te dragen in de opmerkingen hieronder.