Oude code. Lelijke code. Ingewikkelde code. Spaghetti-code. Gibberistische onzin. In twee woorden, Oude code. Dit is een serie die u zal helpen om ermee te werken en ermee om te gaan.
Het is nu tijd om over architectuur te praten en hoe we onze nieuw gevonden codelagen organiseren. Het is tijd om onze aanvraag in te dienen en het in kaart te brengen in theoretisch architectonisch ontwerp.
Dit hebben we gezien in onze artikelen en tutorials. Schone architectuur.
Op een hoog niveau lijkt het op het schema hierboven en ik ben er zeker van dat je er al bekend mee bent. Het is een voorgestelde architectonische oplossing van Robert C. Martin.
In het centrum van onze architectuur staat onze bedrijfslogica. Dit zijn de klassen die de bedrijfsprocessen vertegenwoordigen die onze toepassing probeert op te lossen. Dit zijn de entiteiten en interacties die het domein van ons probleem vertegenwoordigen.
Vervolgens zijn er verschillende andere typen modules of klassen rond onze bedrijfslogica. Deze kunnen worden gezien als eenvoudige ondersteunende hulpmodules. Ze hebben verschillende doelen en de meeste zijn onmisbaar. Ze bieden de verbinding tussen de gebruiker en onze applicatie via een leveringsmechanisme. In ons geval is dit een opdrachtregelinterface. Er is nog een reeks aanvullende klassen die onze bedrijfslogica verbinden met onze persistentielaag en met alle gegevens in die laag, maar we hebben niet zo'n laag in onze toepassing. Dan zijn er helpende klassen zoals fabrieken en bouwers die nieuwe objecten construeren en leveren aan onze bedrijfslogica. Ten slotte zijn er de klassen die het beginpunt van ons systeem vertegenwoordigen. In ons geval, GameRunner
kan als zo'n klasse worden beschouwd, of al onze tests zijn ook op hun eigen manier toegangspunten.
Wat het meest belangrijk is om op te merken in het diagram, is de afhankelijkheidsrichting. Alle hulpklassen zijn afhankelijk van de bedrijfslogica. De bedrijfslogica is niet afhankelijk van iets anders. Als alle objecten in onze bedrijfslogica op magische wijze zouden kunnen verschijnen, met alle gegevens erin, en we zouden kunnen zien wat er direct in onze computer gebeurt, zouden ze moeten kunnen functioneren. Onze bedrijfslogica moet kunnen werken zonder een gebruikersinterface of zonder een persistentielaag. Onze bedrijfslogica moet geïsoleerd zijn, in een luchtbel van een logisch universum.
A. Modulen op hoog niveau mogen niet afhankelijk zijn van modules op een laag niveau. Beide moeten afhangen van abstracties.
B. Abstracties mogen niet afhankelijk zijn van details. Details moeten afhangen van abstracties.
Dit is het, het laatste SOLID-principe en waarschijnlijk degene met het grootste effect op uw code. Het is vrij eenvoudig te begrijpen en vrij eenvoudig te implementeren.
In eenvoudige bewoordingen staat dat concrete dingen altijd afhankelijk moeten zijn van abstracte dingen. Uw database is heel concreet, dus het moet afhangen van iets meer abstracts. Je gebruikersinterface is heel concreet, dus het moet afhangen van iets meer abstracts. Je fabrieken zijn weer heel concreet. Maar hoe zit het met uw bedrijfslogica. In je bedrijfslogica moet je doorgaan met het toepassen van deze ideeën, zodat de klassen die dichter bij de grenzen staan, afhankelijk zijn van klassen die abstracter zijn, meer centraal in je bedrijfslogica..
Een pure bedrijfslogica, die op een abstracte manier de processen en het gedrag van een gedefinieerd domein of bedrijfsmodel weergeeft. Zo'n bedrijfslogica bevat geen specifieke zaken (concrete dingen) zoals waarden, geld, accountnamen, wachtwoorden, de grootte van een knop of het aantal velden in een formulier. De bedrijfslogica moet niet om concrete dingen gaan. Het zou alleen maar om uw bedrijfsprocessen moeten gaan.
Het principe van afhankelijkheid van inversie (DIP) zegt dat we onze afhankelijkheden moeten omkeren wanneer er code is die afhankelijk is van iets concreets. Op dit moment ziet onze afhankelijkheidsstructuur er als volgt uit.
GameRunner
, met behulp van de functies in RunnerFunctions.php
is het maken van een Spel
klasse en gebruikt het dan. Aan de andere kant, onze Spel
klasse, die onze bedrijfslogica representeert, creëert en gebruikt a tonen
voorwerp.
De hardloper is dus afhankelijk van onze bedrijfslogica. Dat is juist. Aan de andere kant, onze Spel
hangt af van tonen
, wat niet goed is. Onze bedrijfslogica mag nooit afhankelijk zijn van onze presentatie.
De eenvoudigste technische truc die we kunnen doen is gebruik te maken van de abstracte constructies in onze programmeertaal. Een traditionele klas is concreter dan een abstracte klasse, die concreter is dan een interface.
Een Abstracte klasse is een speciaal type dat niet kan worden geïnitialiseerd. Het bevat alleen definities en gedeeltelijke implementaties. Een abstracte basisklasse heeft meestal meerdere kinderlessen. Deze onderliggende klassen nemen de gemeenschappelijke gedeeltelijke functionaliteit van de abstracte ouder over, ze voegen hun eigen uitgebreide gedrag toe en ze moeten alle methoden implementeren die zijn gedefinieerd in de abstracte bovenliggend maar niet geïmplementeerd.
Een Interface is een speciaal type dat alleen de definitie van methoden en variabelen toestaat. Het is het meest abstracte construct in objectgeoriënteerd programmeren. Elke implementatie moet altijd alle methoden van de bovenliggende interface implementeren. Een concrete klasse kan verschillende interfaces implementeren.
Met uitzondering van de objectgerichte talen van de C-familie, laten de anderen zoals Java of PHP meerdere overerving niet toe. Dus een concrete klasse kan een enkele abstracte klasse uitbreiden, maar het kan verschillende interfaces implementeren, zelfs op hetzelfde moment als dat nodig is. Of vanuit een ander perspectief gezien, een enkele abstracte klasse kan vele implementaties hebben, terwijl veel interfaces vele implementaties kunnen hebben.
Lees de tutorial over dit SOLID-principe voor een meer complete uitleg van de DIP.
PHP ondersteunt volledig interfaces. Beginnend bij de tonen
Klasse als ons model, kunnen we een interface definiëren met de openbare methoden die alle klassen die verantwoordelijk zijn voor het weergeven van gegevens zullen moeten implementeren.
Kijken naar tonen
's lijst van methoden, zijn er 12 openbare methoden, inclusief de constructor. Dit is een vrij grote interface, je moet dit aantal zo laag mogelijk houden, interfaces blootleggen als clients ze nodig hebben. Het Interface Segregation Principle heeft hier enkele goede ideeën over. Misschien proberen we dit probleem in een toekomstige tutorial op te lossen.
Wat we nu willen bereiken, is een architectuur zoals hieronder.
Op deze manier, in plaats van Spel
afhankelijk van het meer concrete tonen
, ze zijn allebei afhankelijk van de zeer abstracte interface. Spel
maakt gebruik van de interface, terwijl tonen
implementeert het.
Phil Karlton zei: "Er zijn maar twee moeilijke dingen in de informatica: cache-invalidatie en dingen benoemen."
Hoewel we ons niet druk maken om caches, moeten we onze klassen, variabelen en methoden een naam geven. Het benoemen van interfaces kan een hele uitdaging zijn.
In de oude dagen van de Hongaarse notatie hadden we het op deze manier gedaan.
Voor dit diagram hebben we de klassen / bestandsnamen en het werkelijke hoofdlettergebruik gebruikt. De interface wordt "IDisplay" genoemd met een hoofdletter "I" voor "Display". Er waren eigenlijk programmeertalen die een dergelijke naamgeving voor interfaces nodig hadden. Ik weet zeker dat er een paar lezers zijn die ze nog steeds gebruiken en nu glimlachen.
Het probleem met dit naamgevingsschema is de misplaatste zorg. Interfaces behoren tot hun klanten. Onze interface is van Spel
. Dus Spel
moet niet weten dat het een interface of een echt object gebruikt. Spel
moet zich geen zorgen maken over de implementatie die het daadwerkelijk krijgt. Van Spel
's gezichtspunt, het gebruikt gewoon een "Display", dat is alles.
Dit lost de Spel
naar tonen
naamgevingsprobleem. Het gebruik van het achtervoegsel "Impl" voor de implementatie is iets beter. Het helpt de zorg wegnemen Spel
.
Het is ook veel effectiever voor ons. Denken aan Spel
zoals het er nu uitziet. Het gebruikt een tonen
object en weet hoe het te gebruiken. Als we onze interface "Display" een naam geven, zullen we het aantal benodigde wijzigingen verminderen Spel
.
Maar toch, deze naamgeving is slechts marginaal beter dan de vorige. Het staat slechts één implementatie toe voor tonen
en de naam van de implementatie zal ons niet vertellen op wat voor soort display we het hebben.
Nu is dat aanzienlijk beter. Onze implementatie kreeg de naam "CLID-weergave", omdat deze wordt uitgevoerd naar de CLI. Als we een HTML-uitvoer of een gebruikersinterface voor Windows Desktop willen, kunnen we dat eenvoudig toevoegen aan onze architectuur.
Omdat we twee soorten tests hebben, de langzame gouden meester en de snelle testeenheden, willen we zo veel mogelijk vertrouwen op unit-tests, en op gouden meester zo min mogelijk. Dus laten we onze goldenmaster-tests markeren als overgeslagen en proberen te vertrouwen op onze unit-tests. Ze zijn nu aan het passeren en we willen een verandering aanbrengen waardoor ze blijven passeren. Maar hoe kunnen we zoiets doen, zonder alle hierboven voorgestelde veranderingen te doen?
Is er een manier van testen die ons in staat stelt een kleinere stap te zetten??
Er is zo'n manier. Tijdens het testen is er een concept genaamd "Mocking".
Wikipedia definieert Mocking als zodanig: "Bij objectgeoriënteerd programmeren zijn mock-objecten gesimuleerde objecten die op een gecontroleerde manier het gedrag van echte objecten nabootsen."
Een dergelijk object zou ons enorm helpen. We hebben zelfs niet eens iets nodig dat zo complex is als het simuleren van al het gedrag. Alles wat we nodig hebben is een nep, stom object waarnaar we kunnen sturen Spel
in plaats van de echte weergavelogica.
Laten we een interface maken met de naam tonen
met alle openbare methoden van de huidige concrete klasse.
Zoals je kunt zien, het oude Display.php
is hernoemd naar DisplayOld.php
. Dit is slechts een tijdelijke stap, die ons in staat stelt om het uit de weg te ruimen en ons op de interface te concentreren.
interface Display
Dat is alles wat er is om een interface te maken. Je kunt zien dat het wordt gedefinieerd als "interface" en niet als een "klasse". Laten we de methoden toevoegen.
interface Display function statusAfterRoll ($ rolledNumber, $ currentPlayer); function playerSentToPenaltyBox ($ currentPlayer); function playerStaysInPenaltyBox ($ currentPlayer); functiestatusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory); functiestatusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory); function playerAdded ($ playerName, $ numberOfPlayers); functie askQuestion ($ currentCategory); function correctAnswer (); function correctAnswerWithTypo (); function incorrectAnswer (); function playerCoins ($ currentPlayer, $ playerCoins);
Ja. Een interface is slechts een stel functieverklaringen. Stel je het voor als een C-headerbestand. Geen implementaties, alleen verklaringen. Het kan helemaal geen implementatie bevatten. Als u een van de methoden probeert te implementeren, resulteert dit in een fout.
Maar deze zeer abstracte definities laten ons iets wonderbaarlijks toe. Onze Spel
klasse hangt nu van hen af, in plaats van een concrete implementatie. Als we onze tests proberen uit te voeren, zullen ze echter falen.
Fatale fout: weergave van de interface van de interface is niet mogelijk
Dat is omdat Spel
probeert op eigen gelegenheid een nieuw scherm te maken op regel 25 in de constructor.
We weten dat we dat niet kunnen doen. Een interface of een abstracte klasse kan niet worden geïnstantieerd. We hebben een echt object nodig.
We hebben een dummy-object nodig dat in onze tests kan worden gebruikt. Een eenvoudige klasse, die alle methoden van de tonen
interface, maar niets doen. Laten we het direct in onze unit-test schrijven. Als uw programmeertaal niet meerdere klassen in hetzelfde bestand toestaat, kunt u een nieuw bestand voor uw dummyklasse maken.
class DummyDisplay implementeert Display function statusAfterRoll ($ rolledNumber, $ currentPlayer) // TODO: Implementation statusAfterRoll () -methode. function playerSentToPenaltyBox ($ currentPlayer) // TODO: Implementeer playerSentToPenaltyBox () -methode. function playerStaysInPenaltyBox ($ currentPlayer) // TODO: Implementeer de playerStaysInPenaltyBox () -methode. function statusAfterNonPenalizedPlayerMove ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementation statusAfterNonPenalizedPlayerMove () methode. functiestatusAfterPlayerGettingOutOfPenaltyBox ($ currentPlayer, $ currentPlace, $ currentCategory) // TODO: Implementation statusAfterPlayerGettingOutOfPenaltyBox () -methode. function playerAdded ($ playerName, $ numberOfPlayers) // TODO: Implementeer playerAdded () -methode. function askQuestion ($ currentCategory) // TODO: implementeer askQuestion () methode. function correctAnswer () // TODO: correctAnswer () -methode implementeren. function correctAnswerWithTypo () // TODO: De methode correctAnswerWithTypo () implementeren. function incorrectAnswer () // TODO: Implement incorrect & Antwoord () methode. function playerCoins ($ currentPlayer, $ playerCoins) // TODO: Implementeer playerCoins () methode.
Zodra u zegt dat uw klas een interface implementeert, kunt u met de IDE automatisch de ontbrekende methoden invullen. Dit maakt het maken van dergelijke objecten erg snel, in slechts een paar seconden.
Laten we het nu gebruiken Spel
door het te initialiseren in zijn constructor.
function __construct () $ this-> players = array (); $ this-> places = array (0); $ this-> portemonnees = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = nieuwe DummyDisplay ();
Dit zorgt ervoor dat de test slaagt, maar introduceert een enorm probleem. Spel
moet weten over zijn test. We willen dit echt niet. Een test is gewoon een nieuw instappunt. De DummyDisplay
is gewoon een andere gebruikersinterface. Onze bedrijfslogica, de Spel
klasse, mag niet afhankelijk zijn van de gebruikersinterface. Laten we het dus alleen afhankelijk maken van de interface.
function __construct (Display $ display) $ dit-> spelers = array (); $ this-> places = array (0); $ this-> portemonnees = array (0); $ this-> inPenaltyBox = array (0); $ this-> display = $ display;
Maar om te testen Spel
, we moeten de dummy-display van onze tests opsturen.
functie setUp () $ this-> game = new Game (nieuwe DummyDisplay ());
Dat is het. We moesten één enkele lijn wijzigen in onze unit tests. In de setup zullen we als parameter een nieuw exemplaar van verzenden DummyDisplay
. Dat is een injectie met afhankelijkheid. Het gebruik van interfaces en afhankelijkheidsinjectie helpt vooral als u in een team werkt. Bij Syneto zagen we dat het specificeren van een interfacetype voor een klasse en het injecteren ervan ons helpt om de intenties van de klantcode veel beter te communiceren. Iedereen die naar de client kijkt, weet welk type object in de parameters wordt gebruikt. En een leuke bonus is dat uw IDE de methoden voor die parameters automatisch aanvult omdat deze hun typen kunnen bepalen.
De gouden meestertest, voert onze code uit zoals in de echte wereld. Om het te laten slagen, moeten we onze oude weergaveklasse transformeren in een echte implementatie van de interface en deze naar onze bedrijfslogica sturen. Hier is een manier om het te doen.
class CLIDisplay implementeert Display // ... //
Hernoem het naar CLIDisplay
en laat het implementeren tonen
.
function run () $ display = new CLIDisplay (); $ aGame = nieuwe game ($ display); $ AGame-> toe te voegen ( "Chet"); $ AGame-> toe te voegen ( "Pat"); $ AGame-> toe te voegen ( "Sue"); doe $ dobbelstenen = rand (0, 5) + 1; $ AGame-> roll ($ dobbelstenen); while (! didSomebodyWin ($ aGame, isCurrentAnswerCorrect ()));
In RunnerFunctions.php
, in de rennen()
functie, maak een nieuwe weergave voor CLI en geef deze door aan Spel
wanneer het is gemaakt.
Maak een commentaar en voer je gouden hoofdtests uit. Ze gaan voorbij.
Deze oplossing leidt effectief tot een architectuur zoals in het onderstaande schema.
Dus nu maakt onze game runner, die het beginpunt is voor onze applicatie, een concreet geheel CLIDisplay
en hangt er dus van af. CLIDisplay
hangt alleen af van de interface die zich op de grens tussen presentatie en bedrijfslogica bevindt. Onze runner hangt ook direct af van de bedrijfslogica. Dit is hoe onze applicatie eruitziet wanneer deze wordt geprojecteerd op de schone architectuur waarmee we dit artikel zijn begonnen.
Bedankt voor het lezen en mis de volgende zelfstudie niet als we het over mocking en klasseninteractie hebben in meer details.