W poprzednich postach przedstawiłem wzorce projektowe warstwy biznesowej: skrypt transakcji (transaction script), moduł tabeli (table module), aktywny rekord (active record) oraz model domeny (domain model). Napisałem, że dwa ostatnie wzorce posiadają bardzo rozdrobniony interfejs i nie nadają się bezpośrednio do użycia w rozproszonej aplikacji. Dla przypomnienia, AR oraz DM polegają na stworzeniu klasy dla każdej (lub prawie każdej w przypadku DM) tabeli w bazie danych. Przykładowo system sprzedaży posiada obiekty biznesowe takie jak np. Product, Order, ProductParameter, Client czy Invoice.
Załóżmy, że chcemy stworzyć system rozproszony czyli taki, który pracuje na kilku komputerach. Sprzedawcy będą korzystać z aplikacji mobilnej (składanie zamówień, przeglądanie produktów), a centrala będzie znajdowała się na jakimś zdalnym serwerze. Musi zatem istnieć sposób na przekazywanie poleceń z aplikacji mobilnej do centrali. W tym celu centrala będzie eksponować warstwę biznesową za pomocą usługi sieciowej np. WCF:
Wszelkie obliczenia oraz przetwarzanie danych będzie odbywało się na serwerze głównym. Aplikacja mobilna po prostu wysyła żądania – nie zawiera żadnej zaawansowanej logiki. Gdybyśmy chcieli wyeksponować wcześniej wspomniane obiekty biznesowe (Product, Order, Client itp), klient (aplikacja mobilna) otrzymałaby bardzo niewygodny interfejs. Klient nie powinien zagłębiać się w szczegóły techniczne. Kolejną wadą takiego podejścia jest fakt, że obiekty biznesowe zawierają przeważnie bardzo krótkie, atomowe operacje. My jako klient chcielibyśmy jednak aby metody implementowały całe przypadki użycia a nie tylko pojedyncze kroki. Z pomocą oczywiście przychodzi warstwa usług…
Warstwa usług mieści się w architekturze między warstwą biznesową a warstwą prezentacji. Może być zaimplementowana za pomocą wzorca “zdalna fasada”. Innymi słowy, jest to po prostu klasa, która łączy funkcjonalność zawartą w obiektach biznesowych.Zamiast kilku klas (Product, Order, Client), warstwa usług zawiera np. tylko jedną, eksponującą zbiór funkcjonalności:
public class SalesSystemService:ISalesSystemContract
{
public void AddOrder(OrderDto orderDto)
{
//...
}
public void ApproveOrder(Guid id)
{
//...
}
public void DisapproveOrder(Guid id)
{
//...
}
public void UpdateOrder(OrderDto orderDto)
{
//...
}
public void SetOrderFeedback(FeedbackDto feedbackDto)
{
//...
}
public void AddClient(ClientDto clientDto)
{
//...
}
public void UpdateClient(ClientDto clientDto)
{
//...
}
public void RemoveClient(Guid id)
{
//...
}
public void AddProduct(ProductDto productDto)
{
//...
}
public void UpdateProduct(ProductDto productDto)
{
//...
}
}
Klasa powinna udostępniać metody, które realizują najczęstsze przypadki użycia. Ze względu, że warstwę usług projektuje się pod kątem zdalnego wywoływania, powinna być tak napisana, aby klient mógł wykonać swoje operacje z jak najmniejszą liczbą wywołań.
W aplikacji mobilnej programista zatem dodaje referencje wyłącznie do warstwy usług. Klient nie powinien mieć dostępu do sposobu wykonywania wszelkich operacji (m.in dlatego, że jest to niebezpieczne). Aplikacja mobilna wie co i gdzie może wykonać ale nie ma wiedzy o tym jak to dokładnie jest przetwarzane. Ponadto ze względu, że klient nie ma referencji do obiektów biznesowych (Product, Client, ParameterEntry) musi istnieć jakiś zamiennik. Tym zamiennikiem jest tzw. obiekt DTO (Data Transfer Object).
DTO to obiekty wspierające serializację (muszą być zdolne do przekazywania przez sieć) oraz zawierające wyłącznie właściwości. Stanowią czyste kontenery na dane. Przykładowo obiekt biznesowy Order mógłby wyglądać następująco:
public class Order
{
public void Approve()
{
// logika biznesowa
}
public void Disapprove()
{
// logika biznesowa
}
public void Submit()
{
// logika biznesowa
}
public Guid Id{get;set;}
public DateTime Date{get;set;}
public IList<OrderItem> OrderItems{get;set;}
}
Z kolei DTO dla tego obiektu biznesowego, mógłby wyglądać:
public class OrderDto
{
public Guid Id{get;set;}
public DateTime Date{get;set;}
public IList<OrderItemDto> OrderItems{get;set;}
}
Obiekt DTO jest pozbawiony logiki ponieważ służy wyłącznie do przesłania danych do warstwy usług. Warstwa usług po otrzymaniu takiego pakietu, mapuje go na obiekt biznesowy (OrderDto –> Order) a następnie wykonuje wszelką logikę biznesowa poprzez wywołanie odpowiednich metod na obiekcie biznesowym. Opcjonalnie warstwa usług może zwrócić wynik klientowi, również w postaci jakiegoś obiektu DTO.
Warto podkreślić, że nie musi występować relacja jeden do jednego między obiektami biznesowymi a DTO. Przeważnie DTO zawierają mniej informacji (właściwości) niż obiekty biznesowe. DTO może różnić się w zależności od metody warstwy usług. Projektując te obiekty powinniśmy zawrzeć w nich jak najmniejsza liczbę informacji wymaganą do przeprowadzenia operacji.
Na początku wspomniałem o systemie rozproszonym. Czy zatem dla aplikacji pracujących wyłącznie na pojedynczym komputerze powinniśmy kłopotać się z implementacją warstwy usług? Odpowiedź brzmi: przeważnie tak :). Dla małych systemów jest to oczywiście zbędne. Jednak w przypadku średnich nigdy nie wiemy czy za parę miesięcy nie będzie potrzeby trybu pracy rozproszonej. Nie musi być to od razu praca przez Internet ale np. przez LAN. W przypadku systemów sprzedaży, często kilka komputerów pracuje w sieci w ramach tego samego sklepu (kilka kas). Warstwa usług daje możliwość skalowalności , od pojedynczego komputera, pracy w LAN po systemy działające w całkowicie różnych miejscach geograficznych.
Pozostało jeszcze wyjaśnić jak wykonywać mapowanie między DTO a obiektem biznesowym. Tym zajmiemy się w następnym poście – będzie to jeden wzorzec projektowy oraz pewna biblioteka wykonująca większość pracy za nas ;).