Enkelt ansvar (SRP), Open / Closed (OCP), Liskovs Substitution, Interface Segregation, och Dependency Inversion. Fem smidiga principer som bör vägleda dig varje gång du behöver skriva kod.
Programvaruenheter (klasser, moduler, funktioner, etc.) ska vara öppna för förlängning men stängda för modifiering.
Det öppna / stängda principen, OCP i korthet, krediteras Bertrand Mayer, en fransk programmerare, som först publicerade den i sin bok n Object Oriented Software Construction 1988.
Principen ökade i början av 2000-talet när den blev en av SOLID-principerna som definierades av Robert C. Martin i sin bok Agile Software Development, Principles, Patterns, and Practices och senare publicerades i C # versionen av boken Agile Principles, Patterns , och praxis i C #.
Vad vi i grund och botten pratar om här är att utforma våra moduler, klasser och funktioner på ett sätt som när en ny funktionalitet behövs, bör vi inte ändra vår befintliga kod utan snarare skriva ny kod som kommer att användas av befintlig kod. Det här låter lite konstigt, speciellt om vi arbetar på språk som Java, C, C ++ eller C # där det inte bara gäller källkoden utan även binären. Vi vill skapa nya funktioner på sätt som inte kräver att vi omfördelar befintliga binärer, körbara filer eller DLL-filer.
När vi går vidare med dessa handledning kan vi lägga varje ny princip i sammanhanget med de redan diskuterade. Vi diskuterade redan det enda ansvaret (SRP) som påstod att en modul bara skulle ha en anledning att ändra. Om vi tänker på OCP och SRP kan vi observera att de är komplementära. Kod som är speciellt utformad med SRP i åtanke kommer att vara nära OCP-principerna eller lätt att göra det respektera dessa principer. När vi har kod som har en enda anledning att ändra, skapar en ny funktion en sekundär orsak till den förändringen. Så både SRP och OCP skulle brytas. På samma sätt, om vi har kod som bara bör ändras när dess huvudfunktion ändras och borde förbli oförändrad när en ny funktion läggs till den, så att OCP respekteras, kommer de flesta att respektera SRP också.
Detta betyder inte att SRP alltid leder till OCP eller vice versa, men i de flesta fall om en av dem respekteras, är det ganska enkelt att nå den andra..
Ur rent teknisk synpunkt är det öppna / slutna principen mycket enkelt. En enkel relation mellan två klasser, som den som nedan bryter mot OCP.
De Användare
klassen använder Logik
klass direkt. Om vi behöver genomföra en sekund Logik
klass på ett sätt som tillåter oss att använda både den nuvarande och den nya, den befintliga Logik
klassen måste ändras. Användare
är direkt knutet till genomförandet av Logik
, Det finns inget sätt för oss att ge en ny Logik
utan att påverka den nuvarande. Och när vi pratar om statiskt typade språk är det mycket möjligt att Användare
klassen kommer också att kräva ändringar. Om vi pratar om sammanställda språk, så är det säkert både Användare
körbar och Logik
körbart eller dynamiskt bibliotek kommer att kräva omkompilering och omfördelning till våra kunder, en process vi vill undvika när det är möjligt.
Baserat endast på schemat ovan kan man dra slutsatsen att en klass som direkt använder en annan klass faktiskt skulle bryta mot Open / Closed Principle. Och det är rätt, strikt talat. Jag tyckte det var ganska intressant att hitta gränserna, det ögonblick när du ritar linjen och bestämmer att det är svårare att respektera OCP än att ändra befintlig kod eller att arkitektoniska kostnader inte motiverar kostnaden för att ändra befintlig kod.
Låt oss säga att vi vill skriva en klass som kan ge framsteg som en procent för en fil som laddas ned via vår ansökan. Vi kommer att ha två huvudklasser, a Framsteg
och a Fil
, och jag antar att vi kommer att vilja använda dem som i testet nedan.
funktion testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ fil-> längd = 200; $ fil-> skickat = 100; $ progress = nytt framsteg ($ file); $ this-> assertEquals (50, $ progress-> getAsPercent ());
I detta test är vi en användare av Framsteg
. Vi vill få ett värde som procent, oberoende av den faktiska filstorleken. Vi använder Fil
som källa till information för vår Framsteg
. En fil har en längd i byte och ett fält som heter skickat
som representerar mängden data som skickas till den som gör hämtningen. Vi bryr oss inte om hur dessa värden uppdateras i ansökan. Vi kan anta att det finns någon magisk logik som gör det för oss, så i ett test kan vi uttrycka dem tydligt.
klassfil public $ length; offentlig $ skickad;
De Fil
klassen är bara ett enkelt dataobjekt som innehåller de två fälten. Självklart i verkligheten skulle det förmodligen innehålla annan information och beteende också, som filnamn, sökväg, relativ sökväg, aktuell katalog, typ, behörigheter och så vidare.
klassprogress privat $ file; funktion __construct (File $ file) $ this-> file = $ file; funktion getAsPercent () return $ this-> file-> skickat * 100 / $ this-> file-> length;
Framsteg
är helt enkelt en klass som tar a Fil
i sin konstruktör. För tydligheten specificerade vi typen av variabeln i konstruktörens parametrar. Det finns en enda användbar metod på Framsteg
, getAsPercent ()
, som tar de värden som skickas och längden från Fil
och omvandla dem till en procent. Enkelt, och det fungerar.
Testningen startade klockan 17:39 ... PHPUnit 3.7.28 av Sebastian Bergmann ... Tid: 15 ms, Minne: 2,50Mb OK (1 test, 1 påstående)
Denna kod verkar vara rätt, men den bryter mot det öppna / stängda principen. Men varför? Och hur?
Varje applikation som förväntas utvecklas i tid kommer att behöva nya funktioner. En ny funktion för vår ansökan kan vara att tillåta streaming av musik, istället för att bara hämta filer. Fil
längd representeras i byte, musikens längd på några sekunder. Vi vill erbjuda en bra progressiv bar till våra lyssnare, men kan vi återanvända den vi redan har?
Nej vi kan inte. Våra framsteg är bundna till Fil
. Det förstår bara filer, även om det även kan tillämpas på musikinnehåll. Men för att kunna göra det måste vi ändra det, vi måste göra Framsteg
vet om musik
och Fil
. Om vår design skulle respektera OCP, skulle vi inte behöva röra Fil
eller Framsteg
. Vi kunde bara helt enkelt återanvända det befintliga Framsteg
och tillämpa det på musik
.
Dynamiskt typade språk har fördelen att gissa typ av objekt vid körning. Detta tillåter oss att ta bort typsnittet från Framsteg
'konstruktör och koden kommer fortfarande att fungera.
klassprogress privat $ file; funktion __construct ($ file) $ this-> file = $ file; funktion getAsPercent () return $ this-> file-> skickat * 100 / $ this-> file-> length;
Nu kan vi kasta något på Framsteg
. Och med något menar jag bokstavligen någonting:
klassmusik public $ length; offentlig $ skickad; offentlig $ artist; offentligt $ album; offentlig $ releaseDate; funktion getAlbumCoverFile () returnera 'Bilder / Omslag /'. $ this-> artist. '/'. $ this-> albumet. '.Png';
Och a musik
klass som den ovanstående kommer att fungera bra. Vi kan testa det enkelt med ett mycket liknande test till Fil
.
funktionstestItCanGetTheProgressOfAMusicStreamAsAPercent () $ music = new Music (); $ musik-> längd = 200; $ musik-> skickad = 100; $ progress = nya framsteg ($ musik); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Så i princip kan allt mätbart innehåll användas med Framsteg
klass. Kanske bör vi uttrycka detta i kod genom att ändra variabelns namn också:
klassprogress privat $ measurableContent; funktion __construct ($ measurableContent) $ this-> measurableContent = $ measurableContent; funktion getAsPercent () return $ this-> measurableContent-> skickat * 100 / $ this-> measurableContent-> length;
Bra, men vi har ett stort problem med detta tillvägagångssätt. När vi hade Fil
specificerad som typtyp, var vi positiva till vad vår klass kan hantera. Det var tydligt och om något annat kom in, berättade ett fint fel oss så.
Argument 1 skickad till Progress :: __ construct () måste vara en förekomst av File, instance of Music given.
Men utan typsnittet måste vi lita på det faktum att det som kommer in kommer att ha två offentliga variabler av några exakta namn som "längd
"och"skickat
". Annars kommer vi att ha en vägrade erövring.
Nekad erövring: En klass som överträder en metod för en basklass på ett sådant sätt att kontraktet från basklassen inte är hedrad av den härledda klassen. ~ Källa Wikipedia.
Detta är en av kod luktar presenteras i mycket mer detalj i Detecting Code Smells Premium Course. Kort sagt, vi vill inte sluta försöka ringa metoder eller åtkomstfält på objekt som inte överensstämmer med vårt kontrakt. När vi hade en typhyp, specificerades kontraktet av det. Fälten och metoderna hos Fil
klass. Nu när vi inte har något, kan vi skicka in någonting, till och med en sträng och det skulle leda till ett fult fel.
funktionstestItFailsWithAParameterThatDoesNotRespectTheImplicitContract () $ progress = new Progress ('some string'); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Ett test som detta, där vi skickar in en enkel sträng, kommer att producera en vägrad erövring:
Försöker få tillgång till icke-objekt.
Medan slutresultatet är detsamma i båda fallen, vilket innebär att koden bryts, producerade den första ett fint meddelande. Den här är dock mycket obskyrlig. Det finns inget sätt att veta vad variabeln är - en sträng i vårt fall - och vilka egenskaper som söktes och hittades inte. Det är svårt att felsöka och lösa problemet. En programmerare behöver öppna Framsteg
klass och läs det och förstå det. Kontraktet, i det här fallet, när vi inte explicit anger typtypen, definieras av beteendet hos Framsteg
. Det är ett implicit kontrakt, bara känt för Framsteg
. I vårt exempel definieras det av åtkomsten till de två fälten, skickat
och längd
, i getAsPercent ()
metod. I det verkliga livet kan det implicita kontraktet vara väldigt komplext och svårt att upptäcka genom att bara leta efter några sekunder i klassen.
Denna lösning rekommenderas endast om inget av de andra förslagen nedan lätt kan genomföras eller om de skulle medföra allvarliga arkitektoniska förändringar som inte motiverar ansträngningen.
Detta är den vanligaste och förmodligen den mest lämpliga lösningen för att respektera OCP. Det är enkelt och effektivt.
Strategimönstret introducerar helt enkelt användningen av ett gränssnitt. Ett gränssnitt är en särskild typ av enhet i Objektorienterad Programmering (OOP) som definierar ett kontrakt mellan en klient och en serieklass. Båda klasserna följer kontraktet för att säkerställa det förväntade beteendet. Det kan finnas flera, orelaterade, serieklasser som respekterar samma kontrakt så att de kan betjäna samma klientklass.
gränssnitt Mätbar funktion getLength (); funktion getSent ();
I ett gränssnitt kan vi definiera enbart beteende. Det är därför vi istället för att direkt använda offentliga variabler måste tänka på att använda getters och setters. Att anpassa de andra klasserna kommer inte vara svårt vid denna tidpunkt. Vår IDE kan göra det mesta av jobbet.
funktion testItCanGetTheProgressOfAFileAsAPercent () $ file = new File (); $ Fil-> setLength (200); $ Fil-> setSent (100); $ progress = nytt framsteg ($ file); $ this-> assertEquals (50, $ progress-> getAsPercent ());
Som vanligt börjar vi med våra test. Vi måste använda setters för att ställa in värdena. Om det anses obligatoriskt kan dessa setter också definieras i Mätbar
gränssnitt. Var dock försiktig med vad du lägger där. Gränssnittet är att definiera kontraktet mellan klientklassen Framsteg
och de olika serieklasserna som Fil
och musik
. gör Framsteg
behöver du ställa in värdena? Antagligen inte. Så setterna är högst osannolikt att behöva definieras i gränssnittet. Om du skulle definiera setterna där skulle du också tvinga alla serieklasser att implementera setters. För vissa av dem kan det vara logiskt att få setters, men andra kan uppträda helt annorlunda. Vad händer om vi vill använda vår Framsteg
klass för att visa temperaturen på vår ugn? De OvenTemperature
klassen kan initieras med värdena i konstruktören eller få informationen från en tredje klass. Vem vet? Att ha setter på den klassen skulle vara udda.
klassfilen implementerar mätbara privata $ längd; privat $ skickad; offentligt $ filnamn; offentlig $ ägare funktion setLängd ($ längd) $ this-> length = $ length; funktion getLength () returnera $ this-> längd; funktion setSent ($ skickat) $ this-> skickat = $ skickat; funktion getSent () return $ this-> skickat; funktion getRelativePath () return dirname ($ this-> filnamn); funktion getFullPath () returnera realpath ($ this-> getRelativePath ());
De Fil
klassen ändras något för att tillgodose kraven ovan. Det implementerar nu Mätbar
gränssnitt och har setters och getters för de områden vi är intresserade av. musik
är mycket lika, kan du kontrollera innehållet i den bifogade källkoden. Vi är nästan färdiga.
klassprogress privat $ measurableContent; funktion __construct (mätbart $ mätbart innehåll) $ this-> measurableContent = $ measurableContent; funktion getAsPercent () return $ this-> measurableContent-> getSent () * 100 / $ this-> measurableContent-> getLength ();
Framsteg
behövde också en liten uppdatering. Vi kan nu ange en typ med typhinting i konstruktören. Den förväntade typen är Mätbar
. Nu har vi ett tydligt kontrakt. Framsteg
kan vara säker på att de åtkomna metoderna alltid kommer att vara närvarande eftersom de är definierade i Mätbar
gränssnitt. Fil
och musik
kan också vara säker på att de kan ge allt som behövs för Framsteg
genom att helt enkelt implementera alla metoder på gränssnittet, ett krav när en klass implementerar ett gränssnitt.
Detta designmönster förklaras mer ingående i kursen Agile Design Patterns.
Människor brukar namnge gränssnitt med en huvudstad jag
framför dem, eller med ordet "Gränssnitt
"bifogad i slutet, som ifile
eller FileInterface
. Detta är en gammal notation som införs av vissa föråldrade standarder. Vi är så mycket förbi de ungerska notationerna eller behovet av att ange typen av en variabel eller ett objekt i sitt namn för att lättare kunna identifiera det. IDE: er identifierar vad som helst i en delad sekund för oss. Detta gör att vi kan koncentrera oss på vad vi faktiskt vill abstrahera.
Gränssnitt tillhör sina kunder. Ja. När du vill namnge ett gränssnitt måste du tänka på klienten och glömma genomförandet. När vi namngav vårt gränssnitt Measurable så tänkte vi på Progress. Om jag skulle vara en framsteg, vad skulle jag behöva för att kunna ge procenten? Svaret är enkelt, något vi kan mäta. Således namnet Measurable.
En annan anledning är att implementeringen kan komma från olika domäner. I vårt fall finns det filer och musik. Men vi kan mycket väl återanvända vår Framsteg
i en racesimulator. I så fall skulle de uppmätta klasserna vara hastighet, bränsle etc. Trevligt, är det inte?
Template Metods designmönster är mycket lik strategin, men i stället för ett gränssnitt använder den en abstrakt klass. Det rekommenderas att använda ett mallmetodmönster när vi har en klient som är väldigt specifik för vår applikation, med minskad återanvändbarhet och när serieklasserna har vanligt beteende.
Detta designmönster förklaras mer ingående i kursen Agile Design Patterns.
Så hur påverkar allt detta vår arkitektur på hög nivå?
Om bilden ovan representerar den aktuella arkitekturen i vår applikation, bör en ny modul med fem nya klasser (de blåa) påverka måttet på vår design (röd klass).
I de flesta system kan du inte förvänta dig absolut effekt på befintlig kod när nya klasser introduceras. Att respektera det öppna / slutna principen kommer emellertid att avsevärt minska de klasser och moduler som kräver konstant förändring.
Som med någon annan princip, försök att inte tänka på allt från tidigare. Om du gör det kommer du att sluta med ett gränssnitt för varje klass. En sådan design blir svår att behålla och förstå. Vanligtvis är det säkraste sättet att tänka på möjligheterna och om du kan avgöra om det finns andra typer av serverklasser. Många gånger kan du enkelt föreställa dig en ny funktion eller du kan hitta en på projektets eftersläp som kommer att producera en annan serieklass. I så fall lägger du till gränssnittet från början. Om du inte kan bestämma, eller om du är osäker - det mesta av tiden - helt enkelt släppa bort det. Låt nästa programmerare, eller kanske till och med själv, lägga till gränssnittet när du behöver en andra implementering.
Om du följer din disciplin och lägger till gränssnitt så snart som en andra server behövs kommer ändringar att vara få och enkla. Kom ihåg att om kod krävs ändras en gång finns det en stor möjlighet att det kommer att behöva ändras igen. När den möjligheten blir till verklighet kommer OCP att spara mycket tid och ansträngning.
Tack för att du läste.