Skapa verkligen modulär kod utan beroenden
utveckla programvara är bra, men… jag tror att vi alla kan vara överens om att det kan vara lite av en känslomässig berg-och dalbana. I början är allt bra. Du lägger till nya funktioner efter varandra i fråga om dagar om inte timmar. Du är på rulle!
spola framåt några månader, och din utvecklingshastighet minskar. Är det för att du inte arbetar så hårt som tidigare? Egentligen inte. Låt oss spola framåt några månader, och din utvecklingshastighet sjunker ytterligare. Att arbeta med detta projekt är inte kul längre och har blivit ett drag.
det blir värre. Du börjar upptäcka flera buggar i din ansökan. Ofta skapar lösningen av en bugg två nya. Vid denna tidpunkt kan du börja sjunga:
99 små buggar i koden.99 små buggar.Ta en ner, lappa den runt,
…127 små buggar i koden.
vad tycker du om att arbeta med det här projektet nu? Om du är som jag börjar du förmodligen förlora din motivation. Det är bara en smärta att utveckla denna applikation, eftersom varje ändring av befintlig kod kan få oförutsägbara konsekvenser.
denna erfarenhet är vanlig i mjukvaruvärlden och kan förklara varför så många programmerare vill kasta bort källkoden och skriva om allt.
- anledningar till varför mjukvaruutveckling saktar ner över tiden
- stor boll av lera och hur man kan minska det
- en lösning med modulär kod
- hur andra branscher löser detta Problem
- Inversion of Control är din vän
- Problem
- varför Dependency Injection fick allt fel
- lösningen för modulär kod
- Elementmönster
- Elementarkitektur
- Hands-on exempel
- Utveckla Snabbare, Återanvänd Oftare!
anledningar till varför mjukvaruutveckling saktar ner över tiden
så vad är orsaken till detta problem?
huvudorsaken är ökande komplexitet. Från min erfarenhet är den största bidragsgivaren till övergripande komplexitet det faktum att i de allra flesta mjukvaruprojekt är allt anslutet. På grund av de beroenden som varje klass har, om du ändrar någon kod i klassen som skickar e-post, kan dina användare plötsligt inte registrera sig. Varför är det så? Eftersom din registreringskod beror på koden som skickar e-post. Nu kan du inte ändra någonting utan att införa buggar. Det är helt enkelt inte möjligt att spåra alla beroenden.
så där har du det; den verkliga orsaken till våra problem är att öka komplexiteten från alla beroenden som vår kod har.
stor boll av lera och hur man kan minska det
rolig sak är, det här problemet har varit känt i flera år nu. Det är ett vanligt anti-mönster som kallas “big ball of mud.”Jag har sett den typen av arkitektur i nästan alla projekt jag arbetat med genom åren i flera olika företag.
så vad är det här anti-mönstret exakt? Enkelt uttryckt får du en stor boll av lera när varje element har ett beroende med andra element. Nedan kan du se en graf över beroenden från det välkända open source-projektet Apache Hadoop. För att visualisera den stora bollen av lera (eller snarare den stora bollen av garn), ritar du en cirkel och placerar klasser från projektet jämnt på den. Rita bara en linje mellan varje par klasser som är beroende av varandra. Nu kan du se källan till dina problem.
en lösning med modulär kod
så jag ställde mig en fråga: skulle det vara möjligt att minska komplexiteten och fortfarande ha kul som i början av projektet? Sanningen ska fram, Du kan inte eliminera all komplexitet. Om du vill lägga till nya funktioner måste du alltid höja kodkomplexiteten. Ändå kan komplexiteten flyttas och separeras.
hur andra branscher löser detta Problem
Tänk på den mekaniska industrin. När någon liten mekanisk butik skapar maskiner köper de en uppsättning standardelement, skapar några anpassade och sätter dem ihop. De kan göra dessa komponenter helt separat och montera allt i slutet, vilket gör bara några tweaks. Hur är detta möjligt? De vet hur varje element kommer att passa ihop med fastställda industristandarder som bultstorlekar och uppåtgående beslut som storleken på monteringshål och avståndet mellan dem.
varje element i aggregatet ovan kan tillhandahållas av ett separat företag som inte har någon som helst kunskap om slutprodukten eller dess andra delar. Så länge varje modulelement tillverkas enligt specifikationerna kommer du att kunna skapa den slutliga enheten som planerat.
kan vi replikera det i mjukvaruindustrin?
visst kan vi! Genom att använda gränssnitt och inversion av kontrollprincip; det bästa är det faktum att detta tillvägagångssätt kan användas på alla objektorienterade språk: Java, C#, Swift, TypeScript, JavaScript, PHP-listan fortsätter och fortsätter. Du behöver inte någon snygg ram för att tillämpa den här metoden. Du behöver bara hålla dig till några enkla regler och vara disciplinerad.
Inversion of Control är din vän
när jag först hörde talas om inversion of control insåg jag omedelbart att jag hade hittat en lösning. Det är ett koncept att ta befintliga beroenden och invertera dem genom att använda gränssnitt. Gränssnitt är enkla deklarationer av metoder. De ger inte något konkret genomförande. Som ett resultat kan de användas som ett avtal mellan två element om hur man ansluter dem. De kan användas som modulära kontakter, om du vill. Så länge ett element tillhandahåller gränssnittet och ett annat element tillhandahåller implementeringen för det, kan de arbeta tillsammans utan att veta något om varandra. Det är lysande.
Låt oss se på ett enkelt exempel hur kan vi koppla bort vårt system för att skapa modulär kod. Diagrammen nedan har implementerats som enkla Java-applikationer. Du hittar dem på detta GitHub-arkiv.
Problem
låt oss anta att vi har en mycket enkel applikation som endast består av en Main
– klass, tre tjänster och en enda Util
– klass. Dessa element är beroende av varandra på flera sätt. Nedan kan du se en implementering med “big ball of mud” – metoden. Klasser kallar helt enkelt varandra. De är tätt kopplade, och du kan inte bara ta ut ett element utan att röra andra. Program som skapats med den här stilen gör att du initialt kan växa snabbt. Jag tror att den här stilen är lämplig för proof-of-concept-projekt eftersom du enkelt kan leka med saker. Ändå är det inte lämpligt för produktionsklara lösningar eftersom även underhåll kan vara farligt och varje enskild förändring kan skapa oförutsägbara buggar. Diagrammet nedan visar denna stora boll av lera arkitektur.
varför Dependency Injection fick allt fel
i en sökning efter ett bättre tillvägagångssätt kan vi använda en teknik som kallas dependency injection. Denna metod förutsätter att alla komponenter ska användas via gränssnitt. Jag har läst påståenden att det frikopplar element, men gör det verkligen? Nej. Ta en titt på diagrammet nedan.
den enda skillnaden mellan den nuvarande situationen och en stor boll av lera är det faktum att vi nu, istället för att ringa klasser direkt, kallar dem genom deras gränssnitt. Det förbättrar något separerande element från varandra. Om du till exempel vill återanvända Service A
i ett annat projekt kan du göra det genom att ta ut Service A
själv, tillsammans med Interface A
, samt Interface B
och Interface Util
. Som du kan se beror Service A
fortfarande på andra element. Som ett resultat får vi fortfarande problem med att ändra kod på ett ställe och förstöra beteende på ett annat. Det skapar fortfarande problemet att om du ändrar Service B
och Interface B
måste du ändra alla element som är beroende av det. Detta tillvägagångssätt löser ingenting; enligt min mening lägger det bara till ett lager av gränssnitt ovanpå element. Du ska aldrig injicera några beroenden, men istället bör du bli av med dem en gång för alla. Hurra för självständighet!
lösningen för modulär kod
tillvägagångssättet jag tror löser alla huvudhuvudvärk av beroenden gör det genom att inte använda beroenden alls. Du skapar en komponent och dess lyssnare. En lyssnare är ett enkelt gränssnitt. När du behöver ringa en metod utanför det aktuella elementet lägger du helt enkelt till en metod för lyssnaren och kallar den istället. Elementet får endast använda filer, anropsmetoder i sitt paket och använda klasser som tillhandahålls av main framework eller andra använda bibliotek. Nedan kan du se ett diagram över applikationen modifierad för att använda elementarkitektur.
Observera att i den här arkitekturen har endast klassen Main
flera beroenden. Det ledningar alla element tillsammans och inkapslar programmets affärslogik.
tjänster är å andra sidan helt oberoende element. Nu kan du ta ut varje tjänst ur den här applikationen och återanvända dem någon annanstans. De är inte beroende av något annat. Men vänta, det blir bättre: du behöver inte ändra dessa tjänster någonsin igen, så länge du inte ändrar deras beteende. Så länge dessa tjänster gör vad de ska göra, kan de lämnas orörda till slutet av tiden. De kan skapas av en professionell mjukvaruingenjör, eller en första gången kodare komprometterad av den värsta spaghettikoden som någonsin kokats med goto
– uttalanden blandade in. Det spelar ingen roll, för deras logik är inkapslad. Så hemskt som det kan vara, det kommer aldrig att spilla ut till andra klasser. Det ger dig också möjlighet att dela arbete i ett projekt mellan flera utvecklare, där varje utvecklare kan arbeta på sin egen komponent oberoende utan att behöva avbryta en annan eller ens veta om förekomsten av andra utvecklare.
slutligen kan du börja skriva oberoende kod en gång till, precis som i början av ditt senaste projekt.
Elementmönster
låt oss definiera det strukturella elementmönstret så att vi kan skapa det på ett repeterbart sätt.
den enklaste versionen av elementet består av två saker: En huvudelementklass och en lyssnare. Om du vill använda ett element måste du implementera lyssnaren och ringa till huvudklassen. Här är ett diagram över den enklaste konfigurationen:
självklart måste du lägga till mer komplexitet i elementet så småningom men du kan göra det enkelt. Se bara till att ingen av dina logikklasser beror på andra filer i projektet. De kan bara använda huvudramen, importerade bibliotek och andra filer i det här elementet. När det gäller tillgångsfiler som bilder, vyer, ljud etc., de bör också inkapslas i element så att de i framtiden blir lätta att återanvända. Du kan helt enkelt kopiera hela mappen till ett annat projekt och där är det!
nedan kan du se ett exempeldiagram som visar ett mer avancerat element. Lägg märke till att den består av en vy som den använder och det beror inte på några andra programfiler. Om du vill veta en enkel metod för att kontrollera beroenden, titta bara på avsnittet Importera. Finns det några filer utanför det aktuella elementet? Om så är fallet måste du ta bort dessa beroenden genom att antingen flytta dem till elementet eller genom att lägga till ett lämpligt samtal till lyssnaren.
Låt oss också ta en titt på ett enkelt “Hello World” – exempel skapat i Java.
public class Main { interface ElementListener { void printOutput(String message); } static class Element { private ElementListener listener; public Element(ElementListener listener) { this.listener = listener; } public void sayHello() { String message = "Hello World of Elements!"; this.listener.printOutput(message); } } static class App { public App() { } public void start() { // Build listener ElementListener elementListener = message -> System.out.println(message); // Assemble element Element element = new Element(elementListener); element.sayHello(); } } public static void main(String args) { App app = new App(); app.start(); }}
ursprungligen definierar vi ElementListener
för att ange metoden som skriver ut utdata. Elementet själv definieras nedan. När du ringer sayHello
på elementet skriver det helt enkelt ett meddelande med ElementListener
. Observera att elementet är helt oberoende av implementeringen av printOutput
– metoden. Det kan skrivas ut i konsolen, en fysisk skrivare eller ett snyggt användargränssnitt. Elementet beror inte på den implementeringen. På grund av denna abstraktion kan detta element enkelt återanvändas i olika applikationer.
titta Nu på huvudklassen App
. Det implementerar lyssnaren och monterar elementet tillsammans med konkret implementering. Nu kan vi börja använda den.
du kan också köra det här exemplet i JavaScript här
Elementarkitektur
Låt oss ta en titt på att använda elementmönstret i storskaliga applikationer. Det är en sak att visa det i ett litet projekt—det är en annan att tillämpa den på den verkliga världen.
strukturen för en full-stack webbapplikation som jag gillar att använda ser ut som följer:
src├── client│ ├── app│ └── elements│ └── server ├── app └── elements
i en källkodsmapp delar vi initialt klient-och serverfilerna. Det är en rimlig sak att göra, eftersom de körs i två olika miljöer: webbläsaren och back-end-servern.
sedan delar vi koden i varje lager i mappar som heter app and elements. Elements består av mappar med oberoende komponenter, medan appmappen kopplar samman alla element och lagrar all affärslogik.
på så sätt kan element återanvändas mellan olika projekt, medan all applikationsspecifik komplexitet inkapslas i en enda mapp och ganska ofta reduceras till enkla samtal till element.
Hands-on exempel
tro att praktiken alltid trumf teori, låt oss ta en titt på ett verkligt exempel som skapats i Node.js och TypeScript.
verkliga livet exempel
det är en mycket enkel webbapplikation som kan användas som utgångspunkt för mer avancerade lösningar. Det följer elementarkitekturen såväl som det använder ett omfattande strukturellt elementmönster.
från höjdpunkter kan du se att huvudsidan har särskiljats som ett element. Denna sida innehåller en egen vy. Så när du till exempel vill återanvända den kan du helt enkelt kopiera hela mappen och släppa den i ett annat projekt. Bara tråd allt tillsammans och du är inställd.
det är ett grundläggande exempel som visar att du kan börja introducera element i din egen applikation idag. Du kan börja skilja oberoende komponenter och separera deras logik. Det spelar ingen roll hur rörig koden du för närvarande arbetar med är.
Utveckla Snabbare, Återanvänd Oftare!
jag hoppas att du med denna nya uppsättning verktyg lättare kan utveckla kod som är mer underhållbar. Innan du hoppar in i att använda elementmönstret i praktiken, låt oss snabbt sammanfatta alla huvudpunkter:
-
många problem i programvara händer på grund av beroenden mellan flera komponenter.
-
genom att göra en förändring på ett ställe kan du introducera oförutsägbart beteende någon annanstans.
tre vanliga arkitektoniska tillvägagångssätt är:
-
den stora bollen av lera. Det är bra för snabb utveckling, men inte så bra för stabila produktionsändamål.
-
beroende injektion. Det är en halvbakad lösning som du bör undvika.
-
Element arkitektur. Med den här lösningen kan du skapa oberoende komponenter och återanvända dem i andra projekt. Det är underhållbart och lysande för stabila produktionsutgåvor.
grundelementmönstret består av en huvudklass som har alla större metoder samt en lyssnare som är ett enkelt gränssnitt som möjliggör kommunikation med den yttre världen.
för att uppnå full-stack element arkitektur, först du skilja din front-end från back-end koden. Sedan skapar du en mapp i varje för en app och element. Elements-mappen består av alla oberoende element, medan appmappen kopplar allt ihop.
nu kan du gå och börja skapa och dela dina egna element. På lång sikt hjälper det dig att skapa lättillgängliga produkter. Lycka till och låt mig veta vad du skapade!
också, om du befinner dig i förtid optimera din kod, läs Hur man undviker förbannelse för tidig optimering av kolleger Toptaler Kevin Bloch.