Asynchroniczne strony ASP .NET

W celu wyjaśnienia zasady działania asynchronicznych stron, najpierw przyjrzyjmy się jak wygląda standardowe zapytanie do serwera. Klient wysyła żądanie HTTP do serwera np. typu GET w celu uzyskania danej strony www. Następnie serwer używa tzw. puli wątków (thread pool). Po prostu przydziela wątek z puli każdemu nadchodzącemu żądaniu. Tworzenie (a raczej odtwarzanie) wątków z puli jest szybkie (o tym już pisałem kiedyś), jednak liczba wątków jest ograniczona. W przypadku gdy serwer będzie musiał obsłużyć zbyt dużą liczbę żądań, zakończy się to błędem “503 – server unavaiable”. Obsługa większości zapytań nie stanowi problemu, jednak gdy aplikacja ASP .NET korzysta z usługi sieciowej lub przetwarza ogromną ilość danych może to stanowić poważne zredukowanie skalowalności.Wyobraźmy sobie, że nasza aplikacja www korzysta z usługi sieciowej. Ponadto połączenie z usługa jest dość wolne – trwa 30 sekund. Gdy przyjdzie zapytanie do serwera, zostanie zdjęty wątek z puli i będzie on zarezerwowany przez ponad 30 sekund (czas połączenia z usługą + inne operacje). Przez ten cały czas wątek nie będzie mógł być wykorzystany do obsługi innych żądań. Co gorsza, wątek większość czasu straci po prostu na oczekiwanie odpowiedzi od usługi sieciowej. Należy podkreślić, że wątki ASP .NET są zbyt cenne aby marnować je na operacje typu IO. Jeśli w systemie rzeczywiście musimy połączyć się z zewnętrznymi zasobami, zróbmy to asynchronicznie!

W przypadku asynchronicznych stron po odebraniu żądania również jest zdejmowany wątek z puli. Jednak zaraz po stwierdzeniu, że mamy do czynienia z asynchroniczną stroną www, wątek jest z powrotem oddawany puli. W tym momencie rozpoczyna się wykonywanie asynchronicznego kodu – czyli np. ładowania danych z usługi sieciowej czy z bazy danych. Po zakończeniu, wątek z powrotem jest zdejmowany z puli i rozpoczyna się już normalne przetwarzanie strony www. Innymi słowy, używamy asynchronicznego przetwarzania dla bardzo czasochłonnych operacji (przeważnie są to operacje typu IO). Na poniższym rysunku przedstawiam standardowy przebieg obsługi żądania w stronach synchronicznych:

 

 

image

 

 

Nie ma tu nic nadzwyczajnego. W przypadku asynchronicznych stron, wątek z puli będzie zwolniony w pewnym momencie i sterowanie zostanie przekazane odpowiednim asynchronicznym metodom:

 

 

image

Przejdźmy teraz do implementacji tego w ASP .NET. W wersji 2.0 zostało to znacząco uproszczone. Przede wszystkim należy ustawić atrybut Async na true w dyrektywie @Page:

<%@ Page Async="true" Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebApplication1._Default" %>

Ustawienie atrybutu na true tak naprawdę oznacza, że zostanie wykorzystany  IHttpAsyncHandler.

Aby rozpocząć część asynchroniczną strony należy wywołać metodę AddOnPreRenderCompleteAsync przekazując odpowiednie handlery:

protected void Page_Load(object sender, EventArgs e)
{
  AddOnPreRenderCompleteAsync(new BeginEventHandler(BeginAsyncOperation), new EndEventHandler(EndAsyncOperation));
}
private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
AsyncCallback cb, object state)
{
  m_Request = WebRequest.Create("http://www.pzielinski.com");
  return m_Request.BeginGetResponse(cb, state);
}
private void EndAsyncOperation(IAsyncResult ar)
{
  string text;
  using (WebResponse response = this.m_Request.EndGetResponse(ar))
  {
      using (StreamReader reader =
          new StreamReader(response.GetResponseStream()))
      {
          text = reader.ReadToEnd();
      }
  }
   Response.Write(text);
}

