Architektura aplikacji biznesowych

0

W dzisiejszej podróży autobusem naszła mnie lekka retrospektywa, mianowicie zauważyłem, że od pewnego czasu we wszystkich moich aplikacjach typowo biznesowych (jakiś GRUD + logika w zależności od klienta), przyjmuje stałą, jednakową strukturę projektu. Jest mi z nią dobrze, przyzwyczaiłem się i do dziś uważałem ją za jedyną słuszną, ale tak sobie jadąc pomyślałem, że może jednak nie jest ona taka idealna jak sobie wyobrażam. Jako, że staram się przywiązywać wagę do czytelności kodu i architektury programu pomyślałem, że co mi szkodzi, założę nowy temat :)

A więc chciałbym przypomnieć na starcie, że mówimy o typowo biznesowych rozwiązaniach, miejmy na uwadze, że zazwyczaj w tego typu aplikacjach liczy się czas realizacji (ja wiem, że zawsze można się czegoś doczepić, ale bądźmy w miarę wyrozumiali).

A więc moja struktura projektu wygląda następująco:
|-> Project.Base
|------> Project.Base.Model (klasy POCO, zwykle odwzorowanie relacji w bazie danych)
|------> Project.Base.Repository (interfejsy repozytorium)
|------> Project.Base.Service (interfejsy usług/logik biznesowych)
|------> Project.Base.Manager (interfejsy menedżerów, mam na myśli jakiś mechanizm silnie powiązany z jakąś technologią, np. uwierzytelnianie, czy
| jakieś ładnie opakowane mechanizmy typowo SharePoint, CRM, itp).
|-> P0roject.SQL.Repository
|------> Implementacja repozytorium dla SQL
|-> Project.CRM.Repository
|------> Implementacja repozytorium dla CRM
|-> Project.Something.Repository
|-------> Implementacja
|-> Project.Service
|-------> Implementacja service/logic - zwykle są to bezstanowe logiki, ale zdarzają się wyjątki od reguły (rzadko), niezależne od technologii
|-> Project.Manager
|-------> Implementacja managerów (np. uwierzytelnianie w ASP.NET przy pomocy cookie, itp.)
|-> Project.ViewModel
|-------> Klasy POCO, które są używane przez moją warstwę UI
|-> Project.Configuration
|-------> Ten projekt ma odwołania do wszystkich powyższych projektów, i ot tutaj znajduje się konfiguracja jakiegoś DI/IoC
|-> Project.Controller
|-------> Tutaj w zależności od aplikacji znajduje się implementacja kontrolerów/prezentereów (jak zwał tak zwał)
|-> Project.UI
|-------> Implementacja GUI korzystająca z Project.ViewModel

Największy problem mam z tym czy warstwa Project.Service powinna zwracać encje z Modelu czy ViewModelu, myślałem nad tym i powiem szczerze nie wiem. Nie ukrywam, że zwracanie ViewModeli z warstwy Service jest bardzo wygodne, korzystam z kilku repozytorium i sklejam całość w jeden ViewModel, przez co akcje kontrolera są bardzo czytelne, z drugiej strony uzależniam wtedy logikę/serwisy od konkretnej postaci ViewModeli. Co myślicie, które rozwiązanie lepsze? Co tu poprawić? Jakieś uwagi?

1

Tak jak Ty ja również mam swoją koncepcję i szablon projektu, który wszędzie jest używany. Mam sobie tam kontener IoC od razu do użytku, nHibernate od razu przygotowanego do konfiguracji, katalogi na ViewModele, Modele, serwisy i temu podobne rzeczy jak enum'y, helpery, dependency properties etc... Generalnie z zasady ViewModel to coś używanego przez widok i takie klasy powinny być wystawiane dla widoku do prezentacji danych. Z drugiej strony zwracanie VM z serwisów trochę "okalecza" te serwisy i wg mnie jest to chybione podejście, bo uzależnia taką klasę pośrednio od widoku, a takiej zależności być nie powinno. IMHO serwis powinien zarządzać danymi neutralnymi w stosunku do widoku, a obróbką danych dla widoku powinien zająć się VM. A jeżeli VM się rozrośnie przez taką obróbkę to można wsadzić to do osobnej klasy.

