Django, a Domain-Driven Design

Django, a Domain-Driven Design
Photo by Bogdan Zaleski / Unsplash

W społeczności Django co pewien czas pojawia się pytanie, czy warto dodawać dodatkowe warstwy abstrakcji – takie jak Repository (repozytorium) i Service (serwis) – wzorowane na podejściu Domain Driven Design (DDD) znanym z języków jak Java (np. Spring). Intuicją stojącą za tym jest rozdzielenie logiki biznesowej od dostępu do danych. Jednak w przypadku Django takie dodatkowe warstwy zwykle nie są potrzebne, a wręcz mogą zaszkodzić. Poniżej przedstawiamy analizę wydajności, opinie ekspertów, porównanie z innymi frameworkami, przegląd dyskusji w community oraz przykłady kodu ilustrujące, dlaczego Django jest zoptymalizowane pod podejście “fat models” (tłuste modele) i nie wymaga dodatkowych abstrakcji.

Analiza wydajnościowa

Czy dodanie warstw Repository/Service spowalnia aplikację Django? Wprowadzenie każdej dodatkowej warstwy oznacza dodatkowe wywołania funkcji i potencjalne przekładanie danych między obiektami. W języku Python wywołania funkcji nie są „za darmo” – nadmierna abstrakcja może wprowadzać niewielki narzut czasowy, choć często większym problemem jest złożoność niż sam surowy czas działania.

W praktyce największe ryzyko spadku wydajności wynika z utraty optymalizacji oferowanych przez Django ORM. Na przykład, Django korzysta z lazy loading i obiektu QuerySet, który potrafi łączyć filtrowania, opóźniać wykonanie zapytań do bazy i keszować wyniki. Jeśli warstwa Service ukryje przed nami QuerySet (np. zamieniając go od razu na listę prostych obiektów lub własne struktury), tracimy te optymalizacje – albo musimy je odtworzyć we własnym kodzie. Jak zauważa James Bennett, doświadczony developer Django, QuerySet jest sprytną, keszującą abstrakcją; jeśli go schowamy za własną warstwą, to “you either lose that or have to reimplement it yourself” – tracimy jego możliwości albo musimy je ponownie zaimplementować we własnym zakresie (Against service layers in Django).

Co więcej, niejedna osoba przekonała się, że takie warstwy mogą realnie pogorszyć wydajność. Na forum Reddit jeden z programistów opisuje doświadczenie z wprowadzeniem warstwy serwisów do odczytu danych: “using services there not only unnecessarily complicates the whole thing, but also has a negative impact on performance” – dodatkowa warstwa **niepotrzebnie skomplikowała całość i negatywnie wpłynęła na wydajność (Against service layers in Django : r/django). W tym przypadku zespół szybko wycofał się z tego pomysłu (po 2–3 tygodniach) i wrócił do bezpośredniego użycia ORM (Against service layers in Django : r/django).

Podsumowując, każda dodatkowa abstrakcja w Django to potencjalny (choć zwykle mały) narzut wydajnościowy, a w skrajnych wypadkach można utracić wbudowane usprawnienia dostępu do bazy. Django jest już dość dobrze zoptymalizowane – lepiej korzystać z jego mechanizmów bez “owijania” ich kolejnymi warstwami, chyba że mamy ku temu bardzo konkretne powody.

Opinie ekspertów – filozofia “fat models” w Django