W podobny sposób można pobrać dane z bazy danych i przekazać je odpowiedniej kontrolce renderującej (np. ListView).

Ponadto w ASP .NET można definiować tzw. zadania asynchroniczne. Sprawa wygląda podobnie jak w przypadku powyższego kodu. Zadania asynchroniczne wprowadzają dodatkowo kilka rozszerzeń:

  1. Oprócz metod BEGIN i END można zdefiniować metodę odpowiedzialną za wykonanie kodu w razie timeout. Wartość timeout można zdefiniować również w dyrektywie @Page za pomocą atrybutu AsyncTimeout.
  2. Można rejestrować kilka zadań  – dopiero po zakończeniu wszystkich, zostanie wznowione przetwarzanie synchroniczne.
  3. Rejestrując zadanie można dodatkowo przekazać jakiś argument jako object (wykorzystywany w metodach BEGIN) .

Przykład:

protected void Page_Load(object sender, EventArgs e)
{
  RegisterAsyncTask(new PageAsyncTask(BeginAsyncOperation, EndAsyncOperation, TimeoutOperation, "state"));
}
private IAsyncResult BeginAsyncOperation(object sender, EventArgs e,
AsyncCallback cb, object state)
{
  m_Request = WebRequest.Create("http://www.pzielinski.com");
  return m_Request.BeginGetResponse(cb, state);
}
private void EndAsyncOperation(IAsyncResult ar)
{
  string text;
  using (WebResponse response = this.m_Request.EndGetResponse(ar))
  {
      using (StreamReader reader =
          new StreamReader(response.GetResponseStream()))
      {
          text = reader.ReadToEnd();
      }
  }
  Response.Write(text);
}
private void TimeoutOperation(IAsyncResult ar)
{

}

Jak tworzyć instancję repozytorium?

W poprzednim poście opisałem wzorzec repozytorium. Jak już wspomniałem, repozytorium może różnić się  implementacją w zależności od encji. W skrajnych przypadkach może być jedno repozytorium na jedną encję (Order – OrderRepository, Product – ProductRepository itp). Tworzenie każdego repozytorium ręcznie w zależności od przetwarzanej encji jest dość niewygodne. Przydałby się jakiś globalny mechanizm na tworzenie obiektów na podstawie typu encji.

Dobrym rozwiązaniem problemu jest implementacja wzorca service locator, wywodzącego się  z wzorca factory. Ogólnie mówiąc, factory jest to klasa z metodą Create odpowiedzialną za dynamiczną inicjalizację. Spójrzmy na diagram klas wzorca:

image

Jeśli diagram nie jeszcze jasny, przedstawiam dodatkowo szkic kodu:

public interface IWindowFactory
{
   IWindow Create();
}
public class WindowsXpWindowFactory : IWindowFactory
{
   public IWindow Create()
   {
       return new WindowXp();
   }
}

IWindow to interfejs okna. WindowXp, Window98 to okna stylizowane na konkretny system operacyjny. W rozważanym przykładzie, celem jest stworzenie klas, które umożliwiają nam podpinanie różnych skórek czy też kontrolek stylizowanych np. na system operacyjny. W standardowym podejściu, użytkownik w celu stworzenia okna, wykonuje po prostu:

WindowXp window = new WindowXp();

Podejście ma zasadniczą wadę – co w przypadku gdy chcemy zmieniać skórki w czasie działania programu? Za pomocą wzorca factory, programista tworzyłby okna w następujący sposób:

IWindow window = windowFactory.Create();

W zależności od wstrzykniętego factory, okno stylizowane na jakiś system zostanie utworzone.

Można oczywiście stworzyć więcej klas factory. Przykładowo,  jeśli piszemy testy jednostkowe można stworzyć klasę WindowStubFactory, która będzie zwracać stuby\mocki zamiast rzeczywistych obiektów. Ponadto obiekty tworzone (tutaj okna) mogą posiadać wyłącznie konstruktor z modyfikatorem internal – dzięki temu użytkownik biblioteki będzie zmuszony do wykorzystania factory aby stworzyć dany obiekt. Jeśli ktoś jest zainteresowany dokładniejszym opisem wzorca, polecam książkę “Design Patterns, Elements of Reusable Object-Oriented Software”.

