Repository Design Pattern

Repository Design Pattern, definierat av Eric Evens i sin Domain Driven Design-bok, är ett av de mest användbara och mest tillämpliga designmönster som någonsin uppfunnits. Alla ansökningar måste arbeta med uthållighet och med någon typ av objektlista. Dessa kan vara användare, produkter, nätverk, diskar eller vad din ansökan handlar om. Om du till exempel har en blogg måste du hantera listor med blogginlägg och listor med kommentarer. Problemet med att alla dessa listhanteringsloggar har gemensamt är hur man kopplar affärslogik, fabriker och uthållighet.


Fabriksdesignmönstret

Som vi nämnde i den inledande stycket, kommer ett Repository att ansluta Fabriker med Gateways (uthållighet). Dessa är också designmönster och om du inte är bekant med dem kommer den här punkten att ge lite ljus över ämnet.

En fabrik är ett enkelt designmönster som definierar ett bekvämt sätt att skapa objekt. Det är en klass eller uppsättning klasser som är ansvariga för att skapa de objekt som vår affärslogik behöver. En fabrik har traditionellt en metod som heter "göra()" och det kommer att veta hur man tar all information som behövs för att bygga ett objekt och göra föremålet att bygga sig själv och returnera ett färdigt objekt till affärslogiken.

Här är lite mer på Fabriksmönstret i en äldre webbutik + handledning: En nybörjarguide till designmönster. Om du föredrar en djupare vy på fabriksmönstret, kolla in det första mönstret i Agile Design Patterns kursen vi har på Tuts+.


Gateway Mönstret

Också känd som "Table Data Gateway" är ett enkelt mönster som erbjuder kopplingen mellan affärslogiken och databasen själv. Huvudansvaret är att göra frågorna i databasen och ge de hämtade dataen i en datastruktur som är typisk för programmeringsspråket (som en array i PHP). Dessa data filtreras sedan oftast och modifieras i PHP-koden så att vi kan få den information och variabler som behövs för att kasta våra objekt. Denna information måste då skickas till fabrikerna.

Gateway designmönstret förklaras och exemplifieras i ganska stor detalj i en Webuts + handledning om utveckling mot ett persistenslager. Även i samma Agile Design Patterns-kurs handlar det andra designmönstret om detta ämne.


De problem som vi behöver lösa

Duplikering genom datahantering

Det kanske inte är uppenbart vid första anblicken, men att ansluta Gateways to Factories kan leda till mycket dubbelarbete. Alla program med stor storlek behöver skapa samma objekt från olika ställen. På varje ställe måste du använda Gateway för att hämta en uppsättning rådata, filtrera och arbeta den data för att vara redo att skickas till fabrikerna. Från alla dessa ställen kommer du att kalla samma fabriker med samma datastrukturer men uppenbarligen med olika data. Dina objekt kommer att skapas och tillhandahållas av fabrikerna. Detta kommer oundvikligen leda till mycket dubbelarbete i tid. Och dubbelarbete kommer att spridas över avlägsna klasser eller moduler och kommer att vara svår att märka och fixa.

Duplikering genom dataåtervinning Logisk omfördelning

Ett annat problem vi har är hur man uttrycker de frågor vi behöver göra med hjälp av Gateways. Varje gång vi behöver lite information från Gateway måste vi tänka på vad behöver vi exakt? Behöver vi alla uppgifter om ett enda ämne? Behöver vi bara viss specifik information? Vill vi hämta en viss grupp från databasen och göra sortering eller raffinerad filtrering i vårt programmeringsspråk? Alla dessa frågor måste åtgärdas varje gång vi hämtar information från persistensskiktet genom vår Gateway. Varje gång vi gör det måste vi komma fram till en lösning. I takt med att vår ansökan växer kommer vi att konfronteras med samma dilemma på olika ställen i vår ansökan. Oavsiktligt kommer vi att komma med lite olika lösningar på samma problem. Detta tar inte bara extra tid och ansträngning utan leder också till en subtil, för det mesta mycket svår att känna igen, dubbelarbete. Detta är den farligaste typen av dubbelarbete.

Dubbling genom datalistens Logic Reimplementation

I de föregående två styckena pratade vi bara om datainsamling. Men Gateway är dubbelriktad. Vår affärslogik är dubbelriktad. Vi måste på något sätt fortsätta våra föremål. Detta leder igen till en hel del repetition om vi vill genomföra denna logik efter behov i olika moduler och klasser av vår ansökan.


