Logika biznesowa w .NET - jak uporządkować reguły aplikacji

Tworzenie modeli logicznych dla Insight Advisor z logiką biznesową aplikacji.

Spis treści

Gdy aplikacja zaczyna obsługiwać rabaty, limity, statusy zamówień albo uprawnienia, szybko okazuje się, że samo zapisanie danych nie wystarcza. To reguły decydują, co użytkownik może zrobić, kiedy operacja jest poprawna i jakie skutki powinna wywołać. W tym artykule pokazuję, czym jest logika biznesowa aplikacji, gdzie ją umieszczać w projekcie .NET, jakie wzorce pomagają ją uporządkować oraz jak unikać kodu, którego później nikt nie chce dotykać.

Dobra architektura chroni reguły biznesowe przed chaosem

  • Logika biznesowa opisuje zasady, według których aplikacja podejmuje decyzje.
  • Kontroler powinien przyjąć żądanie, ale nie powinien znać wszystkich reguł domeny.
  • Model domenowy sprawdza się przy złożonych zasadach, a prosty serwis aplikacyjny wystarczy przy nieskomplikowanym CRUD-zie.
  • Clean Architecture i architektura heksagonalna izolują reguły od bazy danych, frameworka i zewnętrznych API.
  • Testy jednostkowe powinny sprawdzać najważniejsze decyzje biznesowe bez uruchamiania całej aplikacji.

Co naprawdę należy do logiki biznesowej

Logika biznesowa to część kodu, która odpowiada na pytanie, co w danej sytuacji wolno zrobić. Składają się na nią reguły, ograniczenia, obliczenia i decyzje wynikające z działalności firmy, a nie z technologii użytej do napisania systemu.

W sklepie internetowym będzie to na przykład sprawdzenie, czy produkt jest dostępny, czy klient może użyć kodu rabatowego i czy zamówienie można przełączyć ze stanu „nowe” na „opłacone”. W systemie kadrowym podobne reguły określą, ile dni urlopu pozostało pracownikowi i czy przełożony może zaakceptować dany wniosek.

Najprostszy test jest praktyczny. Jeżeli regułę można opisać językiem domeny, prawdopodobnie należy do modelu biznesowego. Jeżeli opis dotyczy protokołu HTTP, tabeli SQL albo formatu JSON, mówimy raczej o infrastrukturze lub warstwie technicznej.

Przykład Typ odpowiedzialności Gdzie zwykle powinien trafić
Klient nie może wykorzystać wygasłego kuponu Reguła biznesowa Model domenowy lub serwis domenowy
Żądanie ma nagłówek Authorization Obsługa techniczna Middleware lub warstwa API
Zamówienie przechodzi do statusu „wysłane” Reguła procesu Encja, agregat lub serwis aplikacyjny
Obiekt jest zapisywany w SQL Server Dostęp do danych Repozytorium lub adapter infrastruktury

W praktyce najwięcej problemów powstaje wtedy, gdy te kategorie mieszają się w jednym miejscu. Kontroler zaczyna liczyć rabat, odpytuje bazę, wywołuje operatora płatności i ustawia status encji. Taki kod może działać, ale każda zmiana wymaga później ostrożnego przekopywania całego przepływu.

Gdzie umieścić reguły w aplikacji .NET

Nie ma jednej architektury dobrej dla każdego projektu. Inaczej zaprojektuję mały panel administracyjny z kilkoma formularzami, a inaczej system zamówień, w którym reguły zmieniają się co tydzień i mają wpływ na pieniądze. Rozmiar kodu nie jest jedynym kryterium. Ważniejsza jest złożoność decyzji, które aplikacja musi podejmować.

W prostym projekcie wystarczy często klasyczny podział na warstwę API, serwisy aplikacyjne, dostęp do danych i modele. Serwis aplikacyjny koordynuje wtedy przypadek użycia, na przykład „utwórz produkt”, a repozytorium ukrywa szczegóły bazy danych.

Przy większej liczbie reguł lepiej oddzielić domenę od technologii. W Clean Architecture zależności kierują się do środka, czyli w stronę modelu domenowego. Architektura heksagonalna stosuje podobną ideę, używając portów i adapterów. Port to kontrakt, a adapter to konkretna implementacja, na przykład klient REST albo repozytorium Entity Framework Core.