Service locator tak naprawdę nie różni się znacząco od factory. Jak sama nazwa wskazuje, zadaniem wzorca jest lokalizacja jakiś usług. W naszym przypadku będą lokalizowane repozytoria na podstawie typu encji:

public class RepositoryLocator
{
   private Dictionary<Type, IRepository> m_Repositories = null;
   public RepositoryLocator()
   {
       m_Repositories = new Dictionary<Type, IRepository>();
       m_Repositories.Add(typeof(Order),new OrderRepository());
       m_Repositories.Add(typeof(Product),new ProductRepository());
   }
   public IRepository GetForBusinessObject<T>(): where T : BusinessObject
   {
       return GetForBusinessObject(typeof(T));
   }
   public IRepository GetForBusinessObject(Type type)
   {
       if (m_Repositories.ContainsKey(type))
           return m_Repositories[type];
       else
           return null;
   }
}

Użytkownik chcąc stworzyć repozytorium dla danego obiektu biznesowego wywołuje:

RepositoryLocator locator = new RepositoryLocator();
IRepository orderRepository=locator.GetForBusinessObject<Order>();
IRepository productRepository=locator.GetForBusinessObject<Product>();

Wzorzec repozytorium (repository pattern)

Implementując warstwę biznesową za pomocą DomainModel lub ActiveRecord uzyskujemy bardzo rozdrobniony interfejs. Ponadto stworzone obiekty biznesowe zawierają logikę odnoszącą się tylko do konkretnej encji. W poprzednich postach pokazałem prostą klasę Client, implementującą wzorzec DomainModel. Przedstawiona klasa mogła wykonywać tylko operacje dla konkretnego, jednego klienta. W systemie jednak często będziemy musieli brać pod uwagę zbiór encji. Przykładowo może być potrzeba zwrócenia zbioru klientów, dla których suma dokonanych zamówień przekracza jakąś wartość. Ponadto analizując wzorce DM oraz AR łatwo zauważyć, że nie ma tam metod wstawiania nowych encji do bazy danych. Rozwiązaniem na przedstawione problemy jest wzorzec repozytorium. Zawiera on przede wszystkim operacje CRUD (Create, Read, Update, Deleted) oraz wszystkie inne operacje selekcji (np. GetById lub GetByQuery ). Przeważnie definiuje się jedno repozytorium na jeden obiekt biznesowy. Bardzo ważną cechą repozytorium jest niezależność od bazy danych. Użytkownik powinien korzystać z poprawnie zaimplementowanego repozytorium jak ze zwyklej klasy czy kolekcji danych. Dlatego dobrym zwyczajem jest stworzenie najpierw interfejsu a dopiero potem konkretnej klasy (implementacji):

public interface IClientRepository
{    
    Client GetById(Guid id);   
    Client GetByQuery(Query query);
    void Add(Client client);
    void Remove(Client client);
    void Update(Client client);    
        
    /*Inne specyficzne metody dla obiektu "Client".
      Może być to np. wyszukiwanie klientów po sumie zamówien,
      regionie pochodzenia, kupowanych towarach itp.
    */ 

}

Warto zauważyć, że większość metod w każdym repozytorium będzie się powtarzać. Operacje typu CRUD będzie zawierać zarówno repozytorium Client jak i każde inne. Z tego względu lepszym rozwiązaniem jest wprowadzenie generycznego interfejsu bazowego:

public interface IRepository<T>
{
    T GetById(Guid id);   
    T GetByQuery(Query query);
    void Add(T item);
    void Remove(T item);
    void Update(T item);    
}

Jeśli jakieś repozytorium musi zawierać metody niestandardowe to po prostu rozszerza interfejs:

public interface IOrderRepository:IRepository<Order>
{
    Order[] GetOrdersByClientId(Guid idClient);
}

Dobry zwyczajem jest również niezależność konkretnych implementacji repozytorium od bazy danych np:

public class OrderRepository:IOrderRepository)
{
    private IDataContext m_DataContext=null;
    public OrdeRepository()
    {
       // inicjalizacja m_DataContext
    }
    public void Add(Order item)
    {
       m_DataContext.Add(item);
    }
    ...
}

W powyższym przykładzie operacje związane z fizycznym zapisem w bazie danych przekazywane są do klasy z warstwy DAL. Ponadto konkretna implementacja IDataContext może być wstrzykiwana nawet w trakcie działania programu. W końcu jeśli chcemy zawrzeć w repozytorium logikę biznesową, nie ma mowy o jakiejkolwiek zależności klasy z bazą danych – repozytorium jest abstrakcyjne w przeciwieństwie do podobnych rozwiązań z DAL.

Wyobraźmy sobie, że założenia projekty wymagały implementacji repozytorium dla każdego obiektu biznesowego. Powstaje więc pytanie, w jaki sposób wygodnie tworzyć instancję repozytorium dla konkretnego obiektu biznesowego? Nie chcemy w kodzie operować wyłącznie na konkretnych klasach tzn:

OrderRepository orderRepository = new OrderRepository();
ProductRepository productRepository = new ProductRepository();
ClientRepository orderRepository = new ClientRepository();

Lepszym rozwiązaniem jest wykorzystywanie ogólnych interfejsów (np. IRepository). W następnym poście postaram się odpowiedzieć na te pytanie przedstawiając kilka dodatkowych wzorców projektowych (m.in factory, service locator).

Wysyłanie plików na serwer w aplikacji Silverlight

Silverlight jest technologią odpowiedzialną za tworzenie warstwy prezentacji po stronie klienta. Wszelki kod wykonywany jest w przeglądarce zatem nie ma bezpośredniego dostępu do serwera www. Czasami jednak aplikacja musi wysyłać do serwera jakieś pliki np. graficzne (w przypadku modułu galerii zdjęć). Niestety ze względu na opisaną lokalność działania, Silverlight nie wspiera tego. Jedną z metod obejścia jest implementacja własnego HTTP handlera. Zacznijmy więc od stworzenia aplikacji Silverlight hostowanej za pomocą ASP .NET. Następnie dodajemy HTTP handler ( Add –> New Item –> Web –>Generic Handler), który będzie odpowiedzialny za zapis pliku na dysku.  Silverlight będzie wysyłał żądania zapisu pliku na serwerze właśnie   do tego handlera. Kod handler’a nie jest zbyt skomplikowany:

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class FileReceiver : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        string filename = context.Request.QueryString["filename"].ToString();
        string destFileName = "docelowa sciezka";
        using (FileStream fs = File.Create(destFileName))
        {
            SaveFile(context.Request.InputStream, fs, destFileName);
        }            
    }        
    private void SaveFile(Stream stream, FileStream fs)
    {
        byte[] buffer = new byte[4096];
        int bytesRead;
        while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) != 0)
        {
            fs.Write(buffer, 0, bytesRead);
        }
        fs.Close();                              
    }      
    public bool IsReusable
    {
        get
        {
            return false;
        }
    }
}

Jak widać handler po prostu odczytuje nazwę pliku zapisanego w parametrze filename (QueryString) a następnie zapisuje otrzymane dane na dysku. Jeśli użyliśmy szablonu Generic Handler już nic więcej po stronie serwera nie musimy dodawać ani konfigurować. Przejdźmy teraz do aplikacji klienckiej (Silverlight’a). W celu wysłania pliku będziemy wykorzystywać klasę WebClient:

Stream data = fileInfo.OpenRead();   
string address="http://localhosty:1354/FileReceiver.ashx?filename=nazwa_pliku";  
WebClient c = new WebClient();
c.OpenWriteCompleted += (sender, e) =>
  {      
      PushData(data, e.Result);
      e.Result.Close();
      data.Close();
  };
  
c.OpenWriteAsync(address);

gdzie PushData to:

private void PushData(Stream input, Stream output)
{
  byte[] buffer = new byte[4096];
  int bytesRead;

  while ((bytesRead = input.Read(buffer, 0, buffer.Length)) != 0)
  {
      output.Write(buffer, 0, bytesRead);
  }
}

