Crearea unui cod cu adevărat modular fără dependențe
Publicat: 2022-03-11Dezvoltarea de software este grozavă, dar... Cred că putem fi cu toții de acord că poate fi un pic de montagne russe emoționale. La început totul este grozav. Adăugați noi funcții una după alta în câteva zile, dacă nu ore. Ești pe un val!
Înainte rapid cu câteva luni, iar viteza de dezvoltare scade. Este pentru că nu muncești la fel de mult ca înainte? Nu chiar. Să mai avansăm cu câteva luni, iar viteza de dezvoltare scade și mai mult. Lucrul la acest proiect nu mai este distractiv și a devenit un obstacol.
Devine mai rău. Începi să descoperi mai multe erori în aplicația ta. Adesea, rezolvarea unei erori creează două noi. În acest moment, puteți începe să cântați:
99 de erori mici în cod. 99 de bug-uri mici. Luați unul jos, peticeți-l,
…127 de erori mici în cod.
Cum te simți când lucrezi la acest proiect acum? Dacă ești ca mine, probabil că începi să-ți pierzi motivația. Este doar o durere să dezvolți această aplicație, deoarece fiecare modificare a codului existent poate avea consecințe imprevizibile.
Această experiență este comună în lumea software-ului și poate explica de ce atât de mulți programatori doresc să-și arunce codul sursă și să rescrie totul.
Motive pentru care dezvoltarea software-ului încetinește în timp
Deci, care este motivul acestei probleme?
Cauza principală este creșterea complexității. Din experiența mea, cel mai mare contributor la complexitatea generală este faptul că, în marea majoritate a proiectelor software, totul este conectat. Din cauza dependențelor pe care le are fiecare clasă, dacă modificați un cod din clasa care trimite e-mailuri, utilizatorii tăi nu se pot înregistra brusc. De ce este asta? Pentru că codul tău de înregistrare depinde de codul care trimite e-mailurile. Acum nu poți schimba nimic fără a introduce bug-uri. Pur și simplu nu este posibil să urmăriți toate dependențele.
Deci iată-l; adevărata cauză a problemelor noastre este creșterea complexității provenite din toate dependențele pe care le are codul nostru.
Mingea mare de noroi și cum să o reduceți
Lucru amuzant este că această problemă este cunoscută de ani de zile. Este un anti-model comun numit „mersul mare de noroi”. Am văzut acest tip de arhitectură în aproape toate proiectele la care am lucrat de-a lungul anilor în mai multe companii diferite.
Deci, ce este mai exact acest anti-model? Pur și simplu vorbind, obțineți o minge mare de noroi atunci când fiecare element are o dependență de alte elemente. Mai jos, puteți vedea un grafic al dependențelor din binecunoscutul proiect open-source Apache Hadoop. Pentru a vizualiza globul mare de noroi (sau mai bine zis, globul mare de fire), desenați un cerc și plasați uniform clasele din proiect pe el. Doar trageți o linie între fiecare pereche de clase care depind una de cealaltă. Acum poți vedea sursa problemelor tale.
O soluție cu cod modular
Așa că mi-am pus o întrebare: Ar fi posibil să reduc complexitatea și să mă distrez în continuare ca la începutul proiectului? Adevărul să fie spus, nu poți elimina toată complexitatea. Dacă doriți să adăugați noi funcții, va trebui întotdeauna să creșteți complexitatea codului. Cu toate acestea, complexitatea poate fi mutată și separată.
Cum rezolvă alte industrii această problemă
Gândiți-vă la industria mecanică. Când un mic atelier de mecanică creează mașini, cumpără un set de elemente standard, creează câteva personalizate și le pun împreună. Ei pot face acele componente complet separat și pot asambla totul la sfârșit, făcând doar câteva ajustări. Cum este posibil acest lucru? Ei știu cum se va potrivi fiecare element prin standarde stabilite din industrie, cum ar fi dimensiunile șuruburilor și decizii inițiale, cum ar fi dimensiunea găurilor de montare și distanța dintre ele.
Fiecare element din ansamblul de mai sus poate fi furnizat de o companie separată care nu are nicio cunoștință despre produsul final sau despre celelalte piese ale acestuia. Atâta timp cât fiecare element modular este fabricat conform specificațiilor, veți putea crea dispozitivul final așa cum a fost planificat.
Putem replica asta în industria software-ului?
Sigur putem! Prin utilizarea interfețelor și principiul inversării controlului; Cea mai bună parte este faptul că această abordare poate fi utilizată în orice limbaj orientat pe obiecte: Java, C#, Swift, TypeScript, JavaScript, PHP — lista poate continua. Nu aveți nevoie de niciun cadru de lux pentru a aplica această metodă. Trebuie doar să respectați câteva reguli simple și să rămâneți disciplinat.
Inversarea controlului este prietenul tău
Când am auzit prima dată despre inversarea controlului, mi-am dat seama imediat că am găsit o soluție. Este un concept de a lua dependențe existente și de a le inversa prin utilizarea interfețelor. Interfețele sunt simple declarații de metode. Nu oferă nicio implementare concretă. Ca rezultat, ele pot fi folosite ca un acord între două elemente cu privire la modul de conectare. Pot fi folosiți ca conectori modulari, dacă doriți. Atâta timp cât un element oferă interfața și un alt element asigură implementarea acesteia, aceștia pot lucra împreună fără să știe nimic unul despre celălalt. E genial.
Să vedem într-un exemplu simplu cum ne putem decupla sistemul pentru a crea cod modular. Diagramele de mai jos au fost implementate ca simple aplicații Java. Le puteți găsi în acest depozit GitHub.
Problemă
Să presupunem că avem o aplicație foarte simplă constând doar dintr-o clasă Main , trei servicii și o singură clasă Util . Aceste elemente depind unele de altele în mai multe moduri. Mai jos, puteți vedea o implementare folosind abordarea „big ball of noroi”. Clasele pur și simplu se numesc între ele. Ele sunt strâns cuplate și nu puteți elimina pur și simplu un element fără să atingeți alții. Aplicațiile create folosind acest stil vă permit inițial să creșteți rapid. Cred că acest stil este potrivit pentru proiectele de dovadă a conceptului, deoarece vă puteți juca cu lucrurile cu ușurință. Cu toate acestea, nu este potrivit pentru soluțiile pregătite pentru producție, deoarece chiar și întreținerea poate fi periculoasă și orice modificare poate crea erori imprevizibile. Diagrama de mai jos arată această arhitectură mare de noroi.
De ce injecția de dependență a greșit totul
În căutarea unei abordări mai bune, putem folosi o tehnică numită injecție de dependență. Această metodă presupune că toate componentele trebuie utilizate prin interfețe. Am citit afirmații conform cărora decuplează elementele, dar chiar oare, totuși? Nu. Aruncă o privire la diagrama de mai jos.
Singura diferență dintre situația actuală și un mare glob de noroi este faptul că acum, în loc să sunăm direct la cursuri, le chemam prin interfețele lor. Îmbunătățește ușor elementele de separare unele de altele. Dacă, de exemplu, doriți să reutilizați Service A într-un alt proiect, puteți face acest lucru prin eliminarea Service A în sine, împreună cu Interface A , precum și Interface B și Interface Util . După cum puteți vedea, Service A depinde în continuare de alte elemente. Ca urmare, avem în continuare probleme cu schimbarea codului într-un loc și cu comportamentul în dezordine în altul. Încă creează problema că, dacă modificați Service B și Interface B , va trebui să modificați toate elementele care depind de el. Această abordare nu rezolvă nimic; în opinia mea, adaugă doar un strat de interfață deasupra elementelor. Nu ar trebui să injectați niciodată dependențe, ci ar trebui să scăpați de ele o dată pentru totdeauna. Ura independența!
Soluția pentru codul modular
Abordarea pe care cred că rezolvă toate principalele dureri de cap ale dependențelor o face nefolosind deloc dependențe. Creați o componentă și ascultătorul ei. Un ascultător este o interfață simplă. Ori de câte ori trebuie să apelați o metodă din afara elementului curent, pur și simplu adăugați o metodă la ascultător și o apelați în schimb. Elementului i se permite doar să utilizeze fișiere, să apeleze metode din pachetul său și să utilizeze clasele furnizate de framework-ul principal sau de alte biblioteci utilizate. Mai jos, puteți vedea o diagramă a aplicației modificată pentru a utiliza arhitectura elementului.

