Å utvikle programvare er flott, men ... Jeg tror vi alle er enige om at det kan være litt av en følelsesmessig berg-og-dal-bane. I begynnelsen er alt bra. Du legger til nye funksjoner etter hverandre i løpet av noen få dager om ikke timer. Du er på rulle!
Spol frem noen måneder, og utviklingshastigheten din avtar. Er det fordi du ikke jobber så hardt som før? Ikke egentlig. La oss spole frem noen måneder til, og utviklingshastigheten din synker ytterligere. Å jobbe med dette prosjektet er ikke morsomt lenger og har blitt en drag.
Det blir verre. Du begynner å oppdage flere feil i applikasjonen din. Ofte skaper det to nye å løse en feil. På dette tidspunktet kan du begynne å synge:
99 små bugs i koden. 99 små bugs. Ta en ned, lapp den rundt,
... 127 små feil i koden.
Hvordan har du det med å jobbe med dette prosjektet nå? Hvis du er som meg, begynner du sannsynligvis å miste motivasjonen. Det er bare vondt å utvikle denne applikasjonen, siden enhver endring av eksisterende kode kan ha uforutsigbare konsekvenser.
Denne erfaringen er vanlig i programvareverdenen og kan forklare hvorfor så mange programmerere vil kaste kildekoden og omskrive alt.
Så hva er årsaken til dette problemet?
Hovedårsaken er økende kompleksitet. Fra min erfaring er den største bidragsyteren til den samlede kompleksiteten det faktum at i de aller fleste programvareprosjekter er alt koblet sammen. På grunn av avhengighetene hver klasse har, kan brukerne dine plutselig ikke registrere seg hvis du endrer kode i klassen som sender e-post. Hvorfor det? Fordi registreringskoden din avhenger av koden som sender e-post. Nå kan du ikke endre noe uten å introdusere feil. Det er rett og slett ikke mulig å spore alle avhengigheter.
Så der har du det; den virkelige årsaken til våre problemer er å øke kompleksiteten fra alle avhengighetene som koden vår har.
Det morsomme er at dette problemet har vært kjent i mange år nå. Det er et vanlig antimønster som kalles 'den store gjørmebollen'. Jeg har sett den typen arkitektur i nesten alle prosjekter jeg har jobbet med gjennom årene i flere forskjellige selskaper.
Så hva er dette antimønsteret akkurat? Enkelt sagt får du en stor sølekule når hvert element har en avhengighet av andre elementer. Nedenfor kan du se en graf over avhengighetene fra det kjente open source-prosjektet Apache Hadoop. For å visualisere den store søleballen (eller rettere sagt den store garnkulen), tegner du en sirkel og plasserer klasser fra prosjektet jevnt på den. Bare trekk en linje mellom hvert klassepar som er avhengig av hverandre. Nå kan du se kilden til problemene dine.
Så jeg stilte meg et spørsmål: Ville det være mulig å redusere kompleksiteten og fortsatt ha det gøy som i begynnelsen av prosjektet? Sannheten blir fortalt, du kan ikke eliminere alle av kompleksiteten. Hvis du vil legge til nye funksjoner, må du alltid heve kodens kompleksitet. Likevel kan kompleksitet flyttes og skilles.
Tenk på den mekaniske industrien. Når en liten mekanisk butikk lager maskiner, kjøper de et sett med standardelementer, lager noen få tilpassede og setter dem sammen. De kan lage disse komponentene helt separat og sette sammen alt på slutten, og gjøre bare noen få justeringer. Hvordan er dette mulig? De vet hvordan hvert element vil passe sammen ved å stille bransjestandarder som skruestørrelser og forhåndsavgjørelser som størrelsen på monteringshull og avstanden mellom dem.
Hvert element i forsamlingen ovenfor kan leveres av et eget selskap som ikke har noen kunnskap om det endelige produktet eller dets andre deler. Så lenge hvert modulelement er produsert i henhold til spesifikasjonene, vil du kunne lage den endelige enheten som planlagt.
Kan vi kopiere det i programvareindustrien?
Så klart vi kan! Ved å bruke grensesnitt og inversjon av kontrollprinsippet; det beste er det faktum at denne tilnærmingen kan brukes på ethvert objektorientert språk: Java, C #, Swift, TypeScript, JavaScript, PHP - listen fortsetter og fortsetter. Du trenger ikke noe fancy rammeverk for å bruke denne metoden. Du trenger bare å holde deg til noen få enkle regler og holde deg disiplinert.
Da jeg først hørte om inversjon av kontroll, innså jeg umiddelbart at jeg hadde funnet en løsning. Det er et konsept å ta eksisterende avhengigheter og invertere dem ved hjelp av grensesnitt. Grensesnitt er enkle metodedeklarasjoner. De gir ingen konkret implementering. Som et resultat kan de brukes som en avtale mellom to elementer om hvordan de skal kobles sammen. De kan brukes som modulære kontakter, hvis du vil. Så lenge ett element gir grensesnittet og et annet element gir implementeringen for det, kan de jobbe sammen uten å vite noe om hverandre. Det er brilliant.
La oss se på et enkelt eksempel hvordan vi kan koble systemet vårt for å lage modulkode. Diagrammene nedenfor er implementert som enkle Java-applikasjoner. Du finner dem på dette GitHub-depot .
La oss anta at vi har en veldig enkel applikasjon som bare består av en Main
klasse, tre tjenester og en enkelt Util
klasse. Disse elementene er avhengige av hverandre på flere måter. Nedenfor kan du se en implementering ved hjelp av 'big ball of mud' -tilnærmingen. Klasser bare kaller hverandre. De er tett koblet, og du kan ikke bare ta ut ett element uten å berøre andre. Applikasjoner laget med denne stilen lar deg i utgangspunktet vokse raskt. Jeg tror denne stilen er passende for proof-of-concept-prosjekter, siden du lett kan leke med ting. Likevel er det ikke hensiktsmessig for produksjonsklare løsninger fordi selv vedlikehold kan være farlig, og enhver endring kan skape uforutsigbare feil. Diagrammet nedenfor viser denne store kulen av gjørmearkitektur.
I et søk etter en bedre tilnærming, kan vi bruke en teknikk som kalles avhengighetsinjeksjon. Denne metoden forutsetter at alle komponenter skal brukes gjennom grensesnitt. Jeg har lest påstander om at det kobler fra elementer, men gjør det virkelig, skjønt? Nei. Ta en titt på diagrammet nedenfor.
Den eneste forskjellen mellom dagens situasjon og en stor sølekule er det faktum at vi i stedet for å ringe klasser direkte kaller dem gjennom deres grensesnitt. Det forbedrer skilleelementene fra hverandre litt. Hvis du for eksempel vil bruke Service A
i et annet prosjekt, kan du gjøre det ved å ta ut Service A
seg selv, sammen med Interface A
, samt Interface B
og Interface Util
. Som du kan se, Service A
avhenger fortsatt av andre elementer. Som et resultat får vi fortsatt problemer med å endre kode på ett sted og ødelegge atferd på et annet sted. Det skaper fortsatt problemet at hvis du endrer Service B
og Interface B
, må du endre alle elementene som er avhengige av det. Denne tilnærmingen løser ingenting; etter min mening legger det bare til et lag med grensesnitt på toppen av elementene. Du bør aldri injisere noen avhengigheter, men i stedet bør du kvitte deg med dem en gang for alle. Hurra for uavhengighet!
Tilnærmingen tror jeg løser alle de viktigste hodepine av avhengigheter gjør det ved ikke å bruke avhengigheter i det hele tatt. Du oppretter en komponent og dens lytter. En lytter er et enkelt grensesnitt. Når du trenger å ringe en metode utenfor det nåværende elementet, legger du ganske enkelt til en metode til lytteren og kaller den i stedet. Elementet har bare lov til å bruke filer, anropsmetoder i pakken og bruke klasser levert av hovedrammeverk eller andre brukte biblioteker. Nedenfor kan du se et diagram over applikasjonen som er endret for å bruke elementarkitektur.
Vær oppmerksom på at i denne arkitekturen er det bare Main
klasse har flere avhengigheter. Den kobler alle elementene sammen og innkapsler applikasjonens forretningslogikk.
Tjenester er derimot helt uavhengige elementer. Nå kan du ta ut hver tjeneste ut av dette programmet og bruke dem et annet sted. De er ikke avhengige av noe annet. Men vent, det blir bedre: Du trenger ikke å endre disse tjenestene igjen, så lenge du ikke endrer oppførselen deres. Så lenge disse tjenestene gjør det de skulle gjøre, kan de stå urørt til slutten av tiden. De kan opprettes av en profesjonell programvare ingeniør , eller en første gang koderen kompromittert av den verste spaghettikoden noen noensinne har tilberedt med goto
utsagn blandet inn. Det betyr ikke noe, fordi logikken deres er innkapslet. Så fryktelig som det kan være, vil det aldri spyle ut til andre klasser. Det gir deg også muligheten til å dele arbeidet i et prosjekt mellom flere utviklere, der hver utvikler kan jobbe med sin egen komponent uavhengig uten å måtte avbryte en annen eller til og med vite om andre utvikleres eksistens.
Til slutt kan du begynne å skrive uavhengig kode en gang til, akkurat som i begynnelsen av ditt siste prosjekt.
La oss definere det strukturelle elementmønsteret slik at vi kan lage det på en repeterbar måte.
Den enkleste versjonen av elementet består av to ting: En hovedelementklasse og en lytter. Hvis du vil bruke et element, må du implementere lytteren og ringe til hovedklassen. Her er et diagram over den enkleste konfigurasjonen:
Åpenbart må du til slutt legge til mer kompleksitet i elementet, men du kan gjøre det enkelt. Bare vær sikker på at ingen av logikklassene dine er avhengige av andre filer i prosjektet. De kan bare bruke hovedrammeverket, importerte biblioteker og andre filer i dette elementet. Når det gjelder ressursfiler som bilder, visninger, lyder osv., Bør de også være innkapslet i elementer slik at de i fremtiden vil være enkle å gjenbruke. Du kan ganske enkelt kopiere hele mappen til et annet prosjekt, og det er det!
Nedenfor kan du se et eksempel på en graf som viser et mer avansert element. Legg merke til at den består av en visning som den bruker, og at den ikke avhenger av andre applikasjonsfiler. Hvis du vil vite en enkel metode for å sjekke avhengigheter, er det bare å se på importdelen. Er det noen filer utenfor det nåværende elementet? I så fall må du fjerne disse avhengighetene ved å enten flytte dem inn i elementet eller ved å legge til et passende anrop til lytteren.
La oss også se på et enkelt 'Hello World' -eksempel opprettet 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(); } }
I utgangspunktet definerer vi ElementListener
for å spesifisere metoden som skriver ut utdata. Selve elementet er definert nedenfor. Når du ringer sayHello
på elementet skriver den bare ut en melding ved hjelp av ElementListener
. Legg merke til at elementet er helt uavhengig av implementeringen av printOutput
metode. Den kan skrives ut i konsollen, en fysisk skriver eller et fancy brukergrensesnitt. Elementet avhenger ikke av den implementeringen. På grunn av denne abstraksjonen kan dette elementet brukes på nytt i forskjellige applikasjoner.
Ta en titt på hoved App
klasse. Den implementerer lytteren og monterer elementet sammen med konkret implementering. Nå kan vi begynne å bruke den.
Du kan også kjøre dette eksemplet i JavaScript her
La oss ta en titt på bruk av elementmønsteret i store applikasjoner. Det er en ting å vise det i et lite prosjekt - det er en annen å bruke den på den virkelige verden.
Strukturen til en full-stack webapplikasjon som jeg liker å bruke ser slik ut:
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elements
I en kildekodemappe delte vi først klient- og serverfilene. Det er en rimelig ting å gjøre, siden de kjører i to forskjellige miljøer: nettleseren og back-end-serveren.
Deretter deler vi koden i hvert lag i mapper som kalles app og elementer. Elements består av mapper med uavhengige komponenter, mens appmappen kobler alle elementene sammen og lagrer all forretningslogikken.
På den måten kan elementer gjenbrukes mellom forskjellige prosjekter, mens all applikasjonsspesifikk kompleksitet er innkapslet i en enkelt mappe og ganske ofte redusert til enkle samtaler til elementer.
Vi tror at praksis alltid trumfer teori, og la oss se på et eksempel fra virkeligheten opprettet i Node.js og TypeScript.
Det er en veldig enkel webapplikasjon som kan brukes som utgangspunkt for mer avanserte løsninger. Den følger elementarkitekturen, i tillegg til at den bruker et omfattende strukturelt elementmønster.
Fra høydepunkter kan du se at hovedsiden har blitt skilt ut som et element. Denne siden inneholder sin egen visning. Så når du for eksempel vil bruke den på nytt, kan du bare kopiere hele mappen og slippe den i et annet prosjekt. Bare koble alt sammen, så er du klar.
Det er et grunnleggende eksempel som viser at du kan begynne å introdusere elementer i din egen applikasjon i dag. Du kan begynne å skille uavhengige komponenter og skille logikken deres. Det spiller ingen rolle hvor rotete koden du jobber med.
Jeg håper at du med dette nye settet med verktøy lettere vil kunne utvikle kode som er mer vedlikeholdbar. Før du begynner å bruke elementmønsteret i praksis, la oss raskt oppsummere alle hovedpoengene:
Mange problemer i programvare skjer på grunn av avhengighet mellom flere komponenter.
Ved å gjøre en endring på ett sted, kan du innføre uforutsigbar oppførsel et annet sted.
Tre vanlige arkitektoniske tilnærminger er:
Den store gjørmebollen. Det er flott for rask utvikling, men ikke så bra for stabile produksjonsformål.
Avhengighetsinjeksjon. Det er en halvstekt løsning som du bør unngå.
Elementarkitektur. Denne løsningen lar deg lage uavhengige komponenter og gjenbruke dem i andre prosjekter. Det er vedlikeholdsrikt og strålende for stabile produksjonsutgivelser.
Det grunnleggende elementmønsteret består av en hovedklasse som har alle de viktigste metodene, samt en lytter som er et enkelt grensesnitt som gir mulighet for kommunikasjon med den eksterne verden.
For å oppnå full-stack elementarkitektur, skiller du først front-enden fra back-end-koden. Deretter oppretter du en mappe i hver for en app og elementer. Elementmappen består av alle uavhengige elementer, mens appmappen kabler alt sammen.
Nå kan du begynne å lage og dele dine egne elementer. På sikt vil det hjelpe deg med å lage produkter som er enkle å vedlikeholde. Lykke til og gi meg beskjed om hva du opprettet!
Hvis du finner ut at du optimaliserer koden din for tidlig, kan du lese den Hvordan unngå forbannelse av for tidlig optimalisering av andre ApeeScapeer Kevin Bloch.
I slekt: JS Best Practices: Bygg en Discord Bot med TypeScript og Dependency InjectionKode kan være vanskelig å opprettholde på grunn av avhengighet mellom flere komponenter. Som et resultat kan det å gjøre endringer på ett sted introdusere uforutsigbar atferd et annet sted.
Modulær arkitektur betyr å dele en applikasjon i uavhengige elementer. Vi anerkjenner alle avhengigheter mellom prosjekter som årsaken til vanskelige å finne og fikse problemer. Total uavhengighet gjør disse komponentene ekstremt enkle å teste, vedlikeholde, dele og gjenbruke i fremtiden.
Vanlige arkitektoniske tilnærminger er: 1) Den store gjørmebollen: Flott for rask utvikling, men ikke så passende for stabile produksjonsformål. 2) Avhengighetsinjeksjon: En halvstekt løsning som du bør unngå. 3) Elementarkitektur: Den er vedlikeholdbar og strålende for stabile produksjonsutgivelser.