Oczywiście na koniec należy dodać jeszcze jakiś TextBox oraz Button, który będzie wywoływał OpenFileDialog (wybieranie pliku).

Silverlight – lokalizacja aplikacji

Nowoczesne aplikacje web’owe często wymagają obsługi wielu języków. Silverlight podobnie jak ASP .NET wspiera mechanizm globalizacji za pomocą zasobów (resources files). Pliki zasobów są tak naprawdę zwykłymi plikami XML. Stanowi to ogromną zaletę – za pomocą zewnętrznego narzędzia odpowiednie osoby  mogą przygotować tłumaczenie interfejsu.

Najpierw należy stworzyć główny plik zasobów. Zawiera on tłumaczenia dla domyślnego języka. W przypadku gdy użytkownik pochodzi z kraju, którego język nie jest obsługiwany przez naszą aplikację to właśnie domyślne zasoby będą ładowane. Tworzymy więc plik zasobów za pomocą Add –> New Item –>Resources File. Nazwę pliku pozostawiamy domyślną:

image

Naszym domyślnym językiem będzie angielski. Dopisujemy więc przykładowy zasób o nazwie HELLO_WORLD_LABEL z wartością “Hello world!”:

image

Tworzymy drugi zasób odpowiedzialny za język polski. Jako nazwę podajemy “Resource1.pl-PL.resx”. Tym razem nazwa jest bardzo istotna ponieważ po niej właśnie ładowane są pliki w zależności od ustawień regionalnych użytkownika.

image

Podobnie jak w przypadku zasobu domyślnego, tworzymy wpis “HELLO_WORLD_LABEL” z przetłumaczoną zawartością (“Witaj świecie!”).

image

W tej chwili mamy już przygotowane zasoby. Przyszedł czas na wykorzystanie ich w programie. W pliku SL (MainPage.xaml) tworzymy pojedyncza kontrolkę Label:

<UserControl xmlns:dataInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.Input"  x:Class="SilverlightApplication1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">

    <Grid x:Name="LayoutRoot">
        <dataInput:Label x:Name="m_Label"></dataInput:Label>
    </Grid>
</UserControl>

Wykorzystanie zasobów za pomocą kodu jest bardzo proste. Wystarczy użyć automatycznie wygenerowaną klasę Resources1. Odpowiednie przypisanie można wykonać w konstruktorze klasy UserControl (MainPage.xaml):

public MainPage()
{
    InitializeComponent();
    m_Label.Content = Resource1.HELLO_WORLD_LABEL;
}

Na dzień dzisiejszy niestety trzeba dodatkowo ręcznie wyedytować plik projektu Silverlight. Otwieramy zatem SilverlightApplication1.csproj i znajdujemy element SupportedCultures. Wewnątrz tego elementu dopisujemy pl-PL:

<SupportedCultures>
      pl-PL
</SupportedCultures>

Ostatnią rzeczą, którą należy zrobić jest odpowiednie skonfigurowanie obiektu Silverlight w HTML. Należy przekazać dwa dodatkowe parametry – “culture”

oraz “uiculture”:

<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
      <param name="culture" value="pl-PL" />
      <param name="uiculture" value="pl-PL" />
      
      <param name="source" value="ClientBin/SilverlightApplication1.xap"/>
      <param name="onError" value="onSilverlightError" />
      <param name="background" value="white" />
      <param name="minRuntimeVersion" value="3.0.40818.0" />
      <param name="autoUpgrade" value="true" />
      <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40818.0" style="text-decoration:none">
          <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style:none"/>
      </a>
</object>

Wartość ustawiamy  w zależności od języka, który chcemy wykorzystać (w powyższym przypadku jest to język polski). Wartości odpowiadają zarówno za wygląd interfejsu (tłumaczenia) jak i za formatowanie walut, liczb, dat itp.

Po odpaleniu strony powinniśmy ujrzeć polską wersje aplikacji:

image

W celach testowych zmieńmy język na jakiś nieobsługiwany:

<object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
      <param name="uiculture" value="pt-BR" />
      <param name="culture" value="pt-BR" />

      <param name="source" value="ClientBin/SilverlightApplication1.xap"/>
      <param name="onError" value="onSilverlightError" />
      <param name="background" value="white" />
      <param name="minRuntimeVersion" value="3.0.40818.0" />
      <param name="autoUpgrade" value="true" />
      <a href="http://go.microsoft.com/fwlink/?LinkID=149156&v=3.0.40818.0" style="text-decoration:none">
          <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style:none"/>
      </a>
</object>

Po odpaleniu przeglądarki załaduje się domyślny plik zasobów (Resources1.resx) zawierający w naszym przypadku angielskie tłumaczenie:

image

Wiemy jak już wykorzystać zasoby z poziomu kodu. Interfejs jednak powinno definiować się w pliku XAML. Zasoby można podczepiać w XAMl za pomocą binding’u. Oto przykład:

<UserControl xmlns:dataInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.Input"  x:Class="SilverlightApplication1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    
    xmlns:local="clr-namespace:SilverlightApplication1"
             
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480">
    <UserControl.Resources>
        <local:Resource1 x:Key="ResourceHelper"/>        
    </UserControl.Resources>
  <Grid x:Name="LayoutRoot">
    <dataInput:Label Content="{Binding Path=HELLO_WORLD_LABEL,Source={StaticResource ResourceHelper}}" x:Name="m_Label"></dataInput:Label>        
  </Grid>
</UserControl>

W pliku (MainPage.xaml) nastąpiły 3 zmiany:

  1. Została podpięta przestrzeń nazw w której znajduje się klasa zasobów (Resource1):

    xmlns:local="clr-namespace:SilverlightApplication1"
  2. Aby móc korzystać z zasobu w XAML należy go zadeklarować i nadać klucz:

    <UserControl.Resources>

        <local:Resource1 x:Key="ResourceHelper"/>       
    </UserControl.Resources>

  3. Następnie za pomocą statycznego wiązania wystarczy podpiąć HELLO_WORLD_LABEL:

    <dataInput:Label Content="{Binding Path=HELLO_WORLD_LABEL,Source={StaticResource ResourceHelper}}" x:Name="m_Label"></dataInput:Label>       

Ponadto należy zmienić modyfikatory internal na public w pliku Resources1.Designer.cs. Bez tego binding nie zadziała!

Kompletny kod źródłowy znajduje się tutaj.

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

Warstwa biznesowa – model domeny (domain model pattern)

Przyszedł czas na opisanie najbardziej złożonego wzorca warstwy biznesowej – modelu domeny (DM). Jeśli nie znacie dobrze wzorca opisanego w poprzednich postach (aktywny rekord) nie zaczynajcie nawet próbować zrozumieć DM ponieważ jest on po prostu rozszerzeniem AR.

Na początek kilka faktów. DM jest wzorcem w pełni obiektowym, wykorzystującym wszelkie dobrodziejstwa programowania obiektowego (dziedziczenie, polimorfizm itp.). DM w przeciwieństwie do AR nie jest wzorcem zorientowanym na bazę danych. Scenariusz wykorzystania AR często wyglądał tak, że programista tworzył najpierw bazę danych dla danego problemu biznesowego a dopiero potem obiekty biznesowe AR. W przypadku DM podejście wygląda zupełnie inaczej. Problem biznesowy kompletnie abstrahuje od bazy danych. Programista tworzy najpierw klasy rozwiązujące problem biznesowy a baza danych jest już potem tylko dodatkiem.

Podobnie jak AR, klasy DM przechowują informacje na poziomie wiersza. Przykładową klasą DM może być więc:

public class Client: Person
{
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public abstract int GetDiscount();
}

