Warstwa biznesowa – porównanie wzorców

Oczywiście nie ma jednoznacznej odpowiedzi jaki wzorzec używać. Wszystko zależy od konkretnych wymagań aplikacji oraz dostępnego czasu na ukończenie projektu. Wzorce obiektowe na pewno cechują się większą elastycznością od wzorców proceduralnych. Z drugiej strony jeśli projekt nie jest zbyt skomplikowany to po co poświęcać czas na implementacje ich, jeżeli i tak to w przyszłości nie zwróci się (w postaci zaoszczędzonego czasu)?

Generalnie im więcej poświecimy czasu  na początku na implementacje wzorca tym oszczędności będą większe podczas przyszłych rozszerzeń systemu.
W poście chciałbym stworzyć krótkie porównanie wzorców na podstawie pewnych kryteriów, które uznałem za istotne:

  1. Skalowalność – określa jak łatwo system radzi sobie ze wzrastającą liczbą użytkowników.System można skalować poprzez wprowadzenie dodatkowych usług sieciowych czy rozproszonych baz danych . W przypadku skryptu transakcji, aktywnego rekordu i modułu tabeli skalowalność jest ograniczona ponieważ wzorce są mocną zorientowane na bazę danych. Z kolei model dziedziny bardzo dobrze z tym sobie poradzi ponieważ jego główna ideą jest oddzielenie logiki od źródła danych – sam wzorzec nie nakłada żadnych ograniczeń na warstwę persystencji.
  2. Modularność – dobry program podzielony jest na pewne logiczne części, często niezależne od siebie.W przypadku wzorca TS ciężko mówić o jakiejkolwiek modularności. Często cała logika biznesowa składa się z jednej klasy. W przypadku modułu tabeli sytuacja wygląda nieco lepiej ponieważ zasady wzorca mówią już o pewnym podziale problemu na obiekty – nadal jednak jest to za mało aby uznać zbudowany system za modularny. Aktywny rekord z reguły jest modularny ponieważ występuje tam jawny podział na obiekty. AR wyklucza użytkowanie technik programowania obiektowego takich jak np. polimorfizm. Dlatego też  DM uznawany jest za najbardziej modularny wzorzec.
  3. Testowalność – głównym wyzwaniem moim zdaniem są testy integracyjne, które wymagają rozdzielenia całości na fragmenty. Testy integracyjne wymagają wprowadzenia tzw. obiektów mock. Są to obiekty, które po prostu naśladują działanie prawdziwych obiektów. Powiedzmy, że mamy klasę Order, która wykonuje zaawansowaną logikę. Obiekt mock mógłby wyglądać następująco:

    public class OrderMock:Order
    {
        public override int GetDiscount()
        {
            return 20;
        }
        public override bool HasCredit()
        {
            return true;
        }
    }

    Jak widać klasa nie robi nic ciekawego, zwraca na sztywno jakiś wynik (często dynamicznie definiowany). Co nam to daje? Otóż klasa ma jedną wielką zaletę – nie robi nic konkretnego a zatem nie zawiera żadnych błędów. Testy integracyjne mają za zadanie przetestowanie całości systemu razem z połączeniami między modułami. Jeśli byśmy próbowali przetestować całość systemu od razu to w momencie wystąpienia błędu ciężko byłoby powiedzieć gdzie dokładnie wystąpił błąd. Z kolei jeśli zaczniemy testować pojedynczy moduł a resztę klas zastąpimy mockami, w razie błędu wiemy gdzie go szukać dokładnie. Następnie stopniowo będziemy podmieniać obiekty mock na te prawdziwe. Gdyby jakiś test nie poszedłby pozytywnie, wiemy, że prawdopodobnie przyczyna leży w nowo doczepionym węźle.

    Kryterium  zatem jest ściśle powiązane z modularnością opisaną powyżej. Skrypt transakcji oraz moduł tabeli ciężko zastąpić mock’iem. Aktywny rekord co prawda jest modularny ale przeważnie generowany jest automatycznie przez narzędzia ORM i często ciężko przeładować zachowanie dynamicznie wygenerowanych metod i właściwości (ponieważ nie są np. one wirtualne).

  4. Scentralizowany system monitoringu oraz wykonywania logów. W systemie enterprise, wszelkie wystąpienie wyjątków musi być dokładnie odnotowane. Często strategie przechwytywania i zapisywania wyjątków są wstrzykiwane dynamicznie. Nie korzysta się więc ze standardowego bloku try-catch. Przeważnie istnieje klasa, którą wywołuje się w przypadku wystąpienia błędu. Skrypt transakcji ze względu na swoją budowę stanowi doskonałe miejsce na obsługę błędów. W przypadku pozostałych wzorców, prawdopodobnie będzie potrzeba wprowadzenie obsługi błędów w osobnej warstwie (w warstwie usług). Warto zwrócić uwagę na jeszcze jedno niebezpieczeństwo w przypadku DM. Często w niej korzystamy z wirtualnych metod. Użytkownik może zatem przeładować którąś z metod i zapomnieć wywołać metodę bazową, która odpowiedzialna jest za wykonanie odpowiedniego logu.
  5. Wsparcie dla obiektów POCO. Skrypt transakcji oraz moduł tabeli wykonują odczyt bazy danych wewnątrz metod. Nie muszą być zatem oznaczone żadnymi atrybutami mówiącymi na jaką tabele dana klasa ma być zmapowana. Występuje wiec tu pełna zgodność z POCO. Podobnie sprawa wygląda z DM – wzorzec z definicji jest niezależny od źródła danych i musi być czystą klasą. Najgorzej prezentuje się aktywny rekord. Wszelkie ORM domyślnie generują obiekty niezgodne z POCO – oznaczone atrybutami lub dziedziczące po jakieś specjalnej klasie.Warto jednak podkreślić, że nHibernate czy Entity Framework wspierają POCO, z tym, że jest to po prostu niewygodne i wydaje mi się, że większość osób korzysta z domyślnie wygenerowanych klas przez EF.
  6. Ograniczenia czasowe. Rozważając nad wyborem wzorca nie można zapomnieć oczywiście o deadline. Skrypt transakcji oraz moduł tabeli są oczywiście najprostszymi rozwiązaniami i nie wymagają dużej ilości czasu. Implementacja aktywnego rekordu jest również bardzo szybka o ile korzystamy  z narzędzia ORM – większość zrobi za nas dostarczony generator kodu.W przypadku DM sprawa wygląda kompletnie inaczej- ze względu na brak relacji jeden do jednego między obiektami biznesowymi a tabelą w bazie danych, narzędzia ORM nie mogą zrealizować mapowania automatycznie.

