Category Archives: Patterns & Practices

Wprowadzenie do warstwy biznesowej

Zacznijmy od zdefiniowania do czego potrzebna nam jest tzw. warstwa biznesowa w systemie. Sama nazwa może nie wiele mówi i czasami okazuje się  nawet myląca.

Ogólnikowo  jest to rdzeń systemu. Stanowi zdecydowanie najważniejszy punkt każdej aplikacji. Warstwa biznesowa ( w skrócie BL – business layer) zawiera właściwą logikę aplikacji. Jeśli brzmi to zbyt abstrakcyjnie, przedstawmy to na przykładzie systemu sprzedaży (na którym będę często bazował). Co stanowi warstwę biznesową ( a więc logikę)  w systemie sprzedaży? Oczywiście składanie i walidowanie zamówień, śledzenie zmian przysyłki czy wybieranie produktów, które powinny być przecenione. Poniżej przedstawiam kilka według mnie kluczowych zadań BL:

  1. Implementacja reguł biznesowych. Regułą biznesową może być np. “przeceń produkt jeśli jego sprzedaż w ostatnim kwartale zmalała n-razy”. W najprostszym przypadku w kodzie realizujemy to za pomocą instrukcji sterujących (np. if-else). W zaawansowanych przypadkach może okazać się, że potrzebny jest tzw. silnik reguł biznesowych czyli mechanizm umożliwiający dynamicznie definiowanie reguł – w przypadku modułu promocji jest to bardzo prawdopodobny scenariusz.
  2. Walidacja danych. Sprawdzanie poprawności powinno odbywać się w każdej warstwie. W BL jednak jest to niezwykle ważne ponieważ błędne dane mogą spowodować nawet awarie systemu. Proste walidacje będą dotyczyły po prostu typu lub zakresu danych (np. czy podana wartość zawiera się w jakiś przedziale liczb). Z kolei skomplikowane reguły dotyczą wielu obiektów biznesowych jednocześnie oraz nie mają charakteru typowo technicznego( jak typ danych) a raczej biznesowy (wyznaczony przez ekspertów z danej dziedziny).
  3. Monitoring oraz wykonywanie logów. O ile nie zdecydujemy się na dodatkową warstwę usług to wszelkie monitorowanie danych powinniśmy zawrzeć właśnie w BL. Temat jest dość szeroki i samo monitorowe też nie jest takie proste – istnieje dużo technologii wspomagających ten proces. Postaram się w przyszłości o tym napisać.
  4. Buforowanie danych – sytuacja podobna, jeśli nie używamy warstwy usług to wszelkie buforowanie mające na celu przyśpieszyć obsługiwanie zgłoszeń należy zawrzeć w BL. Oczywiście buforowanie danych to również zadanie dla warstwy dostępu do danych a nawet warstwy prezentacji.
  5. Obsługa błędów – w prostym przypadku użyjemy standardowego bloku try-catch. Jednak istotniejsze jest co zrobimy z tymi danymi. W niektórych systemach wszelkie krytyczne błędy powinny być wysyłane bezpośrednio do administratorów( np. za pomocą e-mail lub wiadomości SMS). Wszystko oczywiście zależy od postawionych wymagań oraz QoS ( quality of service).

 

