fbpx

Podcast #2 – Kilka zasad projektowania oprogramowania

Projektowanie aplikacji dla wielu jest przyjemnością. Dla mniej znających temat to zapewne czarna magia. Jednak jestem przekonany, że zarówno jednym, jak i drugim przyda się garść wskazówek, które uprzyjemnią tę pracę. Właśnie dlatego dziś chciałbym opowiedzieć o kilku zasadach projektowania aplikacji, które znacząco ułatwią Wam życie.

Istnieje kilka wspólnych zasad projektowania, które możemy odnieść zarówno do kodu, jak i do architektury systemu.  Zastosowanie tych zasad pozwoli na uzyskanie czystego kodu, co przełoży się na łatwość utrzymywania aplikacji, a to kluczowe. Co więcej, stosowanie tych zasad pozwoli na stworzenie systemu bez ścisłych powiązań komponentów, czyli takiego, który jest łatwo testowalny i łatwo utrzymywalny.

Komunikacja w takim systemie powinna następować za pomocą interfejsów, jeśli mówimy o klasach lub systemów przesyłania wiadomości typu kolejki, jeśli mówimy o architekturze całej aplikacji.

Jednak przejdźmy do konkretów i omówienia poszczególnych zasad.

Zasady podziału odpowiedzialności (ang. separation of concerns)

Zasada ta polega na odseparowaniu części biznesowej, czyli naszej domeny, od interfejsu użytkownika i obsługi infrastruktury czy zapisu do bazy danych.

Doskonałym przykładem wdrożenia tej zasady jest system do wystawiania faktur.

Odpowiedzialnością klasy „FakturaService” będzie jej wystawienie. Zadaniem tej klasy nie będzie wydrukowanie jej bądź wysłanie wygenerowanego dokumentu do naszego klienta.

Odpowiedzialnością klasy „FakturaService” będzie utworzenie faktury i koniec. Żadne inne działania nie będą wchodziły w zakres tej klasy.

Zasada hermetyzacji

Na poziomie warstw, zmiana implementacji z niższej warstwy nie powinna zmieniać kontraktu naszego interfejsu i powodować potrzeby zmiany warstwy wyższej.

O co dokładnie chodzi? Jeśli mamy warstwę biznesową i zmienimy coś w zapisie do bazy danych, nie powinno to wpływać na pozostałe części systemu. Najprościej mówiąc, jeśli myślimy o hermetyzacji klas to powinniśmy to skojarzyć z ograniczeniem dostępu do zmian wewnętrznego stanu obiektu.

Dla przykładu: klasa „FakturaService” powinna mieć metody dodaj pozycję, zmień pozycję, zmień ilość na danej pozycji. Taka klasa nie powinna jednak wystawiać stanu dokumentu, na którym inne obiekty mogłyby dokonywać zmian chociażby w ilościach pozycji. Obiekt faktury jest ukryty wewnątrz klasy „FakturaService”. Zatem tylko FakturaService odpowiada za obsługę bieżącego stanu faktury.

Zasada odwrócenie zależności (ang. Dependency Inversion)

W przypadku zależności bezpośrednich klasa A -> B -> C (klasa A wywołuje metodę na klasie B; klasa B wywołuję metodę bezpośrednio na klasie C), odwrócenie zależności będzie polegało na tym, że klasa A wywołuje metodę abstrakcyjną czy też motodę interfejsu, którą klasa B implementuje, czyli klasa B implementuje interfejs B i ten interfejs B dopiero powinien być  wywoływany przez klasę A.

Powoduje to, że nasze klasy-obiekty są słabo powiązane (loosely coupled), co w efekcie skutkuje utworzeniem systemu, który jest łatwiej testowalny, modularny, i w rezultacie, łatwiej utrzymywalny.

Zasada wymaganie jawnych zależności (ang. Explicit dependencies)

Zasadę wymagania jawnej zależności możemy zastosować przy przekazywaniu zależności klas, jako parametrów w konstruktorze. Jeśli nie mamy żadnych zależności w klasie to możemy utworzyć konstruktor bezparametrowy (parameterless constructor). To nam pokazuje, że ta klasa może zostać utworzona bez żadnych zależności od innych klas. Jeśli takie występują, warto je przekazywać do konstruktora tej klasy. Wtedy jasno i wyraźnie określamy, że taka zależność jest wymagana w tej klasie.

Zasada pojedynczej odpowiedzialności (Single responsibility)

Jeśli zasada ta odnosi się do klasy to nasza klasa powinna posiadać jedną odpowiedzialność, czyli mówiąc najprościej – wykonywać tylko jeden proces (mieć jeden powód do modyfikacji kodu klasy).

Jeśli mamy klasę „Faktura” to ta klasa nie powinna odpowiadać za wysyłanie tego dokumentu do klienta bądź nie powinna wpływać na stany magazynowe. Faktura to jest dokument i tylko jako taki powinien  być obsługiwany.

Jeśli chodzi natomiast o warstwy systemu (architekturę) to zasada pojedynczej odpowiedzialności odnosi się do warstw na przykład dostępu do danych (data access), logiki biznesowej czy interfejsu użytkownika (UI). Jeśli mamy interfejs użytkownika to nie powinien on zapisywać nic w bazie danych. Tak samo, jak logika biznesowa wykonuje jakieś działania to powinna do warstwy zapisu danych przekazać informacje, które przez warstwę bazodanową zostaną dopiero utrwalone.

Taki podział, że jedna warstwa odpowiedzialna jest za jedną rzecz ułatwia testowanie. Jest to tożsame ze światem mikroserwisów, gdzie każdy mikroserwis posiada jedną odpowiedzialność, zajmuje się jedną konkretną dziedziną biznesu. Jeśli mamy mikroserwis magazyny, to magazyny nie zajmują się sprzedażą ani fakturowaniem tylko mikroserwis magazyny będzie odpowiedzialny za stany magazynowe, przesunięcia między różnymi działami firmy lub będzie odpowiadał też za ilość produktów w magazynie.