PS: Piszę z perspektywy programisty WPF używającego mvvm.

0

Czyli dobrze rozumiejąc, VM powinny mieć odwołania do ISomethingService\ISomethingRepository i same składać swoje właściwości/pola? Dotychczas VM traktowałem jako klasy POCO, bez żadnej implementacji. Jakoś nie widzę plusu z takiego podejścia, mógłbyś przytoczyć jakieś argumenty za?

0
Mateusz napisał(a)

Czyli dobrze rozumiejąc, VM powinny mieć odwołania do ISomethingService...

Tak. Ja np. używam do tego Dependency Injection wespół z kontenerem IoC. W konstruktorach VM mam po prostu interfejsy do poszczególnych klas serwisowych jako argumenty, a kontener IoC sam podaje mi do tego odpowiednie, już konkretne klasy. Naturalnie do tych serwisów mam pola w VM.

Mateusz napisał(a)

Dotychczas VM traktowałem jako klasy POCO, bez żadnej implementacji...

Sądząc po tym co piszesz to pewnie piszesz w ASP czyli web etc... WPF jest tak zrobiony, że ViewModel prawie zawsze nie jest prostą klasą POCO, bo posiada w sobie np metody sterujące pośrednio zmianami w GUI np ustawiając odpowiednie publiczne własności VM używane do bindowania danych. Może też dostarczać eventy do wyższej warstwy, które już programiści GUI sobie odpowiednio obsłużą etc...

VM w WPF i mvvm to taki "kontroler" z MVC w dużym cudzysłowie. (Tak... wiem że puryści mogą się zaraz oburzyć ale analogii trochę jednak jest.)
//
PS: W ogóle fajnie żeby wypowiedział się ktoś kto ma trochę większe doświadczenie niż ja z dużymi apkami. Wołam @somekind :)

2

ViewModele w WPF to trochę coś innego niż te w ASP.net.

W ASP.net MVC, VM pełnią role wyłącznie DTO, czyli podejście POCO jest ja najbardziej na miejscu. Przy purystycznym podejściu VM powinny być tworzone w controlerze, nie modelu( serwisy (logika biznesowa u Ciebie) to też jakby nie było część warstwy która zwie się model, a u Ciebie model to tylko model danych).

W WPF nie ma czegoś takiego jak Controler, więc VM mają też trochę odpowiedzialności pochodzącej z controlerów, dlatego zwierają referencje do serwisów/modelu, w ASP.net mvc one się znajdują w controlerze.

W swojej architekturze masz słabo zarysowane warstwy, które trochę naginasz, z punktu widzenia DDD zostałbyś też oskarżony o posiadanie modelu anemicznego. Czy to źle?
Jeśli architektura sprawdza się w boju to nie koniecznie, w końcu w programowaniu chodzi o dostarczanie wartości biznesowej, a nie o purystyczne podejście.

a tak w ogóle to polecam lekturę:
https://www.amazon.com/Microsoft-NET-Architecting-Applications-Enterprise/dp/0735685355/

3
Mateusz napisał(a):

A więc moja struktura projektu wygląda następująco:
|-> Project.Base
|------> Project.Base.Model (klasy POCO, zwykle odwzorowanie relacji w bazie danych)
|------> Project.Base.Repository (interfejsy repozytorium)
|------> Project.Base.Service (interfejsy usług/logik biznesowych)
|------> Project.Base.Manager (interfejsy menedżerów, mam na myśli jakiś mechanizm silnie powiązany z jakąś technologią, np. uwierzytelnianie, czy
| jakieś ładnie opakowane mechanizmy typowo SharePoint, CRM, itp).

Project.Base.Model to oddzielny moduł, czy tylko namespace w ramach Project.Base?
Mam nadzieję, że to pierwsze.

Nie masz nigdzie encji, więc nie robisz DDD. Czemu zatem masz repozytoria? Czym one się u Ciebie zajmują? Wnoszą jakąkolwiek wartość?