Wiemy, już co warstwa powinna wykonywać. Kolejna pytanie brzmi, jak to ma wykonywać? Przede wszystkim kod BL powinien być maksymalnie elastyczny i abstrahować od źródła danych. Początkujący projektanci często popełniają następujące błędy:

  1. Wywołują metody z warstwy prezentacji. Pamiętajmy, że żadna metoda w BL nie może w jakimkolwiek parametrze zawierać referencji do kodu związanego z warstwą prezentacji (w skrócie PL – presentation layer). To PL jest odpowiedzialne za przesłanie niezbędnych danych do BL a BL nie może odczytywać tych wartości samodzielnie z interfejsu użytkownika. Nawet dla najmniejszych aplikacji pamiętajcie aby umieścić BL w osobnym dll i pousuwać wszelkie referencję do warstwy prezentacji(choćby do biblioteki System.Windows.Controls).
  2. Warstwa biznesowa nie formatuje danych. Jeśli BL zwraca cenę produktu to nie w formie uwzględniającej typ oraz zapis waluty – to jest już zadanie warstwy prezentacji. Podobnie należy sobie zdać sprawę, że BL nie jest odpowiedzialna za mechanizm globalizacji – wszelkie informacje muszą być przesyłane w sposób neutralny. Interpretacją oraz wizualizacją ich zajmuje się warstwa prezentacji.
  3. Odczyt wartości z bazy danych bezpośrednio w BL. Wszelkie obliczenia w BL powinny abstrahować od typu i położenia źródła danych. Zatem jeśli potrzebujemy przetworzyć jakieś zamówienie powinniśmy operować na zwykłych klasach a nie pobierać dane bezpośrednio z bazy danych. W przypadku prostych wzorców projektowych takich jak skrypt transakcji należy skorzystać z jakiś bramek – ale to temat na następny post.

Dobre odizolowanie BL daje nam możliwość wyeksponowania kodu za pomocą np. WCF czy zwykłego web service. Należy podkreślić, że jest to właśnie ta warstwa, która odpowiada za realizację rzeczywistego problemu postawionego aplikacji. Pozostałe warstwy zależą głównie od wykorzystywanej technologii. Z kolei dobrze zaprojektowana BL powinna być maksymalnie niezależna. Dobry zwyczajem jest aby każda klasa w BL była typu POCO – ale to również temat na osobny post.

Dlaczego warto zainteresować się trójwarstwowym modelem aplikacji?

Postanowiłem, że zanim przejdę do omawiania kolejnych  warstw systemu, wyjaśnię bardziej szczegółowo po co wprowadzono trójwarstwowy model aplikacji wspomniany w poprzednim poście. Otóż dzięki separacji kodu na warstwy nasza architektura stanie się elastyczniejsza. Model umożliwi nam m.in.:

  1. Przenośność. Kolejne warstwy będą mogły być rozmieszczane na różnych platformach sprzętowych. W każde chwili będziemy mogli np. przenieść warstwę biznesową na zewnętrzny serwer, bez konieczności modyfikowania kodu, Dzięki niezależności warstwy prezentacji od warstwy biznesowej stanie się to bardzo proste w realizacji.
  2. Redukcja zbędnych powiązań między klasami – model definiuje, że warstwa może odwoływać się wyłącznie do warstw znajdujących się bezpośrednio pod nią. Warstwa prezentacji może wywoływać wyłącznie warstwę biznesową(lub usług w przypadku modelu rozszerzonego), warstwa biznesowa z kolei powinna odwoływać się tylko do warstwy dostępu do danych. Sytuacja, w której warstwa biznesowa posiada referencję do warstwy prezentacji jest niedopuszczalna.
  3. Skalowalność – dzięki rozdzieleniu kodu łatwo można zaimplementować np. mechanizm load balancing(równoważenie obciążenia).
  4. Łatwiejsze testy integracyjne. O metodykach testów integracyjnych w szczegółach napiszę innym razem. Ogólnie chodzi o przetestowanie połączeń między modułami. W przypadku gdy mamy podzielony system na warstwy, biblioteki łatwiej jest nam zdefiniować te połączenia. Ponadto każdą warstwę można zastępować tzw. obiektem mock  (obiekt naśladujący prawdziwy obiekt, ale tak naprawdę nie zawierający żadnej logiki – zwraca na sztywno określone wyniki).

Kolejną istotną sprawą w modelu trójwarstwowym jest zdefiniowanie połączeń między warstwami czyli określenie w jaki sposób warstwy będą komunikować się ze sobą. W najprostszym przypadku będzie to po prostu lokalne wywołanie klas. W systemach enterprise, często moduły są rozproszone i istnieje wiele sposób komunikacji. O to schemat rozszerzony o kilka możliwych protokołów komunikacji:

image

TDS to protokół komunikacji z bazą danych (Tabular Data Stream). Zdefiniowanie protokołów zależy już od konkretnej aplikacji. Powyższy rysunek to tylko prosty przykład. Ważniejszy jednak jest kierunek strzałek ponieważ określa on referencję między warstwami – warstwa może wywoływać kod wyłącznie warstwy znajdującej się bezpośrednio pod nią.

W następnym poście zamierzam zacząć opisywanie warstwy biznesowej. Prawdopodobnie będzie to wstęp teoretyczny do tej warstwy oraz pierwszy wzorzec projektowy – skrypt transakcji. Zapraszam!

Czym jest oprogramowanie typu enterprise? Trójwarstwowy model aplikacji

Dziś przyszedł czas na poruszenie tematu architektury aplikacji typu enterprise. Planuje napisać cykl postów m.in. o różnych wzorcach projektowych wykorzystywanych do budowy kolejnych warstw systemu. Zacznę od totalnych podstaw, które mają na celu wyjaśnienie z czego tak naprawdę powinna się składać solidna aplikacja. Przedstawię również kilka prostych zasad inżynierii oprogramowania mających na celu usprawnienie pisania elastycznego kodu.

Zacznijmy od określenia czym jest aplikacja enterprise. Według niektórych definicji jest to oprogramowanie rozwiązujące problem biznesowy (ERM, CRM itp). Definicja według mnie jest  zbyt  ogólna i niewyjaśniająca znaczenia z punktu technicznego. Dla architekta \ programisty oprogramowanie enterprise powinno być kojarzone z następującymi cechami:

  1. Skalowalność – system powinien być przygotowany na wzrost liczby użytkowników zarówno pod względem architektonicznym(rozszerzalność kodu) jak i wydajnościowym(np. load balancing),
  2. Bezpieczeństwo,
  3. Modularność,
  4. Możliwość dostarczania modułów przez różnych dostawców(inna firma piszę CRM a jeszcze inna system sprzedaży),
  5. Jawne oddzielenie logiki od spraw technicznych(interfejs nie zna specyfiki rozwiązywanego problemu ani źródła danych),
  6. Wysoce skomplikowane systemy – implementowane i wdrążane przez lata,
  7. Systemy rozproszone.

Z pewnością do powyższych cech można jeszcze dodać wiele elementów. Uogólniając aplikacjami klasy enterprise są programy solidne, elastyczne i otwarte na wszelkie modyfikacje.

Następne pytanie brzmi, jak zaprojektować taką aplikację? Część odpowiedzi będzie znajdować sie w kolejnych postach. Popatrzmy jednak w tej chwili na wskazany problem bardziej ogólnie. Zdefiniujmy warstwy(części) na które składa się każdy system enterprise. W podejściu Rapid Application Development(RAD), promowanym przez nowoczesne środowiska programistyczne sprawa jest prosta. Pisząc prostą aplikacje np. w Visual Studio tworzymy nowe okno i dodajemy kod obsługi bezpośrednio w pliku źródłowym okna. W poważniejszych aplikacjach niezbędne okazuje się oddzielenie od siebie przynajmniej trzech warstw kodu:

  1. Warstwy prezentacji,
  2. Warstwy biznesowej,
  3. Warstwy dostępu do danych.

Dokładny opis warstw znajdzie się w przyszłych postach – w tej chwili wyjaśnię tylko ogólnikowo. Warstwa prezentacji to wszystko to co widzimy  czyli interfejs użytkownika(GUI). Warstwa biznesowa jest rdzeniem aplikacji i odpowiada za rozwiązywanie problemów dla których została stworzona aplikacja. Czasami rozdziela się warstwę biznesową dodatkowo na warstwę usług aby uniezależnić kwestie technologiczne od implementacji tej warstwy . Warstwa dostępu do danych odpowiada za fizyczny zapis danych do dowolnej bazy danych. Poniżej przedstawiam warstwy wraz z kilkoma wzorcami projektowymi które można wykorzystać:

image

Spora część wzorców będzie omawiana na bieżąco w przyszłych postach zatem zachęcam do czytania.

Singleton a wielowątkowość

Z racji tego, że w ostatnim czasie sporo pisałem o wielowątkowości w C#, dzisiaj pokaże prawidłową implementacje wzorca projektowego singleton przystosowanego do pracy w środowisku współbieżnym. Na początek przyjrzyjmy się klasycznej implementacji:

public sealed class Singleton
{
    private static Singleton m_Instance = null;
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {
            if(m_Instance == null)
                m_Instance = new Signleton();
            return m_Instance;
        }
    }
}

Wyobraźmy sobie sytuację w której dwa wątki jednocześnie próbują utworzyć instancję klasy. Może zdarzyć się, że wątki jednocześnie wejdą do if’a sprawdzającego m_Instance. W takim przypadku zostaną utworzone dwie instancje Signleton. Jak temu zaradzić?

Wystarczy użyć jednego z mechanizmów synchronizacji opisanych we wcześniejszych postach. Polecam lock – w przypadku synchronizacji wewnątrz AppDomain jest to przeważnie najlepsze rozwiązanie.

public sealed class Singleton
{
    private static Singleton m_Instance = null;
    private static readonly object m_Sync = new object();
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {
            lock(m_Sync)
            {
                if(m_Instance == null)
                    m_Instance = new Signleton();
            }
            return m_Instance;
        }
    }
}

Powyższa implementacja jest poprawna, jednak ma jedną wadę – za każdym razem trzeba zakładać blokadę co w środowisku rozproszonym może spowodować istotny spadek wydajności. Spróbujmy więc zakładać blokadę wyłącznie gdy m_Instance jest równy null (czyli podczas pierwszego wywołania właściwości Instance):

public sealed class Singleton
{
    private static Singleton m_Instance = null;
    private static readonly object m_Sync = new object();
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {
            if(m_Instance == null )
            {
                lock(m_Sync)
                {
                    if(m_Instance == null)
                        m_Instance = new Signleton();
                }
            }
            return m_Instance;
        }
    }
}

Z kodu widzimy, że synchronizujemy kod tylko gdy instancja nie została jeszcze utworzona. Po zainicjowaniu obiektu kod działa już w pełni optymalnie, zwracając po prostu obiekt Singleton.

Jeśli nie zależy nam na tzw. lazy loading(opóźnione ładowanie) możemy stworzyć instancję obiektu w momencie deklaracji m_Instance:

public sealed class Singleton
{
    private static Singleton m_Instance = new Singleton();
    
    private Singleton() { }
    
    public static Singleton Instance
    {
        get
        {            
            return m_Instance;
        }
    }
}

Implementacja jest w pełni optymalna oraz bezpieczna w środowisku współbieżnym. Jedyną wadą jest fakt, że Signleton będzie utworzony(podczas np. dostępu do innych statycznych pól klasy) nawet w przypadku gdy  z niego nie będziemy korzystać w programie. Jeśli wiemy, że za każdym razem podczas działania aplikacji będziemy wywoływać klasę to powyższe rozwiązanie może okazać się bardzo dobre.

Czas na najlepsze moim zdaniem rozwiązanie:

public sealed class Singleton
{   
   private Singleton() { }

   public static Singleton Instance
   {
       get
       {
           return Child.m_Instance;
       }
   }
   public static int TestValue { get; set; }
   class Child
   {
       private Child() { }
       internal static readonly Singleton m_Instance = new Singleton();
   }        
}

Przedstawiona implementacja jest optymalna ponieważ nie zakładamy żadnych blokad. Ponadto jest w pełni “leniwa”- inicjujemy obiekt dopiero gdy chcemy mieć do niego dostęp. Jeśli przypiszemy jakąś wartość właściwości TestValue, obiekt Singleton  nie zostanie utworzony – w przypadku wcześniejszej implementacji jakikolwiek dostęp do statycznych właściwośći powodował inicjalizację Singleton. Rozwiązanie jest oczywiście bezpieczne w środowisku współbieżnym.

