Czysta architektura wuja Boba - klasa encji / modelu dla każdej warstwy?

44

TŁO :

Próbuję użyć czystej architektury wuja Boba w mojej aplikacji na Androida. Studiowałem wiele projektów open source, które próbują pokazać właściwy sposób, aby to zrobić, i znalazłem ciekawą implementację opartą na RxAndroid.

CO ZWRÓCIŁEM UWAGĘ:

W każdej warstwie (prezentacja, domena i dane) istnieje klasa modelu dla tego samego obiektu (mówiący UML). Ponadto istnieją klasy mapujące, które zajmują się transformacją obiektu, ilekroć dane przekraczają granice (między warstwami).

PYTANIE:

Czy wymagane jest posiadanie klas modeli w każdej warstwie, gdy wiem, że wszystkie będą miały te same atrybuty, jeśli wszystkie operacje CRUD będą potrzebne? A może jest to reguła lub najlepsza praktyka w przypadku korzystania z czystej architektury?

Rami Jemli
źródło

Odpowiedzi:

52

Moim zdaniem absolutnie nie tak to oznacza. I to jest naruszenie SUCHEGO.

Chodzi o to, że obiekt / domena w środku jest modelowany tak, aby reprezentował domenę tak dobrą i wygodną, ​​jak to możliwe. Jest w centrum wszystkiego i wszystko może od tego zależeć, ponieważ sama domena nie zmienia się przez większość czasu.

Jeśli baza danych na zewnątrz może przechowywać te obiekty bezpośrednio, wówczas mapowanie ich do innego formatu w celu oddzielenia warstw jest nie tylko bezcelowe, ale tworzy duplikaty modelu i nie jest to intencją.

Na początek czysta architektura została stworzona z myślą o innym typowym środowisku / scenariuszu. Aplikacje serwera biznesowego z gigantycznymi warstwami zewnętrznymi, które potrzebują własnych typów specjalnych obiektów. Na przykład bazy danych, które produkują SQLRowobiekty i wymagają SQLTransactionsw zamian aktualizacji elementów. Jeśli chcesz użyć tych w centrum, musisz naruszyć kierunek zależności, ponieważ twój rdzeń będzie zależał od bazy danych.

W przypadku lekkich ORM, które ładują i przechowują obiekty encji, tak nie jest. Dokonują mapowania między SQLRowdomeną wewnętrzną a domeną. Nawet jeśli potrzebujesz wstawić @Entitiyadnotację ORM do obiektu domeny, argumentowałbym, że nie ustanawia to „wzmianki” o zewnętrznej warstwie. Ponieważ adnotacje są tylko metadanymi, żaden kod, który ich specjalnie nie szuka, nie zobaczy ich. Co ważniejsze, nic nie musi się zmienić, jeśli je usuniesz lub zastąpisz adnotacją innej bazy danych.

W przeciwieństwie do tego, jeśli zmienisz domenę i stworzyłeś wszystkich twórców map, musisz bardzo zmienić.


Poprawka: Powyższe jest nieco uproszczone i może się nawet mylić. Ponieważ jest część w czystej architekturze, która chce, abyś tworzył reprezentację dla każdej warstwy. Ale należy to rozpatrywać w kontekście aplikacji.

Mianowicie następujące tutaj https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html

Ważne jest to, że izolowane, proste struktury danych są przekraczane przez granice. Nie chcemy oszukiwać i przekazywać wierszy jednostek ani baz danych. Nie chcemy, aby struktury danych miały jakąkolwiek zależność, która narusza zasadę zależności.

Przenoszenie bytów z centrum w kierunku warstw zewnętrznych nie narusza zasady zależności, ale są one wspomniane. Ma to jednak uzasadnienie w kontekście przewidywanej aplikacji. Przekazywanie jednostek przesuwałoby logikę aplikacji na zewnątrz. Zewnętrzne warstwy musiałyby wiedzieć, jak interpretować obiekty wewnętrzne, musiałyby skutecznie robić to, co powinny robić warstwy wewnętrzne, takie jak warstwa „przypadku użycia”.