De viktigaste koncepten

Repository för datainsamling

Ett arkiv kan fungera på två sätt: datahämtning och data uthållighet.


När det används för att hämta objekt från uthållighet, kommer ett repository att ringas med en anpassad fråga. Denna fråga kan vara en specifik metod med namn eller en mer vanlig metod med parametrar. Repository är ansvarig för att tillhandahålla och genomföra dessa sökmetoder. När en sådan metod kallas kommer förvaret att kontakta Gateway för att hämta de råa data från persistensen. Gatewayen kommer att tillhandahålla råa objektdata (som en array med värden). Då tar förvaret ut dessa data, gör de nödvändiga omvandlingar och ringer till lämpliga fabriksmetoder. Fabrikerna kommer att tillhandahålla objekten konstruerade med data som tillhandahålls av förvaret. Repository kommer att samla in dessa objekt och returnera dem som en uppsättning objekt (som en rad objekt eller ett samlingsobjekt som definieras i kompositmönster-lektionen i kursen Agile Design Patterns).

Förvaringsdatabas för persistens

Det andra sättet ett arkiv kan fungera är att tillhandahålla den logik som behövs för att kunna extrahera informationen från ett objekt och fortsätta det. Detta kan vara så enkelt som att serialisera objektet och skicka serialiserad data till Gateway för att fortsätta det eller så sofistikerat som att skapa matriser med information med alla fält och tillstånd för ett objekt.


När den används för att fortlöpande information är klientklassen den som direkt kommunicerar med fabriken. Tänk på ett scenario när en ny kommentar läggs ut på ett blogginlägg. Ett kommentarobjekt skapas av vår affärslogik (klientklassen) och skickas sedan till förvaret för att vara kvar. Förvaret kommer att bestå av objekten som använder Gateway och eventuellt cache dem i en lokal minneslista. Data måste omvandlas eftersom det bara finns sällsynta fall när verkliga objekt kan sparas direkt till ett persistenssystem.


Ansluta prickarna

Bilden nedan är en högre nivåvy om hur man integrerar arkivet mellan fabrikerna, gatewayen och klienten.


I mitten av schemat finns vårt arkiv. Till vänster är ett gränssnitt för Gateway, en implementering och uthålligheten själv. Till höger finns ett gränssnitt för fabrikerna och en fabriksimplementering. Till sist finns det klientklassen på toppen.

Som det kan observeras från pilens riktning, är beroendet inverterade. Repository beror bara på abstrakta gränssnitt för fabriker och gateways. Gateway beror på gränssnittet och den uthållighet det erbjuder. Fabriken beror endast på dess gränssnitt. Klienten beror på Repository, vilket är acceptabelt eftersom Repository tenderar att vara mindre konkret än Klienten.


Sett i perspektiv respekterar stycket ovan vår högnivåarkitektur och riktningen av beroenden vi vill uppnå.


Hantera kommentarer till blogginlägg med ett arkiv

Nu när vi har sett teorin är det dags för ett praktiskt exempel. Föreställ dig att vi har en blogg där vi har Postobjekt och Kommentarobjekt. Kommentarer hör till inlägg och vi måste hitta ett sätt att fortsätta dem och att hämta dem.

Kommentaren

Vi kommer att börja med ett test som tvingar oss att tänka på vad vårt kommentarobjekt ska innehålla.

klass RepositoryTest utökar PHPUnit_Framework_TestCase funktionstestAcommentHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe har en åsikt om arkivmönstret"; $ commentBody = "Jag tycker att det är en bra idé att använda Repository Pattern för att fortsätta och hämta objekt."; $ comment = new Kommentar ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); 

Vid första anblicken kommer en kommentar bara att vara ett dataobjekt. Det kan inte ha någon funktionalitet, men det är upp till ramen för vår ansökan att bestämma. För detta exempel antar du bara att det är ett enkelt dataobjekt. Konstruerad med en uppsättning variabler.

klass Kommentar 

Bara genom att skapa en tom klass och kräva det i testet gör det att passera.

require_once '... /Comment.php'; klass RepositoryTest utökar PHPUnit_Framework_TestCase [...]