Podejście “fat models, skinny views” (tłuste modele, chude widoki) jest od lat promowane w świecie Django. Oznacza to, że logika domenowa powinna być umieszczona w warstwie modelu (i powiązanych z nim klasach, np. managerach), a widoki mają pozostawać możliwie proste, ograniczając się do obsługi żądania/odpowiedzi. To podejście jest mocno zakorzenione w filozofii frameworka:

  • Twórcy Django wprost wskazują, że modele powinny „zawierać każdy aspekt obiektu”, zgodnie z wzorcem Active Record (Design philosophies | Django documentation | Django). W oficjalnej dokumentacji Django znajdziemy zasadę: “Models should encapsulate every aspect of an ‘object,’ following Martin Fowler’s Active Record design pattern.” (Design philosophies | Django documentation | Django). Innymi słowy – model w Django pełni rolę zarówno struktury danych, jak i miejsca na logikę związaną z tymi danymi. Zawiera pola oraz metody operujące na tych danych. Ta filozofia stoi w kontraście do czystego DDD, gdzie obiekty domenowe i dostęp do bazy są rozdzielone (Encja vs. Repozytorium).
  • James Bennett (wieloletni rdzeniowy developer Django) krytykuje pomysł dodawania dodatkowej warstwy serwisów. W swoim głośnym wpisie “Against service layers in Django” pisze, że “the short version of my opinion on this is: it’s probably not what you want in Django apps.” (Against service layers in Django). Oznacza to, że jego zdaniem w typowych aplikacjach Django warstwa Service/Repository nie jest tym, czego naprawdę potrzebujemy. Dłuższe uzasadnienie Bennetta zawiera wiele argumentów (część z nich omawiamy w tym artykule), ale już sama ta konkluzja mówi wiele. Bennett podkreśla też, że w dobrze zaprojektowanych aplikacjach Django to właśnie modele (oraz ewentualnie customowe klasy Manager/QuerySet) stanowią API dla reszty kodu – i są miejscem, gdzie powinna trafić logika biznesowa (Against service layers in Django). Zamiast rozpraszać logikę po dodatkowych modułach, korzystajmy z tego, co oferuje nam warstwa modelu.
  • Tom Christie (autor Django REST Framework) również opowiada się za trzymaniem logiki w modelach, zwracając uwagę na właściwą enkapsulację. Standardową radę “fat models, thin views” nazywa słuszną, ale nie dość precyzyjną – i proponuje bardziej kategoryczną zasadę: „Never write to a model field or call save() directly. Always use model methods and manager methods for state-changing operations.” (Django models, encapsulation and data integrity - DabApps). Zaleca zatem, by wszelkie zmiany stanu obiektu (tworzenie, modyfikacja, usuwanie) przechodziły przez metody modelu lub metody managera, a nie były wykonywane bezpośrednio na polach modelu w widoku czy formularzu. Taki rygor zapewnia lepszą spójność i integralność danych – cała logika związana ze zmianą stanu jest skupiona w jednym miejscu (w modelu), co ułatwia kontrolę. Christie konkluduje, że stosowanie tej zasady to nic innego jak doprecyzowanie “fat models” i nadal nie wyklucza ewentualnej dodatkowej warstwy logiki biznesowej ponad modelami, jeśli kiedyś okaże się potrzebna (Django models, encapsulation and data integrity - DabApps). Jednak najpierw model musi wyczerpywać swoje możliwości.
  • Inni doświadczeni programiści również wskazują na zalety “fat models”. W dyskusjach często słyszy się stwierdzenie, że Django “lubi” podejście z grubymi modelami – próby nadmiernego rozdzielania logiki są walką z frameworkiem. Przykładowo, w odpowiedzi na Stack Overflow jeden z użytkowników podsumował: “I think the fat models approach is the way to go.” (python - django: Fat models and skinny controllers? - Stack Overflow), krytykując przenoszenie walidacji do formularzy zamiast modeli. Nawet jeżeli formularze w Django przechwytują część logiki (walidacja), to ostatecznie model powinien być samowystarczalny na wypadek, gdy aplikacja zmieni interfejs (np. przejście z tradycyjnych widoków na API REST – wtedy walidacje w formularzach trzeba i tak przenieść, chyba że od początku były w modelu) (python - django: Fat models and skinny controllers? - Stack Overflow) (python - django: Fat models and skinny controllers? - Stack Overflow).

Podsumowując: twórcy Django i czołowi eksperci branży zgadzają się, że trzymanie logiki biznesowej w modelach (ewentualnie managerach) to najlepsza praktyka w Django. Framework został zaprojektowany z takim założeniem, a dodawanie kolejnych warstw często oznacza dublowanie funkcjonalności, którą modele/ORM już oferują.

