[PL] Architecture Aurora Get Noticed 2016 Info Issues

Zmiany w architekturze projektu…

Cześć,

obiecałem Wam, że dziś wieczorem pojawi się nowy wpis, w którym zaimplementujemy kolejny fragment naszej aplikacji. Problem pojawił się dziś koło 2 rano kiedy ślęczałem nad prostą metodą i dumałem jak to zrobić, żeby było ok. W pewnym momencie zdałem sobie sprawę, że skoro tyle czasu mi to zajmuje to znaczy, że wszystko nadto skomplikowałem i wypadałoby tu posprzątać. Z tego też względu pół dzisiejszego dnia spędziłem na refactorze. Dlaczego zatem piszę? Chciałem Was uprzedzić, że kolejne wpisy mogą odbiegać od tego co widzieliście do tej pory. Nie chcę tu także wprowadzać kodu, który opisywałbym przez kolejne 300 linii, dlatego postaram się wypunktować najważniejsze zmiany wraz z krótkim uzasadnieniem ich wprowadzenia:

 

Usunięcie DTO oraz DomainObject. Wprowadzenie ReadModel oraz WriteModel

To była rzecz, która chyba najbardziej mnie irytowała. Założenie było następujące. DTO tylko w warstwie Web, DomainObject tylko w Domain. Wszystko fajnie, ale kiedy chciałem zwrócić sobie np. stronicowaną listę użytkowników musiałem w domenie przycinać IQueryable<UserEntity> do postaci IQueryable<UserDomainObject>, następnie zmaterializować to do listy, po czym w warstwie Proxy zmapować na DTO, które było identyczne. Analogicznie działało to w przypadku zapisu do bazy danych. Wobec tego postanowiłem wprowadzić modele do zapisu i odczytu, które są widoczne zarówno w Web, Proxy jak i Domain. Dzięki temu kod się uprościł i nie irytuje mnie tak jak wcześniej.

 

Pozostawienie tylko generycznego repozytorium. Podział na ReadRepository oraz WriteRepository

Druga rzecz, która wydała mi się bezsensowna to repozytoria np. użytkownika, które dziedziczyły po repozytorium generycznym. Idea była taka, że korzystam z domyślnych CRUD-owych metod bazowych, a w specjalnych przypadkach piszę metodę w repozytorium „potomnym”. Okazało się jednak, że nie jest mi to potrzebne, a wszystkie repozytoria były dosłownie puste. Ich jedyną rolą było przekazanie kontekstu bazodanowego do repozytorium generycznego. Z tego względu wszystko poza generykiem zostało usunięte. Drugim ruchem był podział repozytorium na Read oraz Write. Wydało mi się to naturalne kiedy posiadałem już modele do zapisu i odczytu. Tu też małe dopowiedzenie. W ReadRepository wystawione Query jest oznaczone jako AsNoTracking. Wszystko dzięki temu artykułowi 😉

 

 Zmiana implementacji RepositoryFactory

Jest to implikacja dokonanego podziału. Fabryka posiada teraz dwie metody tj. GetRead oraz GetWrite.

 

Zmiana UnitOfWork i UnitOfWorkFactory

To także, jest pewna implikacja podziału repozytorium. W pierwotnej wersji utworzenie nowego UOW przez fabrykę otwierało jednocześnie transakcję bazodanową. Po co mi jednak transakcja, kiedy chcę tylko pobrać dane? Entity Framework tego nie robi, więc i ja nie powinienem. Wobec tego metoda Get przyjmuje teraz parametr isReadOnly, który determinuje to czy transakcja zostanie otwarta. Aha, do klasy UnitOfWork dodałem także metodę Rollback. Nie wiem czemu jej tam nie było…

 

Typ IResult zwracany zawsze z warstwy Proxy

IResult służy mi do określenia, czy commit na UOW się powiódł. Wszystko odbywało się w warstwie Proxy. Zdarzały się jednak wyjątki, kiedy rezultat był tworzony na podstawie IdentityResult w kontrolerze np. podczas rejestracji użytkownika. To „rozjechanie” też mocno mnie drażniło, dlatego wszystko zostało przeniesione do warstwy Proxy.

 

DTO na kliencie zamienione na Model

Oczywiście na kliencie także nastąpiła zmiana nazewnictwa. Tu jednak nie ma podziału na Read i Write. Jest po prostu Model.

 

To tyle mojej „spowiedzi”. Zachęcam Was do zajrzenia na githuba Aurory. Znajdziecie tam aktualną wersję projektu, a po commitach będziecie w stanie dostrzec wszelkie zmiany, które wprowadziłem. Mam nadzieję, że wyjdzie nam to na lepsze 😉 Tak jak wspomniałem jutro przejdziemy do konkretów, a temat będzie chyba całkiem przydatny. Nie zapomnijcie także śledzić mojego twittera, gdzie wrzucam czasem jakiś LOL kontent, ciekawe techniczne smaczki, lub publikuje najnowsze wpisy 😉