Men det är långt ifrån perfekt. Vårt test testa inte någonting än. Låt oss tvinga oss att skriva alla getters på kommentarklassen.

funktionstestAcommentsHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe har en åsikt om arkivmönstret"; $ commentBody = "Jag tycker att det är en bra idé att använda Repository Pattern för att fortsätta och hämta objekt."; $ comment = new Kommentar ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAuthor, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

För att kontrollera längden på handledningen skrev jag alla påståenden omedelbart och vi kommer att genomföra dem samtidigt. I det verkliga livet, ta dem en efter en.

 klass Kommentar privat $ postId; privat $ författare; privat $ authorEmail; privat $ ämne; privat $ body; funktion __construct ($ postId, $ author, $ authorEmail, $ subject, $ body) $ this-> postId = $ postId; $ this-> author = $ author; $ this-> authorEmail = $ authorEmail; $ this-> subject = $ subject; $ this-> body = $ body;  offentlig funktion getPostId () returnera $ this-> postId;  offentlig funktion getAuthor () return $ this-> author;  offentlig funktion getAuthorEmail () return $ this-> authorEmail;  offentlig funktion getSubject () return $ this-> subject;  offentlig funktion getBody () returnera $ this-> body; 

Med undantag för listan över privata variabler, genererades resten av koden av min IDE, NetBeans, så det kan vara lite omkostnad vissa gånger att testa den genererade automatisk genererade koden. Om du inte skriver dessa linjer själv, är du välkommen att göra dem direkt och inte bry dig om test för setter och konstruktörer. Testet har dock hjälpt oss att bättre avslöja våra idéer och bättre dokumentera vad vår kommentarklass innehåller.

Vi kan också överväga dessa testmetoder och testklasser som våra "Client" -klasser från scheman.


Vår Gateway till Persistens

För att hålla detta exempel så enkelt som möjligt kommer vi att implementera endast en InMemoryPersistence så att vi inte komplicerar vår existens med filsystem eller databaser.

require_once '... /InMemoryPersistence.php'; klass InMemoryPersistenceTest utökar PHPUnit_Framework_TestCase funktion testItCanPerisistAndRetrieveASingleDataArray () $ data = array ('data'); $ persistence = ny InMemoryPersistence (); $ Persistence-> kvarstår (data $); $ this-> assertEquals ($ data, $ persistence-> retrieve (0)); 

Som vanligt börjar vi med det enklaste testet som eventuellt skulle kunna misslyckas och tvinga oss att skriva en kod. Detta test skapar en ny InMemoryPersistence objekt och försöker fortsätta och hämta en uppringd grupp data.

require_once __DIR__. '/Persistence.php'; klass InMemoryPersistence implementerar Persistence private $ data = array (); funktionen kvarstår ($ data) $ this-> data = $ data;  funktionen hämta ($ id) returnera $ this-> data; 

Den enklaste koden för att passera den är bara för att hålla inkommande $ uppgifter i en privat variabel och returnera den i hämta metod. Koden som den är just nu bryr sig inte om den skickade in $ id variabel. Det är det enklaste som kan möjliggöra att provet passerar. Vi tog också friheten att införa och genomföra ett gränssnitt som heter Uthållighet.

gränssnitt Persistence funktion kvarstår ($ data); funktionen hämta ($ ids); 

Detta gränssnitt definierar de två metoder som någon Gateway behöver genomföra. Envisas och hämta. Som du förmodligen redan gissat är vår Gateway vår InMemoryPersistence klass och vår fysiska uthållighet är den privata variabeln som håller vår data i minnet. Men låt oss komma tillbaka till genomförandet av detta i minnespersistens.

funktion testItCanPerisistSeveralElementsAndRetrieveAnyOfThem () $ data1 = array ('data1'); $ data2 = array ('data2'); $ persistence = ny InMemoryPersistence (); $ Persistence-> kvarstår ($ data1); $ Persistence-> kvarstår ($ data2); $ this-> assertEquals ($ data1, $ persistence-> hämta (0)); $ this-> assertEquals ($ data2, $ persistence-> hämta (1)); 

Vi lade till ett annat test. I denna fortsätter vi två olika dataregler. Vi förväntar oss att kunna hämta var och en av dem individuellt.

require_once __DIR__. '/Persistence.php'; klass InMemoryPersistence implementerar Persistence private $ data = array (); funktion kvarstår ($ data) $ this-> data [] = $ data;  funktionen hämta ($ id) returnera $ this-> data [$ id]; 