Poza tym oddziela warstwy, więc zmiany w rdzeniu niekoniecznie wymagają zmian w warstwach zewnętrznych (patrz komentarz SteveCallendera). W tym kontekście łatwo jest zobaczyć, jak obiekty powinny reprezentować konkretny cel, do którego są używane. Również warstwy powinny ze sobą rozmawiać w odniesieniu do obiektów wykonanych specjalnie na potrzeby tej komunikacji. Może to nawet oznaczać, że istnieją 3 reprezentacje, po 1 w każdej warstwie, 1 do transportu między warstwami.

I jest https://blog.8thlight.com/uncle-bob/2011/11/22/Clean-Architecture.html, który porusza się powyżej:

Inni ludzie obawiają się, że wynikiem mojej porady będzie dużo zdublowanego kodu i dużo rutynowego kopiowania danych z jednej struktury danych do drugiej w różnych warstwach systemu. Z pewnością też tego nie chcę; i nic, co zasugerowałem, nieuchronnie doprowadziłoby do powtórzenia struktur danych i nadmiernego kopiowania w terenie.

To IMO implikuje, że zwykłe kopiowanie obiektów 1: 1 to zapach w architekturze, ponieważ tak naprawdę nie używasz odpowiednich warstw i / lub abstrakcji.

Później wyjaśnia, jak wyobraża sobie wszystkie „kopiowanie”

Interfejs użytkownika oddziela się od reguł biznesowych, przekazując między nimi proste struktury danych. Nie pozwalasz swoim kontrolerom nic wiedzieć o regułach biznesowych. Zamiast tego kontrolery rozpakowują obiekt HttpRequest do prostej waniliowej struktury danych, a następnie przekazują tę strukturę danych do obiektu interaktora, który implementuje przypadek użycia, wywołując obiekty biznesowe. Następnie interaktor zbiera dane odpowiedzi do innej struktury danych waniliowych i przekazuje je z powrotem do interfejsu użytkownika. Widoki nie wiedzą o obiektach biznesowych. Po prostu patrzą w tę strukturę danych i przedstawiają odpowiedź.

W tej aplikacji istnieje duża różnica między reprezentacjami. Przepływające dane to nie tylko byty. A to gwarantuje i wymaga różnych klas.

Jednak zastosowany do prostej aplikacji na Androida, takiej jak przeglądarka zdjęć, w której Photopodmiot ma około 0 reguł biznesowych, a „przypadek użycia”, który się nimi zajmuje, prawie nie istnieje i jest bardziej zaniepokojony buforowaniem i pobieraniem (proces ten powinien być IMO reprezentowane bardziej precyzyjnie), punkt, w którym oddzielne przedstawienia zdjęcia zaczynają zanikać. Mam nawet wrażenie, że samo zdjęcie jest obiektem do przesyłania danych, podczas gdy brakuje prawdziwej warstwy rdzenia logiki biznesowej.

Istnieje różnica między „oddzielaniem interfejsu użytkownika od reguł biznesowych poprzez przekazywanie prostych struktur danych między nimi” a „gdy chcesz wyświetlić zdjęcie, zmień jego nazwę 3 razy po drodze” .

Poza tym, widzę, że te aplikacje demonstracyjne zawodzą w reprezentowaniu czystej architektury, ponieważ kładą ogromny nacisk na rozdzielanie warstw w celu oddzielenia warstw, ale skutecznie ukrywają to, co robi aplikacja. Jest to w przeciwieństwie do tego, co zostało powiedziane w https://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html - a mianowicie

architektura aplikacji krzyczy o przypadkach użycia aplikacji

Nie widzę takiego nacisku na rozdzielanie warstw w czystej architekturze. Chodzi o kierunek zależności i skupienie się na reprezentowaniu rdzenia aplikacji - encji i przypadków użycia - w idealnie prostej Javie bez zależności na zewnątrz. Nie chodzi tak bardzo o zależności od tego rdzenia.