Porównanie z innymi frameworkami (Spring/Java vs. Django)

Skąd w ogóle pomysł dodawania warstw Repository i Service? Wywodzi się on z architektur Domain Driven Design i jest powszechny w środowisku Java, np. we frameworku Spring. W aplikacjach Java typowym podziałem jest: Controller -> Service -> Repository -> Database. Encje (obiekty bazodanowe) zwykle są prostymi pojemnikami na dane, zaś cała logika biznesowa trafia do warstwy serwisowej, która korzysta z repozytoriów (abstrakcji nad bazą danych). Repozytoria dostarczają metod typu findById(), save(), itp., często implementowanych dzięki mechanizmom ORM (np. Spring Data JPA automatycznie generuje klasy repozytoriów na podstawie interfejsów). Ten wzorzec ma kilka celów:

  • Izolacja warstwy dostępu do danych: Repozytorium ukrywa szczegóły bazy/ORM. Pozwala to teoretycznie podmienić np. bazę danych lub sposób przechowywania danych bez zmiany logiki biznesowej – serwis zna tylko interfejs repozytorium.
  • Łatwiejsze testowanie jednostkowe: W Java statycznie typowane interfejsy repozytoriów można łatwo zamockować, aby testować serwisy w oderwaniu od bazy danych.
  • Porządkowanie skomplikowanej logiki: Warstwa serwisowa może orkiestrwać operacje obejmujące wiele obiektów/repozytoriów, utrzymując transakcje, itp., przez co kontrolery (odpowiedniki widoków) pozostają proste.

Dlaczego w Django to podejście jest zbędne lub rzadko stosowane? Django używa odmiennego wzorca – Active Record – gdzie model pełni podwójną rolę: jest jednocześnie reprezentacją tabeli (rekordu) w bazie i zawiera związane z nim operacje. Innymi słowy, Django model to już coś w rodzaju “mini-serwisu” dla samego siebie. Dodatkowo, warstwa Manager/QuerySet w Django spełnia rolę zbliżoną do repozytorium: daje w API modelu metody do wyszukiwania, filtrowania i tworzenia obiektów. Np. MyModel.objects.create(...) czy MyModel.objects.filter(...) – to jest właśnie nasz “repository”. Jak trafnie ujął to jeden z użytkowników Stack Overflow: “when using an ORM, the ORM is the repository. You ask it for a model and it returns a model. That is the purpose of a repository.” (python - Django and domain driven design - Stack Overflow). W Django nie musimy pisać własnej klasy PollRepository z metodą fetch_by_id – już mamy Poll.objects.get(id=…). Nie musimy pisać PollRepository.update(poll) – wystarczy poll.save().

