Hur man skriver kod som gynnar förändring

Skriv kod, som är lätt att ändra, är Programmets Heliga Graal. Välkommen till programmering nirvana! Men saker är mycket svårare i verkligheten: källkoden är svår att förstå, beroende beror i otaliga riktningar, koppling är irriterande, och du känner snart värmen i programmering helvete. I den här handledningen diskuteras några principer, tekniker och idéer som hjälper dig att skriva kod som är lätt att ändra.


Några objektorienterade koncept

Objektorienterad programmering (OOP) blev populär på grund av sitt löfte om kodorganisation och återanvändning. det misslyckades helt i denna strävan. Vi har använt OOP-koncept i många år nu, men vi fortsätter att upprepade gånger genomföra samma logik i våra projekt. OOP införde en uppsättning bra grundläggande principer som, om de används korrekt, kan leda till bättre, renare kod.

sammanhållning

De saker som tillhör varandra borde hållas ihop; annars borde de flyttas någon annanstans. Det här är vad termen, sammanhållning, refererar till. Det bästa exemplet på sammanhållning kan demonstreras med en klass:

klass ANOTChesiveClass privat $ firstNumber; privat $ secondNumber; privat $ längd; privat $ bredd; funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funktion setLängd ($ längd) $ this-> length = $ length;  funktion setHight ($ höjd) $ this-> width = $ height;  funktion add () returnera $ this-> firstNumber + $ this-> secondNumber;  funktion subtrahera () returnera $ this-> firstNumber - $ this-> secondNumber;  funktionsområde () returnera $ this-> längd * $ this-> width; 

I det här exemplet definieras en klass med fält som representerar antal och storlekar. Dessa egenskaper, som endast döms av deras namn, hör inte ihop. Vi har då två metoder, Lägg till() och subtrahera (), som fungerar endast på de två talvariablerna. Vi har vidare en område() metod som fungerar på längd och bredd fält.

Det är uppenbart att den här klassen är ansvarig för separata grupper av information. Den har mycket låg sammanhållning. Låt oss refactor det.

klass ACohesiveClass privat $ firstNumber; privat $ secondNumber; funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funktion add () returnera $ this-> firstNumber + $ this-> secondNumber;  funktion subtrahera () returnera $ this-> firstNumber - $ this-> secondNumber; 

Det här är en hög sammanhängande klass. Varför? Eftersom varje del av denna klass hör till varandra. Du bör sträva efter sammanhållning, men akta dig, det kan vara svårt att uppnå.

ortogonalitet

I enkla termer hänvisar ortogonalitet till isolering eller eliminering av biverkningar. En metod, klass eller modul som ändrar tillståndet för andra orelaterade klasser eller moduler är inte ortogonalt. Till exempel är en flygplans svart låda ortogonal. Den har sin inre funktionalitet, inre strömkälla, mikrofoner och sensorer. Det har ingen inverkan på det flygplan som ligger i eller i den yttre världen. Det ger bara en mekanism för att spela in och hämta flygdata.

Ett exempel på ett sådant icke-ortogonalt system är din bils elektronik. Öka din bils hastighet har flera biverkningar, till exempel ökad radiovolym (bland annat). Hastigheten är inte ortogonal mot bilen.

klasskalkylator privat $ firstNumber; privat $ secondNumber; funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funktion add () $ sum = $ this-> firstNumber + $ this-> secondNumber; om ($ sum> 100) (new AlertMechanism ()) -> tooBigNumber ($ sum);  returnera $ sum;  funktion subtrahera () returnera $ this-> firstNumber - $ this-> secondNumber;  klass AlertMechanism funktion tooBigNumber ($ number) echo $ nummer. 'är för stor!'; 

I det här exemplet är Kalkylator klassens Lägg till() Metod uppvisar oväntat beteende: det skapar en AlertMechanism objekt och kallar en av dess metoder. Detta är oväntat och oönskat beteende; bibliotekskonsumenter kommer aldrig att förvänta sig ett meddelande som skrivs ut på skärmen. I stället förväntar de sig bara summan av de angivna siffrorna.