|-> Project.Manager
|-------> Implementacja managerów (np. uwierzytelnianie w ASP.NET przy pomocy cookie, itp.)

Czyli Manager różni się od Service tym, że jest związany z infrastrukturą, a nie z logiką biznesową?

Największy problem mam z tym czy warstwa Project.Service powinna zwracać encje z Modelu czy ViewModelu, myślałem nad tym i powiem szczerze nie wiem. Nie ukrywam, że zwracanie ViewModeli z warstwy Service jest bardzo wygodne, korzystam z kilku repozytorium i sklejam całość w jeden ViewModel, przez co akcje kontrolera są bardzo czytelne, z drugiej strony uzależniam wtedy logikę/serwisy od konkretnej postaci ViewModeli. Co myślicie, które rozwiązanie lepsze?

A to są serwisy biznesowe czy aplikacyjne? Skoro nie masz DDD, to nie robisz nic złego zwracając ViewModel z serwisu - jest on po prostu kontraktem Twojej aplikacji. Równie dobrze możesz ten swój serwis opakować w jakąś technologię webserwisów i wystawić na świat dla zupełnie innej aplikacji niż Twoje GUI.

Twoje podejście wydaje mi się strasznie skomplikowane. Moja "ulubiona architektura" jest prostsza:

  1. Project.Model - definicje klas mapowanych na relacje.
  2. Project.Persistence- konfiguracja NHibernate: mapowania, filtry, konwencje, itd.
  3. Project.Contracts - kontrakty między Application a WebClient
    a. ViewModels - viewmodele, czyli DTO, które latają między GUI, a aplikacją.
    b. Services - interfejsy serwisów, które operują na Project.Model, na bazie za pomocą ORMa, a ich API to ViewModele.
  4. Project.Application - implementacja serwisów
  5. Project.WebClient - cała aplikacja webowa z kontrolerami, widokami i wszystkim co operuje na HTTP/HTML/CSS/JS.
  6. Project.Common - rzeczy, które da się dzielić między Model, a ViewModel, w praktyce głównie enumy.
  7. Project.Utils - rzeczy infrastrukturalne (np. związane z security), wszelkie rzeczy globalnie przydatne.

Gdybym miał dodawać integrację z jakimiś systemami zewnętrznymi, to bym dodał pewno Project.Integration i jego używał z Project.Application.

neves napisał(a):

Przy purystycznym podejściu VM powinny być tworzone w controlerze

Wątpię, żeby ktoś gdzieś tak stwierdził. Można niby składać VM w kontrolerze, ale równie dobrze kontroler może dostać VM od aplikacji.

0
somekind napisał(a):
neves napisał(a):

Przy purystycznym podejściu VM powinny być tworzone w controlerze

Wątpię, żeby ktoś gdzieś tak stwierdził. Można niby składać VM w kontrolerze, ale równie dobrze kontroler może dostać VM od aplikacji.

To zależy co rozumiesz przez "aplikacje", bo jak zauważyłeś w swoim poście architektura może zawierać wiele różnych warstw które bez doprecyzowania mogą oznaczać różne rzeczy:

  • jeśli orkiestrujemy nasz use casy w controlerach i mamy serwisy domenowe a nie mamy serwisów aplikacyjnych, to VM powstają wyłącznie w controlerach
  • jeśli mamy serwisy aplikacyjne, to VM będę głównie w nich powstawały
  • jeśli brniemy w coś w stylu CQRS, to dla części query VM będą tworzone w naszej warstwie dostępu do danych
    dogmatycznie rzecz biorąc dla standardowych aplikacji biznesowych, a w praktyce nie zawsze podążanie za dogmatami jest konieczne czy nawet wskazane

Podobnie jest z Repository, o ile w świecie Javowym ten wzorzec jest jednoznacznie utożsamiony z DDD, o tyle w świecie .net w aplikacjach które nie stosują DDD to co nazywa się Repository zwykle pełni funkcje DAO(przez zaszłości historyczne). Z dogmatycznego punktu widzenia to poważny błąd, z praktycznego, to tylko nazwa .... co prowadzi nas do tego że po samych nazwach zwłaszcza jednowyrazowych ciężko stwierdzić co to jest, i przydałoby się chociaż jedno zdanie o odpowiedzialnościach pełnionych przez daną warstwę/komponent.