Oto kilka konkretnych powodów, czemu w Django (oraz ogólnie w dynamicznym Pythonie) dodatkowe warstwy z DDD nie dają takich korzyści jak w Javie:

  • ORM jako wbudowane repozytorium: Jak wspomniano, Django ORM już jest abstrakcją nad bazą danych. Dodawanie kolejnej abstrakcji (Repository) oznacza często tylko opakowanie istniejących metod ORM w nowe funkcje o tych samych zadaniach. To generuje duplikację. Bennett zauważa wręcz, że tworząc taką warstwę „serwis+repozytorium” nad Django ORM, de facto piszemy własny mini-ORM: “adopting the service approach essentially means ... developing and maintaining something close to your own private ORM” (Against service layers in Django) (Against service layers in Django). Z czasem i tak będziemy musieli dodać do niego znaczną część funkcji, które oferuje prawdziwy ORM (Against service layers in Django) (Against service layers in Django). Skoro więc Django daje gotowy ORM, lepiej go nie owijać kolejnym.
  • Zmiana silnika baz danych/ORM jest rzadkością: Jednym z argumentów za repozytorium jest to, że “kiedyś może zmienimy bazę danych lub ORM”. W praktyce jednak zdarza się to niezwykle rzadko. Bennett pisze dosadnie: “how often does that really happen in practice? ... it happens almost never” jeśli chodzi o podmianę ORM-u czy sposobu persystencji danych (Against service layers in Django). Szczególnie w Django wymiana ORM-u mija się z celem, bo ORM jest kluczowym, silnie zintegrowanym komponentem całego frameworka. Wiele części Django zakłada jego użycie (admin, system autoryzacji, sesje, itp.). Jeśli ktoś przestaje używać ORM Django, to rodzi się pytanie “po co w ogóle nadal używać Django?” (Against service layers in Django). Dlatego tworzenie abstrakcji pod hipotetyczną zmianę bazy/ORM to przerost formy nad treścią (YAGNI – “You Ain’t Gonna Need It” – jak sam przypomina Bennett (Against service layers in Django)).
  • Testowanie: W Javie konieczność mockowania wynika z ciężkości uruchamiania kontekstu bazy danych i braku dynamiczności. W Pythonie testy możemy uruchamiać na lekkiej bazie SQLite in-memory, możemy łatwo użyć faktycznego ORM w testach (Django ma świetne wsparcie dla testów z bazą). Nie potrzebujemy więc tworzyć interface’ów i mocków dla każdej klasy dostępu do danych – często łatwiej jest po prostu użyć rzeczywistego modelu. Jeśli już chcemy odseparować test od bazy, Python pozwala nam dynamicznie podmienić lub nadpisać metody (monkey patching) lub użyć bibliotek typu fake-factory do generowania danych. Innymi słowy, problemy które w statycznym języku rozwiązujemy przez warstwy, w Pythonie/Django rozwiązuje się inaczej (często prościej).
  • Mniej kodu, mniej szablonu: Django – zgodnie ze swoją filozofią – stawia na minimalizm kodu i wykorzystanie możliwości dynamicznych języka (Design philosophies | Django documentation | Django). Każda dodatkowa warstwa to więcej klas/metod do napisania i utrzymania (tzw. boilerplate). Programista Django może zapytać: po co mam pisać 10 dodatkowych klas serwisów i repozytoriów, skoro mogę wywołać Order.objects.cancel(id) lub lepiej – order.cancel() metodę modelu? W Javie taki boilerplate jest normą (częściowo generowaną automatycznie przez frameworki), natomiast w Django wyróżnia się dążenie do prostoty. Kiedy kod jest prostszy i krótszy, łatwiej go też utrzymać i zrozumieć nowym członkom zespołu.

Reasumując, w innych ekosystemach (np. Java/Spring) dodatkowe warstwy są częścią kultury i rozwiązywania problemów specyficznych dla tamtych technologii. W Django wiele z tych problemów jest już rozwiązanych na poziomie frameworka lub języka. Jak ujął to jeden z ekspertów: “when using an ORM, the ORM is the repository... wrapping it in a class called 'repository'” służy głównie celom takim jak testy czy niezależność od technologii (python - Django and domain driven design - Stack Overflow) – ale te cele w Django osiąga się bez tego. Dlatego próby przenoszenia 1:1 wzorców z Javowych aplikacji często prowadzą do przerostu formy w aplikacjach django.

Debaty i kontrowersje w community