Podejście Gdzie trafiają reguły Kiedy ma sens Ryzyko
Prosta architektura warstwowa Serwisy i modele domenowe Małe i średnie aplikacje CRUD Serwisy mogą stać się klasami „od wszystkiego”
Clean Architecture Warstwa domeny i przypadki użycia Systemy rozwijane przez dłuższy czas Więcej projektów, interfejsów i kodu pomocniczego
Architektura heksagonalna Rdzeń domeny niezależny od adapterów Wiele integracji i potrzeba łatwego testowania Abstrakcje mogą być przesadne przy prostych funkcjach
DDD Encje, obiekty wartości, agregaty i serwisy domenowe Złożone domeny z istotnymi regułami Niepotrzebna formalizacja prostych ekranów

Sam nie zaczynam projektu od tworzenia pięciu warstw tylko dlatego, że wygląda to profesjonalnie. Najpierw sprawdzam, gdzie znajduje się ryzyko. Jeśli aplikacja ma głównie odczyty i proste formularze, rozbudowane DDD może spowolnić pracę. Gdy jednak reguły są krytyczne, izolacja domeny szybko zwraca koszt początkowej organizacji.

Schemat przedstawia warstwową architekturę aplikacji, gdzie logika biznesowa jest sercem, otoczonym przez warstwy odpowiedzialne za interfejsy i infrastrukturę.

Jak poprowadzić przypadek użycia od API do domeny

Dobrym sposobem projektowania jest rozpoczęcie od przypadku użycia, a nie od tabeli w bazie. Załóżmy, że użytkownik chce złożyć zamówienie. Aplikacja powinna sprawdzić pozycje, wyliczyć kwotę, zweryfikować reguły rabatowe, zarezerwować towary i dopiero potem zapisać wynik.

Kontroler może przyjąć dane wejściowe i przekazać je do handlera albo serwisu aplikacyjnego. Nie powinien jednak sam decydować, czy zamówienie może zostać złożone. Ta decyzja należy do obiektu, który zna zasady zamówienia.

public sealed class Order
{
    private readonly List _lines = new();

    public OrderStatus Status { get; private set; } = OrderStatus.Draft;

    public void Submit()
    {
        if (_lines.Count == 0)
            throw new DomainException("Zamówienie nie zawiera produktów.");

        if (Status != OrderStatus.Draft)
            throw new DomainException("Tego zamówienia nie można już złożyć.");

        Status = OrderStatus.Submitted;
    }
}

To niewielki przykład, ale pokazuje ważną granicę. Metoda Submit chroni niezmienniki, czyli warunki, które zawsze muszą być prawdziwe dla zamówienia. Dzięki temu reguła nie zależy od tego, czy zamówienie zmienia kontroler HTTP, zadanie w tle czy test jednostkowy.

Model domenowy powinien pilnować własnych zasad

Encja nie musi zawierać całej logiki systemu, ale powinna bronić się przed niepoprawnym stanem. Obiekt wartości, taki jak Money, EmailAddress albo DateRange, dobrze nadaje się do zamknięcia małej reguły i walidacji.

Jeśli operacja dotyczy kilku obiektów i nie pasuje naturalnie do jednej encji, można użyć serwisu domenowego. Przykładem będzie wyliczenie ceny zależne od koszyka, klienta i aktualnej kampanii. Serwis domenowy powinien nadal operować na pojęciach biznesowych, a nie na DbContext czy obiektach żądania HTTP.

Serwis aplikacyjny powinien orkiestrwać

Serwis aplikacyjny koordynuje kroki, ale nie powinien przejmować każdej reguły. Pobiera agregat, wywołuje jego metodę, korzysta z portu do rezerwacji magazynowej i zapisuje zmiany. Orkiestracja odpowiada za kolejność działań, natomiast domena odpowiada za ich poprawność.

To rozróżnienie przydaje się szczególnie przy integracjach. Wywołanie operatora płatności może zakończyć się błędem technicznym, ale decyzja, czy zamówienie jest gotowe do opłacenia, nadal powinna wynikać z reguł domenowych.

Wzorce, które porządkują reguły biznesowe

Wzorzec nie jest obowiązkowym elementem każdej klasy. Traktuję go jako nazwany sposób rozwiązania powtarzalnego problemu. Najlepszy wzorzec to ten, który upraszcza zmianę, testowanie albo czytanie kodu.