W ogóle ocenianie architektury powinno sprowadzać się do analizy zależności występujących pomiędzy warstwami/komponentami, a nie struktury folderów/namespaców, bo ona może być złudna ;)

0
neves napisał(a):

To zależy co rozumiesz przez "aplikacje"

Aplikacja to takie coś, co przyjmuje dane i realizuje procesy biznesowe. Np. wysyłasz id klienta oraz listę par id produktu -> ilość, i w efekcie dostajesz wystawioną fakturę. Nie jest ważne, czy wysłałeś te dane z jakiegoś GUI, systemu zewnętrznego czy frameworka testowego, aplikacja ma działać niezależnie od swojego interfejsu.

  • jeśli orkiestrujemy nasz use casy w controlerach i mamy serwisy domenowe a nie mamy serwisów aplikacyjnych, to VM powstają wyłącznie w controlerach

Jeśli orkiestrujemy przepływ biznesowy w kontrolerach, to nie mamy MVC, ani nie mamy aplikacji, bo wszystko jest zespawane z GUI, więc po prostu nie robimy tego.

  • jeśli mamy serwisy aplikacyjne, to VM będę głównie w nich powstawały
  • jeśli brniemy w coś w stylu CQRS, to dla części query VM będą tworzone w naszej warstwie dostępu do danych

No i te dwa podejścia mają sens.

Podobnie jest z Repository, o ile w świecie Javowym ten wzorzec jest jednoznacznie utożsamiony z DDD, o tyle w świecie .net w aplikacjach które nie stosują DDD to co nazywa się Repository zwykle pełni funkcje DAO(przez zaszłości historyczne).

Nieprawda, Repozytorium ma jedno znaczenie i jest ono nierozerwalnie związane z DDD. To, że jacyś niepełnosprytni programiści kiedyś zaczęli wprowadzać DDD poprzez nazywanie swoich DAO repozytoriami, a obecnie wielu bezmyślnie to powtarza, to nie jest żadne wytłumaczenie.
I ten błąd popełniają zarówno programiści Javy, .NET, jak i PHP.

Z dogmatycznego punktu widzenia to poważny błąd, z praktycznego, to tylko nazwa ...

To nie jest "tylko nazwa". Precyzyjne używanie nazw ma wielkie znaczenie, bo używając losowych słów w losowych znaczeniach, nie sposób się dogadać.

W ogóle ocenianie architektury powinno sprowadzać się do analizy zależności występujących pomiędzy warstwami/komponentami, a nie struktury folderów/namespaców, bo ona może być złudna ;)

Zgadzam się, chociaż z takiej struktury wiele można wywnioskować.

0
somekind napisał(a):
Mateusz napisał(a):

A więc moja struktura projektu wygląda następująco:
|-> Project.Base
|------> Project.Base.Model (klasy POCO, zwykle odwzorowanie relacji w bazie danych)
|------> Project.Base.Repository (interfejsy repozytorium)
|------> Project.Base.Service (interfejsy usług/logik biznesowych)
|------> Project.Base.Manager (interfejsy menedżerów, mam na myśli jakiś mechanizm silnie powiązany z jakąś technologią, np. uwierzytelnianie, czy
| jakieś ładnie opakowane mechanizmy typowo SharePoint, CRM, itp).

Project.Base.Model to oddzielny moduł, czy tylko namespace w ramach Project.Base?
Mam nadzieję, że to pierwsze.

To jedynie namespace, Project.Base.Model zawiera właśnie encje, które są głównie odwzorowaniem encji z SQL/SP/CRM. Reszta namespaców to jedynie interfejsy. Uważałem, że dobrym rozwiązaniem jest trzymanie klas modelu (encji) i interfejsów w jednym spójnym projekcie - jako całość abstrakcji dla reszty aplikacji. Projekt ten pełni zazwyczaj dla mnie funkcję takiego jakby "rusztowania". Dopisując nową funkcjonalność do systemu to tutaj jest mój punkt startowy. Tworzę potrzebne mi interfejsy repozytorium/serwisów/managerów, następnie dodaję pustą implementację w odpowiednich (innych) projektach. To zawsze daje mi do przemyślenia nad spójnością systemu przed przystąpieniem do właściwej implementacji.

