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

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.

wizualizacja wielkiej kuli błota Apache Hadoop, z kilkudziesięciu węzłów i setek linii łączących je ze sobą.

Apache Hadoop ‘ s “big ball of mud”

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.

schemat techniczny mechanizmu fizycznego i sposobu, w jaki jego elementy pasują do siebie. Kawałki są numerowane w kolejności, w której należy dołączyć następny, ale kolejność od lewej do prawej idzie 5, 3, 4, 1, 2.

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.

Main korzysta z usług A, B I C, z których każdy korzysta z Util. Usługa C korzysta również z usługi A.

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.

poprzednia architektura, ale z iniekcją zależności. Teraz Main korzysta z usług interfejsu A, B I C, które są realizowane przez odpowiednie usługi. Usługi a i C korzystają zarówno z usługi interfejsu B, jak i interfejsu Util, który jest zaimplementowany przez Util. Usługa C korzysta również z usługi interfejsu A. każda usługa wraz z interfejsem jest uważana za element.

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.

schemat aplikacji zmodyfikowany w celu wykorzystania architektury elementu. Main wykorzystuje Util i wszystkie trzy usługi. Main implementuje również słuchacza dla każdej usługi, która jest używana przez tę usługę. Słuchacz i służba razem są uważane za element.

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:

diagram pojedynczego elementu i jego słuchacza w aplikacji. Podobnie jak poprzednio, aplikacja korzysta z elementu, który używa swojego słuchacza, który jest zaimplementowany przez aplikację.

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.

prosty schemat bardziej złożonego elementu. Większe znaczenie słowa "element" składa się tutaj z sześciu części: widoku; logiki A, B I C; elementu; i elementu słuchacza. Relacje między tymi dwoma ostatnimi a aplikacją są takie same jak wcześniej, ale Wewnętrzny Element używa również logiki a i C. logika C używa logiki a I B. logika A wykorzystuje logikę B i widok.

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 sayHellopo 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.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.