Struktura projektu na przykładzie Django i FastAPI
Styczeń 10, 2024 | #django , #python , #fastapi
011100
010110
100001
Planując nową aplikację decydujemy się na wybór technologii, z pomocą których będziemy go realizować. Zazwyczaj opieramy się o język i narzędzia, z którymi mamy już jakieś pozytywne doświadczenie i gwarancję sukcesu, ale czasami z tych lub innych względów stajemy przed koniecznością zaprzyjaźnienia się z nieznaną nam technologią. Nowy język to zbyt duże wyzwanie, żeby na start myśleć o samodzielnej realizacji poważnego projektu, ale nowy framework to dla doświadczonego developera ciekawa przygoda.
Ważne żeby dobrze zacząć. Większość tutoriali, czy to z oficjalnej dokumentacji danego narzędzia, czy też przygotowanych przez użytkowników traktuje o podstawach, nie przywiązując wagi do prawidłowej architektury projektu. Przykładowe endpointy, połączenie z bazą danych, autoryzacja itp. realizowane są w jednym pliku co nie jest tragicznym pomysłem w przypadku mikroserwisu, ale zaczyna szybko kłuć w miarę rozwoju aplikacji.
Django MVT
Elastyczny framework umożliwia realizację dowolnej struktury projektu. Nie mniej na start lepiej jest podążać za dobrymi praktykami - rozwiązaniami które się sprawdziły i są powszechnie stosowane. Zaczynając pracę z Django od samego początku jesteśmy zaznajamiani z konwencją, zgodnie z którą struktura katalogów opiera się głównie na podziale projektu na aplikacje oraz oddzielaniu różnych aspektów projektu w logiczne segmenty.
Django używa wariantu wzorca architektonicznego MVC (Model-View-Controller), który jest często nazywany wzorcem MVT (Model-View-Template). W klasycznym MVC :
Model (Model) reprezentuje dane aplikacji ale też logikę biznesową potrzebną do manipulowania danymi,
Widok (View) odpowiada za prezentację tych danych użytkownikowi,
Kontroler (Controller) łączy te dwie warstwy. W praktyce kontroler interpretuje dane wejściowe z widoku, przekazuje je do modelu, a następnie odbiera aktualizacje modelu i przekazuje je z powrotem do widoku.
W Django rolę kontrolera pełnią widoki (View), a rolę widoków szablony (Template) z kolei w Django Rest Framework (DRF) przeznaczonym do budowania API REST-owego szablony są zastąpione przez serializery (Serializer), które przekształcają dane najczęściej do formatu JSON i możemy je uznać za warstwę prezentacji. W DRF możemy mówić o wariancie MVS (Model-VIew-Serializer).
Przykładowa struktura katalogów dla typowego projektu Django wygląda następująco
myproject/
manage.py
myproject/
__init__.py
settings.py
urls.py
wsgi.py
app1/
migrations/
static/
templates/
admin.py
apps.py
models.py
tests.py
views.py
app2/
migrations/
static/
templates/
admin.py
apps.py
models.py
tests.py
views.py
static/
templates/
media/
Każdy developer dołączając do projektu bazującego na Django natychmiast odnajduje się w kodzie. Wie gdzie szukać konfiguracji, gdzie definiować modele, czy klasy odpowiedzialne za generowanie panelu administracyjnego, gdzie serializery w przypadku Django Rest Framework, czy endpointy jak w Django Ninja. Framework ten i jego pochodne mają swoje problemy ale jeśli chodzi o strukturę katalogów i plików to przyjęte podejście bardzo ułatwia programistom rozpoczęcie nauki lub też wdrożenie się w już istniejący projekt.
FastAPI 3-Tier
FastAPI nie forsuje preferowanej struktury projektu. W przypadku większych aplikacji podpowiada możliwą strukturę plików, ale nie jest wprost powiedziane do jakiego wzorca architektonicznego nawiązuje.
├── app
│ ├── __init__.py
│ ├── main.py
│ ├── dependencies.py
│ └── routers
│ │ ├── __init__.py
│ │ ├── items.py
│ │ └── users.py
│ └── internal
│ ├── __init__.py
│ └── admin.py
Na podstawie zaprezentowanej w dokumentacji próbki możemy jednak od razu dostrzec, że Sebastián Ramírez nie zdecydował się na grupowanie plików według aplikacji, jak to ma miejsce w Django.
Wzorzec architektoniczny 3-Tier, znany również jako trójwarstwowa architektura, jest wzorcem projektowym stosowanym w projektowaniu aplikacji oprogramowania, w którym struktura aplikacji jest podzielona na trzy główne poziomy lub warstwy:
Warstwa prezentacji (Presentation Layer): Jest to najwyższa warstwa architektury. Odpowiada za interakcję z użytkownikiem końcowym, zbieranie danych wejściowych użytkownika i prezentowanie informacji.
Warstwa logiki biznesowej (Business Logic Layer, BLL): Ta warstwa znajduje się pomiędzy warstwą prezentacji a warstwą danych. Jest odpowiedzialna za przetwarzanie danych wejściowych z warstwy prezentacji, stosowanie określonych reguł biznesowych, wykonywanie operacji i algorytmów, a następnie przekazywanie wyników z powrotem do warstwy prezentacji lub dalej do warstwy danych.
Warstwa danych (Data Layer): Obejmuje ona przechowywanie, zarządzanie i dostęp do danych. Warstwa danych może obejmować bazy danych, serwisy plików, lub inne mechanizmy przechowywania danych. Jest odpowiedzialna za dostarczanie danych do warstwy logiki biznesowej, a także za ich odbiór i zapisywanie.
Wzorzec 3-Tier w podstawowych założeniach jest podobny do architektury MVC, ale jest bardziej związany z fizycznym podziałem aplikacji, gdzie każda warstwa może być rozłożona na różne serwery lub klastry lub jak w tym przypadku zgrupowana w osobne foldery. W związku z tym wstępny podział plików na aplikacje mijał by się z celem. Dlatego struktura projektu opartego na frameworku FastAPI zgodna z 3-Tier mogłaby się kształtować zgodnie z tym co znajdziemy w artykule pt. Structuring FastAPI Project Using 3-Tier Design Pattern
app/
├── db/
│ ├── config.py
│ └── session.py
├── crud/
│ ├── auth.py
│ ├── item.py
│ └── ...
├── models/
│ ├── auth.py
│ ├── base.py
│ ├── item.py
│ └── ...
├── routers/
│ ├── auth.py
│ ├── item.py
│ └── ...
├── schemas/
│ ├── auth.py
│ ├── item.py
│ └── ...
├── services/
│ ├── auth.py
│ ├── base.py
│ └── ...
└── main.py
gdzie:
Warstwa prezentacji składa się z endpointów API, które są definicją interfejsów, przez które użytkownicy komunikują się z aplikacją.
app/
├── routers/
│ ├── auth.py
│ └── ...
├── schemas/
│ ├── auth.py
│ └── ...
└── main.py
Warstwa logiki biznesowej zawiera logikę specyficzną dla aplikacji.
app/
├── services/
│ ├── auth.py
│ └── ...
Warstwa danych zajmuje się wszystkim, co jest związane z przechowywaniem, pobieraniem i zarządzaniem danymi.
app/
├── db/
│ └── session.py
├── crud/
│ ├── auth.py
│ └── ...
├── models/
│ ├── auth.py
│ └── ...
W przeciwieństwie do Django w FastAPI nie ma ustalonej konwencji nazewnictwa. Operacje na modelach bazy danych mogą być trzymane w katalogu crud
albo repositories
, endpointy odnajdziemy w api
albo routers
a logika znajdzie swoje swoje miejsce w services
, ale może też się znaleźć w core
czy utils
.
Z analogiczną architekturą spotkamy się zarówno boilerplate-ach - ułatwiających szybki start projektu opartego na FastAPI, ale też w większości tutoriali.
Alternatywne architektury
Yerassyl Zhanymkanov w artykule pt. FastAPI Best Practices zaprezentował z kolei strukturę inspirowaną narzędziem Dispatch (narzędzie open-source stworzone przez Netflix, które służy do zarządzania kryzysami i incydentami), twierdząc, że lepiej spełnia ona wymagania mocno rozbudowanych projektów czy wręcz monolitów.
app/
├── alembic/
├── src
│ ├── auth
│ │ ├── router.py
│ │ ├── schemas.py
│ │ ├── models.py
│ │ ├── dependencies.py
│ │ ├── config.py
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ ├── service.py
│ │ └── utils.py
│ ├── aws
│ │ ├── client.py
│ │ ├── schemas.py
│ │ ├── config.py
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ └── utils.py
│ └── posts
│ │ ├── router.py
│ │ ├── schemas.py
│ │ ├── models.py
│ │ ├── dependencies.py
│ │ ├── constants.py
│ │ ├── exceptions.py
│ │ ├── service.py
│ │ └── utils.py
│ ├── config.py
│ ├── models.py
│ ├── exceptions.py
│ ├── pagination.py
│ ├── database.py
│ └── main.py
├── tests/
│ ├── auth
│ ├── aws
│ └── posts
├── templates/
│ └── index.html
├── requirements
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── .env
├── .gitignore
├── logging.ini
└── alembic.ini
Jest wiele wzorców architektonicznych (MVVM, MVP, SPA) i wiele z nich ma wpływ logikę podziału kodu często odzwierciedloną przez hierarchię plików i katalogów. Ostateczny kształt struktury ma niebagatelny wpływ na kilka aspektów związanych z projektowaniem i implementacją aplikacji a są to:
Separacja obaw (Separation of Concerns): Wzorce architektoniczne pomagają oddzielić różne aspekty aplikacji (np. interfejs użytkownika, logikę biznesową, dostęp do danych) i jest to podział wertykalny, ale może też odnosić się do podziału horyzontalnego, który w praktyce daje np. możliwość wyłączenia z działania części funkcjonalności.
Ponowne użycie kodu (Code Reusability): Modułowa budowa, ułatwia wykorzystanie istniejącego kodu w różnych częściach aplikacji.
Elastyczność i skalowalność: Dobrze zaprojektowana struktura plików umożliwia łatwe wprowadzanie zmian, rozbudowę aplikacji i skalowanie bez konieczności przeprojektowywania całej aplikacji. W razie potrzeby ułatwia też wydzielenie pewnych funkcjonalności do nowych projektów lub wycofywanie zdeprecjonowanych rozwiązań.
Testowanie i konserwacja: Wyraźne oddzielenie komponentów ułatwia testowanie jednostkowe i debugowanie. Składniki aplikacji mogą być testowane niezależnie, co znacząco poprawia jakość kodu.
Unifikacja pracy: Wprowadzenie konwencji ułatwia wdrażanie nowych programistów do zespołu, co pozytywnie wpływa na współpracę i komunikację.
Integracja: Zdefiniowane interfejsy i moduły ułatwiają integrację z zewnętrznymi systemami, bibliotekami lub serwisami.
Przewidywalność: Uznane struktury oparte na wzorcach architektonicznych pozwalając unikać błędów popełnionych już przez kogoś innego. Z reguły są dobrze opisane i wiadomo jakie mają mocne i słabe strony i czym się charakteryzują.
Nie ma jednego idealnego przepisu i dobór rozwiązania zależy od specyfiki projektu. O ile domyślna architektura aplikacji zbudowanych na FastAPI jest postrzegana jako preferowana dla małych projektów typu mikroserwisy, o tyle duże projekty, bogate w zależności i rozbudowaną hierarchię wewnętrzną mogą wymagać już innej struktury lepiej oddającą wewnętrzne podziały.