Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Wskazówka
Ta treść jest fragmentem eBooka, Architektura nowoczesnych aplikacji internetowych z ASP.NET Core i Azure, dostępny na .NET Docs lub jako bezpłatny plik PDF do pobrania, który można czytać w trybie offline.
"Jeśli budowniczowie zbudowali budynki w sposób, w jaki programiści pisali programy, to pierwszy dzięcioł, który przyszedł, zniszczy cywilizację."
- Gerald Weinberg
Należy zaprojektować rozwiązania programowe z myślą o łatwą konserwację. "Zasady opisane w tej sekcji mogą pomóc w podejmowaniu decyzji dotyczących architektury, które zaowocują czystymi, łatwymi do utrzymania aplikacjami." Ogólnie rzecz biorąc, te zasady prowadzą cię do tworzenia aplikacji z odrębnych składników, które nie są ściśle powiązane z innymi częściami aplikacji, ale raczej komunikują się za pośrednictwem jawnych interfejsów lub systemów obsługi komunikatów.
Typowe zasady projektowania
Separacja obaw
Wiodącą zasadą podczas opracowywania jest separacja obaw. Ta zasada potwierdza, że oprogramowanie powinno być oddzielone na podstawie rodzaju wykonywanej pracy. Rozważmy na przykład aplikację, która zawiera logikę identyfikowania godnych uwagi elementów do wyświetlenia użytkownikowi oraz formatuje takie elementy w określony sposób, aby były bardziej zauważalne. Zachowanie odpowiedzialne za wybór elementów do sformatowania powinny być oddzielone od zachowania odpowiedzialnego za formatowanie elementów, ponieważ te zachowania są oddzielnymi problemami, które są tylko przypadkowo powiązane ze sobą.
Architekturą aplikacje mogą być tworzone logicznie, aby postępować zgodnie z tą zasadą, oddzielając podstawowe zachowania biznesowe od infrastruktury i logiki interfejsu użytkownika. W idealnym przypadku reguły biznesowe i logika powinny znajdować się w osobnym projekcie, który nie powinien zależeć od innych projektów w aplikacji. Ta separacja pomaga zagwarantować, że model biznesowy jest łatwy do przetestowania i może ewoluować bez ścisłego połączenia ze szczegółami implementacji niskiego poziomu (pomaga również, jeśli problemy z infrastrukturą zależą od abstrakcji zdefiniowanych w warstwie biznesowej). Separacja problemów jest kluczowym zagadnieniem dotyczącym używania warstw w architekturach aplikacji.
Hermetyzacja
Różne części aplikacji powinny używać hermetyzacji , aby odizolować je od innych części aplikacji. Składniki i warstwy aplikacji powinny być w stanie dostosować swoją implementację wewnętrzną bez przerywania współpracy, o ile umowy zewnętrzne nie zostaną naruszone. Odpowiednie zastosowanie hermetyzacji pomaga osiągnąć luźne sprzężenie i modułowość w projektach aplikacji, ponieważ obiekty i pakiety można zastąpić alternatywnymi implementacjami, o ile ten sam interfejs jest utrzymywany.
W klasach hermetyzacja jest osiągana przez ograniczenie dostępu zewnętrznego do stanu wewnętrznego klasy. Jeśli zewnętrzny aktor chce manipulować stanem obiektu, powinien to zrobić za pomocą dobrze zdefiniowanej funkcji (lub ustawiacza właściwości), a nie bezpośredniego dostępu do stanu prywatnego obiektu. Podobnie same składniki aplikacji i aplikacje powinny uwidaczniać dobrze zdefiniowane interfejsy dla swoich współpracowników, a nie zezwalać na bezpośrednie modyfikowanie ich stanu. Takie podejście pozwala na ewolucję wewnętrznego projektu aplikacji w miarę upływu czasu, bez obaw o zakłócenie pracy współpracowników, o ile kontrakty publiczne są utrzymywane.
Modyfikowalny stan globalny jest sprzeczny z enkapsulacją. Nie można polegać na wartości pobranej z modyfikowalnego stanu globalnego w jednej funkcji, aby mieć tę samą wartość w innej funkcji (lub jeszcze bardziej w tej samej funkcji). Zrozumienie problemów z modyfikowalnym stanem globalnym jest jednym z powodów, dla których języki programowania, takie jak C#, obsługują różne reguły określania zakresu, które są używane wszędzie od instrukcji do metod do klas. Warto zauważyć, że architektury oparte na danych, które opierają się na centralnej bazie danych na potrzeby integracji między aplikacjami, są same w sobie zależne od modyfikowalnego stanu globalnego reprezentowanego przez bazę danych. Kluczową kwestią w projektowaniu opartym na domenie i czystej architekturze jest sposób zabezpieczenia dostępu do danych oraz zapewnienia, że stan aplikacji nie zostanie unieważniony przez bezpośredni dostęp do jego formatu przechowywania.
Inwersja zależności
Kierunek zależności w aplikacji powinien być w kierunku abstrakcji, a nie szczegółów implementacji. Większość aplikacji jest napisana w taki sposób, że przepływ zależności czasu kompilacji kieruje się w stronę wykonywania na etapie uruchamiania, tworząc bezpośredni wykres zależności. Oznacza to, że jeśli klasa A wywołuje metodę klasy B i klasy B wywołuje metodę klasy C, wówczas w kompilowaniu klasa A będzie zależeć od klasy B, a klasa B będzie zależeć od klasy C, jak pokazano na rysunku 4-1.
Rysunek 4–1. Wykres zależności bezpośrednich.
Zastosowanie zasady inwersji zależności umożliwia A wywołanie metod na abstrakcji implementowanej przez B, dzięki czemu A może wywołać B w czasie wykonywania, ale B jest zależne od interfejsu kontrolowanego przez A w czasie kompilacji (a tym samym odwraca typową zależność czasu kompilacji). W czasie wykonywania przepływ wykonywania programu pozostaje niezmieniony, ale wprowadzenie interfejsów oznacza, że różne implementacje tych interfejsów można łatwo podłączyć.
Rysunek 4–2. Odwrócony wykres zależności.
Inwersja zależności jest kluczową częścią tworzenia luźno powiązanych aplikacji, ponieważ szczegóły implementacji można zapisywać tak, aby zależały od abstrakcji wyższego poziomu, a nie odwrotnie. Wynikowe aplikacje są bardziej testowalne, modułowe i konserwowalne w rezultacie. Praktyka wstrzykiwania zależności jest możliwa poprzez przestrzeganie zasady inwersji zależności.
Jawne zależności
Metody i klasy powinny jawnie wymagać wszelkich potrzebnych obiektów współpracy w celu poprawnego działania. Jest ona nazywana zasadą jawnych zależności. Konstruktory klas umożliwiają klasom identyfikowanie rzeczy, których potrzebują, aby były w prawidłowym stanie i działały prawidłowo. Jeśli zdefiniujesz klasy, które można skonstruować i wywoływać, ale będą działać prawidłowo tylko wtedy, gdy istnieją pewne składniki globalne lub infrastruktury, te klasy są nieuczciwe dla ich klientów. Kontrakt konstruktora informuje klienta, że potrzebuje tylko określonych elementów (być może nic, jeśli klasa używa tylko konstruktora bez parametrów), ale w czasie wykonywania okazuje się, że obiekt naprawdę potrzebował czegoś innego.
Postępując zgodnie z zasadą jawnych zależności, klasy i metody uczciwie informują swoich klientów, czego potrzebują, aby funkcjonować. Zasada ta sprawia, że kod staje się bardziej samodokumentujący się, a kontrakty kodowania stają się bardziej zrozumiałe dla użytkownika, ponieważ użytkownicy będą ufać, że tak długo, jak dostarczają wymagane parametry metody lub konstruktora, obiekty, z którymi pracują, będą działać prawidłowo podczas wykonywania programu.
Pojedyncza odpowiedzialność
Pojedyncza zasada odpowiedzialności ma zastosowanie do projektowania zorientowanego na obiekty, ale można również uznać za zasadę architektury podobną do rozdzielenia zagadnień. Stwierdza, że obiekty powinny mieć tylko jedną odpowiedzialność i że powinny mieć tylko jeden powód do zmiany. W szczególności jedyną sytuacją, w której obiekt powinien ulec zmianie, jest to, że należy zaktualizować sposób, w jaki wykonuje jedną odpowiedzialność. Przestrzeganie tej zasady ułatwia tworzenie luźniej powiązanych i modułowych systemów, gdyż wiele rodzajów nowych zachowań można zaimplementować jako nowe klasy, zamiast dodawać dodatkową odpowiedzialność do istniejących klas. Dodawanie nowych klas jest zawsze bezpieczniejsze niż zmiana istniejących klas, ponieważ żaden kod nie zależy jeszcze od nowych klas.
W aplikacji monolitycznej możemy zastosować pojedynczą zasadę odpowiedzialności na wysokim poziomie do warstw w aplikacji. Odpowiedzialność za prezentację powinna pozostać w projekcie interfejsu użytkownika, podczas gdy odpowiedzialność za dostęp do danych powinna być przechowywana w projekcie infrastruktury. Logika biznesowa powinna być przechowywana w podstawowym projekcie aplikacji, gdzie można ją łatwo przetestować i może ewoluować niezależnie od innych obowiązków.
Gdy ta zasada zostanie zastosowana do architektury aplikacji i zostanie przeniesiona do jej logicznego punktu końcowego, uzyskasz mikrousługi. Dana mikrousługa powinna mieć jedną odpowiedzialność. Jeśli musisz rozszerzyć zachowanie systemu, zwykle lepiej jest to zrobić, dodając dodatkowe mikrousługi, a nie dodając odpowiedzialności za istniejącą.
Dowiedz się więcej o architekturze mikrousług
Nie powtarzaj (DRY)
Aplikacja powinna unikać określania zachowania związanego z określoną koncepcją w wielu miejscach, ponieważ jest to częste źródło błędów. W pewnym momencie zmiana wymagań będzie wymagać zmiany tego zachowania. Prawdopodobnie nie uda się zaktualizować co najmniej jednego wystąpienia zachowania, a system będzie działał niespójnie.
Zamiast duplikować logikę, hermetyzują ją w konstrukcji programowania. Utwórz tę konstrukcję jako jedyną władzę nad tym zachowaniem, a każda inna część aplikacji, która wymaga tego zachowania, powinna używać nowej konstrukcji.
Uwaga / Notatka
Unikaj łączenia ze sobą zachowania, które jest tylko przypadkowo powtarzalne. Na przykład, ponieważ dwie różne stałe mają tę samą wartość, nie oznacza to, że powinna istnieć tylko jedna stała, jeśli koncepcyjnie odwołują się do różnych rzeczy. Duplikowanie zawsze jest lepsze niż sprzęganie z niewłaściwą abstrakcją.
Ignorancja wobec trwałości
Ignorancja trwałości odnosi się do typów, które muszą być utrwalane, ale których kod nie ma wpływu na wybór technologii trwałości. Takie typy na platformie .NET są czasami nazywane zwykłymi starymi obiektami CLR (POC), ponieważ nie muszą dziedziczyć z konkretnej klasy bazowej ani implementować określonego interfejsu. Ignorancja trwałości jest cenna, ponieważ umożliwia utrwalanie tego samego modelu biznesowego na wiele sposobów, oferując dodatkową elastyczność aplikacji. Opcje trwałości mogą ulec zmianie w czasie, z jednej technologii bazy danych na inną lub mogą być wymagane dodatkowe formy trwałości oprócz tego, z czym aplikacja została uruchomiona (na przykład przy użyciu pamięci podręcznej Redis lub usługi Azure Cosmos DB oprócz relacyjnej bazy danych).
Oto kilka przykładów naruszeń tej zasady:
Wymagana klasa bazowa.
Wymagana implementacja interfejsu.
Klasy odpowiedzialne za zapisywanie siebie (takie jak wzorzec Active Record).
Wymagany konstruktor bez parametrów.
Właściwości wymagające wirtualnego słowa kluczowego.
Wymagane atrybuty specyficzne dla trwałości.
Wymaganie, aby klasy miały jakiekolwiek z powyższych cech lub zachowań, dodaje sprzężenie między typami, które mają być utrwalane, oraz wybór technologii trwałości, co utrudnia wdrażanie nowych strategii dostępu do danych w przyszłości.
Konteksty ograniczone
Ograniczone konteksty są centralnym wzorcem w projekcie Domain-Driven. Zapewniają one sposób radzenia sobie ze złożonością w dużych aplikacjach lub organizacjach, dzieląc je na oddzielne moduły koncepcyjne. Każdy moduł koncepcyjny reprezentuje następnie kontekst oddzielony od innych kontekstów (a zatem ograniczony) i może ewoluować niezależnie. Każdy ograniczony kontekst powinien mieć idealną swobodę do nadawania własnych nazw pojęciom w jego obrębie i powinien mieć wyłączny dostęp do własnego repozytorium danych.
Co najmniej poszczególne aplikacje internetowe powinny dążyć do tego, aby stanowić odrębny kontekst, z własnym repozytorium danych dla swojego modelu biznesowego, zamiast udostępniać bazę danych innym aplikacjom. Komunikacja między powiązanymi kontekstami odbywa się za pośrednictwem interfejsów programowych, a nie za pośrednictwem udostępnionej bazy danych, co umożliwia wykonywanie logiki biznesowej i zdarzeń w odpowiedzi na zmiany, które mają miejsce. Ograniczone konteksty ściśle odpowiadają mikrousługom, które idealnie są również implementowane jako ich własne, indywidualne konteksty ograniczone.