Jeśli więc aplikacja ma rdzeń reprezentujący reguły biznesowe i przypadki użycia i / lub różne osoby pracują na różnych warstwach, należy je rozdzielić w zamierzony sposób. Jeśli natomiast piszesz prostą aplikację samodzielnie, nie przesadzaj. 2 warstwy z płynnymi granicami mogą być więcej niż wystarczające. Warstwy można również dodać później.

zapl
źródło
1
@RamiJemli Idealnie jednostki są takie same we wszystkich aplikacjach. Na tym polega różnica między „regułami biznesowymi obowiązującymi w całym przedsiębiorstwie” a „regułami biznesowymi dotyczącymi aplikacji” (czasem logika biznesowa vs. Rdzeń jest bardzo abstrakcyjną reprezentacją twoich bytów, która jest na tyle ogólna, że ​​możesz z niej korzystać wszędzie. Wyobraź sobie bank, który ma wiele aplikacji, jedną do obsługi klienta, jedną działającą na bankomatach, drugą jako interfejs WWW dla samych klientów. Wszyscy mogą korzystać z tego samego, BankAccountale z regułami specyficznymi dla aplikacji, co możesz zrobić z tym kontem.
4
Myślę, że ważnym punktem w czystej architekturze jest to, że używając warstwy adaptera interfejsu do konwersji (lub, jak mówisz, mapy) między reprezentacjami różnych warstw bytu, zmniejszasz zależność od tego bytu. Jeśli nastąpi zmiana w warstwie Usecase lub Entity (mam nadzieję, że jest to mało prawdopodobne, ale wraz ze zmianą wymagań te warstwy się zmienią), wpływ zmiany jest zawarty w warstwie adaptera. Jeśli zdecydujesz się używać tej samej reprezentacji bytu w całej swojej architekturze, wpływ tej zmiany byłby znacznie większy.
SteveCallender,
1
@RamiJemli dobrze jest używać frameworków, które upraszczają życie, chodzi o to, abyś był ostrożny, gdy twoja architektura na nich polega i zaczynasz umieszczać je w centrum wszystkiego. Oto nawet artykuł na temat blogu RxJava.8thlight.com/uncle-bob/2015/08/06/let-the-magic-die.html - nie oznacza to, że nie powinieneś go używać. To bardziej: Widziałem to, za rok będzie inaczej, a kiedy twoja aplikacja jest nadal w pobliżu, utkniesz z nią. Zrób to szczegółowo i rób najważniejsze rzeczy w zwykłej starej Javie, stosując jednocześnie zwykłe stare zasady SOLID.
zapl
1
@zapl Czy tak samo myślisz o warstwie usług sieciowych? Innymi słowy, czy umieściłbyś @SerializedNameadnotacje Gsona w modelu domeny? Czy stworzyłbyś nowy obiekt odpowiedzialny za mapowanie odpowiedzi sieciowej na model domeny?
tir38
2
@ tir38 Sama separacja nie przynosi korzyści, lecz koszty przyszłych zmian, które z nią idą. => Zależy od aplikacji. 1) utworzenie i utrzymanie dodanego etapu, który przekształca się między różnymi reprezentacjami, kosztuje czas. Na przykład dodanie pola do domeny i zapomnienie o dodaniu go w innym miejscu nie jest niespotykane. Nie da się tego zrobić z prostym podejściem. 2) Późniejsze przejście do bardziej złożonej konfiguracji kosztuje, jeśli okaże się, że jest to potrzebne. Dodawanie warstw nie jest łatwe, dlatego w dużych aplikacjach łatwiej jest usprawiedliwić więcej warstw, które nie są natychmiast potrzebne
zapl
7

Właściwie to masz rację. I nie ma naruszenia DRY, ponieważ akceptujesz SRP.