Nie masz nigdzie encji, więc nie robisz DDD. Czemu zatem masz repozytoria? Czym one się u Ciebie zajmują? Wnoszą jakąkolwiek wartość?

Są, właśnie w Project.Base.Model ;) W moich rozwiązaniach repozytorium jest to warstwa, która wykorzystuje API danej technologi właśnie do zwracania ów encji/kolekcji.

|-> Project.Manager
|-------> Implementacja managerów (np. uwierzytelnianie w ASP.NET przy pomocy cookie, itp.)

Czyli Manager różni się od Service tym, że jest związany z infrastrukturą, a nie z logiką biznesową?

Dokładnie tak, strasznie mi się nie podoba kiedy Project.Service ma jakieś odwołania do System.SharePoint.Client.dll, itp. Wolę wyraźnie odseparować logikę/serwisy od danej platformy.

Największy problem mam z tym czy warstwa Project.Service powinna zwracać encje z Modelu czy ViewModelu, myślałem nad tym i powiem szczerze nie wiem. Nie ukrywam, że zwracanie ViewModeli z warstwy Service jest bardzo wygodne, korzystam z kilku repozytorium i sklejam całość w jeden ViewModel, przez co akcje kontrolera są bardzo czytelne, z drugiej strony uzależniam wtedy logikę/serwisy od konkretnej postaci ViewModeli. Co myślicie, które rozwiązanie lepsze?

A to są serwisy biznesowe czy aplikacyjne? Skoro nie masz DDD, to nie robisz nic złego zwracając ViewModel z serwisu - jest on po prostu kontraktem Twojej aplikacji. Równie dobrze możesz ten swój serwis opakować w jakąś technologię webserwisów i wystawić na świat dla zupełnie innej aplikacji niż Twoje GUI.

Ok, w takim razie mnie nieco uspokoiłeś.

0

@somekind zapytam jeszcze jak rozwiązujesz problem z mechanizmami mocno związanymi z daną platformą? Zawsze miałem problem, na przykład z takim ASP.NET Identity? Domyślam się, że implementację do niego przechowujesz w projekcie Project.Utils, ale tworzysz jakąś abstrakcje/interface, które je opakowuje, jakieś IIdentityManager czy korzystasz bezpośrednio z implementacji (mam na myśli UserManagera) w Project.WebClient?

Nic nie wspomniałeś również o konfiguracji dla kontenera DI - konfigurację przechowujesz w Project.WebClient? Osobiście trzymam ją w osobnym projekcie, przez co np. projekt z testami, korzysta z tej samej konfiguracji (podmieniając DbContext na mock'ową wersję), a same testy można pisać jeszcze kiedy nie ma się WebClienta ;)

1
Mateusz napisał(a):

To jedynie namespace, Project.Base.Model zawiera właśnie encje, które są głównie odwzorowaniem encji z SQL/SP/CRM. Reszta namespaców to jedynie interfejsy. Uważałem, że dobrym rozwiązaniem jest trzymanie klas modelu (encji) i interfejsów w jednym spójnym projekcie - jako całość abstrakcji dla reszty aplikacji.

Czyli mieszasz implementacje z abstrakcjami z trzech różnych warstw (infrastruktura, biznes, dostęp do danych) w jednym module. Mnie to wygląda na bałagan...

Tworzę potrzebne mi interfejsy repozytorium/serwisów/managerów, następnie dodaję pustą implementację w odpowiednich (innych) projektach. To zawsze daje mi do przemyślenia nad spójnością systemu przed przystąpieniem do właściwej implementacji.

No dobrze, a po co Project.CRM.Repository ma być świadomy istnienia interfejsów z Project.Base.Service? To jest warstwę wyżej i w ogóle powinno być niedostępne w tym module.

Są, właśnie w Project.Base.Model ;)