Na koniec tabela, przedstawiająca powyższe rozważania w liczbach (skala od 0 do 3):

Kryterium

Skrypt transakcji

Moduł tabeli

Aktywny rekord

Model dziedziny

Skalowalność

2

2

2

3

Modularność

0

1

2

3

Testowalność

0

0

1

3

Niezależność od źródła danych

1

1

0

3

Scentralizowanie zarządzanie logami oraz wyjątkami

3

1

0

0

Zgodność z POCO

3

3

1

3

Wymagany czas

3

3

3

1

7 thoughts on “Warstwa biznesowa – porównanie wzorców”

  1. “Użytkownik może zatem przeładować którąś z metod i zapomnieć wywołać metodę bazową, która odpowiedzialna jest za wykonanie odpowiedniego logu.”

    Na takie przypadki mam sposób, tzn nie udostępnianie klasom dziedziczącym logiki bezpośrednio – jeśli to kogoś interesuje:

    w abstracyjnej klasie bazowej
    – prywatna metoda DoLog() tworząca log.
    – protected abstrakcyjna metoda DoMethod() robiąca właściwą metodę.
    – publiczna metoda Do() wywołująca DoMethod() i DoLog()

    w dziedziczonej klasie wystarczy nadpisać DoMethod() -> i już mamy pewność że log zostanie wykonany.

  2. Do logów lepiej, do obsługi błędów nie, bo jak?
    Poza tym programowanie aspektowe w .NET wciąż kuleje.

  3. Czy programowanie aspektowe kuleje… Spring.NET ma swoją implementację, EntLib ma PIAB, a z rozwiązań “niezaleznych” mamy PostSharpa. Nie wspominając o ContextBoundObjects w gołym .NET.
    Do wyboru do koloru, kulenia nie zauważam:).

  4. Hmmm, PIAB to nie jest programowanie aspektowe i zostało do innego celu stworzone 🙂 Czy jest dla .NET implementacja języka aspectJ?

  5. Źle się trochę wyraziłem się. Miałem na myśli, że PIAB to nie jest czyste AOP i jako takiego oficjalnego wsparcia nie ma. Ale można wykorzystać go jako AOP. W przypadku logów nic więcej nam nie trzeba 🙂

Leave a Reply

Your email address will not be published.