Temat dodatkowych warstw w Django wywoływał liczne dyskusje na forach, blogach i w mediach społecznościowych. Co ciekawe, większość tych debat kończy się konkluzją, że “Django radzi sobie lepiej bez zbędnej abstrakcji”. Oto kilka przykładów takich dyskusji i argumentów:

  • Enterprise Django Style Guide vs. “Let Django be Django”: W 2020 roku pewien publicznie dostępny style guide do Django (autorstwa firmy HackSoftware) zdobył uwagę w sieci. Promował on podejście rodem z aplikacji enterprise, w tym dodanie warstwy services do aplikacji Django (oddzielenie logiki od widoków/forms). Wywołało to reakcję wielu doświadczonych developerów. James Bennett opublikował swój artykuł “Against service layers in Django” właśnie w odpowiedzi na ten trend (Against service layers in Django). Już sam tytuł („Przeciw warstwom serwisowym w Django”) zdradza jego stanowisko. Bennett przedstawił szereg argumentów przeciw, z których część omówiliśmy powyżej (m.in. YAGNI, duplikowanie ORM, utrata integracji z Django). Jego głos odbił się szerokim echem – wpis doczekał się setek komentarzy i nawet follow-upu (drugiej części) na jego blogu.
  • Django Forum i wypowiedzi core devs: Na oficjalnym forum Django toczyła się rozmowa na temat stosowania warstwy usług w dużych projektach. Głos zabrały znane nazwiska, m.in. Andrew Godwin (autor migracji i Channels) oraz James Bennett (ubernostrum), a także wielu innych. W dyskusji padło określenie “let Django be Django” – by nie na siłę uniezależniać się od frameworka, tylko wykorzystać go zgodnie z przeznaczeniem (Structuring large/complex Django projects, and using a services layer in Django projects - Getting Started - Django Forum) (Structuring large/complex Django projects, and using a services layer in Django projects - Getting Started - Django Forum). Wspominano też alternatywne sposoby opanowania złożoności w bardzo dużych projektach, np. poprzez sygnały (Django Signals) lub wzorce typu pub/sub, zamiast dokładania własnych warstw wszędzie (Structuring large/complex Django projects, and using a services layer in Django projects - Getting Started - Django Forum) (Structuring large/complex Django projects, and using a services layer in Django projects - Getting Started - Django Forum). Ogólny konsensus był taki, że Django dostarcza “baterii” (battery-included), z których szkoda rezygnować bez potrzeby. Użytkownik forum @yee_mon trafnie zauważył, że jeśli próbujemy zbudować warstwę serwisową abstrahującą operacje ORM, to “Django really doesn’t want to work that way”, a taka abstrakcja “will break as soon as you use any of the thousands [of] things that make up Django and rely on [the ORM]” (Against service layers in Django : r/django). Innymi słowy, prędzej czy później natrafimy na problem, bo reszta ekosystemu Django (np. administracja, widoki generyczne, formularze, serializery DRF) i tak zakłada bezpośrednią pracę z modelami.
  • Zweryfikowane w praktyce: Wielu programistów opisało własne doświadczenia z prób “uczelnienia” architektury Django. Wspomniany wcześniej komentarz na Reddit (użytkownik EnforcerPL) to przykład osoby z doświadczeniem w Clean Architecture, która spróbowała wdrożyć warstwy w Pythonie. Koniec końców przyznała, że dla odczytu danych to się nie sprawdza: “we tried that but changed our mind within 2, maybe 3 weeks and resorted back to using ORM directly” (Against service layers in Django : r/django). Co ciekawe, zaproponowała kompromis – korzystać z serwisów tylko dla operacji modyfikujących dane (logika write), a odczyty (read) wykonywać bezpośrednio przez ORM. Jest to podejście nazywane CQRS (Command Query Responsibility Segregation). Rzeczywiście, niektórzy architekci radzą, że jeśli już chcemy jakiejś separacji, to niech dotyczy ona bardzo skomplikowanych procesów (np. transakcji obejmujących wiele modeli, zewnętrzne integracje itp.), natomiast proste operacje CRUD nie powinny być nadmiernie obudowane. Jednak nawet takie podejście budzi pytania: czy nie lepiej skorzystać z mechanizmów Django (transakcje, sygnały) lub wydzielić logikę do dobrze nazwanego modułu pomocniczego, zamiast tworzyć pełnoprawną warstwę usług?
  • Alternatywne podejścia DDD w Pythonie: W odpowiedzi na rosnącą popularność wzorców architektonicznych, pojawiły się zasoby pokazujące, jak połączyć DDD z Django, ale zwykle zaznaczają one, że jest to uzasadnione przy dużej złożoności domenowej. Przykładem jest książka “Architecture Patterns with Python” (Cosmic Python), gdzie autorzy (Harry Percival, Bob Gregory) pokazują implementację DDD/hexagonal architecture z użyciem Pythona. Tworzą tam np. własne klasy Repository, ale... nie korzystają przy tym z Django ORM (używają SQLAlchemy jako data mapper) albo stosują te wzorce w warstwie powyżej Django dla bardzo złożonej logiki. Z kolei większość typowych projektów Django (aplikacje CRUD, proste systemy CMS, typowe aplikacje web) nie osiąga takiego poziomu złożoności, by opłacało się wprowadzać pełne DDD. To, co w Javie bywa koniecznością przy ogromnym monolicie korporacyjnym, w Django byłoby sztuką dla sztuki.