Aktualizacja interfejsu z drugiego wątku(windows forms i WPF)

Na różnych forach często użytkownicy mają problem z aktualizacją kontrolek z innego wątku. Załóżmy, że odpaliliśmy sobie BackgroundWorker lub po prostu Thread. Wykonujemy jakieś operację, np. łączymy się ze zdalnymi zasobami. Stworzenie osobnego wątku w takim scenariuszu jest bardzo pożądane ponieważ nie blokujemy wtedy aktualizacji interfejsu. W trakcie pobierania informacji z Internetu chcemy aktualizować interfejs aby informować użytkownika o postępach np.

progressBar.Value = progessValue;

Jeśli powyższy kod jest wywołany z obcego wątku użytkownik dostanie następujący komunikat o błędzie:

Cross-thread operation not valid:
Control accessed from a thread other than the thread it was created on.

Wyjątek jest wyrzucany ponieważ zarówno w WindowsForms jak i w WPF nie można aktualizować interfejsu z innego wątku niż z tego w którym została stworzona kontrolka. Musimy więc w jakiś sposób dostać się do wątku macierzystego dla kontrolki i tam wykonać aktualizacje tej kontrolki. Służy do tego metoda Control.Invoke(windows forms) lub ControlDispatcher.Invoke(Wpf). Chcąc więc zaktualizować ProgressBar w WinForms musimy napisać:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
progressBar.Invoke(updateAction,32);

Powyższy kod będzie działał w każdym przypadku ale ma jedną wadę związaną z wydajnością kodu. Co w przypadku gdy mamy osobną klasę do aktualizacji interfejsu i jest ona wywoływana zarówno z wątku kontrolki jak i z obcego wątku? Dla drugiego przypadku(z obcego wątku) kod jest maksymalnie optymalny. Z kolei w sytuacji gdy wywołujemy ją z wątku macierzystego dla kontrolki nie potrzebnie będzie wykonywana masa operacji związanych z wpompowaniem operacji w kolejkę dla wątku interfejsu. Na szczęście istnieje gotowa metoda, która stwierdza czy dla aktualnego przypadku użycia jest wymagane wywołanie Invoke:

Action<int> updateAction = new Action<int>((value) => progressBar.Value = value);
if (progressBar.InvokeRequired)
    progressBar.Invoke(updateAction,5);
else
    updateAction(4);

Posiadamy już optymalną wersje kodu. Jednak pisanie powyższego kodu za każdym razem gdy chcemy zaktualizować interfejs(nigdy nie wiemy czy kod nie będzie wywoływany w przyszłości z innego wątku) jest co najmniej niewygodne. Z ratunkiem przychodzą nam tzw. rozszerzenia(extensions,c# 3.0). Możemy przecież stworzyć własną metodę nazwaną powiedzmy InvokeIfRequired:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (control.InvokeRequired)
           control.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (control.InvokeRequired)
           control.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Za pomocą takiego rozwiązania aktualizacja kontrolki sprowadzi się wyłącznie do poniższego kodu:

this.InvokeIfRequired((value) => progressBar.Value = value, 10);

W przypadku Wpf rozwiązanie jest bardzo podobne:

public static class ControlExtensions
{
   public static void InvokeIfRequired(this Control control, Action action)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action);
       else
           action();
   }
   public static void InvokeIfRequired<T>(this Control control, Action<T> action, T parameter)
   {
       if (System.Threading.Thread.CurrentThread!=control.Dispatcher.Thread)
           control.Dispatcher.Invoke(action, parameter);
       else
           action(parameter);
   }
}

Z kolei użycie metody jest identyczne jak w przypadku WinForms:

this.InvokeIfRequired((value) => bar.Value = value, 10);