Sama klasa wygląda tak jak AR. Jednak trzeba być świadomym kilku faktów:

  1. Klasa nic nie wie o docelowej bazie danych. Obiekt takiej klasy po prostu reprezentuje kawałek logiki biznesowej. Kwestia zapisu jest już tylko sprawą techniczną i jak to zostanie zrobione nie należy to do kompetencji DM,
  2. Klasa jest typu POCO. Klasy POCO muszą spełniać kilka warunków m.in nie mogą zawierać atrybutów czy  z góry narzuconych interfejsów zdefiniowanych przez ORM. Innymi słowy jest to po prostu czysta klasa c#,
  3. Klasa Client dziedziczy po Person. Jest to jak najbardziej wskazane w DM,
  4. Zawiera abstrakcyjną metodę – tak jak wspomniałem DM ze względu na pełną niezależność od bazy danych może wykorzystywać pełen zestaw dobrodziejstw programowania obiektowego,
  5. Klasa Client nie musi odpowiadać jednej tabeli w bazie danych. Często pojedyncza klasa DM mapowana jest na wiele tabel. Występuje tu pełna niezależność ze względu na fakt, że zapisem do bazy nie zajmują się już obiekty biznesowe.

Zalety:

  1. Zapobiega duplikacji logiki,
  2. Rozłożenie problemu na obiekty biznesowej powoduje wzrost czytelności kodu,
  3. Niezależność od źródła danych,
  4. Obiekty są typu POCO, zatem mogą korzystać z rozszerzeń OOP,
  5. Znacznie ułatwienia w wykonywaniu testów jednoskokowych ze względu na oddzielenie obiektów odo źródła danych.

Wady:

  1. Bardzo czasochłonna implementacja,
  2. Rozdrobniony interfejs,
  3. Potrzeba wykorzystania dodatkowego wzorca (remote facade) w celu wyeksponowania logiki przez web service.

Nie będę pisać konkretnego kodu ponieważ wartość metod zależy już od budowanego systemu. Zamiast tego uważam, że lepszym wyjściem jest pokazanie diagramu klas. W końcu nie ważne co znajduje się w środku tylko jaka jest relacjami pomiędzy klasami. Poniżej krótki przykład DM w akcji dla systemu sprzedaży:

image

Aktywny rekord i Entity Framework

Dzisiaj krótki post o wsparciu narzędzi ORM (konkretnie EF) da wzorca aktywny rekord. Tak naprawdę to co generuje nam EF jest już aktywnym rekordem. Wystarczy tylko uzupełnić wygenerowane klasy o logikę biznesową ponieważ w przeciwnym wypadku będzie to tylko czysta warstwa dostępu do danych.

Załóżmy, że mamy już wygenerowany jakiś diagram encji EF.Na tą chwile mamy wyłącznie  zaimplementowaną (a raczej wygenerowaną) warstwę dostępu do danych. W celu dodawania właściwej logiki biznesowej, należy stworzyć klasy częściowe dla wygenerowanych encji. Przypuśćmy, że w EF zmalowaliśmy tabelę Orders na encję Order. Aby dodać logikę biznesową powinniśmy napisać:

 

public partial  class Order
{
   public bool Validate()
   {
       // ...         
   }
   public int ComputeTotalDiscount()
   {
       // ...
   }
   public int EstimateDeliveryTime()
   {
       // ...
   }
}

 

Jak widać użycie AR w połączeniu z ORM jest bardzo proste. Nie piszemy żadnego nadmiarowego kodu. Wszelki powtarzalny kod (DAL) wygeneruje za nas EF. W naprawdę dużych projektach takie rozwiązanie jednak ma kilka istotnych wad ale o nich napisze w następnych poście, który zostanie poświęcony Domain Model.

Warstwa biznesowa – aktywny rekord (Active Record pattern)

Dzisiaj przyszedł czas na pierwszy wzorzec obiektowy – aktywny rekord(AR). Zaczynamy od diagramu klas:

 

image

W przeciwieństwie do poprzednich wzorców, każda klasa definiuje fragment logiki biznesowej. Obiekty klas występują w relacji jeden do jednego względem wierszy w tabeli. Klasa Order będzie zawierała zatem logikę dla konkretnego zamówienia a nie dla zbioru zamówień. Klasy tworzymy zwykle dla każdej tabeli z bazy danych a ich instancje jak już wspomniałem dla każdego przetwarzanego wiersza.

