W typowej (dobrze zaprojektowanej) aplikacji internetowej MVC baza danych nie zna kodu modelu, kod modelu nie zna kodu kontrolera, a kod kontrolera nie jest świadomy kodu widoku. (Wyobrażam sobie, że możesz zacząć nawet od sprzętu, a może nawet dalej, a wzór może być taki sam.)
Idąc w innym kierunku, możesz zejść tylko o jedną warstwę w dół. Widok może uwzględniać kontroler, ale nie model; kontroler może być świadomy modelu, ale nie bazy danych; model może być świadomy bazy danych, ale nie systemu operacyjnego. (Coś głębszego jest prawdopodobnie nieistotne).
Potrafię intuicyjnie zrozumieć, dlaczego to dobry pomysł, ale nie potrafię go wyrazić. Dlaczego ten jednokierunkowy styl nakładania warstw jest dobrym pomysłem?
architecture
mvc
encapsulation
layers
Jason Swett
źródło
źródło
Odpowiedzi:
Warstwy, moduły, a nawet sama architektura, ułatwiają zrozumienie programów komputerowych przez ludzi . Numerycznie optymalną metodą rozwiązania problemu jest prawie zawsze bezbożny splątany bałagan niemodularnego, samoreferencyjnego lub nawet samodmodyfikującego się kodu - niezależnie od tego, czy jest to wysoce zoptymalizowany kod asemblera w systemach osadzonych z paraliżującymi ograniczeniami pamięci lub sekwencjami DNA po milionach lat presji selekcyjnej. Takie systemy nie mają warstw, nie mają dostrzegalnego kierunku przepływu informacji, w rzeczywistości nie mają struktury, którą możemy w ogóle dostrzec. Wydaje się, że dla wszystkich oprócz ich autorów działa czysta magia.
W inżynierii oprogramowania chcemy tego uniknąć. Dobra architektura to celowa decyzja poświęcenia pewnej wydajności w celu uczynienia systemu zrozumiałym dla zwykłych ludzi. Zrozumienie jednej rzeczy na raz jest łatwiejsze niż zrozumienie dwóch rzeczy, które mają sens tylko wtedy, gdy są używane razem. Dlatego moduły i warstwy są dobrym pomysłem.
Ale nieuchronnie moduły muszą wywoływać funkcje od siebie nawzajem, a warstwy muszą być tworzone jeden na drugim. W praktyce więc zawsze konieczne jest konstruowanie systemów, aby niektóre części wymagały innych części. Preferowanym kompromisem jest zbudowanie ich w taki sposób, aby jedna część wymagała drugiej, ale ta część nie wymaga przywrócenia pierwszej. I właśnie to zapewnia nam jednokierunkowe nakładanie warstw: możliwe jest zrozumienie schematu bazy danych bez znajomości reguł biznesowych i zrozumienie reguł biznesowych bez znajomości interfejsu użytkownika. Byłoby miło mieć niezależność w obu kierunkach - pozwalając ktoś zaprogramować nowy interfejs użytkownika, nie wiedząc nicw ogóle o regułach biznesowych - ale w praktyce jest to praktycznie niemożliwe. Praktyczne zasady, takie jak „Brak cyklicznych zależności” lub „Zależności muszą sięgać tylko jednego poziomu”, po prostu uchwycą praktycznie osiągalną granicę podstawowej idei, że jedna rzecz na raz jest łatwiejsza do zrozumienia niż dwie rzeczy.
źródło
Podstawowa motywacja jest taka: chcesz być w stanie wyrwać całą warstwę i zastąpić ją całkowicie inną (przepisaną), a NIKT NIE POWINIEN (RÓWNIEŻ BYĆ) ZAUWAŻYĆ RÓŻNICĘ.
Najbardziej oczywistym przykładem jest odrywanie dolnej warstwy i zastępowanie jej inną. To jest to, co robisz, gdy rozwijasz górną warstwę (warstwy) w oparciu o symulację sprzętu, a następnie zastępujesz w prawdziwym sprzęcie.
Następnym przykładem jest oderwanie środkowej warstwy i zastąpienie jej inną. Rozważ aplikację korzystającą z protokołu działającego na RS-232. Pewnego dnia musisz całkowicie zmienić kodowanie protokołu, ponieważ „coś innego się zmieniło”. (Przykład: przejście z prostego kodowania ASCII na kodowanie strumieni ASCII metodą Reeda-Solomona, ponieważ pracujesz nad łączem radiowym z centrum LA do Marina Del Rey, a teraz pracujesz nad łączem radiowym z centrum LA do sondy krążącej po Europie , jeden z księżyców Jowisza, a ten link wymaga znacznie lepszej korekcji błędów przekazywania).
Jedynym sposobem, aby to działało, jest to, że każda warstwa eksportuje znany, zdefiniowany interfejs do warstwy powyżej i oczekuje znanego, zdefiniowanego interfejsu do warstwy poniżej.
Teraz nie jest tak, że niższe warstwy NIE wiedzą nic o wyższych warstwach. Dolna warstwa wie raczej, że warstwa znajdująca się bezpośrednio nad nią będzie działać dokładnie zgodnie ze zdefiniowanym interfejsem. Nie może wiedzieć nic więcej, ponieważ z definicji wszystko, czego nie ma w zdefiniowanym interfejsie, może ulec zmianie BEZ POWIADOMIENIA.
Warstwa RS-232 nie wie, czy działa ASCII, Reed-Solomon, Unicode (arabska strona kodowa, japońska strona kodowa, Rigellian Beta strona kodowa), czy co. Po prostu wie, że pobiera sekwencję bajtów i zapisuje te bajty do portu. W przyszłym tygodniu może uzyskać zupełnie inną sekwencję bajtów od czegoś zupełnie innego. Nie obchodzi go to. On po prostu przenosi bajty.
Pierwszym (i najlepszym) wyjaśnieniem projektowania warstwowego jest klasyczny artykuł Dijkstry „Struktura systemu wieloprogramowego” . W tej branży wymagana jest lektura.
źródło
Ponieważ wyższe poziomy
mogąsię zmienić.Kiedy tak się dzieje, czy ze względu na zmiany wymagań nowi użytkownicy, inna technologia, modułowa (tj. Jednokierunkowo warstwowa) aplikacja powinna wymagać mniej konserwacji i być łatwiej dostosowywana do nowych potrzeb.
źródło
Myślę, że głównym powodem jest to, że sprawia, że sprawy są ściślej powiązane. Im ściślejsze sprzężenie, tym większe prawdopodobieństwo późniejszych problemów. Zobacz ten artykuł więcej informacji: Łączenie
Oto fragment:
Biorąc to pod uwagę, powód posiadania mocniej sprzężonego systemu jest spowodowany wydajnością. Artykuł, o którym wspomniałem, również zawiera pewne informacje na ten temat.
źródło
IMO, to bardzo proste. Nie możesz ponownie użyć czegoś, co wciąż odwołuje się do kontekstu, w którym jest używany.
źródło
Warstwy nie powinny mieć zależności dwukierunkowych
Zalety architektury warstwowej polegają na tym, że warstwy powinny być użyteczne niezależnie:
Warunki te są zasadniczo symetryczne . Wyjaśniają, dlaczego ogólnie lepiej jest mieć tylko jeden kierunek zależności, ale nie który .
Kierunek zależności powinien być zgodny z kierunkiem polecenia
Powodem, dla którego preferujemy strukturę zależności od góry, jest to, że górne obiekty tworzą i wykorzystują dolne obiekty . Zależność jest w zasadzie relacją, która oznacza, że „A zależy od B, jeśli A nie może działać bez B”. Więc jeśli obiekty w A używają obiektów w B, tak powinny wyglądać zależności.
Jest to w pewnym sensie arbitralne. W innych wzorach, takich jak MVVM, kontrola łatwo przepływa z dolnych warstw. Na przykład możesz ustawić etykietę, której widoczny podpis jest powiązany ze zmienną i zmienia się wraz z nią. Zwykle jednak preferowane są zależności odgórne, ponieważ główne obiekty są zawsze tymi, z którymi użytkownik wchodzi w interakcje, a te obiekty wykonują większość pracy.
Podczas gdy z góry na dół używamy wywoływania metod, z dołu do góry (zwykle) używamy zdarzeń. Zdarzenia pozwalają na odgórne zależności, nawet gdy formant przepływa w drugą stronę. Obiekty górnej warstwy subskrybują zdarzenia na dolnej warstwie. Dolna warstwa nie wie nic o górnej warstwie, która działa jak wtyczka.
Istnieją również inne sposoby utrzymania jednego kierunku, na przykład:
źródło
Chciałbym dodać moje dwa centy do tego, co już wyjaśnili Matt Fenwick i Kilian Foth.
Jedną z zasad architektury architektury jest to, że złożone programy powinny być budowane przez komponowanie mniejszych, samodzielnych bloków (czarnych skrzynek): minimalizuje to zależności, tym samym zmniejszając złożoność. Tak więc ta jednokierunkowa zależność jest dobrym pomysłem, ponieważ ułatwia zrozumienie oprogramowania, a zarządzanie złożonością jest jednym z najważniejszych problemów w rozwoju oprogramowania.
Tak więc w architekturze warstwowej dolne warstwy są czarnymi skrzynkami, które implementują warstwy abstrakcji, na których górne warstwy są zbudowane. Jeśli dolna warstwa (powiedzmy warstwa B) może zobaczyć szczegóły górnej warstwy A, to B nie jest już czarną skrzynką: szczegóły jej implementacji zależą od niektórych szczegółów własnego użytkownika, ale idea czarnej skrzynki polega na tym, że jej treść (jej implementacja) nie ma znaczenia dla jego użytkownika!
źródło
Dla żartu.
Pomyśl o piramidzie cheerleaderek. Dolny rząd podtrzymuje rzędy nad nimi.
Jeśli cheerleaderka w tym rzędzie patrzy w dół, jest stabilna i pozostanie zrównoważona, aby osoby nad nią nie spadły.
Jeśli spojrzy w górę, aby zobaczyć, jak sobie radzą wszyscy nad nią, straci równowagę, powodując upadek całego stosu.
Nie bardzo techniczny, ale pomyślałem, że może to pomóc.
źródło
Chociaż łatwość zrozumienia i do pewnego stopnia wymienne komponenty są z pewnością dobrym powodem, równie ważnym powodem (i prawdopodobnie powodem, dla którego warstwy zostały wymyślone w pierwszej kolejności) jest z punktu widzenia konserwacji oprogramowania. Najważniejsze jest to, że zależności powodują potencjalne uszkodzenie.
Załóżmy na przykład, że A zależy od B. Ponieważ nic nie zależy od A, programiści mogą dowolnie zmieniać A na zawartość swoich serc, nie martwiąc się, że mogą złamać wszystko inne niż A. Jednak jeśli deweloper chce zmienić B, to każda zmiana w utworzonym B może potencjalnie uszkodzić A. To był częsty problem we wczesnych dniach komputerowych (pomyśl o programowaniu strukturalnym), gdzie programiści naprawiali błąd w jednej części programu i powodowali błędy w pozornie całkowicie niezwiązanych częściach programu w innym miejscu. Wszystko z powodu zależności.
Kontynuując przykład, załóżmy teraz, że A zależy od B, a B zależy od A. IOW, zależności cyklicznej. Teraz, za każdym razem, gdy wprowadzana jest zmiana w dowolnym miejscu, może to potencjalnie uszkodzić inny moduł. Zmiana B nadal mogłaby przerwać A, ale teraz zmiana A mogłaby również złamać B.
Tak więc w swoim pierwotnym pytaniu, jeśli jesteś w małym zespole przy małym projekcie, to wszystko jest dość przesadne, ponieważ możesz swobodnie zmieniać moduły według własnego uznania. Jeśli jednak masz duży projekt, jeśli wszystkie moduły zależą od innych, to za każdym razem, gdy potrzebna jest zmiana, może to potencjalnie uszkodzić inne moduły. W dużym projekcie znajomość wszystkich wpływów może być trudna do ustalenia, więc prawdopodobnie przegapisz niektóre z nich.
Gorzej w przypadku dużego projektu, w którym jest wielu programistów (np. Niektórzy pracujący tylko w warstwie A, niektórzy w warstwie B i niektórzy w warstwie C). Ponieważ staje się prawdopodobne, że każda zmiana musi zostać przejrzana / omówiona z członkami na innych warstwach, aby upewnić się, że zmiany nie ulegają zerwaniu ani nie wymuszają przeróbki tego, nad czym pracują. Jeśli twoje zmiany wymuszają zmiany na innych, musisz ich przekonać, że powinni dokonać zmiany, ponieważ nie będą chcieli podejmować więcej pracy tylko dlatego, że masz świetny nowy sposób robienia rzeczy w swoim module. IOW, biurokratyczny koszmar.
Ale jeśli ograniczyłeś zależności do A, zależy od B, B zależy od C, to tylko ludzie z warstwy C muszą koordynować swoje zmiany w obu zespołach. Warstwa B musi tylko koordynować zmiany z zespołem Warstwy A, a zespół warstwy A może robić, co chce, ponieważ ich kod nie wpływa na warstwy B lub C. Idealnie, zaprojektujesz swoje warstwy, więc warstwa C zmieni się bardzo trochę, warstwa B nieco się zmienia, a warstwa A robi większość zmian.
źródło
Najbardziej podstawowym powodem, dla którego niższe warstwy nie powinny być świadome wyższych warstw, jest to, że istnieje o wiele więcej rodzajów wyższych warstw. Na przykład istnieją tysiące różnych programów w systemie Linux, ale wywołują tę samą
malloc
funkcję biblioteki C. Tak więc zależność jest od tych programów do tej biblioteki.Zauważ, że „niższe warstwy” są w rzeczywistości warstwami środkowymi.
Pomyśl o aplikacji, która komunikuje się przez świat zewnętrzny za pośrednictwem niektórych sterowników urządzeń. System operacyjny jest w środku .
System operacyjny nie zależy od szczegółów w aplikacjach ani od sterowników urządzeń. Istnieje wiele rodzajów sterowników urządzeń tego samego typu i współużytkują tę samą platformę sterowników urządzeń. Czasami hakerzy jądra muszą wstawić do frameworka jakieś specjalne sprawy ze względu na konkretny sprzęt lub urządzenie (ostatnio natknąłem się na: kod specyficzny dla PL2303 w systemie szeregowym USB w Linuksie). Kiedy tak się dzieje, zwykle komentują, ile to jest do bani i powinno zostać usunięte. Mimo że system operacyjny wywołuje funkcje w sterownikach, połączenia przechodzą przez haczyki, które sprawiają, że sterowniki wyglądają tak samo, natomiast gdy sterowniki wywołują system operacyjny, często używają określonych funkcji bezpośrednio według nazwy.
Tak więc, pod pewnymi względami, system operacyjny jest naprawdę niższą warstwą z perspektywy aplikacji i z perspektywy aplikacji: rodzaj centrum komunikacyjnego, w którym łączą się rzeczy i dane są przełączane, aby przejść odpowiednią ścieżkę. Pomaga projektowi koncentratora komunikacyjnego wyeksportować elastyczną usługę, z której może korzystać każdy, i nie przenosić żadnych hacków specyficznych dla urządzenia lub aplikacji do koncentratora.
źródło
Rozdzielenie obaw i podejście typu dziel / zwyciężaj może być kolejnym wyjaśnieniem tych pytań. Rozdzielenie problemów daje możliwość przenoszenia, aw niektórych bardziej złożonych architekturach daje niezależne od platformy korzyści skalowania i wydajności.
W tym kontekście, jeśli myślisz o 5-warstwowej architekturze (klient, prezentacja, biznes, integracja i warstwa zasobów), niższy poziom architektury nie powinien być świadomy logiki i biznesu wyższych poziomów i vice versa. Mam na myśli niższy poziom jako poziom integracji i zasobów. Interfejsy integracji baz danych dostarczane w ramach integracji i rzeczywistych baz danych i usług internetowych (niezależni dostawcy danych) należą do warstwy zasobów. Załóżmy, że zmienisz bazę danych MySQL na DB dokumentu NoSQL, takiego jak MangoDB pod względem skalowalności lub cokolwiek innego.
W tym podejściu warstwa biznesowa nie przejmuje się tym, w jaki sposób warstwa integracji zapewnia połączenie / transmisję przez zasób. Szuka jedynie obiektów dostępu do danych udostępnianych przez warstwę integracji. Można to rozszerzyć na więcej scenariuszy, ale w gruncie rzeczy oddzielenie obaw może być przyczyną numer jeden.
źródło
Rozwijając odpowiedź Kiliana Fotha, ten kierunek nakładania warstw odpowiada kierunkowi, w którym człowiek bada system.
Wyobraź sobie, że jesteś nowym programistą, którego zadaniem jest naprawienie błędu w systemie warstwowym.
Błędy są zwykle niezgodnością między tym, czego potrzebuje klient, a tym, co dostaje. Gdy klient komunikuje się z systemem za pomocą interfejsu użytkownika i uzyskuje wynik za pośrednictwem interfejsu użytkownika (interfejs użytkownika dosłownie oznacza „interfejs użytkownika”), błędy są zgłaszane również w kontekście interfejsu użytkownika. Tak więc, jako programista, nie masz dużego wyboru, musisz zacząć patrzeć również na interfejs użytkownika, aby dowiedzieć się, co się stało.
Dlatego konieczne jest posiadanie połączeń warstwy odgórnej. Dlaczego nie mamy połączeń działających w obie strony?
Cóż, masz trzy scenariusze, jak ten błąd może wystąpić.
Może się to zdarzyć w samym kodzie interfejsu użytkownika, więc można go tam zlokalizować. To proste, wystarczy znaleźć miejsce i naprawić je.
Może się to zdarzyć w innych częściach systemu w wyniku połączeń wykonanych z interfejsu użytkownika. Co jest średnio trudne, przeszukujesz drzewo połączeń, znajdujesz miejsce, w którym występuje błąd i naprawiasz go.
I może to nastąpić w wyniku wywołania INTO twojego kodu interfejsu użytkownika. Co jest trudne, musisz złapać połączenie, znaleźć jego źródło, a następnie dowiedzieć się, gdzie występuje błąd. Biorąc pod uwagę, że punkt, od którego zaczynasz, znajduje się głęboko w jednej gałęzi drzewa wywołań, I najpierw musisz znaleźć prawidłowe drzewo wywołań, może być kilka wywołań w kodzie interfejsu użytkownika, masz dla ciebie wycięcie debugowania.
Aby wyeliminować najtrudniejszy przypadek w jak największym stopniu, zależności od kół są zdecydowanie odradzane, warstwy łączą się głównie w sposób odgórny. Nawet gdy potrzebne jest połączenie w inny sposób, jest ono zwykle ograniczone i jasno określone. Na przykład nawet w przypadku wywołań zwrotnych, które są rodzajem połączenia zwrotnego, kod wywoływany w trybie wywołania zwrotnego zwykle zapewnia to wywołanie zwrotne w pierwszej kolejności, implementując rodzaj „opt-in” dla połączeń zwrotnych i ograniczając ich wpływ na zrozumienie system.
Nakładanie warstw jest narzędziem skierowanym przede wszystkim do programistów obsługujących istniejący system. Cóż, połączenia między warstwami również to odzwierciedlają.
źródło
Innym powodem, dla którego chciałbym tutaj wyraźnie wspomnieć, jest możliwość ponownego użycia kodu . Mamy już przykład nośnika RS232, który został wymieniony, więc przejdźmy o krok dalej ...
Wyobraź sobie, że tworzysz sterowniki. To twoja praca i piszesz sporo. Protokoły prawdopodobnie zaczną się powtarzać w pewnym momencie, podobnie jak nośniki fizyczne.
Więc zaczniesz robić - chyba że jesteś wielkim fanem robienia tego samego w kółko - jest pisanie warstw wielokrotnego użytku do tych rzeczy.
Załóżmy, że musisz napisać 5 sterowników dla urządzeń Modbus. Jeden z nich używa Modbus TCP, dwa używają Modbus na RS485, a reszta korzysta z RS232. Nie zamierzasz ponownie wdrożyć Modbus 5 razy, ponieważ piszesz 5 sterowników. Nie zamierzasz także ponownie wprowadzać Modbusa 3 razy, ponieważ masz pod sobą 3 różne warstwy fizyczne.
To, co robisz, to piszesz dostęp do mediów TCP, dostęp do mediów RS485 i prawdopodobnie dostęp do mediów RS232. Czy mądrze jest wiedzieć, że w tym momencie będzie powyżej warstwy Modbus? Prawdopodobnie nie. Następny sterownik, który zamierzasz wdrożyć, może również używać Ethernet, ale używać HTTP-REST. Szkoda byłoby, gdybyś musiał ponownie wdrożyć Ethernet Media Access w celu komunikacji przez HTTP.
Jedna warstwa powyżej, zaimplementujesz Modbus tylko raz. Ta warstwa Modbus po raz kolejny nie będzie wiedziała o sterownikach, które są o jedną warstwę wyżej. Ci kierowcy oczywiście będą musieli wiedzieć, że powinni rozmawiać przez Modbus i powinni wiedzieć, że używają Ethernetu. Jednak wdrożyłem sposób, w jaki właśnie to opisałem, nie tylko mogłeś po prostu wyrwać warstwę i zastąpić ją. możesz oczywiście - i to dla mnie jest największą korzyścią ze wszystkich - śmiało wykorzystaj istniejącą warstwę Ethernet do czegoś absolutnie niezwiązanego z projektem, który pierwotnie spowodował jego powstanie.
Jest to coś, co prawdopodobnie postrzegamy każdego dnia jako programistów, co oszczędza nam mnóstwo czasu. Istnieje niezliczona ilość bibliotek dla wszelkiego rodzaju protokołów i innych rzeczy. Istnieją one z powodu takich zasad, jak kierunek zależności podążający za poleceniem, co pozwala nam budować warstwy oprogramowania wielokrotnego użytku.
źródło