Podsumowując debatę – większość głosów skłania się ku temu, że „Django knows best”, tzn. najlepiej trzymać się idiomatyk i narzędzi dostarczonych przez framework, zamiast budować własne na boku. Gdy czujemy, że nasz kod modeli robi się zbyt „gruby”, rozwiązaniem niekoniecznie jest wprowadzenie warstwy usług – częściej wystarczy refaktoryzacja modelu na mniejsze metody, dodanie managerów lub podział aplikacji na mniejsze moduły (appki). Ważne jest zachowanie przejrzystości, ale można to osiągnąć bez mnożenia poziomów abstrakcji.

Przykłady kodu – z i bez warstw Repository/Service

Na koniec przyjrzyjmy się, jak wygląda kod Django bez dodatkowych warstw oraz z nimi, aby zobaczyć różnicę w przejrzystości i ilości kodu.

Scenariusz: Załóżmy prosty model blogowego wpisu z mechanizmem publikacji. Chcemy mieć możliwość oznaczenia wpisu jako opublikowany. Wzorzec DDD sugeruje, by nie wywoływać metod ORM bezpośrednio w logice – więc mógłby powstać serwis BlogService i repozytorium BlogRepository. Zobaczmy obie wersje:

Kod Django – podejście idiomatyczne (fat model, bez osobnych warstw):

# models.py
class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    is_published = models.BooleanField(default=False)
    published_at = models.DateTimeField(null=True, blank=True)
    
    def publish(self):
        """Biznesowa operacja publikacji wpisu."""
        if not self.is_published:
            self.is_published = True
            self.published_at = timezone.now()
            self.save()

# views.py (lub w logice aplikacji)
post = Post.objects.get(pk=post_id)
post.publish()  # wywołujemy metodę modelu, która kapsułkuje logikę

W powyższym podejściu metoda Post.publish() zawiera całą logikę związaną z publikacją wpisu (sprawdzenie stanu, ustawienie pól, zapis). Widok jest czysty – po prostu pobiera obiekt i wywołuje odpowiednią metodę modelu. Model korzysta z ORM (save()), ale robi to wewnętrznie.

Kod z dodatkowymi warstwami (Service + Repository):

# repository.py
class PostRepository:
    def __init__(self):
        self.model = Post  # odwołanie do klasy modelu (Django ORM)
    def get(self, post_id):
        return self.model.objects.get(pk=post_id)
    def save(self, post):
        post.save()
    # ewentualnie inne metody jak list(), delete()...

# services.py
class PostService:
    def __init__(self, repository):
        self.repo = repository
    def publish_post(self, post_id):
        post = self.repo.get(post_id)
        if not post.is_published:
            post.is_published = True
            post.published_at = timezone.now()
            self.repo.save(post)

# użycie w widoku
repo = PostRepository()
service = PostService(repo)
service.publish_post(post_id)