Warto podkreślić, że środowisko .NET wspiera wzorzec aktywny rekord poprzez narzędzia ORM – Entity Framework czy LINQ to SQL. Przykładowa klasa warstwy biznesowej, zaimplementowana za pomocą aktywnego rekordu mogłaby wyglądać następująco:

public class Client
{
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public string Email {get;set;}
    // logika biznesowa
    public int GetSubscriptionLevel()
    {
    }
    public int GetMaxCredit()
    {    
    }    
}

Zalety:

  1. Prosta implementacja,
  2. Solidne wsparcie środowisk programistycznych,
  3. Zapobiega duplikacji logiki biznesowej,
  4. Rozdzielenie problemu biznesowego na obiekty zwiększa czytelność kodu.

Wady:

  1. Rozdrobnienie klas (podobnie jak w przypadku TM),
  2. Wymaga wprowadzenia dodatkowej klasy w celu wyeksponowania danych przez usługę sieciową,
  3. Wzorzec zbyt mocno zorientowany na bazę danych,
  4. W przypadku gdy nie korzystamy z gotowych narzędzi ORM, implementacja wzorca jest bardzo skomplikowana i w takiej sytuacji znacznie lepiej zainteresować się wzorcem Domain Model.

Warstwa biznesowa – moduł tabeli (table module pattern)

Moduł tabeli (TM – table module) jest również wzorcem proceduralnym (podobnie jak TS). Rozwiązuje jednak podstawowy problem TS – brak zdefiniowania metody podziału logiki biznesowej na klasy. W przypadku TS sam wzorzec nie określał ile klas należy stworzyć. Wszystko zależało od programisty. Mógł on stworzyć jedną klasę zarządzającą zarówno zamówieniami jak i produktami. Moduł tabeli mówi po prostu, że należy stworzyć osobną klasę dla każdej tabeli w bazie danych. Czyli w przypadku gdy mamy tabele “Orders, Products, Invoices” będziemy potrzebować trzech klas TM. Warto jeszcze raz podkreślić, że wzorce proceduralne zarządzają całymi tabelami a nie pojedynczymi wierszami. Innymi słowy klasy TS lub TM dla zamówienia powinny być w następującej postaci:

public class OrderManager
{
    public void AddOrder(DataRow order)
    {
        // dodaj nowy wiersz do tabeli
    }
    public void RemoveOrder(Guid id)
    {
        // usuń wiersz o podanym ID
    }
    public DataRow GetOrderById(Guid id)
    {
        // znadź dany wiersz
    }    
}

a nie:

public class Order
{
    public DateTime OrderDateTime {get;set}
    public OrderItems[] OrderItems {get;set;}
    public Client Client {get;set;}
}

Pierwsza klasa jak widać zarządza całymi wierszami. Z kolei druga zawiera konkretne informacje dla pojedynczego wiersza.

Diagram klas wygląda bardzo podobnie jak diagram TS:

image

Na zakończenie standardowo przedstawiam zalety i wady, które uważam za istotne:

Zalety:

  1. Prosta implementacja,
  2. Zapobiega duplikacji logiki biznesowej poprzez jawny podział klas,
  3. Podobnie jak w przypadku TS, środowiska programistyczne ułatwiają jego implementacje (np. poprzez klasy DataRow, bezpołączeniowy model dostępu do danych).

Wady:

  1. Wzorzec zorientowany na dane – klasa tworzona na każdą tabele w bazie danych. Może to spowodować zbyt wielkie rozdrobnienie klas,
  2. Ze względu na zbyt duże rozdrobnienie, wzorzec nie nadaje się bezpośrednio do wyeksponowania przez np. usługę sieciową. W przypadku TS często występuje tylko jedna klasa, która może bezpośrednio być wyeksponowana ponieważ stanowi ona tak naprawdę implementację wzorca “remote facade”,
  3. Wzorzec moim zdaniem jest dość niewygodny w użyciu w przypadku zaawansowanych środowisk programistycznych, wspierających narzędzia typu ORM. Dzisiaj dla średnich projektów znacznie łatwiej użyć wzorca aktywny rekord (opiszę go w następnym poście).