Testet tvingade oss att ändra vår kod något. Vi behöver nu lägga till data i vår array, inte bara ersätta den med den som skickas in till kvarstår (). Vi måste också överväga $ id parameter och returnera elementet vid det indexet.

Detta räcker för våra InMemoryPersistence. Om det behövs kan vi ändra det senare.


Vår fabrik

Vi har en Client (våra test), en uthållighet med en Gateway, och kommentarobjekt att fortsätta. Nästa sak saknas är vår fabrik.

Vi började vår kodning med en RepositoryTest fil. Detta test har dock faktiskt skapat en Kommentar objekt. Nu måste vi skapa test för att verifiera om vår fabrik kommer att kunna skapa Kommentar objekt. Det verkar som om vi hade ett fel i bedömningen och vårt test är mer troligt ett test för vår kommande Factory än för vårt Repository. Vi kan flytta den till en annan testfil, CommentFactoryTest.

require_once '... /Comment.php'; class CommentFactoryTest utökar PHPUnit_Framework_TestCase funktionstestACommentsHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe har en åsikt om arkivmönstret"; $ commentBody = "Jag tycker att det är en bra idé att använda Repository Pattern för att fortsätta och hämta objekt."; $ comment = new Kommentar ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAuthor, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Nu passerar detta test självklart. Och medan det är ett korrekt test, bör vi överväga att ändra det. Vi vill skapa en Fabrik objekt, passera i en array och be om att skapa en Kommentar för oss.

require_once '... /CommentFactory.php'; class CommentFactoryTest utökar PHPUnit_Framework_TestCase funktionstestACommentsHasAllItsComposingParts () $ postId = 1; $ commentAuthor = "Joe"; $ commentAuthorEmail = "[email protected]"; $ commentSubject = "Joe har en åsikt om arkivmönstret"; $ commentBody = "Jag tycker att det är en bra idé att använda Repository Pattern för att fortsätta och hämta objekt."; $ commentData = array ($ postId, $ commentAuthor, $ commentAuthorEmail, $ commentSubject, $ commentBody); $ comment = (new CommentFactory ()) -> make ($ commentData); $ this-> assertEquals ($ postId, $ comment-> getPostId ()); $ this-> assertEquals ($ commentAuthor, $ comment-> getAuthor ()); $ this-> assertEquals ($ commentAuthorEmail, $ comment-> getAuthorEmail ()); $ this-> assertEquals ($ commentSubject, $ comment-> getSubject ()); $ this-> assertEquals ($ commentBody, $ comment-> getBody ()); 

Vi bör aldrig namnge våra klasser baserat på det designmönster de genomför, men Fabrik och Repository representerar mer än bara designmönstret självt. Personligen har jag inget emot att inkludera dessa två ord i klassens namn. Jag rekommenderar dock starkt och respekterar begreppet att inte namnge våra klasser efter de designmönster vi använder för resten av mönstren.

Detta test är bara något annorlunda än det föregående, men det misslyckas. Det försöker skapa en CommentFactory objekt. Den klassen existerar inte än. Vi försöker också att ringa a göra() metod på den med en matris som innehåller all information om en kommentar som en matris. Denna metod definieras i Fabrik gränssnitt.

gränssnitt Factory function make ($ data); 

Detta är en mycket vanlig Fabrik gränssnitt. Det definierade den enda nödvändiga metoden för en fabrik, den metod som faktiskt skapar de objekt vi vill ha.

require_once __DIR__. '/Factory.php'; require_once __DIR__. '/Comment.php'; klass CommentFactory redskap Factory function make ($ components) returnera ny kommentar ($ komponenter [0], $ komponenter [1], $ komponenter [2], $ components [3], $ components [4]); 

Och CommentFactory implementerar Fabrik gränssnitt framgångsrikt genom att ta $ komponenter parameter i dess göra() metod, skapar och returnerar en ny Kommentar objekt med informationen därifrån.

Vi kommer att hålla vår persistens och objekt skapande logik så enkelt som möjligt. Vi kan för denna handledning säkert ignorera eventuell felhantering, validering och undantagsspridning. Vi kommer att sluta här med genomförandet av persistens och objektskapande.


Använda ett arkiv för att fortsätta kommentarer