Wzorzec Problem, który rozwiązuje Przykład zastosowania
Strategy Jedna decyzja ma kilka wymiennych wariantów Różne sposoby naliczania dostawy lub rabatu
Specification Warunki wyboru są złożone lub używane w wielu miejscach Klient kwalifikujący się do promocji
State Zachowanie zależy od aktualnego statusu Zamówienie, które można anulować tylko przed wysyłką
Domain Service Reguła dotyczy kilku obiektów Przeliczenie ceny koszyka według cennika
Factory Utworzenie poprawnego obiektu wymaga kilku decyzji Budowanie polisy lub zlecenia z odpowiednim typem
CQRS Odczyty i modyfikacje mają różne potrzeby Rozbudowane raporty obok złożonych operacji zapisu

Najczęściej zaczynam od zwykłej metody i dopiero po pojawieniu się drugiego wariantu wydzielam strategię. Tworzenie fabryki, strategii i specyfikacji na zapas daje poczucie porządku, ale często kończy się dużą liczbą małych klas, które nie mają jeszcze realnej odpowiedzialności.

Warto też uważać na CQRS. Rozdzielenie ścieżek odczytu i zapisu może poprawić czytelność oraz wydajność, lecz nie rozwiązuje samo z siebie złego modelu domenowego. Przy prostym formularzu edycji produktu dodatkowa infrastruktura będzie zwykle większym kosztem niż korzyścią.

Najczęstsze błędy i sposób ich naprawy

Reguły ukryte w kontrolerach

Kontroler z kilkudziesięcioma warunkami trudno testować i łatwo przypadkowo ominąć z innego miejsca aplikacji. Przeniesienie decyzji do modelu lub serwisu domenowego pozwala użyć jej niezależnie od kanału wejściowego.

Serwis, który robi wszystko

Klasa o nazwie OrderService może z czasem walidować dane, liczyć ceny, wysyłać e-maile, odpalać płatności i zarządzać transakcją. Taki obiekt staje się centrum zależności. Pomaga podział według przypadków użycia i rozdzielenie reguł od integracji.

Model anemiczny

Model anemiczny to zestaw klas z samymi właściwościami, podczas gdy cała wiedza znajduje się w zewnętrznych serwisach. Taki układ nie zawsze jest błędem, szczególnie w prostych systemach, ale przy złożonej domenie prowadzi do rozproszenia reguł i łatwego tworzenia niepoprawnych stanów.

Powielanie tej samej reguły

Jeśli warunek dotyczący dostępności produktu pojawia się w kontrolerze, zadaniu cyklicznym i procesie importu, prędzej czy później wersje zaczną się różnić. Jedno źródło prawdy nie oznacza, że każda funkcja ma korzystać z tej samej klasy, ale że decyzja powinna mieć jasno określone miejsce.

Przeczytaj również: YAGNI w .NET - kiedy upraszcza architekturę, a kiedy szkodzi?

Testowanie wyłącznie przez API

Testy end-to-end są cenne, ale są wolniejsze i bardziej podatne na problemy infrastruktury. Reguły takie jak „nie można anulować wysłanego zamówienia” powinny mieć szybkie testy jednostkowe, a osobne testy integracyjne powinny sprawdzać repozytoria, transakcje i komunikację z usługami zewnętrznymi.

Przy projektowaniu testów zapisuję najpierw scenariusze biznesowe. Dla jednej reguły warto pokryć przypadek poprawny, graniczny i odrzucony. Taki zestaw jest bardziej użyteczny niż mechaniczne dążenie do określonego procentu pokrycia kodu.

Jak dobrać poziom architektury do projektu

Najrozsądniejsza architektura rośnie razem z problemem. Na początku wystarczy czytelny podział odpowiedzialności i kilka prostych zasad. Dopiero gdy pojawiają się kolejne kanały wejściowe, integracje, warianty procesu albo częste zmiany wymagań, warto inwestować w bardziej formalne granice.

  • Prosty CRUD może działać z warstwą API, serwisem i repozytorium.
  • Proces z wieloma regułami skorzysta z encji, obiektów wartości i serwisów domenowych.
  • Wiele integracji uzasadnia porty, adaptery i wyraźne interfejsy.
  • Duża domena może wymagać podziału na bounded contexts, czyli odrębne obszary modelu z własnym słownikiem i regułami.
  • Mikroserwisy mają sens wtedy, gdy granice biznesowe są już zrozumiałe, a nie jako sposób na uporządkowanie przypadkowo podzielonego monolitu.

