Tworzenie kodu modułowego bez zależności
tworzenie oprogramowania jest świetne, ale … myślę, że wszyscy możemy się zgodzić, że może to być trochę emocjonalny rollercoaster. Na początku wszystko jest super. Dodajesz nowe funkcje jeden po drugim w ciągu kilku dni, jeśli nie godzin. Jesteś na fali!
przewiń do przodu o kilka miesięcy, a szybkość rozwoju spadnie. Czy to dlatego, że nie pracujesz tak ciężko jak wcześniej? Nie bardzo. Przewińmy do przodu o kilka miesięcy, a twoje tempo rozwoju jeszcze bardziej spadnie. Praca nad tym projektem nie jest już zabawna i stała się męcząca.
jest coraz gorzej. Zaczynasz odkrywać wiele błędów w swojej aplikacji. Często rozwiązanie jednego błędu tworzy dwa nowe. W tym momencie możesz zacząć śpiewać:
99 małe błędy w kodzie.99 małych robaków.Ściągnij jeden, załataj,
… 127 małych błędów w kodzie.
co sądzisz o pracy nad tym projektem teraz? Jeśli jesteś taki jak ja, prawdopodobnie zaczynasz tracić motywację. Rozwój tej aplikacji jest uciążliwy, ponieważ każda zmiana istniejącego kodu może mieć nieprzewidywalne konsekwencje.
to doświadczenie jest powszechne w świecie oprogramowania i może wyjaśnić, dlaczego tak wielu programistów chce wyrzucić swój kod źródłowy i przepisać wszystko.
- powody, dla których Rozwój oprogramowania zwalnia z czasem
- wielka kula błota i jak ją zmniejszyć
- rozwiązanie z modularnym kodem
- jak inne branże rozwiązują ten Problem
- Inwersja sterowania jest twoim przyjacielem
- Problem
- dlaczego Dependency Injection wszystko się pomyliło
- rozwiązanie dla kodu modularnego
- wzór elementu
- Architektura elementu
- praktyczny przykład
- Rozwijaj Się Szybciej, Częściej Używaj!
powody, dla których Rozwój oprogramowania zwalnia z czasem
więc jaki jest powód tego problemu?
główną przyczyną jest rosnąca złożoność. Z mojego doświadczenia wynika, że największy wpływ na ogólną złożoność ma fakt, że w zdecydowanej większości projektów programistycznych wszystko jest ze sobą powiązane. Ze względu na Zależności, które mają każda klasa, jeśli zmienisz kod w klasie wysyłającej e-maile, użytkownicy nagle nie będą mogli się zarejestrować. Dlaczego? Ponieważ kod rejestracyjny zależy od kodu, który wysyła wiadomości e-mail. Teraz nie możesz nic zmienić bez wprowadzenia błędów. Po prostu nie jest możliwe śledzenie wszystkich zależności.
więc masz to; prawdziwą przyczyną naszych problemów jest podniesienie złożoności pochodzącej ze wszystkich zależności, które posiada nasz kod.
wielka kula błota i jak ją zmniejszyć
zabawne jest to, że ten problem jest znany od lat. To częsty anty-wzór zwany ” wielką kulą błota.”Widziałem ten typ architektury w prawie wszystkich projektach, nad którymi pracowałem przez lata w wielu różnych firmach.
więc czym dokładnie jest ten anty-wzorzec? Mówiąc najprościej, otrzymujesz dużą kulę błota, gdy każdy element ma zależność od innych elementów. Poniżej możesz zobaczyć wykres zależności od znanego projektu open-source Apache Hadoop. Aby zwizualizować dużą kulę błota (a raczej dużą kulę przędzy), narysuj okrąg i równomiernie umieść na nim klasy z projektu. Wystarczy narysować linię między każdą parą klas, które zależą od siebie. Teraz możecie zobaczyć źródło waszych problemów.
rozwiązanie z modularnym kodem
więc zadałem sobie pytanie: czy można zmniejszyć złożoność i nadal dobrze się bawić jak na początku projektu? Prawdę mówiąc, nie można wyeliminować całej złożoności. Jeśli chcesz dodać nowe funkcje, zawsze będziesz musiał zwiększyć złożoność kodu. Niemniej jednak złożoność może być przenoszona i oddzielana.
jak inne branże rozwiązują ten Problem
pomyśl o przemyśle mechanicznym. Kiedy jakiś mały warsztat mechaniczny tworzy maszyny, kupuje zestaw standardowych elementów, tworzy kilka niestandardowych i składa je razem. Mogą tworzyć te komponenty całkowicie osobno i montować wszystko na końcu, dokonując zaledwie kilku poprawek. Jak to możliwe? Wiedzą, jak każdy element będzie pasował do siebie, ustalając standardy branżowe, takie jak rozmiary śrub, i podejmując decyzje z góry, takie jak rozmiar otworów montażowych i odległość między nimi.
każdy element w powyższym montażu może być dostarczony przez oddzielną firmę, która nie ma żadnej wiedzy na temat produktu końcowego lub jego innych elementów. Tak długo, jak każdy element modułowy jest produkowany zgodnie ze specyfikacją, będziesz mógł stworzyć urządzenie końcowe zgodnie z planem.
Czy możemy to powtórzyć w branży oprogramowania?
pewnie, że możemy! Za pomocą interfejsów i inwersji zasady sterowania; najlepsze jest to, że takie podejście może być stosowane w dowolnym języku obiektowym: Java, C#, Swift, TypeScript, JavaScript, PHP-lista jest długa. Nie potrzebujesz żadnych fantazyjnych RAM, aby zastosować tę metodę. Musisz tylko trzymać się kilku prostych zasad i pozostać zdyscyplinowany.
Inwersja sterowania jest twoim przyjacielem
kiedy po raz pierwszy usłyszałem o inwersji sterowania, natychmiast zrozumiałem, że znalazłem rozwiązanie. Jest to koncepcja pobierania istniejących zależności i odwracania ich za pomocą interfejsów. Interfejsy są prostymi deklaracjami metod. Nie zapewniają one żadnej konkretnej realizacji. W rezultacie można je wykorzystać jako porozumienie między dwoma elementami dotyczące sposobu ich połączenia. Mogą być używane jako Złącza modułowe, jeśli chcesz. Tak długo, jak jeden element zapewnia interfejs, a inny zapewnia implementację dla niego, mogą współpracować, nie wiedząc nic o sobie nawzajem. To genialne.
zobaczmy na prostym przykładzie, jak możemy oddzielić nasz system, aby utworzyć kod modułowy. Poniższe diagramy zostały zaimplementowane jako proste aplikacje Java. Możesz je znaleźć w tym repozytorium GitHub.
Problem
Załóżmy, że mamy bardzo prostą aplikację składającą się tylko z klasy Main
, trzech usług i jednej klasy Util
. Te elementy zależą od siebie na wiele sposobów. Poniżej możesz zobaczyć implementację wykorzystującą podejście “big ball of mud”. Klasy po prostu do siebie dzwonią. Są one ściśle ze sobą powiązane i nie można po prostu wyjąć jednego elementu bez dotykania innych. Aplikacje utworzone przy użyciu tego stylu pozwalają początkowo szybko się rozwijać. Uważam, że ten styl jest odpowiedni do projektów proof-of-concept, ponieważ można łatwo bawić się rzeczami. Nie jest to jednak właściwe dla rozwiązań gotowych do produkcji, ponieważ nawet konserwacja może być niebezpieczna, a każda pojedyncza zmiana może spowodować nieprzewidywalne błędy. Poniższy diagram pokazuje tę wielką kulę architektury błotnej.
dlaczego Dependency Injection wszystko się pomyliło
w poszukiwaniu lepszego podejścia możemy użyć techniki zwanej dependency injection. Metoda ta zakłada, że wszystkie komponenty powinny być używane przez interfejsy. Czytałem, że oddziela elementy, ale czy naprawdę? Nie. Spójrz na poniższy schemat.
jedyną różnicą między obecną sytuacją a Wielką kulą błota jest fakt, że teraz, zamiast wywoływać klasy bezpośrednio, wywołujemy je poprzez ich interfejsy. Nieco poprawia oddzielanie elementów od siebie. Jeśli na przykład chcesz ponownie użyć Service A
w innym projekcie, możesz to zrobić, usuwając samą Service A
wraz z Interface A
, a także Interface B
i Interface Util
. Jak widać, Service A
nadal zależy od innych elementów. W rezultacie nadal mamy problemy ze zmianą kodu w jednym miejscu, a zaburzeniem zachowania w innym. Nadal tworzy problem, że jeśli zmodyfikujesz Service B
i Interface B
, będziesz musiał zmienić wszystkie elementy, które od niej zależą. Takie podejście niczego nie rozwiązuje; moim zdaniem dodaje tylko warstwę interfejsu na elementach. Nigdy nie należy wstrzykiwać żadnych zależności, ale zamiast tego należy się ich pozbyć raz na zawsze. Niech żyje niepodległość!
rozwiązanie dla kodu modularnego
podejście, które, jak sądzę, rozwiązuje wszystkie główne problemy zależności, robi to, nie używając w ogóle zależności. Tworzysz komponent i jego słuchacz. Słuchacz to prosty interfejs. Za każdym razem, gdy musisz wywołać metodę spoza bieżącego elementu, po prostu Dodaj metodę do słuchacza i wywołaj ją zamiast tego. Element może używać tylko plików, wywoływać metody w swoim pakiecie i używać klas dostarczanych przez main framework lub inne używane biblioteki. Poniżej możesz zobaczyć diagram aplikacji zmodyfikowany w celu użycia architektury elementu.
należy pamiętać, że w tej architekturze tylko Klasa Main
ma wiele zależności. Łączy wszystkie elementy i zawiera logikę biznesową aplikacji.
z drugiej strony usługi są całkowicie niezależnymi elementami. Teraz możesz wyjąć każdą usługę z tej aplikacji i ponownie użyć ich w innym miejscu. Nie zależą od niczego innego. Ale poczekaj, jest jeszcze lepiej: nie musisz już modyfikować tych usług, o ile nie zmienisz ich zachowania. Dopóki służby te robią to, co powinny, mogą pozostać nietknięte do końca czasu. Mogą być tworzone przez profesjonalnego inżyniera oprogramowania lub pierwszego kodera, który skompromitował najgorszy kod spaghetti, jaki kiedykolwiek gotował z wymieszanymi instrukcjami goto
. To bez znaczenia, ponieważ ich logika jest zamknięta. Jakkolwiek straszne to może być, to nigdy nie rozleje się na inne klasy. Daje to również możliwość podziału pracy w projekcie pomiędzy wielu programistów, gdzie każdy programista może pracować nad własnym komponentem niezależnie bez konieczności przerywania innego lub nawet wiedząc o istnieniu innych programistów.
wreszcie możesz zacząć pisać niezależny Kod jeszcze raz, tak jak na początku ostatniego projektu.
wzór elementu
zdefiniujmy wzór elementu strukturalnego tak, abyśmy mogli go tworzyć w powtarzalny sposób.
najprostsza wersja elementu składa się z dwóch rzeczy: Główny element klasy i słuchacza. Jeśli chcesz użyć elementu, musisz zaimplementować słuchacza i wykonać połączenia do głównej klasy. Oto schemat najprostszej konfiguracji:
oczywiście, w końcu będziesz musiał dodać więcej złożoności do elementu, ale możesz to zrobić łatwo. Upewnij się tylko, że żadna z Twoich klas logicznych nie zależy od innych plików w projekcie. Mogą używać tylko głównego frameworka, importowanych bibliotek i innych plików w tym elemencie. Jeśli chodzi o pliki zasobów, takie jak obrazy, widoki, dźwięki itp., powinny być również zamknięte w elementach, aby w przyszłości były łatwe do ponownego użycia. Możesz po prostu skopiować cały folder do innego projektu i tam jest!
poniżej możesz zobaczyć przykładowy wykres pokazujący bardziej zaawansowany element. Zauważ, że składa się z widoku, którego używa i nie zależy od żadnych innych plików aplikacji. Jeśli chcesz poznać prostą metodę sprawdzania zależności, zajrzyj do sekcji import. Czy są jakieś pliki spoza bieżącego elementu? Jeśli tak, musisz usunąć te zależności, przenosząc je do elementu lub dodając odpowiednie wywołanie do słuchacza.
rzućmy również okiem na prosty przykład “Hello World” stworzony w Javie.
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(); }}
początkowo definiujemy ElementListener
, aby określić metodę, która wyświetla wyjście. Sam element jest zdefiniowany poniżej. Podczas wywoływania elementu sayHello
po prostu wypisuje wiadomość używając ElementListener
. Zauważ, że element jest całkowicie niezależny od implementacji metody printOutput
. Można go wydrukować na konsoli, fizycznej drukarce lub fantazyjnym interfejsie użytkownika. Element nie zależy od tej implementacji. Z powodu tej abstrakcji element ten może być łatwo ponownie użyty w różnych aplikacjach.
teraz spójrz na główną App
klasę. Wdraża słuchacza i montuje element wraz z konkretną realizacją. Teraz możemy zacząć go używać.
możesz również uruchomić ten przykład w JavaScript tutaj
Architektura elementu
spójrzmy na używanie wzorca elementu w dużych aplikacjach. Pokazanie go w małym projekcie to jedno—a zastosowanie go w realnym świecie to drugie.
struktura pełnej aplikacji internetowej, której lubię używać, wygląda następująco:
src├── client│ ├── app│ └── elements│ └── server ├── app └── elements
w folderze z kodem źródłowym początkowo dzielimy pliki klienta i serwera. Jest to rozsądna rzecz, ponieważ działają one w dwóch różnych środowiskach: przeglądarce i serwerze zaplecza.
następnie dzielimy kod w każdej warstwie na foldery o nazwie app i elements. Elements składa się z folderów z niezależnymi komponentami, podczas gdy folder aplikacji łączy wszystkie elementy i przechowuje całą logikę biznesową.
w ten sposób elementy mogą być ponownie używane między różnymi projektami, podczas gdy cała złożoność specyficzna dla aplikacji jest zamknięta w jednym folderze i często sprowadza się do prostych wywołań elementów.
praktyczny przykład
wierząc, że praktyka zawsze przebija teorię, rzućmy okiem na prawdziwy przykład stworzony w Node.js i maszynopis.
przykład z prawdziwego zdarzenia
jest to bardzo prosta aplikacja internetowa, która może być używana jako punkt wyjścia dla bardziej zaawansowanych rozwiązań. Podąża za architekturą elementów, a także wykorzystuje szeroko strukturalny wzór elementów.
z wyróżnień widać, że strona główna została wyróżniona jako element. Ta strona zawiera własny widok. Tak więc, gdy, na przykład, chcesz go ponownie użyć, możesz po prostu skopiować cały folder i upuścić go do innego projektu. Po prostu połącz wszystko i jesteś gotowy.
to podstawowy przykład, który pokazuje, że możesz zacząć wprowadzać elementy we własnej aplikacji już dziś. Możesz zacząć rozróżniać niezależne komponenty i oddzielić ich logikę. Nie ma znaczenia, jak brudny jest kod, nad którym aktualnie pracujesz.
Rozwijaj Się Szybciej, Częściej Używaj!
mam nadzieję, że dzięki temu nowemu zestawowi narzędzi będziesz mógł łatwiej opracować kod, który będzie łatwiejszy do utrzymania. Zanim zaczniesz używać wzorca elementów w praktyce, szybko podsumujmy wszystkie główne punkty:
-
wiele problemów w oprogramowaniu dzieje się z powodu zależności między wieloma komponentami.
-
dokonując zmiany w jednym miejscu, możesz wprowadzić nieprzewidywalne zachowanie w innym miejscu.
trzy wspólne podejścia architektoniczne to:
-
wielka kula błota. Świetnie nadaje się do szybkiego rozwoju, ale nie tak dobrze do stabilnych celów produkcyjnych.
-
wstrzykiwanie zależności. To pół-upieczone rozwiązanie, którego należy unikać.
-
Architektura elementów. Rozwiązanie to pozwala tworzyć niezależne komponenty i wykorzystywać je ponownie w innych projektach. Jest łatwy w utrzymaniu i doskonały do stabilnych wydań produkcyjnych.
podstawowy element wzorca składa się z klasy głównej, która ma wszystkie główne metody, a także słuchacza, który jest prostym interfejsem, który pozwala na komunikację ze światem zewnętrznym.
aby uzyskać architekturę elementów z pełnym stosem, najpierw oddziel swój front-end od kodu back-end. Następnie tworzysz w każdym folderze dla aplikacji i elementów. Folder elements składa się ze wszystkich niezależnych elementów, podczas gdy folder aplikacji łączy wszystko ze sobą.
teraz możesz zacząć tworzyć i udostępniać własne elementy. Na dłuższą metę pomoże Ci stworzyć łatwe do utrzymania produkty. Powodzenia i daj mi znać, co stworzyłeś!
również, jeśli znajdziesz się przedwcześnie optymalizacji kodu, przeczytaj Jak uniknąć klątwy przedwczesnej optymalizacji przez kolegę Toptaler Kevin Bloch.