klasskalkylator privat $ firstNumber; privat $ secondNumber; funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funktion add () returnera $ this-> firstNumber + $ this-> secondNumber;  funktion subtrahera () returnera $ this-> firstNumber - $ this-> secondNumber;  klass AlertMechanism funktion checkLimits ($ firstNumber, $ secondNumber) $ sum = (ny kalkylator ($ firstNumber, $ secondNumber)) -> add (); om ($ sum> 100) $ this-> tooBigNumber ($ sum);  funktion tooBigNumber ($ number) echo $ nummer. 'är för stor!'; 

Detta är bättre. AlertMechanism har ingen effekt på Kalkylator. Istället, AlertMechanism använder vad som helst för att avgöra om en varning ska utfärdas.

Beroende och koppling

I de flesta fall är dessa två ord utbytbara; men i vissa fall föredras en term över en annan.

Så vad är det? beroendet? När objektet en behöver använda objekt B, För att kunna utföra sitt föreskrivna beteende säger vi det en beror på B. I OOP är beroenden extremt vanliga. Objekt arbetar ofta med och beror på varandra. Så, när du eliminerar beroende är en ädlad strävan, är det nästan omöjligt att göra det. Det är dock föredraget att kontrollera beroenden och minska dem.

Villkoren, tung-koppling och lös-koppling, brukar hänvisa till hur mycket ett objekt beror på andra objekt.

I ett löst kopplat system har ändringar i ett objekt minskad effekt på de andra objekten som är beroende av det. I sådana system beror klasser på gränssnitt istället för konkreta implementeringar (vi kommer att prata mer om det senare). Därför är löstkopplade system mer öppna för modifikationer.

Koppling i ett fält

Låt oss överväga ett exempel:

klass Visa privat $ kalkylator; funktion __construct () $ this-> calculator = ny kalkylator (1,2); 

Det är vanligt att se den här typen av kod. En klass, Visa i detta fall beror på Kalkylator klass genom att direkt hänvisa till den klassen. I ovanstående kod, Visa's $ kalkylator fältet är av typ Kalkylator. Objektet som innehåller fältet är ett resultat av att direkt ringer Kalkylatorkonstruktör.

Koppling genom att komma åt andra klassmetoder

Granska följande kod för en demonstration av denna typ av koppling:

klass Visa privat $ kalkylator; funktion __construct () $ this-> calculator = ny kalkylator (1, 2);  funktion printSum () echo $ this-> calculator-> add (); 

De Visa klassen kallar Kalkylator objekt Lägg till() metod. Detta är en annan form av koppling, eftersom en klass får tillgång till den andra metoden.

Koppling med metodreferens

Du kan koppla klasser med metodreferenser också. Till exempel:

 klass Visa privat $ kalkylator; funktion __construct () $ this-> calculator = $ this-> makeCalculator ();  funktion printSum () echo $ this-> calculator-> add ();  funktion makeCalculator () returnera ny kalkylator (1, 2); 

Det är viktigt att notera att makeCalculator () Metoden returnerar a Kalkylator objekt. Detta är ett beroende.

Koppling genom polymorfism

Erfarenhet är förmodligen den starkaste formen av beroende:

klass AdvancedCalculator utökar kalkylator funktion sinus ($ värde) return sin ($ value); 

Inte bara kan AdvancedCalculator inte jobba utan Kalkylator, men det kunde inte ens existera utan det.

Minskar koppling genom beroendeinsprutning

Man kan minska kopplingen genom att injicera ett beroende. Här är ett sådant exempel:

klass Visa privat $ kalkylator; funktion __construct (Calculator $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  // ... //

Genom att injicera Kalkylator objekt genom Visas konstruktör, vi minskade Visaberoende av Kalkylator klass. Men det här är bara hälften av lösningen.

Minska koppling med gränssnitt

Vi kan ytterligare minska kopplingen genom att använda gränssnitt. Till exempel:

gränssnitt CanCompute funktion add (); funktion subtrahera ();  klass Calculator implementerar CanCompute privat $ firstNumber; privat $ secondNumber; funktion __construct ($ firstNumber, $ secondNumber) $ this-> firstNumber = $ firstNumber; $ this-> secondNumber = $ secondNumber;  funktion add () returnera $ this-> firstNumber + $ this-> secondNumber;  funktion subtrahera () returnera $ this-> firstNumber - $ this-> secondNumber;  klass Visa privat $ kalkylator; funktion __construct (CanCompute $ calculator = null) $ this-> calculator = $ calculator? : $ this-> makeCalculator ();  funktion printSum () echo $ this-> calculator-> add ();  funktion makeCalculator () returnera ny kalkylator (1, 2); 

Du kan tänka på Internetleverantören som en högre sammanhållningsprincip.

Denna kod introducerar CanCompute gränssnitt. Ett gränssnitt är lika abstrakt som du kan få i OOP; Det definierar de medlemmar som en klass måste genomföra. I fallet med ovanstående exempel, Kalkylator implementerar CanCompute gränssnitt.

VisaKonstruktör förväntar sig ett objekt som implementerar CanCompute. Vid denna punkt, Visaberoende av Kalkylator är effektivt bruten. Vi kan när som helst skapa en annan klass som implementerar CanCompute och vidarebefordra ett objekt av den klassen till Visakonstruktör. Visa Nu beror bara på CanCompute gränssnitt, men även det beroendet är valfritt. Om vi ​​inte skickar några argument till Visakonstruktör, det kommer helt enkelt skapa en klassiker Kalkylator objekt genom att ringa makeCalculator (). Denna teknik används ofta och är extremt användbar för testdriven utveckling (TDD).


SOLID-principerna

SOLID är en uppsättning principer för att skriva ren kod, vilket gör det lättare att ändra, behålla och utöka i framtiden. De är rekommendationer som, när de tillämpas på källkod, har en positiv effekt på hållbarheten.

En liten historia

SOLID-principerna, även kända som Agile-principer, definierades initialt av Robert C. Martin. Trots att han inte uppfann alla dessa principer var han den som satte dem ihop. Du kan läsa mer om dem i sin bok: Agile Software Development, Principles, Patterns, and Practices. SOLIDs principer täcker ett brett spektrum av ämnen, men jag kommer presentera dem så enkelt som möjligt. Fråga dig om ytterligare detaljer i kommentarerna, om det behövs.

Princip för enkelansvar (SRP)

En klass har ett enda ansvar. Det låter enkelt, men det kan ibland vara svårt att förstå och genomföra.

klass Reporter funktion generateIncomeReports (); funktion generatePaymentsReports (); funktionen computeBalance (); funktion printReport (); 

Vem tycker du om fördelar med detta klassers beteende? Tja, en bokföringsavdelning är ett alternativ (för balansen), finansavdelningen kan vara en annan (för inkomst- / betalningsrapporter), och även arkivavdelningen skulle kunna skriva ut och arkivera rapporterna.

Det finns fyra anledningar till varför du kanske måste ändra den här klassen. varje avdelning kan vilja att deras respektive metoder anpassas efter deras behov.

SRP rekommenderar att sådana klasser bryts i mindre, beahvior-specifika klasser, var och en har bara en anledning att ändra. Sådana klasser tenderar att vara mycket sammanhängande och löst kopplade. På ett sätt definieras SRP sammanhållning ur användarnas synvinkel.

Öppet stängt princip (OCP)

Klasser (och moduler) bör välkomna utvidgningen av deras funktionalitet, samt motstå modifikationer av deras nuvarande funktionalitet. Låt oss spela med det klassiska exemplet på en elfläkt. Du har en strömbrytare och du vill styra fläkten. Så kan du skriva något enligt följande:

klass Switch_ privat $ fan; funktion __construct () $ this-> fan = new Fan ();  funktion turnOn () $ this-> fan-> på ();  funktion turnOff () $ this-> fan-> off (); 

Arv är förmodligen den starkaste formen av beroende.

Denna kod definierar a Växla_ klass som skapar och kontrollerar a Fläkt objekt. Observera underskriften efter "Switch_". PHP tillåter dig inte att definiera en klass med namnet "Switch".

Din chef bestämmer att han vill styra ljuset med samma strömbrytare. Detta är ett problem, för att du måste byta Växla_.

Eventuella ändringar av befintlig kod är en risk; Andra delar av systemet kan påverkas och kräva ytterligare ändringar. Det är alltid att föredra att lämna befintlig funktionalitet ensam, när man lägger till ny funktionalitet.

I OOP-terminologi kan du se det Växla_ har ett starkt beroende av Fläkt. Det är här vårt problem ligger, och var vi borde göra våra förändringar.

gränssnitt Switchable funktion på (); funktion av ();  klassen Fläktutrustningar Växlingsbar allmän funktion på () // kod för att starta fläkten offentlig funktion av () // kod för att stoppa fläkten klassen Switch_ privat $ switchable; funktion __construct (Switchable $ switchable) $ this-> switchable = $ switchable;  funktion turnOn () $ this-> switchable-> på ();  funktion turnOff () $ this-> switchable-> off (); 

Denna lösning introducerar omkopplingsbar gränssnitt. Det definierar de metoder som alla växlingsaktiverade objekt behöver implementera. De Fläkt redskap omkopplingsbar, och Växla_ accepterar en hänvisning till a omkopplingsbar objekt inom sin konstruktör.

Hur hjälper det oss till?

För det första bryter denna lösning beroendet mellan Växla_ och Fläkt. Växla_ har ingen aning om att det börjar bli en fan, och det bryr sig inte om det. För det andra introducerar a Ljus klassen påverkar inte Växla_ eller omkopplingsbar. Vill du styra a Ljus objekt med din Växla_ klass? Skapa bara en Ljus objekt och vidarebefordra det till Växla_, så här:

klass Ljusanordningar Växlabar (allmän funktion på () // kod för att stänga av] allmän funktion av () // kod för att stänga av klass SomeWhereInYourCode function controlLight () $ light = new Light (); $ switch = ny switch _ ($ light); $ Kopplings-> TURNON (); $ Kopplings-> avstängning (); 

Liskov Substitution Principle (LSP)

LSP säger att en barnklass aldrig ska bryta moderklassens funktionalitet. Detta är extremt viktigt eftersom konsumenterna i en förälderklass förväntar sig att klassen ska agera på ett visst sätt. Att skicka en barnklass till en konsument bör bara fungera och inte påverka den ursprungliga funktionaliteten.

Detta är förvirrande vid första anblicken, så låt oss titta på ett annat klassiskt exempel:

klass rektangel privat $ bredd; privat $ höjd; funktion setWidth ($ bredd) $ this-> width = $ width;  funktion setHeigth ($ heightth) $ this-> height = $ heightth;  funktionsområde () returnera $ this-> width * $ this-> height; 

Detta exempel definierar en enkel Rektangel klass. Vi kan ställa in höjd och bredd, och dess område() Metoden ger rektangelns område. Använda Rektangel klassen kan se ut som följande:

klass geometri funktion rectArea (rektangel $ rektangel) $ rektangel-> setWidth (10); $ Rectangle-> setHeigth (5); returnera $ rektangel-> område (); 

De rectArea () Metoden accepterar a Rektangel objekt som argument, ställer in höjd och bredd och returnerar formens yta.

I skolan lär vi oss att fyrkanter är rektanglar. Detta antyder att om vi modellerar vårt program till vårt geometriska objekt, a Fyrkant klassen bör förlänga a Rektangel klass. Hur skulle en sådan klass se ut?

klass kvadrat sträcker sig rektangel // Vilken kod att skriva här? 

Jag har svårt att ta reda på vad jag ska skriva i Fyrkant klass. Vi har flera alternativ. Vi skulle kunna åsidosätta område() metod och returnera torget av $ bredd:

klass rektangel protected $ width; skyddad $ höjd; // ... // klass Square sträcker sig rektangel funktionsområde () returnera $ this-> width ^ 2; 

Observera att jag ändrade Rektangels fält till skyddad, ger Fyrkant tillgång till dessa fält. Detta ser rimligt ur en geometrisk synvinkel. En fyrkant har lika sidor; återvänder torgets bredd är rimligt.

Vi har dock ett problem från en programmeringspunkt-of-view. Om Fyrkant är en Rektangel, Vi borde inte ha några problem med att mata in det Geometri klass. Men genom att göra så kan du se det GeometriKodens kod ger ingen mening; den ställer in två olika värden för höjd och bredd. Det är därför en kvadrat är inte en rektangel i programmering. LSP kränkts.

Gränssnitt Segregation Princip (ISP)

Enhetstester ska springa fort - mycket snabbt.

Denna princip koncentrerar sig på att bryta stora gränssnitt till små, specialiserade gränssnitt. Grundtanken är att olika konsumenter i samma klass inte borde veta om de olika gränssnitten - bara de gränssnitt konsumenten behöver använda. Även om en konsument inte direkt använder alla offentliga metoder på ett objekt, beror det fortfarande på alla metoder. Så varför inte ge gränssnitt med det bara förklara de metoder som varje användare behöver?

Det är nära att gränssnitt ska tillhöra kunderna och inte till genomförandet. Om du skräddarsy dina gränssnitt till de konsumtiva klasserna kommer de att respektera ISP. Implementeringen i sig kan vara unik, eftersom en klass kan implementera flera gränssnitt.

Låt oss föreställa oss att vi genomför en börsapplikation. Vi har en mäklare som köper och säljer aktier, och det kan rapportera dess dagliga vinster och förluster. En mycket enkel implementering skulle innehålla något som a Mäklare gränssnitt, a NYSEBroker klass som implementerar Mäklare och ett par användargränssnittskurser: en för att skapa transaktioner (TransactionsUI) och en för rapportering (DailyReporter). Koden för ett sådant system kan likna följande:

gränssnittsmäklare funktionsköp ($ symbol, $ volym); funktion sälja ($ symbol, $ volym); funktion dailyLoss ($ date); funktion dailyEarnings ($ date);  klass NYSEBroker implementerar Broker public function buy ($ symbol, $ volym) // implementations går här offentlig funktion currentBalance () // implementations går här offentlig funktion dailyEarnings ($ date) // implementations går här public funktion dailyLoss ($ date) // implementations går här public function sell ($ symbol, $ volym) // implementations går här class TransactionsUI privat $ broker; funktion __construct (Broker $ broker) $ this-> broker = $ broker;  funktion buyStocks () // UI logic här för att få information från en blankett till $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volym']);  funktion sellStocks () // UI logic här för att få information från en blankett till $ data $ this-> broker-> sälja ($ data ['sybmol'], $ data ['volume']);  klass DailyReporter privat $ mäklare; funktion __construct (Broker $ broker) $ this-> broker = $ broker;  funktion currentBalance () echo 'Aktuell balans för idag'. datum Tid()) . "\ N"; eko "Resultat:". $ this-> broker-> dailyEarnings (tid ()). "\ N"; eko "Förluster:". $ this-> broker-> dailyLoss (tid ()). "\ N"; 

Medan den här koden kan fungera bryter den till Internetleverantören. Både DailyReporter och TransactionUI beror på Mäklare gränssnitt. Dock använder de bara en bråkdel av gränssnittet. TransactionUI använder köpa() och sälja() metoder, medan DailyReporter använder dailyEarnings () och dailyLoss () metoder.

Du kan argumentera för det Mäklare är inte sammanhängande eftersom det har metoder som inte är relaterade och därmed inte tillhör varandra.

Detta kan vara sant, men svaret beror på genomförandet av Mäklare; Försäljning och köp kan vara starkt relaterad till nuvarande förluster och resultat. Till exempel får du inte tillåtas köpa aktier om du förlorar pengar.

Du kan också argumentera för det Mäklare bryter också mot SRP. Eftersom vi har två klasser som använder den på olika sätt kan det finnas två olika användare. Jo, jag säger nej. Den enda användaren är förmodligen den faktiska mäklaren. Han / hon vill köpa, sälja och se deras nuvarande medel. Men återigen beror det faktiska svaret på hela systemet och verksamheten.

Internetleverantören bryts säkert. Bägge UI-klasserna beror på hela Mäklare. Detta är ett vanligt problem, om du tror att gränssnitt tillhör deras implementeringar. Om du ändrar din synvinkel kan du föreslå följande design:

gränssnitt BrokerTransactions funktionsköp ($ symbol, $ volym); funktion sälja ($ symbol, $ volym);  gränssnitt BrokerStatistics funktion dailyLoss ($ date); funktion dailyEarnings ($ date);  klass NYSEBroker implementerar BrokerTransactions, BrokerStatistics public function buy $ symbol, $ volym // implementations går här offentlig funktion currentBalance () // implementations går här public function dailyEarnings ($ date) // implementations går här  offentlig funktion dailyLoss ($ date) // implementations går här public function sell ($ symbol, $ volym) // implementations går här class TransactionsUI privat $ broker; funktion __construct (BrokerTransactions $ broker) $ this-> broker = $ broker;  funktion buyStocks () // UI logic här för att få information från en blankett till $ data $ this-> broker-> buy ($ data ['sybmol'], $ data ['volym']);  funktion sellStocks () // UI logic här för att få information från en blankett till $ data $ this-> broker-> sälja ($ data ['sybmol'], $ data ['volume']);  klass DailyReporter privat $ mäklare; funktion __construct (BrokerStatistics $ broker) $ this-> broker = $ broker;  funktion currentBalance () echo 'Aktuell balans för idag'. datum Tid()) . "\ N"; eko "Resultat:". $ this-> broker-> dailyEarnings (tid ()). "\ N"; eko "Förluster:". $ this-> broker-> dailyLoss (tid ()). "\ N"; 

Detta är faktiskt meningsfullt och respekterar Internetleverantören. DailyReporter beror endast på BrokerStatistics; det bryr sig inte och behöver inte veta om några försäljnings- och köpoperationer. TransactionsUI, Å andra sidan vet bara om köp och försäljning. De NYSEBroker är identisk med vår tidigare klass, förutom att den nu implementerar BrokerTransactions och BrokerStatistics gränssnitt.

Du kan tänka på Internetleverantören som en högre sammanhållningsprincip.

När båda gränssnittsklasserna berodde på Mäklare gränssnittet liknade de två klasser, var och en med fyra fält, varav två användes i en metod och de andra två i en annan metod. Klassen skulle inte ha varit mycket sammanhängande.

Ett mer komplext exempel på denna princip finns i en av Robert C. Martins första dokument om ämnet: Gränssnittssegregationsprincipen.

Dependency Inversion Princip (DIP)

Denna princip säger att högnivåmoduler inte bör bero på lågnivåmoduler. båda bör bero på abstraktioner. Abstraktioner bör inte bero på detaljer; detaljer bör bero på abstraktioner. Enkelt sagt, du borde vara beroende av abstraktioner så mycket som möjligt och aldrig på konkreta implementeringar.

Tricket med DIP är att du vill vända beroende, men vill alltid hålla kontrollflödet. Låt oss granska vårt exempel från OCP (the Växla och Ljus klasser). I den ursprungliga implementeringen hade vi en strömbrytare som direkt kontrollerade ett ljus.

Som du kan se, beror både beroendet och kontrollen från Växla mot Ljus. Medan det här är vad vi vill, vill vi inte direkt bero på Ljus. Så vi introducerade ett gränssnitt.

Det är fantastiskt hur enkelt att införa ett gränssnitt gör att vår kod respekterar både DIP och OCP. Som ni kan se nej, beror klassen på det konkreta genomförandet av Ljus, och båda Ljus och Växla beror på omkopplingsbar gränssnitt. Vi inverterade beroendet, och kontrollflödet var oförändrat.


Hög nivå design

En annan viktig aspekt av din kod är din design på hög nivå och allmän arkitektur. En entangled arkitektur producerar kod som är svår att modifiera. Att hålla en ren arkitektur är avgörande, och det första steget är att förstå hur man skiljer din kods olika problem.

I den här bilden försökte jag sammanfatta de viktigaste problemen. I centrum av schemat är vår affärslogik. Det ska vara väl isolerat från resten av världen och kunna arbeta och uppträda som förväntat utan att någon av de andra delarna finns. Se den som ortogonalitet på en högre nivå.

Från höger har du din "huvud" - ingångspunkten till applikationen - och de fabriker som skapar objekt. En idealisk lösning skulle få sina föremål från specialiserade fabriker, men det är oftast omöjligt eller opraktiskt. Fortfarande bör du använda fabriker när du har möjlighet att göra det och behålla dem utanför din affärslogik.

Sedan, längst ner (i orange) har vi uthållighet (databaser, filåtkomst, nätverkskommunikation) i syfte att beständig information. Inget objekt i vår affärslogik borde veta hur uthållighet fungerar.

Till vänster är leveransmekanismen.

En MVC, som Laravel eller CakePHP, borde bara vara leveransmekanismen, inget mer.

Detta låter dig byta en mekanism med en annan utan att röra din affärslogik. Det här kan vara skandalöst för några av er. Vi får veta att vår affärslogik ska placeras i våra modeller. Jo, jag håller inte med mig. Våra modeller ska vara "request models", dvs dumma dataobjekt som används för att överföra information från MVC till affärslogiken. Eventuellt ser jag inga problem inklusive inmatningsvalidering i modellerna, men inget mer. Affärslogik bör inte finnas i modellerna.

När du tittar på programmets arkitektur eller katalogstruktur bör du se en struktur som föreslår vad programmet gör i motsats till vilken ram eller databas du använde.

Slutligen, se till att alla beroenden pekar mot vår affärslogik. Användargränssnitt, fabriker, databaser är mycket konkreta implementeringar, och du borde aldrig bero på dem. Omvända beroenden för att peka mot vår affärslogik modularizes vårt system, så att vi kan ändra beroenden utan att ändra affärslogiken.


Några tankar om designmönster

Designmönster spelar en viktig roll för att göra kod enklare att modifiera genom att erbjuda en gemensam designlösning som alla programmerare kan förstå. Ur en strukturell synvinkel är designmönster naturligtvis fördelaktiga. De är väl testade och genomtänkta lösningar.

Om du vill lära dig mer om designmönster skapade jag en Tuts + Premium kurs på dem!


Kraften av testning

Testdriven utveckling uppmuntrar skrivkod som är lätt att testa. TDD tvingar dig att respektera de flesta ovanstående principer för att göra din kod lätt att testa. Injicera beroenden och skriva ortogonala klasser är väsentliga; annars slutar du med stora testmetoder. Enhetstesterna bör springa fort - mycket snabbt, faktiskt, och allt som inte testas ska vara bespottat. Mocking många komplexa klasser för ett enkelt test kan vara överväldigande. Så när du upptäcker att du tappar tio objekt för att testa en enda metod i en klass kan du ha problem med din kod ... inte ditt test.


Slutgiltiga tankar

I slutet av dagen kommer allt ner till hur mycket du bryr dig om din källkod. Att ha teknisk kunskap räcker inte. du måste använda den kunskapen om och om igen, aldrig vara 100% nöjd med din kod. Du måste vilja göra din kod lätt att underhålla, rena och öppna för förändring.

Tack för att du läser och känner dig fri att bidra med dina tekniker i kommentarerna nedan.