Som vi har sett ovan kan vi använda ett arkiv på två sätt. Att hämta information från uthållighet och även att fortsätta information om persistensskiktet. Med hjälp av TDD är det för det mesta lättare att börja med den sparade (kvarstående) delen av logiken och sedan använda den befintliga implementeringen för att testa datainsamling.

require_once "... / ... / ... /vendor/autoload.php '; require_once '... /CommentRepository.php'; require_once '... /CommentFactory.php'; klass RepositoryTest utökar PHPUnit_Framework_TestCase protected function tearDown () \ Mockery :: close ();  funktion testItCallsThePersistenceWhenAddingAComment () $ persistanceGateway = \ Mockery :: mock ('Persistence'); $ commentRepository = nytt CommentRepository ($ persistanceGateway); $ commentData = array (1, 'x', 'x', 'x', 'x'); $ comment = (new CommentFactory ()) -> make ($ commentData); $ PersistanceGateway-> shouldReceive (kvarstå) -> en gång () -> med ($ commentData); $ CommentRepository-> Lägg ($ kommentar); 

Vi använder Mockery för att mocka vår uthållighet och injicera det spotta objektet till förvaret. Då ringer vi Lägg till() på förvaret. Denna metod har en parameter av typen Kommentar. Vi förväntar oss att persistensen ska kallas med en mängd data som liknar $ commentData.

require_once __DIR__. '/InMemoryPersistence.php'; klass KommentarRepository privat $ persistence; funktion __construct (Persistens $ persistence = null) $ this-> persistence = $ persistence? : ny InMemoryPersistence ();  lägg till (Kommentera $ kommentaren) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject ), $ comment-> getBody ())); 

Som du kan se, är Lägg till() Metoden är ganska smart. Den inkapslar kunskapen om hur man omvandlar ett PHP-objekt till en vanlig grupp som kan användas av uthålligheten. Kom ihåg att vår persistensgateway är vanligtvis ett allmänt föremål för alla våra data. Det kan och kommer att fortsätta alla data i vår ansökan, så att skicka till objekt skulle göra det gör för mycket: både omvandling och effektiv uthållighet.

När du har en InMemoryPersistence klass som vi gör, det är väldigt snabbt. Vi kan använda det som ett alternativ till att mocka gatewayen.

funktionstestAPersistedCommentCanBeRetrievedFromTheGateway () $ persistanceGateway = ny InMemoryPersistence (); $ commentRepository = nytt CommentRepository ($ persistanceGateway); $ commentData = array (1, 'x', 'x', 'x', 'x'); $ comment = (new CommentFactory ()) -> make ($ commentData); $ CommentRepository-> Lägg ($ kommentar); $ this-> assertEquals ($ commentData, $ persistanceGateway-> hämta (0)); 

Självklart, om du inte har ett minnesutförande av din uthållighet, är mocking det enda rimliga sättet att gå. Annars är ditt test bara för långsamt för att vara praktiskt.

funktionstestItCanAddMultipleCommentsAtOnce () $ persistanceGateway = \ Mockery :: mock ('Persistence'); $ commentRepository = nytt CommentRepository ($ persistanceGateway); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (2, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ PersistanceGateway-> shouldReceive (kvarstå) -> en gång () -> med ($ commentData1); $ PersistanceGateway-> shouldReceive (kvarstå) -> en gång () -> med ($ commentData2); $ commentRepository-> add (array ($ comment1, $ comment2)); 

Vårt nästa logiska steg är att genomföra ett sätt att lägga till flera kommentarer på en gång. Ditt projekt kanske inte kräver denna funktion och det är inte något som krävs av mönstret. Faktum är att Repository Pattern bara säger att det kommer att ge ett anpassat frågeformulär och persistens språk för vår affärslogik. Så om vår bushiness logik känner behovet av att lägga till flera kommentarer på en gång, är förvarsplatsen den plats där logiken borde ligga.

funktionen lägg till ($ commentData) if (is_array ($ commentData)) foreach ($ commentData as $ comment) $ this-> persistence-> fortsätter (array ($ comment-> getPostId (), $ comment-> getAuthor $ comment-> getAuthorEmail (), $ comment-> getSubject (), $ comment-> getBody ())); annars $ this-> persistence-> persistence (array ($ commentData-> getPostId (), $ commentData-> getAuthor (), $ commentData-> getAuthorEmail (), $ commentData-> getSubject (), $ commentData-> getBody ))); 