Nie rozbijam aplikacji na mikroserwisy tylko po to, by każda część miała własny projekt i bazę. Złożona komunikacja, opóźnienia i problemy z transakcjami są uzasadnione dopiero wtedy, gdy niezależne skalowanie, wdrażanie lub własność zespołów przynoszą konkretną korzyść.

Reguły, które warto ustalić przed pierwszym refaktorem

Przed zmianą architektury spisałbym kilka najważniejszych przypadków użycia i zaznaczył, gdzie dziś podejmowane są decyzje. Następnie sprawdziłbym, które fragmenty zależą od ASP.NET Core, Entity Framework Core, brokera wiadomości lub zewnętrznego API.

Dobrym celem nie jest „mieć Clean Architecture”, tylko móc zmienić regułę rabatową bez edycji kontrolera, migracji bazy i pięciu integracji. Architektura jest udana wtedy, gdy zmiana biznesowa trafia w mały, przewidywalny obszar kodu, a test szybko pokazuje, czy system nadal działa zgodnie z zasadami.

Najpierw zabezpieczyłbym testami reguły, które wpływają na pieniądze, uprawnienia i przejścia statusów. Potem wydzieliłbym zależności techniczne, a dopiero na końcu rozważał cięższe wzorce. Taka kolejność zwykle daje więcej niż efektowna struktura katalogów zbudowana zanim domena zostanie naprawdę poznana.

FAQ - Najczęstsze pytania

W małym projekcie CRUD zwykle wystarczy podział na API, serwisy aplikacyjne, repozytorium i modele. Serwis może koordynować przypadek użycia, a repozytorium ukrywa szczegóły bazy danych. Reguły, które chronią poprawny stan obiektu, warto jednak umieścić w modelu domenowym.

Model domenowy pilnuje własnych zasad i niezmienników, na przykład blokuje złożenie pustego zamówienia albo niedozwoloną zmianę statusu. Serwis aplikacyjny orkiestruje kolejne kroki: pobiera agregat, wywołuje jego metodę, korzysta z portów integracyjnych i zapisuje zmiany. Nie powinien przejmować wszystkich reguł biznesowych.

Te podejścia mają sens, gdy system jest rozwijany przez dłuższy czas, zawiera wiele integracji albo ma złożone reguły wymagające łatwego testowania. Izolują domenę od ASP.NET Core, Entity Framework Core, zewnętrznych API i bazy danych za pomocą zależności kierowanych do środka oraz portów i adapterów. Przy prostych funkcjach dodatkowe warstwy mogą być niepotrzebnym kosztem.

Najważniejsze decyzje biznesowe należy sprawdzać szybkimi testami jednostkowymi bez uruchamiania całej aplikacji. Dla każdej reguły warto pokryć scenariusz poprawny, graniczny i odrzucony, na przykład próbę anulowania wysłanego zamówienia. Testy integracyjne powinny dodatkowo obejmować repozytoria, transakcje i komunikację z usługami zewnętrznymi.

Oceń artykuł

Ocena: 0.00 Liczba głosów: 0

Tagi

.net
cqrs
architektura heksagonalna
testy jednostkowe
ddd
Autor Przemysław Kwiatkowski
Przemysław Kwiatkowski
Jestem Przemysław i od 15 lat zajmuję się programowaniem .NET, chmurą Azure oraz sztuczną inteligencją. Moja przygoda z tymi technologiami zaczęła się od fascynacji możliwościami, jakie dają, a z czasem przerodziła się w pasję do tworzenia rozwiązań, które realnie wpływają na pracę i życie ludzi. Na kursdotnet.pl staram się dzielić się swoją wiedzą w sposób przystępny, tłumacząc złożone zagadnienia i pomagając zrozumieć, jak te dynamicznie rozwijające się obszary IT mogą być wykorzystane w praktyce. Dokładam wszelkich starań, aby prezentowane przeze mnie materiały były rzetelne, aktualne i oparte na sprawdzonych źródłach, a także aby uporządkować wiedzę w sposób ułatwiający jej przyswojenie.

Udostępnij artykuł

Napisz komentarz