Wcześniej napisałeś: Project.Base.Model (klasy POCO, zwykle odwzorowanie relacji w bazie danych). To w końcu to są encje czy POCO, bo jedno jest wręcz zaprzeczeniem drugiego?

W moich rozwiązaniach repozytorium jest to warstwa, która wykorzystuje API danej technologi właśnie do zwracania ów encji/kolekcji.

Ale jeśli masz POCO, a nie encje, to nie masz DDD, więc nie masz repozytorium tylko jakieś DAO.
No i o ile wydzielenie takiego tworu ma sens dla integracji z systemami zewnętrznymi, to już dla bazy danych jest po prostu stratą czasu.
http://commitandrun.pl/2016/05/11/Repozytorium_najbardziej_niepotrzebny_wzorzec_projektowy/

Dokładnie tak, strasznie mi się nie podoba kiedy Project.Service ma jakieś odwołania do System.SharePoint.Client.dll, itp. Wolę wyraźnie odseparować logikę/serwisy od danej platformy.

Ok, no jest to jakaś konwencja. Tylko mi np. Manager kojarzy się z jakimiś takimi god-objectami od wszystkiego. Ogólnie starałbym się stosować bardziej precyzyjne nazwy, mówiące o tym, co dokładnie dana klasa robi, no i też bardziej dzielić te projekty. No bo jak rozumiem, u Ciebie Project.Manager może mieć odwołania np. do API SharePointa i PayPala. Ja bym raczej robił Project.Integration.Sharepoint, Project.Integration.PayPal, itd.

Mateusz napisał(a):

@somekind zapytam jeszcze jak rozwiązujesz problem z mechanizmami mocno związanymi z daną platformą? Zawsze miałem problem, na przykład z takim ASP.NET Identity? Domyślam się, że implementację do niego przechowujesz w projekcie Project.Utils, ale tworzysz jakąś abstrakcje/interface, które je opakowuje, jakieś IIdentityManager czy korzystasz bezpośrednio z implementacji (mam na myśli UserManagera) w Project.WebClient?

Informacje o aktualnym użytkowniku są potrzebne w różnych warstwach aplikacji: oczywiście do wyświetlenia w GUI, w logice aplikacyjnej i biznesowej zarówno do przypisywania go do innych encji jak i do jakiejś autoryzacji, w warstwie dostępu do danych (np. do audytu operacji na bazie), dlatego trzeba stworzyć jakąś współdzieloną między warstwami abstrakcję.
Ja tworzę interfejs IUserContext, który zwraca jakiegoś tam mojego IUser z Id w projekcie dostępnym we wszystkich tych warstwach (w przykładzie wyżej to Project.Contracts). To daje możliwość wykorzystania go wszędzie, a także pisania testów. Natomiast implementacja zależna jest tak naprawdę od technologi GUI (bo to web), dlatego umieszczam ją w Project.WebClient.

Nic nie wspomniałeś również o konfiguracji dla kontenera DI - konfigurację przechowujesz w Project.WebClient?

Nie widzę sensu w tworzeniu specjalnego projektu na konfigurację DI - w ogóle nie widzę sensu w oddzielaniu konfiguracji klas od modułu, w którym się znajdują. A w WebClient przy starcie aplikacji jedynie wczytuję wszystkie dllki, i rejestruję wszystkie moduły kontenera, które rejestrują już swoje zależności.
Kwestia użycia kontenera, który na to pozwala (jeżeli nie pozwala, to jest do kitu i nie należy go używać).

Osobiście trzymam ją w osobnym projekcie, przez co np. projekt z testami, korzysta z tej samej konfiguracji (podmieniając DbContext na mock'ową wersję), a same testy można pisać jeszcze kiedy nie ma się WebClienta ;)

Przy moim podejściu jest właściwie tak samo - trzeba tylko wstrzyknąć zamockowany IUserContext i inny connection string. Oczywiście to do testów integracyjnych, bo do jednostkowych to raczej nie używa się kontenera.

1 użytkowników online, w tym zalogowanych: 0, gości: 1