Do jutra !

  • witek1902

    Dlaczego modele umieściłeś w module Infrastructure?

    • Dariusz Pawlukiewicz

      Witek,
      z tego „miejsca” są one widoczne zarówno dla Web, Proxy oraz Domain.

      • witek1902

        Moduł infrastruktury nie służy do przechowywania informacji o modelu. Powinny w nim być utils’y, helper’y, konfiguracje itp. Przemyśl to jeszcze, bo, moim zdaniem, to co zrobiłeś na początku, czyli DomainModel i DTO/ViewModel jest dużo bardziej czytelne. Podział na Read/Write, czyli poniekąd zastosowanie wzorca CQ(R)S to oddzielna kwestia. Jeśli irytuje Cię mapowanie to spróbuj użyć jakiegoś narzędzia do auto-mapowania, np. AutoMappera, który wspiera .NET Core (https://lostechies.com/jimmybogard/2015/02/02/automapper-support-for-asp-net-5-0-and-asp-net-core-5-0/) 🙂

        • Dariusz Pawlukiewicz

          Witek,
          AutoMappera używałem i teraz służy mi do mapowania WriteModel -> Entity przy zapisie. Co do czytelności to jak dla mnie sprawa jest sporna. Jeśli chodzi o względy praktyczne to załamałem się kiedy musiałem robić coś takiego jak opisałem we wpisie. Pobieram listę użytkowników i domena zwraca IEnumerable UserDomainObject, w proxy musiałem dodatkowo robić :

          return users.Select(c => c.AsDto());

          Jaki był tego benefit? Jak dla mnie tylko taki, że musiałem pisać metody rozszerzające dla każdego:

          – DTO na DomainObject
          -DomainObject na DTO
          -DomainObject na Entity

          Wracając do czytelności, to moim zdaniem właśnie W/R model jest bardziej czytelny bo wiem w którą stronę „płyną” dane. DTO mogło odnosić się zarówno do obiektu, który wysłał klient jak i zmapowanego rezultu z domeny, który zostaje klientowi zwrócony. Można powiedzieć, że próbuje się bawić w takiego biednego CQS-a 😛 Jakoś mi to po prostu wszystko nie grało.

          • witek1902

            Hmmm, a po co podział na DomainObject i Entity, jakie korzyści z tego płynęły? Nie mów tylko, że ‚może kiedyś będę chciał zmienić ORMa’, bo przecież każdy wie, że nigdy tak się nie robi 😉
            Moim zdaniem w zupełności wystarczy Domain Model + View model.
            Zamiast konkretnych DTO można używać bardziej szczegółowego podziału, np.: PolicyCreateForm, PolicyList (PolicyListItem). Konkretne DTO/Filter object’y (SearchCriteria) opakowywać w ViewModel’e/Command’y (w Javie tak to nazywają ;P ).

        • Dariusz Pawlukiewicz

          Ahhh, i jeszcze kwestia modułu infrastruktury. Proponujesz nowy projekt tylko z modelami ?

  • Chyba coś nie tak było z tymi repozytoriami od początku, skoro w domenie miałeś IQueryable. A jeśli modele zapisywane do bazy danych są identyczne jak modele widoczne w interfejsie użytkownika, to jest to chyba jakiś ekstremalny i niespotykany w rzeczywistości przypadek. Czy będziesz w obecnej architekturze w stanie pobrać efektywnie z bazy danych obiekt, którego połowa właściwości jest w jednej tabeli, a druga połowa w innej?

    • Dariusz Pawlukiewicz

      Dawid,
      co do modeli to chodziło bardziej o to, że pobrane dane musiały przejść przez warstwy jednocześnie zamieniając się z DO na DTO. Modele do odczytu (UI) nie były tożsame z tymi do zapisu. Co do repozytorium… IQueryable dalej jest widoczne w domenie. Powiem szczerze, że jestem maksymalnie skołowany jeśli chodzi o ten wzorzec. Stosuje go jakiś czas i co spotkam programistę to słyszę inne zdanie o tym jak to powinno wyglądać. Czytałem ostatnio wpis http://jakubskoczen.pl/dlaczego-nie-zwracac-typu-iqueryablet-w-repo/, w którym się udzieliłeś (rozumiem, że Twoje pytanie jest po części z tym związane). Żeby nie było, argumenty tam podane do mnie przemawiają: testowalność, SRP i przede wszystkim brak uzależnienia domeny od ORM. Wszystko się zgadza. Problemem jest dla mnie raczej sposób zwracania danych z repozytorium. Raz czytam, że dobra praktyka to metoda per model, kolejnym razem czytam budowaniu zapytania w oparciu o QueryModel, a jeszcze gdzieś indziej widzę implementację Repository.GetAll().Select(…), gdzie na bazie wykonało się SELECT *, a my operujemy sobie wesoło w pamięci. Powiem Ci, że zanim odpisałem jeszcze raz „przejechałem się” po blogach/ forach w poszukiwaniu złotego środka. Wszedłem tu :https://forum.4programmers.net/Inzynieria_oprogramowania/214921-wzorzec_repozytorium . I znów co programista to wzajemne oskarżanie się o to, że generyki to antywzorce repozytorium i nie są żadnym kontraktem, a taka implementacja to też syf… Z tego co wyczytałem z Twoich komentarzy jesteś zwolennikiem metod per model. I wydaje mi się, że to jest chyba najbardziej ok. Tylko znów pojawia się dla mnie problem i jest związany z Twoim pytaniem. Co jeśli potrzebuje pobrać model gdzie połowa property jest pobrana z tabeli A , a druga połowa z tabeli B? Wstrzyknąć repo A do Repo B ? ? Chyba nie ? Ja w domenie mogę w select zawsze zawrzeć drugie IQueryable z innego repozytorium i pobrać dane w jednym round tripie do bazy. Daj znać proszę jak to widzisz i jakie masz propozycje, bo zgadzam się z Tobą, że to rozwiązanie najlepsze nie jest.

  • Pingback: law & political()