Och det enklaste sättet att göra provet är att bara verifiera om parametern vi får är en array eller inte. Om det är en matris, cyklar vi igenom varje element och kallar uthålligheten med den matris vi genererar från en enda Kommentar objekt. Och medan den här koden är syntaktiskt korrekt och gör testet klar, introducerar det en liten dubbelarbete som vi kan bli av med ganska enkelt.

funktion add ($ commentData) if (is_array ($ commentData)) foreach ($ commentData som $ comment) $ this-> addOne ($ comment); annars $ this-> addOne ($ commentData);  privat funktion addOne (Kommentar $ comment) $ this-> persistence-> persist (array ($ comment-> getPostId (), $ comment-> getAuthor (), $ comment-> getAuthorEmail (), $ comment-> getSubject (), $ comment-> getBody ())); 

När alla tester är gröna är det alltid dags att refactoring innan vi fortsätter med nästa felprov. Och det gjorde vi bara med Lägg till() metod. Vi extraherade tillägget av en enda kommentar till en privat metod och kallade den från två olika platser i vår publik Lägg till() metod. Detta reducerade inte bara dubbelarbete utan öppnade också möjligheten att göra Lägg till ett() metod offentlig och låta affärslogiken bestämma om den vill lägga till en eller flera kommentarer i taget. Detta skulle leda till en annan implementering av vårt arkiv med en Lägg till ett() och en annan addMany () metoder. Det skulle vara ett fullständigt legitimt genomförande av Repository Pattern.


Hämtar kommentarer med vårt arkiv

Ett Repository tillhandahåller ett anpassat fråge språk för affärslogiken. Så namnen och funktionaliteterna för frågemetoderna för ett repository är enormt upp till kraven i affärslogiken. Du bygger ditt förråd när du bygger din affärslogik, eftersom du behöver en annan anpassad sökmetod. Det finns dock åtminstone en eller två metoder som du hittar på nästan alla Repository.

funktion testItCanFindAllComments () $ repository = new CommentRepository (); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (2, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ Repository-> lägga ($ comment1); $ Repository-> lägga ($ comment2); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findAll ()); 

Den första sådan metod kallas hitta alla(). Detta bör återge alla föremål som förvaret ansvarar för, i vårt fall kommentarer. Testet är enkelt, vi lägger till en kommentar, sedan en annan, och äntligen vill vi ringa hitta alla() och få en lista med båda kommentarerna. Detta är dock inte möjligt med vårt InMemoryPersistence som det är just nu. En liten uppdatering krävs.

funktion retrieveAll () returnera $ this-> data; 

Det är allt. Vi lade till en retrieveAll () metod som bara returnerar hela $ uppgifter array från klassen. Enkelt och effektivt. Det är dags att genomföra hitta alla()CommentRepository nu.

funktion findAll () $ allCommentsData = $ this-> persistence-> retrieveAll (); $ comments = array (); foreach ($ allCommentsData as $ commentData) $ comments [] = $ this-> commentFactory-> make ($ commentData); returnera $ comments; 

hitta alla() kommer att ringa retrieveAll () metod på vår uthållighet. Den metoden ger en rå uppsättning data. hitta alla() kommer att cykla genom varje element och använda de data som behövs för att skickas till fabriken. Fabriken kommer att ge en Kommentar en tid. En uppsättning med dessa kommentarer kommer att byggas och returneras i slutet av hitta alla(). Enkelt och effektivt.

En annan vanlig metod som du hittar på repositories är att söka efter ett visst objekt eller en grupp objekt baserat på deras karakteristiska nyckel. Till exempel är alla våra kommentarer kopplade till ett blogginlägg av en $ postID intern variabel. Jag kan tänka mig att vi i bloggens affärslogik nästan alltid vill hitta alla kommentarer relaterade till ett blogginlägg när den bloggposten visas. Så en metod som heter findByPostId ($ id) låter rimligt för mig.

funktion testItCanFindCommentsByBlogPostId () $ repository = new CommentRepository (); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (1, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ commentData3 = array (3, 'y', 'y', 'y', 'y'); $ comment3 = (new CommentFactory ()) -> make ($ commentData3); $ repository-> add (array ($ comment1, $ comment2)); $ Repository-> lägga ($ comment3); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findByPostId (1)); 