Tutaj logika została rozdzielona: serwis PostService zawiera metodę biznesową publish_post(), ale sam nie operuje na bazie – zamiast tego korzysta z repozytorium PostRepository (repo.get() i repo.save()). Na pierwszy rzut oka wszystko działa i warstwy są “czysto” oddzielone. Ale jakim kosztem?

  • Mamy dwie nowe klasy (repozytorium i serwis) oraz dodatkowy moduł. Kod jest dłuższy. W metodzie publish_post widać właściwie tę samą logikę, którą wcześniej mieliśmy w Post.publish, tylko że rozbitą na dwie klasy.
  • Wciąż wewnątrz serwisu korzystamy z obiektu Django modelu (post to instancja Post). Aby naprawdę “uniezależnić” warstwę biznesową, niektórzy poszliby dalej – np. mapowali model na osobny obiekt DTO/Domain. To jeszcze bardziej skomplikowałoby przykład.
  • Brak kompatybilności z resztą Django: zauważmy, że nasza warstwa serwisów obchodzi metodę Post.publish() istniejącą w modelu – bo wywołuje logikę samodzielnie. Jeśli ktoś doda ważną logikę do Post.publish(), to wywołanie serwisu jej nie użyje, bo nie korzysta z tej metody. Tworzymy równoległą ścieżkę. Ponadto, co jeśli gdzieś indziej w kodzie ktoś jednak użyje post.publish()? Mamy duplikację logiki – dwa miejsca do utrzymania.
  • Utrudnione użycie Django: Taki serwis nie zadziała np. z Django admin bez dodatkowego kodu – bo w adminie zwykle korzystalibyśmy z metody modelu lub sygnału pre_save. Podobnie widoki genericzne (CreateView, UpdateView) zakładają operacje bezpośrednio na modelu. By korzystać z nich w pełni, musielibyśmy mocno je nadpisywać lub rezygnować z ich wygody.

Przykład ten ilustruje, że dodanie warstw Repository/Service w Django skutkuje większą ilością kodu i powieleniem odpowiedzialności, a nie daje proporcjonalnych korzyści. Kod “idiomatyczny” jest krótszy i lepiej zintegrowany z resztą frameworka.

Jak utrzymać porządek bez niepotrzebnej abstrakcji?

Oczywiście, realne projekty bywają bardziej złożone niż powyższy przykład. Można zapytać: czy wrzucanie całej logiki do modeli nie spowoduje, że te modele staną się zbyt duże i trudne w utrzymaniu? To prawda, że należy dbać o przejrzystość – ale Django daje nam ku temu narzędzia:

  • Dziel odpowiedzialności w modelach: Jeśli model robi “za dużo”, rozważ podział domeny na mniejsze modele lub użycie klas pomocniczych. Nie każda funkcja musi być metodą modelu – czasem może być zwykłą funkcją w module (np. gdy dotyczy interakcji między kilkoma modelami). Django nie narzuca trzymania całej logiki w jednej klasie, chodzi raczej o unikanie sztucznych warstw.
  • Wykorzystuj Managery i QuerySety: Django pozwala definiować custom Manager i QuerySet dla modeli. To świetne miejsce na umieszczenie logiki dotyczącej zestawów obiektów lub wyszukiwania danych. Zamiast tworzyć BlogRepository.list_recent_posts(), można dodać do PostManager metodę recent() zwracającą QuerySet najnowszych wpisów. Ba, można nawet zdefiniować własny QuerySet z metodami i przypiąć go do managera. W ten sposób zapytania i operacje na wielu obiektach są enkapsulowane, a wywołuje się je zgrabnie: Post.objects.recent().published()... etc. Bennett radzi: “if you have a custom, complex and/or often-used query ... do it as either a manager method ... or as a QuerySet method” (Against service layers in Django).
  • Metody modelu dla operacji na pojedynczych obiektach: Wszelkie operacje typu “zmień stan obiektu”, “wykonaj akcję w kontekście tego obiektu” – umieszczaj jako metody modelu. Dzięki temu każdy, kto ma instancję, może łatwo wywołać taką akcję, a implementacja jest w jednym miejscu. Przykład: zamiast mieć serwis OrderService.cancel_order(order_id), lepiej dać metodę Order.cancel(). Według Bennetta: “Any sort of logical operation on only a single model instance should be a method on the model class” (Against service layers in Django).
  • Operacje na wielu modelach: Czasem logika biznesowa dotyczy interakcji między kilkoma modelami (np. transfer środków z konta na konto). Wtedy rzeczywiście ciężko to wpisać w jedną klasę modelu. Ale i tu Django oferuje rozwiązania:
    • Można napisać funkcję w module (np. w pliku services.py w danej aplikacji) bez nadmiernego formalizmu – po prostu jako funkcję łączącą wywołania kilku metod modeli wewnątrz transakcji. To wciąż prostsze niż pełna warstwa serwisów z interfejsami – ot, trochę podziału dla czytelności.
    • Agregacja w jednym modelu: Czasem warto rozważyć wprowadzenie modelu, który reprezentuje proces lub encję wyższego rzędu, zamiast rozstrzelania logiki. Np. jeśli mamy skomplikowany proces zamówienia angażujący wiele tabel, można mieć model Order z metodami typu add_item(), complete(), które wewnątrz tworzą lub modyfikują inne obiekty (OrderItem, Payment, Shipment). To podejście DDD określa jako Aggregate Root. W Django można je zastosować, znów bez tworzenia osobnych serwisów.
  • Keep it DRY i KISS: Korzystajmy z zasady DRY (Don’t Repeat Yourself) – jeżeli widzimy, że jakiś fragment logiki powtarza się w wielu widokach, formularzach czy zadaniach, to zamiast kopiować go, przenieśmy np. do metody modelu lub utility. Jednocześnie stosujmy KISS (Keep It Simple, Stupid) – preferujmy proste rozwiązanie zamiast skomplikowanego, jeśli spełnia cel. Często zwykła funkcja lub metoda wystarczy, nie trzeba od razu tworzyć pełnej klasy serwisu z konstruktorem itd.