Na przykład: masz metodę biznesową createX (nazwa ciągu), a następnie możesz mieć metodę createX (nazwa ciągu) w warstwie DAO, wywoływaną w ramach metody biznesowej. Mogą mieć ten sam podpis i być może jest tylko delegacja, ale mają różne cele. Możesz także mieć createX (nazwa ciągu) na UseCase. Nawet wtedy nie jest to zbędne. Mam na myśli to: te same podpisy nie oznaczają tej samej semantyki. Wybierz inne nazwy, aby wyczyścić semantykę. Samo nazywanie to wcale nie wpływa na SRP.

UseCase odpowiada za logikę specyficzną dla aplikacji, obiekt biznesowy odpowiada za logikę niezależną od aplikacji, a DAO jest odpowiedzialne za przechowywanie.

Ze względu na różną semantykę wszystkie warstwy mogą mieć własne modele reprezentacji i komunikacji. Często widzisz „byty” jako „obiekty biznesowe” i często nie widzisz konieczności ich oddzielania. Ale w „dużych” projektach należy dołożyć starań, aby odpowiednio rozdzielić warstwy. Im większy projekt, tym większa możliwość, że potrzebna będzie inna semantyka reprezentowana na różnych warstwach i klasach.

Możesz pomyśleć o różnych aspektach tego samego semantycznego. Obiekt użytkownika musi być wyświetlany na ekranie, ma pewne wewnętrzne zasady spójności i musi być gdzieś przechowywany. Każdy aspekt powinien być reprezentowany w innej klasie (SRP). Tworzenie twórców map może być uciążliwe, więc w większości projektów, w których pracowałem, te aspekty zostały połączone w jedną klasę. Jest to wyraźnie naruszenie SRP, ale tak naprawdę nikogo to nie obchodzi.

Zastosowanie czystej architektury i SOLID nazywam „nieakceptowalnym społecznie”. Pracowałbym z tym, gdyby mi wolno. Obecnie nie wolno mi tego robić. Czekam na moment, w którym będziemy musieli poważnie potraktować SOLID.

user205407
źródło
Myślę, że żadna metoda w warstwie danych nie powinna mieć takiego samego podpisu jak każda metoda w warstwie domeny. W warstwie Domeny stosujesz konwencje nazewnictwa związane z biznesem, takie jak signUp lub login, aw warstwie Danych używasz save (jeśli wzorzec DAO) lub dodajesz (jeśli Repository, ponieważ ten wzorzec używa Collection jako metafory). Wreszcie, nie mówię o bytach (danych) i modelu (domenie), podkreślam bezużyteczność UserModel i jego Mapera (warstwa prezentacji). Możesz wywołać klasę użytkownika domeny w prezentacji, co nie narusza reguły zależności.
Rami Jemli,
Zgadzam się z Ramim, mapowanie jest niepotrzebne, ponieważ można wykonać mapowanie bezpośrednio w implementacji interactor.
Christopher Perry
5

Nie, nie musisz tworzyć klas modeli w każdej warstwie.

Entity ( DATA_LAYER) - to pełna lub częściowa reprezentacja obiektu Database.DATA_LAYER

Mapper ( DOMAIN_LAYER) - w rzeczywistości jest klasą, która konwertuje Entity na ModelClass, na której będzie używanaDOMAIN_LAYER

Spójrz: https://github.com/lifedemons/photoviewer

deathember
źródło
1
Oczywiście nie jestem przeciwko jednostkom w warstwie danych, ale w twoim przykładzie klasa PhotoModel w warstwie prezentacji ma takie same atrybuty jak klasa Photo w warstwie domeny. Technicznie jest to ta sama klasa. czy to konieczne?
Myślę, że coś jest nie tak w twoim przykładzie, ponieważ warstwa domeny nie powinna zależeć od innych warstw, ponieważ w twoim przykładzie twoi twórcy map zależą od bytów w twojej warstwie danych, którym IMO, powinno być odwrotnie
navid_gh