Zasada – Nie powtarzaj się (ang. Don’t Repeat Yourself (DRY))

Złamanie tej zasady skutkuje powielaniem kodu oraz powielaniem modułów, z których zbudowany jest nasz system. Skutkuje to tym, że jeśli chcemy zmienić działanie kodu to musimy wyszukać wszystkie miejsca występowania analogicznego kodu i dopiero wówczas dokonać zmian.

Jeśli ten kod mamy w jednej klasie i używamy go w jednej postaci to miejsce jego zmiany jest tylko jedno. Natomiast należy tutaj się zastanowić i rozważyć wady i zalety wynikające z powielenia kodu. Ponieważ istnieje zagrożenie, że zmiana kodu w jednym miejscu spowoduje wyrzucenie błędu w innej części systemu. Jeśli ten kod ma kilka odpowiedzialności (złamanie zasady pojedynczej odpowiedzialności), czyli np. jeśli mamy kod wydruku dokumentu i chcemy wydrukować fakturę bądź jakikolwiek inny dokument i w jednym miejscu nagle zmienimy parametry wydruku to wpłyniemy na wydruk innych dokumentów. Tutaj trzeba rozważyć, czy lepiej powielić ten kod na dwie różne klasy czy też w taki sposób projektować system, żeby mógł działać w jednym miejscu nie powodując zmiany w innym.

Zasada persistence ignorance

Zasada ta dotyczy tworzenia obiektów odnoszących się do typów utrwalanych, czyli zapisywanych do np. bazy danych. Mówi, że powinniśmy zastosować obiekty zwane POCO (lub POJO z Javie). Są to najprostsze klasy służące do zapisu stanu.

Wracając do przykładu z fakturą: klasa „Faktura” będzie obsługiwana przez klasę „FakturaService”. W klasie „Faktura” będziemy mieli na pewno numer faktury, listę sprzedawanych przedmiotów, termin zapłaty, dane kupującego i sprzedającego. Z całą pewnością obiekt, który będziemy zapisywali w bazie danych, czy też będziemy zapisywali jego stan, nie powinien dziedziczyć po innych klasach i implementować żadnych interfejsów. Nie powinien też posiadać żadnych metod samoczynnego zapisywania się. Powinny być to najczęściej pola danych typów prostych. Mogą to być też listy pozycji, jednak nic ponadto.

Bounded Contexts – główny wzorzec w Domain-Driven Design

Bardzo ważną zasadą projektowania systemu naszych klas jest zasada zapożyczone z Domain-Driven Design (DDD). Chodzi w niej o podział na moduły koncepcyjne. Nasz system powinniśmy podzielić w taki sposób, żeby jeden moduł nie był zależny od drugiego w swojej odpowiedzialności. Bounded contexts są aktualnie bardzo ściśle powiązane z mikroserwisami, gdzie każdy taki mikroserwis zajmuje się swoją częścią biznesu. Powinien on być oddzielony od systemu i reszty mikroserwisów. Powinien również mieć własną bazę danych i komunikować się za pomocą systemu wysyłania wiadomości. Jednkaże zdecydowanie nie powinien być ściśle zależny od innego modułu.

I tutaj odnieśmy się do naszego prostego przykładu z fakturą i magazynem. W jednej części powinniśmy mieć mikroserwisy dokumentów, czyli na przykład dokumentację sprzedaży. Drugim mikroserwisem będzie część magazynowa. Obie części będą komunikować się ze sobą za pomocą wysyłania wiadomości. Załóżmy, że sprzedałem pięć komputerów: najpierw należy zdjąć je ze stanu magazynowego. Następnie będziemy mieli kolejny moduł Wysyłka (delivery), który to będzie komunikował się z modułem magazynowych otrzymując wiadomość: Wyślij do kontrahenta 5 paczek określonych produktów.


Zasady które wymieniłem służą głównie do podziału logicznego zarówno klas jak, warstw systemów czy podziału tego systemu na serwisy.

Wszystko to służy łatwości utrzymania i łatwości wytworzenia naszej aplikacji. Jeśli będziemy mieli zachowane zasady podziału łatwo nam będzie zidentyfikować gdzie występuje błąd lub jaką część naszego systemu należy zmienić.

Na dzisiaj to wszystko. Zapraszam do śledzenia moich kolejnych publikacji.

Szafrański Michał
Jako Architekt IT, nie tylko projektuję systemy informatyczne, ale również moje życie jest zaprojektowane w taki sposób, aby działać jak dobrze zaprojektowany system - jestem zawsze gotowy na wszelkie wyzwania i problemy. Podobnie jak każdy system, który projektuję, staram się być skalowalny i elastyczny, a czasem trudno przewidzieć, kiedy potrzebna będzie aktualizacja. Często słyszę pytanie: "Kiedy zostanie wydany update twojego życia?" A ja odpowiadam: "Kiedyś, ale zanim to nastąpi, muszę zebrać więcej danych i przeprowadzić odpowiednie testy." Moje życie to nie tylko kodowanie i projektowanie, ale również ciągłe doskonalenie i uczenie się nowych technologii. Nieustannie próbuję wprowadzać ulepszenia, zarówno w moim życiu osobistym, jak i zawodowym. A jeśli coś nie działa, nie boję się eksperymentować i próbować różnych rozwiązań, aby znaleźć najlepsze rozwiązanie. Nie jestem tylko architektem IT - jestem również architektem swojego życia, zaprojektowanym w taki sposób, aby działał jak dobrze zaprojektowany system.

Leave a Reply Text