Podsumowując ten fragment: Django daje możliwość utrzymania porządku w kodzie bez potrzeby dokładania sztucznych warstw. Kluczem jest świadome korzystanie z mechanizmów modeli, managerów, sygnałów i modułów pomocniczych. To pozwala zachować czytelność i organizację, jednocześnie nie rezygnując z wygody i mocy wbudowanego ORM oraz innych “baterii” Django.

Podsumowanie

Django zostało stworzone jako framework “dla perfekcjonistów z deadline’ami”, co oznacza, że stara się dać nam maksymalną funkcjonalność przy minimalnym narzucie pracy. Filozofia “fat models” jest wbudowana w jego rdzeń – modele mają być centrum naszej aplikacji, łącząc dane i logikę. Dodawanie na siłę wzorców z innych ekosystemów (jak dodatkowe warstwy Repository/Service) zwykle nie przystaje do idiomów Django i prowadzi do niepotrzebnej komplikacji.

Analiza wydajności nie wykazała istotnych korzyści z takich warstw, za to pokazała potencjalne straty (utrata optymalizacji, dodatkowy narzut). Głosy ekspertów – od twórców Django po autorów popularnych bibliotek – również wskazują, że “grube modele” to dobra praktyka, a framework dostarcza już narzędzi do właściwego utrzymania logiki w modelach. W porównaniu do np. Springa, Django po prostu rozwiązuje pewne problemy inaczej, dzięki czemu możemy pisać mniej kodu. Bogate dyskusje społeczności potwierdzają, że “utrzymanie prostoty” popłaca: Django najlepiej sprawdza się, gdy wykorzystujemy jego wbudowane mechanizmy zamiast je obudowywać własnymi.

Na koniec dnia, celem jest czytelny, łatwy w utrzymaniu kod, który dobrze modeluje naszą domenę. Django umożliwia to w prosty sposób przy założeniu, że zaufamy jego podejściu. Zamiast mnożyć byty (serwisy, repozytoria, fabryki, DTO itp.) – w większości przypadków wystarczy poprawnie używać modeli, managerów i widoków. Jak ujął to James Bennett: “the models ... are the API exposed to other code. Which in turn means that they are the place where the ‘business logic’ should be implemented.” (Against service layers in Django). Trzymając się tej zasady, napiszemy aplikacje zgodne z duchem Django – a co za tym idzie, wydajne, przejrzyste i szybkie w rozwoju.

Źródła i dodatkowa lektura: