Et av de mest forvirrende designmønsterene er utholdenhet. Behovet for en søknad om å fortsette sin interne tilstand og data er så enorm at det er sannsynligvis tiere - om ikke hundrevis - forskjellige teknologier for å løse dette enkeltproblemet. Dessverre er ingen teknologi en magisk kulde. Hver applikasjon, og noen ganger hver komponent i applikasjonen, er unik på sin egen måte - og krever derfor en unik løsning.
I denne opplæringen vil jeg lære deg noen gode metoder for å hjelpe deg med å finne ut hvilken tilnærming du skal ta når du jobber med fremtidige applikasjoner. Jeg vil kort diskutere noen høytliggende designproblemer og -prinsipper, etterfulgt av en mer detaljert visning av Active Record-designmønsteret, kombinert med noen få ord om designdata mønsteret Table Data Gateway.
Selvfølgelig vil jeg ikke bare lære deg teorien bak designet, men jeg vil også veilede deg gjennom et eksempel som begynner som tilfeldig kode og omdanner til en strukturert persistensløsning.
I dag kan ingen programmerer forstå dette arkaiske systemet.
Det eldste prosjektet jeg må jobbe på, begynte i år 2000. Deretter startet et team av programmerere et nytt prosjekt ved å vurdere ulike krav, tenkt på arbeidsbelastningene søknaden må håndtere, teste forskjellige teknologier og kom til en konklusjon: alle PHP-koden til søknaden, bortsett fra index.php
fil, bør oppholde seg i en MySQL-database. Deres beslutning kan høres opprørende i dag, men det var akseptabelt for tolv år siden (OK ... kanskje ikke).
De startet med å lage sine grunnbord, og deretter andre tabeller for hver nettside. Løsningen virket ... for en tid. De opprinnelige forfatterne visste hvordan de skulle opprettholde det, men da forlot hver forfatter en etter en - forlot kodebasen i hendene på andre nykommere.
I dag kan ingen programmerer forstå dette arkaiske systemet. Alt starter med en MySQL-spørring fra index.php
. Resultatet av spørringen returnerer noen PHP-kode som utfører enda flere søk. Det enkleste scenariet innebærer minst fem databasetabeller. Det er selvsagt ingen tester eller spesifikasjoner. Endring av noe er en no-go, og vi må bare omskrive hele modulen hvis noe går galt.
De opprinnelige utviklerne ignorerte det faktum at en database bare skal inneholde data, ikke forretningslogikk eller presentasjon. De blandet PHP og HTML-kode med MySQL og ignorerte designkoncepter på høyt nivå.
Alle applikasjoner bør konsentrere seg om å respektere en ren design på høyt nivå.
Etter hvert som tiden gikk, trengte de nye programmørene å legge til flere funksjoner i systemet, samtidig som de fikser gamle bugs. Det var ingen måte å fortsette å bruke MySQL-tabeller for alt, og alle involvert i å opprettholde koden var enige om at designen var forferdelig feil. Så evaluerte de nye programmene forskjellige krav, tenkte på arbeidsbelastningene søknaden må håndtere, testet ulike teknologier og kom til en konklusjon: de bestemte seg for å flytte så mye kode som mulig til den endelige presentasjonen. Igjen, denne avgjørelsen kan høres opprørende i dag, men det var lyse år fra den tidligere opprørende utformingen.
Utviklerne vedtok et templerende rammeverk og baserte applikasjonen rundt den, og startet hver ny funksjon og modul med en ny mal. Det var lett; malen var beskrivende og de visste hvor de skulle finne koden som utfører en bestemt oppgave. Men det var slik de endte med malfiler som inneholdt motorens Domenespesifikke Språk (DSL), HTML, PHP og selvfølgelig MySQL-spørringer.
I dag ser teamet mitt bare på og underverker. Det er et mirakel at mange av synspunktene faktisk fungerer. Det kan ta en heftig mengde tid bare for å bestemme hvordan informasjonen kommer fra databasen til visningen. Som sin forgjenger er det alt et stort rot!
Disse utviklerne ignorerte det faktum at en visning ikke skal inneholde forretnings- eller persistenslogikk. De blandet PHP og HTML-kode med MySQL og ignorerte designkoncepter på høyt nivå.
En mock er et objekt som virker som sin virkelige motstykke, men utfører ikke den virkelige koden.
Alle applikasjoner bør konsentrere seg om å respektere en ren design på høyt nivå. Dette er ikke alltid oppnåelig, men det bør ha høy prioritet. Et godt design på høyt nivå har god isolert forretningslogikk. Objektskaping, utholdenhet og levering er utenfor kjerne og avhengigheter peker bare mot forretningslogikken.
Å isolere forretningslogikken åpner døren for gode muligheter, og alt blir noe av et plugin, hvis de eksterne avhengighetene alltid peker mot forretningslogikken. For eksempel kan du bytte den tunge MySQL-databasen med en lett SQLite3-database.
For bedre å identifisere problemene med en dårlig, om enn arbeid, design, vil jeg starte med et enkelt eksempel på, du gjettet det, en blogg. Gjennom denne opplæringen vil jeg følge noen prinsipper for testdrevet utvikling (TDD) og gjøre testene lett forståelige - selv om du ikke har TDD-opplevelse. La oss forestille deg at du bruker et MVC-rammeverk. Når du lagrer et blogginnlegg, er en kontroller kalt Blogg innlegg
utfører a lagre()
metode. Denne metoden kobles til en SQLite-database for å lagre et blogginnlegg i databasen.
La oss lage en mappe som heter Data i kodens mappe og bla til den katalogen i konsollen. Opprett en database og et bord, slik som dette:
$ sqlite3 MyBlog SQLite versjon 3.7.13 2012-06-11 02:05:22 Skriv inn ".help" for instruksjoner Skriv inn SQL-setninger avsluttet med en ";" sqlite> opprett tabell BlogPosts (tittel varchar (120) primærnøkkel, innholds tekst, published_timestamp tidsstempel);
Våre lagre()
Metoden får verdiene fra skjemaet som en gruppe, kalt $ data
:
class BlogPostController funksjon lagre ($ data) $ dbhandle = new SQLite3 ('Data / MyBlog'); $ query = 'INSERT TIL BlogPosts VALUES ("'. $ data ['title']. '", "'. $ data ['innhold']. '", "' tid (). '")'; $ Dbhandle-> exec ($ query);
Denne koden fungerer, og du kan bekrefte den ved å ringe den fra en annen klasse, og passere en forhåndsdefinert $ data
array, slik:
$ this-> object = new BlogPostController; $ data ['title'] = 'Første innleggstitel'; $ data ['content'] = 'Noen kule innhold for det første innlegget'; $ data ['published_timestamp'] = tid (); $ Dette-> objekt> Lagre ($ data);
Innholdet i $ data
variabel var faktisk lagret i databasen:
sqlite> velg * fra BlogPosts; Første innleggstittel | Noen kule innhold for første innlegg | 1345665216
Arv er den sterkeste typen avhengighet.
En karakteriseringstest beskriver og verifiserer nåværende oppførsel av eksisterende kode. Det er oftest brukt til å karakterisere arvskoden, og det gjør det enklere å refactoring den koden.
En karakteriseringstest kan teste en modul, en enhet, eller gå helt fra brukergrensesnittet til databasen; alt avhenger av hva vi vil teste. I så fall skal en slik test utøve kontrolleren og verifisere innholdet i databasen. Dette er ikke en typisk enhet, funksjonell eller integreringstest, og det kan vanligvis ikke knyttes til noen av disse testnivåene.
Karakteriseringstester er et midlertidig sikkerhetsnett, og vi sletter dem vanligvis etter at koden er korrekt refactored og enhetstestet. Her er en implementering av en test, plassert i Test mappe:
require_once '... /BlogPostController.php'; klassen BlogPostControllerTest utvider PHPUnit_Framework_TestCase private $ object; privat $ dbhandle; funksjon setUp () $ this-> object = new BlogPostController; $ this-> dbhandle = ny SQLite3 ('... / Data / MyBlog'); funksjonstestSave () $ this-> cleanUPDatabase (); $ data ['title'] = 'Første innleggstitel'; $ data ['content'] = 'Noen kule innhold for det første innlegget'; $ data ['published_timestamp'] = tid (); $ Dette-> objekt> Lagre ($ data); $ this-> assertEquals ($ data, $ this-> getPostsFromDB ()); privat funksjon cleanUPDatabase () $ this-> dbhandle-> exec ('DELETE FROM BlogPosts'); privat funksjon getPostsFromDB () $ result = $ this-> dbhandle-> spørring ('VELG * FRA BlogPosts'); returnere $ result-> fetchArray (SQLITE3_ASSOC);
Denne testen oppretter et nytt kontrollerobjekt og utfører det lagre()
metode. Testen leser deretter informasjonen fra databasen og sammenligner den med forhåndsdefinerte $ data []
array. Vi preform denne sammenligningen ved å bruke $ Dette-> assertEquals ()
metode, en påstand som antar at parametrene er like. Hvis de er forskjellige, svikter testen. Også, vi rengjør blogginnlegg
database tabell hver gang vi kjører testen.
Eldre kode er ikke testet kode. - Michael Feathers
Med testen oppe, må vi rense litt av koden. Åpne databasen med hele katalognavnet og bruk sprintf ()
å komponere spørringsstrengen. Dette resulterer i mye enklere kode:
class BlogPostController funksjon lagre ($ data) $ dbhandle = ny SQLite3 (__ DIR__. '/ Data / MyBlog'); $ query = sprintf ('INSERT INTO BlogPosts VALUES ("% s", "% s", "% s")', $ data ['title'], $ data ['content'], time ()); $ Dbhandle-> exec ($ query);
Vi erkjenner at koden vår må flyttes fra kontrolleren til forretningslogikken og utholdenhetslaget, og Gateway Pattern kan hjelpe oss med å komme i gang nedover den banen. Her er den reviderte testSave ()
metode:
funksjon testItCanPersistABlogPost () $ data = array ('title' => 'Første innleggstitel', 'content' => 'Noen innhold.', 'timestamp' => tid ()); $ blogPost = ny BlogPost ($ data ['title'], $ data ['content'], $ data ['tidsstempel']); $ mockedPersistence = $ this-> getMock ('SqlitePost'); $ MockedPersistence-> forventer ($ dette-> en gang til ()) -> metode ( 'vedvarer') -> med ($ Book); $ controller = ny BlogPostController ($ mockedPersistence); $ Controller-> Lagre ($ data);
Dette representerer hvordan vi vil bruke lagre()
metode på kontrolleren. Vi forventer at kontrolleren kaller en metode som heter vedvare ($ blogPostObject)
på gateway-objektet. La oss forandre vår BlogPostController
å gjøre det:
klassen BlogPostController privat $ gateway; funksjon __construct (Gateway $ gateway = null) $ this-> gateway = $ gateway? : Ny SqlitePost (); funksjonssparing ($ data) $ this-> gateway-> vedvarer (ny BlogPost ($ data ['title'], $ data ['content'], $ data ['tidsstempel']));
Et godt høyt nivå design har en godt isolert forretningslogikk.
Hyggelig! Våre BlogPostController
ble mye enklere. Den bruker gatewayen (enten levert eller instantiated) for å fortsette dataene ved å ringe til den fortsette()
metode. Det er absolutt ingen kunnskap om hvordan dataene er vedvarende; Persistenslogikken ble modulær.
I den forrige testen opprettet vi kontrolleren med en håne persistensobjekt, slik at data aldri blir skrevet til databasen når testen utføres. I produksjonskode oppretter kontrolleren sitt eget vedvarende objekt for å vedvare dataene ved hjelp av a SqlitePost
gjenstand. En mock er et objekt som virker som sin virkelige motstykke, men den utfører ikke den virkelige koden.
La oss nå hente et blogginnlegg fra datalageret. Det er like enkelt som å lagre data, men vær oppmerksom på at jeg refactored testen litt.
require_once '... /BlogPostController.php'; require_once '... /BlogPost.php'; require_once '... /SqlitePost.php'; klassen BlogPostControllerTest utvider PHPUnit_Framework_TestCase private $ mockedPersistence; privat $ kontroller; private $ data; funksjon setUp () $ this-> mockedPersistence = $ this-> getMock ('SqlitePost'); $ this-> controller = ny BlogPostController ($ this-> mockedPersistence); $ this-> data = array ('title' => 'Første innleggstitel', 'innhold' => 'Noen innhold.', 'timestamp' => tid ()); funksjon testItCanPersistABlogPost () $ blogPost = $ this-> aBlogPost (); $ Dette-> mockedPersistence-> forventer ($ dette-> en gang til ()) -> metode ( 'vedvarer') -> med ($ Book); $ Dette-> controller-> Lagre ($ dette-> data); funksjon testItCanRetrievABlogPostByTitle () $ expectedBlogpost = $ this-> aBlogPost (); $ this-> mockedPersistence-> forventer ($ this-> once ()) -> metode ('findByTitle') -> med ($ this-> data ['title']) -> vil ($ this-> returnValue $ expectedBlogpost)); $ this-> assertEquals ($ expectedBlogpost, $ this-> controller-> findByTitle ($ this-> data ['title'])); offentlig funksjon aBlogPost () returner ny BlogPost ($ this-> data ['title'], $ this-> data ['content'], $ this-> data ['timestamp']);
Og implementeringen i BlogPostController
er bare en uttalelsesmetode:
funksjon findByTitle ($ title) return $ this-> gateway-> findByTitle ($ title);
Er det ikke kult? De Blogg innlegg
klassen er nå en del av forretningslogikken (husk designplanen på høyt nivå fra oven). UI / MVC skaper Blogg innlegg
gjenstander og bruker betong Inngangsport
implementeringer for å vedvare dataene. Alle avhengigheter peker på forretningslogikken.
Det er bare ett skritt igjen: Lag en konkret implementering av Inngangsport
. Følgende er SqlitePost
klasse:
require_once 'Gateway.php'; klasse SqlitePost implementerer Gateway private $ dbhandle; funksjon __construct ($ dbhandle = null) $ this-> dbhandle = $ dbhandle? : Ny SQLite3 (__ DIR__. '/ Data / MyBlog'); publiseringsfunksjon vedvarer (BlogPost $ blogPost) $ query = sprintf ('INSERT INTO BlogPosts VALUES ("% s", "% s", "% s")', $ bloggPost-> tittel, $ bloggPost-> innhold, $ blogPost-> timestamp); $ Dette-> dbhandle-> exec ($ query); offentlig funksjon findByTitle ($ title) $ SqliteResult = $ this-> dbhandle-> spørring (sprintf ('VELG * FRA BlogPosts WHERE title = "% s"', $ title)); $ blogPostAsString = $ SqliteResult-> fetchArray (SQLITE3_ASSOC); returnere ny BlogPost ($ blogPostAsString ['title'], $ blogPostAsString ['content'], $ blogPostAsString ['timestamp']);
Merk: Testen for denne implementeringen er også tilgjengelig i kildekoden, men på grunn av dens kompleksitet og lengde har jeg ikke tatt med den her.
Active Record er et av de mest kontroversielle mønstrene. Noen omfavner det (som Rails and CakePHP), og andre unngår det. Mange Objektrelasjonell kartlegging (ORM) applikasjoner bruker dette mønsteret til å lagre objekter i tabeller. Her er det skjemaet:
Som du kan se, kan Active Record-baserte objekter fortsette og hente seg selv. Dette oppnås vanligvis ved å utvide en ActiveRecordBase
klasse, en klasse som vet hvordan man skal jobbe med databasen.
Det største problemet med Active Record er strekker avhengighet. Som vi alle vet, er arv den sterkeste typen avhengighet, og det er best å unngå det mesteparten av tiden.
Før vi går videre, er her hvor vi er akkurat nå:
Gateway-grensesnittet tilhører forretningslogikken, og dens betongimplementeringer tilhører persistenslaget. Våre BlogPostController
har to avhengigheter, begge peker mot forretningslogikken: SqlitePost
gateway og Blogg innlegg
klasse.
Det er mange andre mønstre, som Proxy Mønster, som er nært knyttet til utholdenhet.
Hvis vi skulle følge Active Record-mønsteret nøyaktig slik det ble presentert av Martin Fowler i 2003-boken, Patterns of Enterprise Application Architecture, da måtte vi flytte SQL-spørringene til Blogg innlegg
klasse. Dette har imidlertid problemet med å bryte begge Dependency Inversion Principle og Åpne lukket prinsipp. Dependency Inversion Principle sier at:
Og det åpne lukkede prinsippet sier at programvare-enheter (klasser, moduler, funksjoner, etc.) skal være åpne for utvidelse, men lukket for modifikasjon. Vi vil ta en mer interessant tilnærming og integrere gatewayen i vår Active Record-løsning.
Hvis du prøver å gjøre dette på egen hånd, har du sannsynligvis allerede innsett at å legge til Active Record-mønsteret til koden, vil ødelegge ting. Av denne grunn tok jeg muligheten til å deaktivere kontrolleren og SqlitePost
tester for å konsentrere seg kun på Blogg innlegg
klasse. De første trinnene er: lage Blogg innlegg
last inn selv ved å sette konstruktøren som privat og koble den til gateway-grensesnittet. Her er den første versjonen av BlogPostTest
fil:
require_once '... /BlogPost.php'; require_once '... /InMemoryPost.php'; require_once '... /ActiveRecordBase.php'; klasse BlogPostTest utvider PHPUnit_Framework_TestCase funksjon testItCanConnectPostToGateway () $ blogPost = BlogPost :: load (); $ BlogPost-> setGateway ($ dette-> inMemoryPost ()); $ this-> assertEquals ($ blogPost-> getGateway (), $ this-> inMemoryPost ()); funksjon testItCanCreateANewAndEmptyBlogPost () $ blogPost = BlogPost :: load (); $ Dette-> assertNull ($ blogPost-> tittel); $ Dette-> assertNull ($ blogPost-> innhold); $ Dette-> assertNull ($ blogPost-> timestamp); $ this-> assertInstanceOf ('Gateway', $ blogPost-> getGateway ()); privat funksjon iMemoryPost () returner ny InMemoryPost ();
Det tester at et blogginnlegg er riktig initialisert, og at det kan ha en gateway hvis det er satt. Det er en god praksis å bruke flere påstander når de alle tester det samme konseptet og logikken.
Vår andre test har flere påstander, men alle refererer til det samme vanlige konseptet tomt blogginnlegg. Selvfølgelig, den Blogg innlegg
klassen har også blitt endret:
klasse blogpost privat $ title; privat $ innhold privat $ timestamp; privat statisk $ gateway; privat funksjon __construct ($ title = null, $ content = null, $ timestamp = null) $ this-> title = $ title; $ this-> content = $ content; $ this-> timestamp = $ tidsstempel; funksjon __get ($ navn) return $ this -> $ name; funksjon setGateway ($ gateway) self :: $ gateway = $ gateway; funksjon getGateway () return self: $ gateway; statisk funksjonslast () hvis (! selv: $ gateway) selv :: $ gateway = ny SqlitePost (); returnere nytt selv;
Den har nå a laste()
metode som returnerer et nytt objekt med en gyldig gateway. Fra dette punktet vil vi fortsette med implementeringen av a belastning ($ tittel)
metode for å lage en ny Blogg innlegg
med informasjon fra databasen. For enkel testing, implementerte jeg en InMemoryPost
klasse for utholdenhet. Det holder bare en liste over objekter i minnet og returnerer informasjonen som ønsket:
klasse InMemoryPost implementerer Gateway private $ blogPosts = array (); offentlig funksjon findByTitle ($ blogPostTitle) return array ('title' => $ this-> blogPosts [$ blogPostTitle] -> title, 'content' => $ this-> blogPosts [$ blogPostTitle] -> innhold, => $ this-> blogPosts [$ blogPostTitle] -> tidsstempel); Offentlig funksjon vedvarer (BlogPost $ blogPostObject) $ this-> blogPosts [$ blogPostObject-> title] = $ blogPostObject;
Deretter innså jeg at den første ideen om å koble til Blogg innlegg
til en gateway via en egen metode var ubrukelig. Så, jeg endret testene, følgelig:
klassen BlogPostTest utvider PHPUnit_Framework_TestCase funksjon testItCanCreateANewAndEmptyBlogPost () $ blogPost = BlogPost :: load (); $ Dette-> assertNull ($ blogPost-> tittel); $ Dette-> assertNull ($ blogPost-> innhold); $ Dette-> assertNull ($ blogPost-> timestamp); funksjon testItCanLoadABlogPostByTitle () $ gateway = $ this-> inMemoryPost (); $ aBlogPosWithData = $ this-> aBlogPostWithData ($ gateway); $ Gateway> vedvare ($ aBlogPosWithData); $ this-> assertEquals ($ aBlogPosWithData, BlogPost :: load ('some_title', null, null, $ gateway)); privat funksjon iMemoryPost () returner ny InMemoryPost (); privat funksjon aBlogPostWithData ($ gateway = null) return BlogPost :: last ('some_title', 'some content', '123', $ gateway);
Som du kan se, endret jeg radikalt Blogg innlegg
benyttes.
klasse blogpost privat $ title; privat $ innhold privat $ timestamp; privat funksjon __construct ($ title = null, $ content = null, $ timestamp = null) $ this-> title = $ title; $ this-> content = $ content; $ this-> timestamp = $ tidsstempel; funksjon __get ($ navn) return $ this -> $ name; statisk funksjonslast ($ title = null, $ content = null, $ timestamp = null, $ gateway = null) $ gateway = $ gateway? : Ny SqlitePost (); hvis (! $ innhold) $ postArray = $ gateway-> findByTitle ($ title); hvis ($ postArray) returnere nytt selv ($ postArray ['title'], $ postArray ['content'], $ postArray ['timestamp']); returnere nytt selv ($ title, $ content, $ timestamp);
De laste()
metode sjekker $ innhold
parameter for en verdi og oppretter en ny Blogg innlegg
hvis en verdi ble levert. Hvis ikke, prøver metoden å finne et blogginnlegg med den oppgitte tittelen. Hvis et innlegg er funnet, returneres det; Hvis det ikke er noen, oppretter metoden en tom måte Blogg innlegg
gjenstand.
For at denne koden skal fungere, må vi også endre hvordan gatewayen fungerer. Vår implementering må returnere en assosiativ array med tittel
, innhold
, og tidsstempel
elementer i stedet for selve objektet. Dette er en konvensjon jeg har valgt. Du kan finne andre varianter, som en vanlig matrise, mer attraktiv. Her er modifikasjonene i SqlitePostTest
:
funksjon testItCanRetrieveABlogPostByItsTitle () [...] // vi forventer en matrise i stedet for et objekt $ this-> assertEquals ($ this-> blogPostAsArray, $ gateway-> findByTitle ($ this-> blogPostAsArray ['title'])); privat funksjon aBlogPostWithValues () // vi bruker statisk belastning i stedet for konstruktør ring tilbake $ blogPost = BlogPost :: load ($ this-> blogPostAsArray ['title'], $ this-> blogPostAsArray ['content'], $ this -> blogPostAsArray [ 'stempelet']);
Og implementeringsendringene er:
offentlig funksjon findByTitle ($ title) $ SqliteResult = $ this-> dbhandle-> spørring (sprintf ('VELG * FRA BlogPosts WHERE title = "% s"', $ title)); // returnere resultatet direkte, ikke konstruer objektet tilbake $ SqliteResult-> fetchArray (SQLITE3_ASSOC);
Vi er nesten ferdige. Legg til en fortsette()
metode til Blogg innlegg
og ring alle de nylig implementerte metodene fra kontrolleren. Her er fortsette()
metode som bare vil bruke gatewayens fortsette()
metode:
Privat funksjon vedvarer () $ this-> gateway-> vedvarer ($ dette);
Og kontrolleren:
klasse BlogPostController funksjon lagre ($ data) $ blogPost = BlogPost :: last ($ data ['title'], $ data ['content'], $ data ['tidsstempel']); $ BlogPost-> vedvare (); funksjon findByTitle ($ title) return BlogPost :: load ($ title);
De BlogPostController
ble så enkelt at jeg fjernet alle sine tester. Det kaller bare Blogg innlegg
objektets fortsette()
metode. Naturligvis vil du legge til tester hvis, og når du har mer kode i kontrolleren. Kodnedlastingen inneholder fortsatt en testfil for BlogPostController
, men innholdet er kommentert.
Dette er bare toppen av isfjellet.
Du har sett to forskjellige vedholdende implementeringer: Inngangsport og Aktiv post mønstre. Fra dette punktet kan du implementere en ActiveRecordBase
abstrakt klasse for å utvide for alle klassene dine som trenger utholdenhet. Denne abstrakte klassen kan bruke forskjellige gateways for å fortsette data, og hver implementering kan til og med bruke forskjellig logikk som passer dine behov.
Men dette er bare toppen av isfjellet. Det er mange andre mønstre, for eksempel Proxy Mønster, som er nært knyttet til utholdenhet; hvert mønster fungerer for en bestemt situasjon. Jeg anbefaler at du alltid implementerer den enkleste løsningen først, og deretter implementer et annet mønster når dine behov endres.
Jeg håper du likte denne opplæringen, og jeg venter ivrig på dine meninger og alternative implementeringer til min løsning i kommentarene nedenfor.