Skrivekode, som er lett å endre, er Programmets Hellige Graal. Velkommen til programmering nirvana! Men det er mye vanskeligere i virkeligheten: kildekode er vanskelig å forstå, avhengigheter peker i utallige retninger, kobling er irriterende, og du føler deg snart varm med programmering helvete. I denne veiledningen vil vi diskutere noen prinsipper, teknikker og ideer som vil hjelpe deg med å skrive kode som er lett å endre.
Objektorientert programmering (OOP) ble populær, på grunn av sitt løfte om kodeorganisasjon og gjenbruk; det mislyktes helt i dette forsøket. Vi har brukt OOP konsepter i mange år nå, men vi fortsetter å gjentatte ganger implementere samme logikk i våre prosjekter. OOP introduserte et sett med gode grunnleggende prinsipper som, hvis de brukes riktig, kan føre til bedre, renere kode.
Tingene som hører sammen bør holdes sammen; Ellers bør de flyttes andre steder. Dette er hva begrepet sammenheng refererer til. Det beste eksempelet på sammenhold kan demonstreres med en klasse:
klasse ANOTChesiveClass private $ firstNumber; privat $ secondNumber; privat $ lengde; privat $ bredde; funksjon __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; funksjon setLength ($ lengde) $ this-> lengde = $ lengde; funksjon setHet ($ høyde) $ this-> width = $ height; funksjon legge til () return $ this-> firstNumber + $ this-> secondNumber; Funksjon subtrahere () return $ this-> firstNumber - $ this-> secondNumber; funksjonsområde () return $ this-> lengde * $ this-> width;
Dette eksemplet definerer en klasse med felt som representerer tall og størrelser. Disse egenskapene, dømt bare av navnene deres, tilhører ikke sammen. Vi har da to metoder, Legg til()
og subtrahere ()
, som opererer på bare de to tallvariablene. Vi har videre en område()
metode, som opererer på lengde
og bredde
Enger.
Det er åpenbart at denne klassen er ansvarlig for separate grupper av informasjon. Den har svært lav sammenheng. La oss reflektere det.
klasse acohesiveClass private $ firstNumber; privat $ secondNumber; funksjon __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; funksjon legge til () return $ this-> firstNumber + $ this-> secondNumber; Funksjon subtrahere () return $ this-> firstNumber - $ this-> secondNumber;
Dette er en høyt sammenhengende klasse. Hvorfor? Fordi hver del av denne klassen tilhører hverandre. Du bør streve for sammenhold, men vær forsiktig, det kan være vanskelig å oppnå.
Enkelt sagt, ortogonalitet refererer til isolering eller eliminering av bivirkninger. En metode, klasse eller modul som endrer tilstanden til andre ikke-relaterte klasser eller moduler, er ikke ortogonal. For eksempel er et flys sorte boks ortogonalt. Den har sin indre funksjonalitet, indre strømkilde, mikrofoner og sensorer. Det har ingen effekt på flyet det ligger i, eller i den ytre verden. Det gir bare en mekanisme for å registrere og hente flydata.
Et eksempel på et slikt ikke-ortogonalt system er bilens elektronikk. Øke bilens hastighet har flere bivirkninger, som for eksempel økt radiovolum (blant annet). Hastigheten er ikke ortogonal mot bilen.
klassekalkulator privat $ firstNumber; privat $ secondNumber; funksjon __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; funksjon legge til () $ sum = $ this-> firstNumber + $ this-> secondNumber; hvis ($ sum> 100) (ny AlertMechanism ()) -> tooBigNumber ($ sum); returner $ sum; Funksjon subtrahere () return $ this-> firstNumber - $ this-> secondNumber; klasse AlertMechanism funksjon forBigNummer ($ nummer) echo $ nummer. 'er for stor!';
I dette eksemplet er Kalkulator
klassens Legg til()
Metoden viser uventet oppførsel: det skaper en AlertMechanism
objekt og kaller en av metodene sine. Dette er uventet og uønsket oppførsel; bibliotek forbrukere vil aldri forvente en melding trykt på skjermen. I stedet forregner de bare summen av de angitte tallene.
klassekalkulator privat $ firstNumber; privat $ secondNumber; funksjon __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; funksjon legge til () return $ this-> firstNumber + $ this-> secondNumber; Funksjon subtrahere () return $ this-> firstNumber - $ this-> secondNumber; klasse AlertMechanism function checkLimits ($ firstNumber, $ secondNumber) $ sum = (ny kalkulator ($ firstNumber, $ secondNumber)) -> add (); hvis ($ sum> 100) $ this-> tooBigNumber ($ sum); funksjon forBigNummer ($ nummer) echo $ nummer. 'er for stor!';
Dette er bedre. AlertMechanism
har ingen effekt på Kalkulator
. I stedet, AlertMechanism
bruker alt det trenger for å avgjøre om det skal utstedes et varsel.
I de fleste tilfeller er disse to ordene utskiftbare; men i noen tilfeller er et begrep foretrukket over en annen.
Så hva er en avhengighet? Når objektet EN
må bruke objekt B
, For å utføre sin foreskrevne oppførsel, sier vi det EN
kommer an på B
. I OOP er avhengighet ekstremt vanlig. Objekter jobber ofte med og avhenger av hverandre. Så, mens eliminering av avhengighet er en edel forfølgelse, er det nesten umulig å gjøre det. Det er imidlertid å foretrekke å kontrollere avhengigheter og redusere dem.
Vilkårene, tung-kopling og løs-kopling, Vanligvis refererer du til hvor mye et objekt avhenger av andre objekter.
I et løskoblet system har endringer i ett objekt redusert effekt på de andre objektene som avhenger av det. I slike systemer avhenger klasser av grensesnitt i stedet for konkrete implementeringer (vi vil snakke mer om det senere). Dette er grunnen til at løst koblede systemer er mer åpne for modifikasjoner.
La oss vurdere et eksempel:
klasse Vis privat $ kalkulator; funksjon __construct () $ this-> kalkulator = ny kalkulator (1,2);
Det er vanlig å se denne typen kode. En klasse, Vise
i dette tilfellet avhenger av Kalkulator
klasse ved å direkte henvise til den klassen. I ovennevnte kode, Vise
's $ kalkulator
feltet er av typen Kalkulator
. Objektet som inneholder, er et resultat av direkte anrop Kalkulator
konstruktør.
Gå gjennom følgende kode for en demonstrasjon av denne typen kobling:
klasse Vis privat $ kalkulator; funksjon __construct () $ this-> kalkulator = ny kalkulator (1, 2); funksjon printSum () echo $ this-> kalkulator-> legg til ();
De Vise
klassen ringer på Kalkulator
objektets Legg til()
metode. Dette er en annen form for kobling, fordi en klasse får tilgang til den andre metoden.
Du kan også kombinere klasser med metode referanser, også. For eksempel:
klasse Vis privat $ kalkulator; funksjon __construct () $ this-> calculator = $ this-> makeCalculator (); funksjon printSum () echo $ this-> kalkulator-> legg til (); funksjon makeCalculator () returner ny kalkulator (1, 2);
Det er viktig å merke seg at makeCalculator ()
metode returnerer a Kalkulator
gjenstand. Dette er en avhengighet.
Arv er trolig den sterkeste formen av avhengighet:
klasse AdvancedCalculator utvider kalkulator funksjon sinus ($ verdi) return sin ($ verdi);
Ikke bare kan AdvancedCalculator
ikke gjør jobben sin uten Kalkulator
, men det kunne ikke engang eksistere uten det.
Man kan redusere koblingen ved å injisere en avhengighet. Her er et slikt eksempel:
klasse Vis privat $ kalkulator; funksjon __construct (Kalkulator $ kalkulator = null) $ this-> kalkulator = $ kalkulator? : $ this-> makeCalculator (); // // //
Ved å injisere Kalkulator
objekt gjennom Vise
Konstruktør, vi reduserte Vise
s avhengighet av Kalkulator
klasse. Men dette er bare halvparten av løsningen.
Vi kan videre redusere koblingen ved hjelp av grensesnitt. For eksempel:
grensesnitt CanCompute funksjon add (); funksjon subtrahere (); klassekalkulator implementerer CanCompute private $ firstNumber; privat $ secondNumber; funksjon __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber; funksjon legge til () return $ this-> firstNumber + $ this-> secondNumber; Funksjon subtrahere () return $ this-> firstNumber - $ this-> secondNumber; klasse Vis privat $ kalkulator; funksjon __construct (CanCompute $ kalkulator = null) $ this-> kalkulator = $ kalkulator? : $ this-> makeCalculator (); funksjon printSum () echo $ this-> kalkulator-> legg til (); funksjon makeCalculator () returner ny kalkulator (1, 2);
Du kan tenke på ISP som et høyere nivå av kohesjonsprinsipp.
Denne koden introduserer CanCompute
grensesnitt. Et grensesnitt er like abstrakt som du kan få i OOP; Det definerer medlemmene som en klasse må gjennomføre. I tilfelle av eksemplet ovenfor, Kalkulator
implementerer CanCompute
grensesnitt.
Vise
Konstruktør forventer en gjenstand som implementerer CanCompute
. På dette punktet, Vise
s avhengighet med Kalkulator
er effektivt ødelagt. Når som helst, kan vi opprette en annen klasse som implementerer CanCompute
og pass et objekt av den klassen til Vise
konstruktør. Vise
Avhenger nå bare av CanCompute
grensesnitt, men selv den avhengigheten er valgfri. Hvis vi ikke sender noen argumenter til Vise
Konstruktør, det vil bare skape en klassiker Kalkulator
objekt ved å ringe makeCalculator ()
. Denne teknikken brukes ofte, og er ekstremt nyttig for testdrevet utvikling (TDD).
SOLID er et sett med prinsipper for å skrive ren kode, som gjør det lettere å endre, vedlikeholde og forlenge i fremtiden. De er anbefalinger som, når de brukes på kildekoden, har en positiv effekt på vedlikehold.
SOLID-prinsippene, også kjent som Agile-prinsipper, ble først definert av Robert C. Martin. Selv om han ikke oppfant alle disse prinsippene, var han den som satte dem sammen. Du kan lese mer om dem i sin bok: Agile Software Development, Principles, Patterns, and Practices. SOLIDs prinsipper dekker et bredt spekter av emner, men jeg vil presentere dem så enkelt som mulig. Du er velkommen til å be om ytterligere detaljer i kommentarene, om nødvendig.
En klasse har et enkelt ansvar. Dette kan høres enkelt, men det kan noen ganger være vanskelig å forstå og sette i bruk.
klasse reporter funksjon generateIncomeReports (); funksjon generatePaymentsReports (); funksjonscomputeBalance (); funksjon printReport ();
Hvem tror du på fordelene med denne klassens oppførsel? Vel, en regnskapsavdeling er et alternativ (for balansen), finansdepartementet kan være en annen (for inntekts- / betalingsrapporter), og selv arkiveringsavdelingen kan skrive ut og arkivere rapportene.
Det er fire grunner til at du kanskje må bytte denne klassen; hver avdeling kan ønske at deres respektive metoder tilpasses deres behov.
SRP anbefaler å bryte slike klasser i mindre, beahvior-spesifikke klasser, hver med bare en grunn til å endre. Slike klasser har en tendens til å være svært sammenhengende og løst koblet. På en måte defineres SRP kohesjon fra brukerens synspunkt.
Klasser (og moduler) bør ønske utvidelsen av funksjonaliteten deres, samt motstå modifikasjoner av deres nåværende funksjonalitet. La oss leke med det klassiske eksempelet på en elektrisk vifte. Du har en bryter og du vil kontrollere viften. Så, kan du skrive noe i tråd med:
klasse Switch_ private $ fan; funksjon __construct () $ this-> fan = new Fan (); funksjon turnOn () $ this-> fan-> på (); funksjon turnOff () $ this-> fan-> off ();
Arv er trolig den sterkeste formen av avhengighet.
Denne koden definerer a Bytte om_
klasse som skaper og kontrollerer a Fan
gjenstand. Vær oppmerksom på understreken etter "Switch_". PHP tillater deg ikke å definere en klasse med navnet "Switch".
Sjefen din bestemmer at han vil kontrollere lyset med samme bryter. Dette er et problem, fordi du må endre seg Bytte om_
.
Eventuelle endringer i eksisterende kode er en risiko; Andre deler av systemet kan bli påvirket og kreve enda ytterligere modifikasjoner. Det er alltid å foretrekke å forlate eksisterende funksjonalitet alene når man legger til ny funksjonalitet.
I OOP-terminologi kan du se det Bytte om_
har en sterk avhengighet av Fan
. Det er her vårt problem ligger, og hvor vi skal gjøre endringene våre.
grensesnitt Switchable funksjon på (); funksjon av (); Klasse Vifteutstyr Variable (offentlig funksjon på () // kode for å starte viften offentlig funksjon av () // kode for å stoppe viften klasse Switch_ private $ switchable; funksjon __construct (Switchable $ switchable) $ this-> switchable = $ switchable; funksjon turnOn () $ this-> switchable-> on (); funksjon turnOff () $ this-> switchable-> off ();
Denne løsningen introduserer switch~~POS=TRUNC
grensesnitt. Det definerer metodene som alle bytteaktiverte objekter må implementere. De Fan
redskaper switch~~POS=TRUNC
, og Bytte om_
aksepterer en referanse til a switch~~POS=TRUNC
objekt innenfor sin konstruktør.
Hvordan hjelper dette oss?
For det første bryter denne løsningen avhengigheten mellom Bytte om_
og Fan
. Bytte om_
har ingen anelse om at det starter en fan, og det bryr seg heller ikke. For det andre, introduserer en Lys
klassen vil ikke påvirke Bytte om_
eller switch~~POS=TRUNC
. Ønsker du å kontrollere en Lys
objekt med din Bytte om_
klasse? Bare opprett en Lys
objekt og send det til Bytte om_
, som dette:
klasse Light implementer Switchable offentlig funksjon på () // kode for å slå lett på offentlig funksjon av () // kode for å slå av klasse SomeWhereInYourCode function controlLight () $ light = new Light (); $ bryter = ny bryter _ ($ lys); $ Sentralbord> turnon (); $ Sentralbord> avkjøring ();
LSP sier at en barneklasse aldri bør bryte funksjonaliteten til foreldreklassen. Dette er ekstremt viktig fordi forbrukere av en foreldersklasse forventer at klassen skal oppføre seg på en bestemt måte. Å overføre en barneklass til en forbruker bør bare fungere og ikke påvirke den opprinnelige funksjonaliteten.
Dette er forvirrende ved første øyekast, så la oss se på et annet klassisk eksempel:
klasse rektangel privat $ bredde; privat $ høyde; funksjon setWidth ($ width) $ this-> width = $ width; funksjon setHeigth ($ heightth) $ this-> height = $ heightth; funksjonsområde () return $ this-> width * $ this-> height;
Dette eksemplet definerer en enkel Rektangel
klasse. Vi kan sette sin høyde og bredde, og dens område()
Metoden gir rektangelens område. Bruker Rektangel
klassen kan se ut som følgende:
klasse geometri funksjon rectArea (rektangel $ rektangel) $ rektangel-> setWidth (10); $ Rectangle-> setHeigth (5); returnere $ rektangel-> område ();
De rectArea ()
metoden aksepterer a Rektangel
objekt som argument, angir høyde og bredde, og returnerer formens område.
I skolen blir vi lært at kvadrater er rektangler. Dette hint at hvis vi modellerer vårt program til vårt geometriske objekt, a Torget
klassen skal utvide a Rektangel
klasse. Hvordan ville en slik klasse se ut?
klassen firkantet strekker seg rektangel // hvilken kode å skrive her?
Jeg har det vanskelig å finne ut hva jeg skal skrive i Torget
klasse. Vi har flere alternativer. Vi kunne overstyre område()
metode og returnere kvadratet av $ bredde
:
klasse rektangel beskyttet $ width; beskyttet $ høyde; // ... // klasse Square utvider rektangel funksjonsområde () return $ this-> width ^ 2;
Merk at jeg endret Rektangel
s felter til beskyttet
, gi Torget
tilgang til disse feltene. Dette ser rimelig ut fra et geometrisk synspunkt. En firkant har like sider; å returnere kvadratet av bredden er rimelig.
Vi har imidlertid et problem fra et programmeringspunktspunkt. Hvis Torget
er en Rektangel
, Vi burde ikke ha noe problem å mate det inn i Geometry
klasse. Men ved å gjøre det kan du se det Geometry
kodeksen gir ingen mening den setter to forskjellige verdier for høyde og bredde. Det er derfor et torg er ikke et rektangel i programmering. LSP overtrådt.
Enhetstester skal løpe fort - veldig raskt.
Dette prinsippet fokuserer på å bryte store grensesnitt i små, spesialiserte grensesnitt. Den grunnleggende ideen er at ulike forbrukere av samme klasse ikke skal vite om de forskjellige grensesnittene - bare grensesnittene forbrukeren trenger å bruke. Selv om en forbruker ikke direkte bruker alle offentlige metoder på et objekt, er det fortsatt avhengig av alle metodene. Så hvorfor ikke gi grensesnitt med det kun erklære metodene som hver bruker trenger?
Dette er i nært samsvar at grensesnitt skal tilhøre kundene og ikke til implementeringen. Hvis du skreddersy grensesnittene dine til forbrukerklassen, respekterer de ISP. Implementeringen i seg selv kan være unik, da en klasse kan implementere flere grensesnitt.
La oss forestille oss at vi implementerer et aksjemarkedsapplikasjon. Vi har en megler som kjøper og selger aksjer, og den kan rapportere sin daglige inntjening og tap. En veldig enkel implementering vil inkludere noe som a Megler
grensesnitt, a NYSEBroker
klasse som implementerer Megler
og et par brukergrensesnittklasser: en for å skape transaksjoner (TransactionsUI
) og en for rapportering (DailyReporter
). Koden for et slikt system kan lignes på følgende:
grensesnitt Broker funksjonskjøp ($ symbol, $ volum); funksjon selge ($ symbol, $ volum); funksjon dagligLoss ($ date); Funksjon dagligEarnings ($ date); klasse NYSEBroker implementerer megler (offentlig funksjon kjøp ($ symbol, $ volum) // implementeringer går her offentlig funksjon currentBalance () // implementeringer går her offentlig funksjon dailyEarnings ($ date) // implementeringer går her offentlig funksjon dailyLoss ($ date) // implementeringer går her offentlig funksjon selge ($ symbol, $ volum) // implementeringer går her klasse TransaksjonerUI privat $ megler; funksjon __construct (Broker $ megler) $ this-> megler = $ megler; funksjon buyStocks () // UI logikk her for å få informasjon fra et skjema til $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volum']); funksjon sellStocks () // UI logikk her for å få informasjon fra et skjema til $ data $ this-> broker-> selge ($ data ['sybmol'], $ data ['volum']); klasse DailyReporter privat $ megler; funksjon __construct (Broker $ megler) $ this-> megler = $ megler; funksjon currentBalance () echo 'Nåværende balanse for i dag'. dato tid()) . "\ N"; ekko 'Inntjening:'. $ this-> broker-> dailyEarnings (tid ()). "\ N"; ekko 'Tap:'. $ this-> broker-> dailyLoss (tid ()). "\ N";
Selv om denne koden kan fungere, krenker den Internett-leverandøren. Både DailyReporter
og TransactionUI
avhenger av Megler
grensesnitt. Imidlertid bruker de bare en brøkdel av grensesnittet. TransactionUI
bruker kjøpe()
og selge()
metoder, mens DailyReporter
bruker dailyEarnings ()
og dailyLoss ()
fremgangsmåter.
Du kan hevde det
Megler
er ikke sammenhengende fordi den har metoder som ikke er tilknyttet, og dermed ikke tilhører sammen.
Dette kan være sant, men svaret avhenger av implementeringen av Megler
; salg og kjøp kan være sterkt knyttet til nåværende tap og inntjening. For eksempel kan du ikke få lov til å kjøpe aksjer hvis du mister penger.
Du kan også argumentere for det Megler
bryter også mot SRP. Fordi vi har to klasser som bruker den på forskjellige måter, kan det være to forskjellige brukere. Vel, jeg sier nei. Den eneste brukeren er trolig den faktiske megleren. Han / hun ønsker å kjøpe, selge og se sine nåværende midler. Men igjen er det faktiske svaret avhengig av hele systemet og virksomheten.
ISP er sikkert brutt. Begge UI-klassene avhenger av det hele Megler
. Dette er et vanlig problem, hvis du tror at grensesnitt tilhører deres implementeringer. Hvis du skifter synspunktet ditt, kan du imidlertid foreslå følgende design:
grensesnitt BrokerTransactions funksjonskjøp ($ symbol, $ volum); funksjon selge ($ symbol, $ volum); grensesnitt BrokerStatistics funksjon dailyLoss ($ date); Funksjon dagligEarnings ($ date); klasse NYSEBroker implementerer BrokerTransactions, BrokerStatistics offentlig funksjon kjøp ($ symbol, $ volum) // implementeringer går her offentlig funksjon currentBalance () // implementeringer går her offentlig funksjon dailyEarnings ($ date) // implementeringer går her offentlig funksjon dailyLoss ($ date) // implementeringer går her offentlig funksjon selge ($ symbol, $ volum) // implementeringer går her klasse TransaksjonerUI privat $ megler; funksjon __construct (BrokerTransactions $ megler) $ this-> megler = $ megler; funksjon buyStocks () // UI logikk her for å få informasjon fra et skjema til $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volum']); funksjon sellStocks () // UI logikk her for å få informasjon fra et skjema til $ data $ this-> broker-> selge ($ data ['sybmol'], $ data ['volum']); klasse DailyReporter privat $ megler; funksjon __construct (BrokerStatistics $ megler) $ this-> megler = $ megler; funksjon currentBalance () echo 'Nåværende balanse for i dag'. dato tid()) . "\ N"; ekko 'Inntjening:'. $ this-> broker-> dailyEarnings (tid ()). "\ N"; ekko 'Tap:'. $ this-> broker-> dailyLoss (tid ()). "\ N";
Dette gir faktisk mening og respekterer Internett-leverandøren. DailyReporter
Avhenger bare av BrokerStatistics
; det bryr seg ikke og trenger ikke å vite om salg og kjøp. TransactionsUI
, på den annen side vet bare om kjøp og salg. De NYSEBroker
er identisk med vår forrige klasse, bortsett fra at den nå implementerer BrokerTransactions
og BrokerStatistics
grensesnitt.
Du kan tenke på ISP som et høyere nivå av kohesjonsprinsipp.
Når begge UI-klassene var avhengig av Megler
grensesnitt, de lignet to klasser, hver med fire felt, hvorav to ble brukt i en metode og de andre to i en annen metode. Klassen ville ikke vært veldig sammenhengende.
Et mer komplekst eksempel på dette prinsippet finnes i en av Robert C. Martins første artikler om emnet: Grensesnitt Segregeringsprinsippet.
Dette prinsippet fastslår at moduler på høyt nivå ikke bør avhenge av lavt nivå moduler; Begge bør avhenge av abstraksjoner. Abstraksjoner bør ikke avhenge av detaljer; detaljer bør avhenge av abstraksjoner. Enkelt sagt, du bør stole på abstraksjoner så mye som mulig og aldri på konkrete implementeringer.
Trikset med DIP er at du vil reversere avhengigheten, men vil alltid beholde kontrollen. La oss se gjennom vårt eksempel fra OCP (the Bytte om
og Lys
klasser). I den opprinnelige implementeringen hadde vi en bryter som kontrollerte direkte et lys.
Som du ser kan både avhengighet og kontroll flyte fra Bytte om
mot Lys
. Selv om dette er hva vi vil, vil vi ikke direkte avhenge av Lys
. Så vi introduserte et grensesnitt.
Det er utrolig hvordan bare å introdusere et grensesnitt gjør vår kode respekt for både DIP og OCP. Som du ser nei, avhenger klassen av den konkrete gjennomføringen av Lys
, og begge deler Lys
og Bytte om
avhenger av switch~~POS=TRUNC
grensesnitt. Vi vendte om avhengigheten, og kontrollen var uendret.
Et annet viktig aspekt av koden er design og generell arkitektur på høyt nivå. En entangled arkitektur produserer kode som er vanskelig å modifisere. Å holde en ren arkitektur er viktig, og det første trinnet er å forstå hvordan du skiller kodenes forskjellige bekymringer.
I dette bildet forsøkte jeg å oppsummere de viktigste bekymringene. I sentrum av skjemaet er vår forretningslogikk. Det skal være godt isolert fra resten av verden, og kunne jobbe og oppføre seg som forventet uten at noen av de andre delene eksisterer. Se det som ortogonalitet på et høyere nivå.
Fra høyre har du din "hoved" - inngangspunktet til søknaden - og fabrikkene som lager objekter. En ideell løsning vil få sine gjenstander fra spesialiserte fabrikker, men det er for det meste umulig eller upraktisk. Likevel bør du bruke fabrikker når du har muligheten til å gjøre det, og holde dem utenfor forretningslogikken din.
Deretter, nederst (i oransje), har vi utholdenhet (databaser, filtilgang, nettverkskommunikasjon) med sikte på vedvarende informasjon. Ingen objekt i vår forretningslogikk burde vite hvor utholdenhet fungerer.
Til venstre er leveringsmekanismen.
En MVC, som Laravel eller CakePHP, bør bare være leveringsmekanismen, ikke noe mer.
Dette lar deg bytte en mekanisme med en annen uten å berøre forretningslogikken din. Dette kan høres opprørende mot noen av dere. Vi blir fortalt at vår forretningslogikk skal plasseres i våre modeller. Vel, jeg er uenig. Våre modeller skal være "forespørsel modeller", dvs. dumme dataobjekter som brukes til å overføre informasjon fra MVC til forretningslogikken. Eventuelt ser jeg ikke noe problem inkludert inndatvalidering i modellene, men ikke noe mer. Forretningslogikk bør ikke være i modellene.
Når du ser på programmets arkitektur eller katalogstruktur, bør du se en struktur som antyder hva programmet gjør i motsetning til hvilket rammeverk eller database du brukte.
Til slutt, sørg for at alle avhengigheter peker mot vår forretningslogikk. Brukergrensesnitt, fabrikker, databaser er svært konkrete implementeringer, og du bør aldri stole på dem. Omvendt avhengighetene til å peke mot forretningslogikken modulerer vårt system, slik at vi kan endre avhengigheter uten å endre forretningslogikken.
Designmønstre spiller en viktig rolle for å gjøre kode enklere å endre ved å tilby en felles designløsning som alle programmerer kan forstå. Fra et strukturelt synspunkt er designmønstre åpenbart fordelaktige. De er godt testede og gjennomtenkte løsninger.
Hvis du vil lære mer om designmønstre, har jeg opprettet et Tuts + Premium kurs på dem!
Testdrevet utvikling oppfordrer til skrivekode som er lett å teste. TDD tvinger deg til å respektere de fleste av de ovennevnte prinsippene for å gjøre koden din lett å teste. Injiserende avhengigheter og skrive ortogonale klasser er avgjørende; Ellers ende du med store testmetoder. Enhetstestene bør løpe fort - veldig raskt, faktisk, og alt som ikke er testet, skal bli stappet. Mocking mange komplekse klasser for en enkel test kan være overveldende. Så når du finner deg selv tullende ti objekter for å teste en enkelt metode på en klasse, kan det hende du har et problem med koden din ... ikke testen din.
På slutten av dagen kommer alt ned til hvor mye du bryr deg om kildekoden din. Å ha teknisk kunnskap er ikke nok; du må bruke den kunnskapen igjen og igjen, aldri å være 100% fornøyd med koden din. Du må gjøre koden din enkel å vedlikeholde, ren og åpen for å endre.
Takk for at du leser og gjerne bidrar med dine teknikker i kommentarene nedenfor.