Vi skapar bara tre enkla kommentarer. De två första har samma $ postId = 1, den tredje har $ postID = 3. Vi lägger till dem alla i förvaret och då förväntar vi oss en grupp med de två första när vi gör ett findByPostId () för $ postId = 1.

funktion findByPostId ($ postId) return array_filter ($ this-> findAll (), funktion ($ comment) använd ($ postId) return $ comment-> getPostId () == $ postId;); 

Implementeringen kunde inte vara enklare. Vi hittar alla kommentarer med vår redan genomförda hitta alla() metod och vi filtrerar arrayen. Vi har inget sätt att fråga efterhålligheten att göra filtreringen för oss, så vi ska göra det här. Koden kommer att fråga varje Kommentar objekt och jämföra dess $ postID med den som vi skickade in som parameter. Bra. Testet passerar. Men jag känner att vi saknat något.

funktion testItCanFindCommentsByBlogPostId () $ repository = new CommentRepository (); $ commentData1 = array (1, 'x', 'x', 'x', 'x'); $ comment1 = (new CommentFactory ()) -> make ($ commentData1); $ commentData2 = array (1, 'y', 'y', 'y', 'y'); $ comment2 = (new CommentFactory ()) -> make ($ commentData2); $ commentData3 = array (3, 'y', 'y', 'y', 'y'); $ comment3 = (new CommentFactory ()) -> make ($ commentData3); $ repository-> add (array ($ comment1, $ comment2)); $ Repository-> lägga ($ comment3); $ this-> assertEquals (array ($ comment1, $ comment2), $ repository-> findByPostId (1)); $ this-> assertEquals (array ($ comment3), $ repository-> findByPostId (3)); 

Lägger till en andra påstående för att få den tredje kommentaren med findByPostID () Metod avslöjar vårt misstag. När du lätt kan testa extra banor eller fall, som i vårt fall med en enkel extra påstående, borde du. Dessa enkla extra påståenden eller testmetoder kan avslöja dolda problem. Som i vårt fall, array_filter () Reindexerar inte den resulterande arrayen. Och medan vi har en matris med de rätta elementen, är indexerna krossade.

1) RepositoryTest :: testItCanFindCommentsByBlogPostId Misslyckades hävdar att två arrays är lika. --- Förväntat +++ Verkligt @@ @@ Array (- 0 => Kommentarobjekt (...) + 2 => Kommentera objekt (...))

Nu kan du betrakta detta som en brist på PHPUnit eller en brist i din affärslogik. Jag tenderar att vara rigorös med arrayindex eftersom jag brände mina händer med dem några gånger. Så vi bör överväga felet ett problem med vår logik i CommentRepository.

funktion findByPostId ($ postId) return array_values ​​(array_filter ($ this-> findAll (), funktion ($ comment) använd ($ postId) return $ comment-> getPostId () == $ postId;)); 

Japp. Så enkelt. Vi kör bara resultatet genom array_values ​​() innan du returnerar den. Det kommer att reindexa vår array snyggt. Uppdrag slutfört.


Slutgiltiga tankar

Och det är också uppdraget för vårt Repository. Vi har en klass som kan användas av någon annan affärslogik klass som erbjuder ett enkelt sätt att fortsätta och hämta objekt. Det avkallar också affärslogiken från fabrikerna och data-persistensportarna. Det reducerade logisk dubbelarbete och förenklar betydande persistens och retrieval för våra kommentarer.

Kom ihåg att det här mönstret kan användas för alla typer av listor och när du börjar använda det, kommer du att se användbarheten. I grund och botten, när du måste arbeta med flera föremål av samma typ, bör du överväga att införa ett arkiv för dem. Repositories är specialiserade efter objekttyp och inte generellt. Så för en bloggapplikation kan du ha olika repositorier för blogginlägg, för kommentarer, för användare, för användarkonfigurationer, för teman, för mönster, för eller allt du kan ha flera instanser av.

Och innan du avslutar detta kan ett förlag ha en egen lista över objekt och det kan göra en lokal cachning av objekt. Om ett objekt inte kan hittas i den lokala listan, hämtar vi det från uthålligheten, annars tjänar vi den från vår lista. Om det används med cachning, kan ett Repository kombineras med Singleton Design Pattern.

Som vanligt tack för din tid och jag