Vă rugăm să rețineți că, în această arhitectură, numai clasa Main are dependențe multiple. Conectează toate elementele împreună și încapsulează logica de afaceri a aplicației.
Serviciile, pe de altă parte, sunt elemente complet independente. Acum, puteți elimina fiecare serviciu din această aplicație și îl puteți reutiliza în altă parte. Ei nu depind de nimic altceva. Dar stați, totul devine mai bine: nu trebuie să modificați acele servicii vreodată, atâta timp cât nu le schimbați comportamentul. Atâta timp cât acele servicii fac ceea ce trebuiau să facă, ele pot fi lăsate neatinse până la sfârșitul timpului. Ele pot fi create de un inginer de software profesionist sau de un codificator pentru prima dată compromis cu cel mai prost cod spaghetti pe care cineva l-a gătit vreodată cu declarații goto amestecate. Nu contează, pentru că logica lor este încapsulată. Oricât de oribil ar fi, nu se va răspândi niciodată în alte clase. Acest lucru vă oferă, de asemenea, puterea de a împărți munca într-un proiect între mai mulți dezvoltatori, în care fiecare dezvoltator poate lucra pe propria sa componentă independent, fără a fi nevoie să o întrerupă pe alta sau chiar să știe despre existența altor dezvoltatori.
În cele din urmă, puteți începe să scrieți cod independent încă o dată, la fel ca la începutul ultimului proiect.
Model de elemente
Să definim modelul elementului structural astfel încât să-l putem crea într-o manieră repetabilă.
Cea mai simplă versiune a elementului constă din două lucruri: o clasă de element principal și un ascultător. Dacă doriți să utilizați un element, atunci trebuie să implementați ascultătorul și să efectuați apeluri către clasa principală. Iată o diagramă cu cea mai simplă configurație:
Evident, va trebui să adăugați mai multă complexitate elementului în cele din urmă, dar puteți face acest lucru cu ușurință. Doar asigurați-vă că niciuna dintre clasele dvs. de logică nu depinde de alte fișiere din proiect. Ei pot folosi doar cadrul principal, bibliotecile importate și alte fișiere din acest element. Când vine vorba de fișiere de active, cum ar fi imagini, vizualizări, sunete etc., acestea ar trebui, de asemenea, să fie încapsulate în elemente, astfel încât în viitor să fie ușor de reutilizat. Pur și simplu puteți copia întregul folder într-un alt proiect și iată-l!
Mai jos, puteți vedea un exemplu de grafic care arată un element mai avansat. Observați că constă dintr-o vizualizare pe care o folosește și nu depinde de niciun alt fișier de aplicație. Dacă doriți să cunoașteți o metodă simplă de verificare a dependențelor, priviți secțiunea de import. Există fișiere din afara elementului curent? Dacă da, atunci trebuie să eliminați aceste dependențe fie mutându-le în element, fie adăugând un apel adecvat ascultătorului.
Să aruncăm o privire și la un exemplu simplu „Hello World” creat în 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(); } } Inițial, definim ElementListener pentru a specifica metoda care imprimă rezultatul. Elementul în sine este definit mai jos. La apelarea sayHello pe element, pur și simplu tipărește un mesaj folosind ElementListener . Observați că elementul este complet independent de implementarea metodei printOutput . Poate fi imprimat în consolă, într-o imprimantă fizică sau într-o interfață de utilizare elegantă. Elementul nu depinde de acea implementare. Din cauza acestei abstractizări, acest element poate fi reutilizat cu ușurință în diferite aplicații.
Acum aruncați o privire la clasa principală de App . Implementează ascultătorul și asamblează elementul împreună cu implementarea concretă. Acum putem începe să-l folosim.
De asemenea, puteți rula acest exemplu în JavaScript aici
Arhitectura Elementelor
Să aruncăm o privire la utilizarea modelului elementului în aplicații la scară largă. Una este să o arăți într-un proiect mic – alta este să o aplici în lumea reală.
Structura unei aplicații web full-stack pe care îmi place să o folosesc arată după cum urmează:
src ├── client │ ├── app │ └── elements │ └── server ├── app └── elementsÎntr-un folder de cod sursă, inițial împărțim fișierele client și server. Este un lucru rezonabil de făcut, deoarece rulează în două medii diferite: browser și serverul back-end.
Apoi împărțim codul din fiecare strat în foldere numite aplicație și elemente. Elements este format din foldere cu componente independente, în timp ce folderul aplicației conectează toate elementele și stochează toată logica de afaceri.
În acest fel, elementele pot fi reutilizate între diferite proiecte, în timp ce toată complexitatea specifică aplicației este încapsulată într-un singur folder și destul de des redusă la simple apeluri la elemente.
Exemplu practic
Considerând că practica depășește întotdeauna teoria, să aruncăm o privire la un exemplu din viața reală creat în Node.js și TypeScript.
Exemplu de viață reală
Este o aplicație web foarte simplă care poate fi folosită ca punct de plecare pentru soluții mai avansate. Urmează arhitectura elementului și folosește un model extensiv de elemente structurale.
Din momentele evidențiate, puteți vedea că pagina principală a fost distinsă ca element. Această pagină include propria sa vizualizare. Deci, când, de exemplu, doriți să-l reutilizați, puteți pur și simplu să copiați întregul folder și să îl plasați într-un proiect diferit. Doar conectați totul împreună și sunteți gata.
Este un exemplu de bază care demonstrează că poți începe să introduci elemente în propria ta aplicație astăzi. Puteți începe să distingeți componentele independente și să le separați logica. Nu contează cât de dezordonat este codul la care lucrați în prezent.
Dezvoltați-vă mai rapid, reutilizați mai des!
Sper că, cu acest nou set de instrumente, veți putea dezvolta mai ușor un cod mai ușor de întreținut. Înainte de a trece la utilizarea modelului de elemente în practică, să recapitulăm rapid toate punctele principale:
O mulțime de probleme în software apar din cauza dependențelor dintre mai multe componente.
Făcând o schimbare într-un singur loc, puteți introduce un comportament imprevizibil în altă parte.
Trei abordări arhitecturale comune sunt:
Mingea mare de noroi. Este grozav pentru dezvoltarea rapidă, dar nu atât de grozav pentru scopuri de producție stabilă.
Injecție de dependență. Este o soluție pe jumătate coaptă pe care ar trebui să o evitați.
Arhitectura elementelor. Această soluție vă permite să creați componente independente și să le reutilizați în alte proiecte. Este întreținut și genial pentru lansări stabile de producție.
Modelul elementului de bază constă dintr-o clasă principală care are toate metodele majore, precum și un ascultător care este o interfață simplă care permite comunicarea cu lumea externă.
Pentru a obține o arhitectură de elemente full-stack, mai întâi vă separați front-end-ul de codul back-end. Apoi creați un folder în fiecare pentru o aplicație și elemente. Dosarul cu elemente este format din toate elementele independente, în timp ce folderul aplicației leagă totul împreună.
Acum puteți începe să creați și să vă împărtășiți propriile elemente. Pe termen lung, vă va ajuta să creați produse ușor de întreținut. Succes și spune-mi ce ai creat!
De asemenea, dacă vă găsiți că vă optimizați prematur codul, citiți Cum să evitați blestemul optimizării premature de către colegul Toptaler Kevin Bloch.
