Posts tagged DDDSample
LINQ 2 SQL in DDDSample
Feb 24th
Yesterday I committed a preview of LINQ to SQL branch of CQRS version of DDDSample. As you might guess, I replaced NHibernate in reporting subsystem with L2S. Why? Because I wanted to show that reporting is simple in terms of data access and doesn’t need such a sophisticated framework as NHibernate.
I must confess that I am not very proficient in L2S so the implementation might not be optimal. My goal is to just make it working.
For now L2S version is only available through source code download. I encourage all of you to take a look at the implementation. It is quite simple, yet I think it shows the power of CQRS.
If you think about implementing a production system using DDD/CQRS approach, there is one additional think worth noticing. Thanks to the separation of command and query handling you can assign less experienced developers (or ones more focused on UI then on modeling) to query side. They probably can handle implementing it using such a simple tool like LINQ to SQL. In the same time you can assign your senior staff to command processing. This is where business logic is implemented and where good coding and modeling skills bring most value.
CQRS w praktyce
Feb 19th
Dużo piszę ostatnio o CQRS (Command Query Responsibility Segregation), ale nie pokazałem ani razu jak to podejście wygląda w praktyce. Postaram się dziś naprawić to niedopatrzenie. Posłużę się w tym celu projektem DDDSample w najnowszej wersji CQRS.
Układ solution
Tak wygląda układ solution Visual Studio:
Kod podzielony jest na cztery główne obszary:
- Domain — tutaj znajduje się logika biznesowa aplikacji, której zadaniem jest przetwarzanie transakcji (komend). Oprócz centralnego projektu “Domain”, w obszarze tym znajdują się dwa wspomagające. Idąc od góry, pierwszy z nich zawiera obsługę zdarzeń domenowych a’la Udi Dahan, drugi zaś kod dostępu do danych (w oparciu o NHiberate)
- Reporting — ten obszar ma za zadanie obsługiwać zapytania o dane do wyświetlenia na GUI. W razie potrzeby może on także służyć do generowania raportów. Główny projekt (“Reporting”) zawiera definicję struktur danych. Pozostałe to dostęp do danych oraz obsługa komunikatów reprezentujących zdarzenia aktualizacji modelu.
- Infrastructure — tutaj znajduje się wszelki kod nie związany bezpośrednio z problemem, ale w jakiś sposób wspierający jego rozwiązanie, np. projekt “Messages” zawierający komunikaty przesyłane między podsystemem obsługi komend, a podsystemem obsługi zapytań.
- W najwyższym poziomie hierarchii, bezpośrednio pod solution znajdują się dwa projekty scalające: “Application” oraz “UI”. Ten pierwszy stanowi fasadę ukrywającą złożoność całego rozwiązania przed tym drugim. “UI” to zwykły interfejs użytkownika wykorzystujący ASP.NET MVC.
Workflow
Skoro już wiecie, jak z grubsza wygląda rozwiązanie, chciałbym teraz omówić zachowanie systemu podczas standardowej interakcji, która przedstawia się tak:
Na powyższym diagramie MDK oznacza Model Domeny dla obsługi komend (projekt “Domain”), a MDZ to model dla raportowania (projekt “Reporting”).
W pierwszym żądaniu użytkownik systemu chce zobaczyć dane związane z obiektem biznesowym. System ładuje je z prosto z relacyjnej bazy danych MDZ. Drugie żądanie to już modyfikacja danych. Jest ona wykonywana na obiektach Modelu Domeny, które następnie są zapisywane w formie danych relacyjnych. Dokonane zmiany są także (w jakiejś formie) publikowane do MDZ, gdzie służą do aktualizacji struktur danych.
Zobaczmy, jak wygląda realizacja tego schematu w aplikacji DDDSample. Posłużę się przykładem komendy “Zmień docelową lokalizację dla towaru”.
Pobranie danych
Operacja pobrania danych zaczyna się od odpowiedniej akcji kontrolera:
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult ChangeDestination(string trackingId)
{
Reporting.Cargo cargo = _bookingFacade.LoadCargoForRouting(trackingId);
//...
która odwołuje się do fasady:
public Reporting.Cargo LoadCargoForRouting(string trackingId)
{
Reporting.Cargo c = _cargoDataAccess.Find(trackingId);
if (c == null)
{
throw new ArgumentException("Cargo with specified tracking id not found.");
}
return c;
}
Fasada z kolei wykorzystuje bezpośrednio obiekt dostępu do danych:
public Cargo Find(string trackingId)
{
const string query = @"from DDDSample.Reporting.Cargo c where c.TrackingId = :trackingId";
return _sessionFactory.GetCurrentSession().CreateQuery(query).SetString("trackingId", trackingId)
.UniqueResult<Cargo>();
}
Jak widzicie, nie ma tu żadnego przekształcenia danych po ich zwróceniu przez NHibernate. Żadnego DTO. Ponieważ kod podsystemu obsługi zapytań nie jest szczególnie wartościowy, mogę pozwolić sobie na nieco luźniejszą politykę zarządzania zależnościami. Na przykład obiekt dostępu do danych (CargoDataAccess) wykorzystywany jest jako konkretna klasa — nie mam jego abstrakcyjnej definicji. Nie muszę dbać o długowieczność tej części systemu. Kiedy pojawi się lepsza technologia, po prostu wyrzucę całą implementację, począwszy od fasady, aż do samego dołu.
Tak naprawdę użycie NHibernate tutaj może być nawet traktowane jako nadmierne skomplikowanie. Linq2SQL byłoby pewnie lepsze. Ale to temat na osobną notkę…
Wykonanie komendy
Wykonanie komendy także zaczyna się od akcji na kontrolerze. Zwróćcie uwagę, że tym razem akcja jest odpowiedzią na żądanie POST.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ChangeDestination(string trackingId, string destination)
{
_bookingFacade.ChangeDestination(trackingId, destination);
return RedirectToDetails(trackingId);
}
Co kryje się za metodą fasady?
public void ChangeDestination(string trackingId, string destination)
{
_bookingService.ChangeDestination(new TrackingId(trackingId),
new UnLocode(destination));
}
Przekazujemy sterowanie do warstwy Application (obiekt _bookingService). Zaczyna się właściwe wykonanie komendy. W tym momencie włącza się lekki moduł programowania aspektowego (AOP) w moim kontenerze Unity, który zawija całe wywołanie ChangeDestination w transakcję.
Właściwa implementacja komendy zmiany punktu docelowego wygląda tak:
public void ChangeDestination(TrackingId trackingId, UnLocode destinationUnLocode)
{
Location destination = _locationRepository.Find(destinationUnLocode);
Cargo cargo = _cargoRepository.Find(trackingId);
cargo.SpecifyNewRoute(destination);
}
Pierwszym krokiem utworzenie obiektu reprezentującego nową lokalizację docelową towaru. W tym celu wykorzystujemy (abstrakcyjne) repozytorium lokalizacji. Następnie pobieramy z bazy obiekt towaru, którego docelowa lokalizacja ma być zmieniona. Ostatecznie wywołujemy odpowiednią metodę na obiekcie reprezentującym towar. Wygląda ona tak:
public virtual void SpecifyNewRoute(Location.Location destination)
{
if (destination == null)
{
throw new ArgumentNullException("destination");
}
RouteSpecification routeSpecification = new RouteSpecification(_routeSpecification.Origin, destination, _routeSpecification.ArrivalDeadline);
Delivery delivery = Delivery.DerivedFrom(routeSpecification, _itinerary, _lastHandlingEvent);
CargoDestinationChangedEvent @event = new CargoDestinationChangedEvent(this, routeSpecification, _routeSpecification, delivery);
_routeSpecification = routeSpecification;
DomainEvents.Raise(@event);
}
Pomińmy walidację argumentów. Zaczynamy od stworzenia obiektu reprezentującego nową specyfikację trasy tego towaru. RouteSpecification jest tzw. value object-em, więc nie modyfikujemy istniejącej instancji, ale tworzymy nową. Następnie wykorzystujemy obiekt Delivery do określenia nowych danych dotyczących dostawy towaru. Dane te w kolejne linii są wykorzystywane do utworzenia zdarzenia informującego o zmianie lokalizacji docelowej dla towaru. Na koniec ustawiamy nową specyfikację (a wraz z nią lokalizację docelową) i publikujemy zdarzenie.
Asynchroniczna synchronizacja danych
Fajnie mi się napisało. W klasycznym systemie przetwarzanie byłoby już zakończone, ale nie w CQRS. Teraz ma miejsce ostatni etap, jakim jest uwzględnienie dokonanych zmian do w bazie obsługującej zapytania. Pierwsza faza właściwie już się wykonała — była to publikacja zdarzenia. Kolejna to jego obsługa. Zajmuje się nią następująca klasa (zlokalizowana w projekcie “Domain.EventHandlers”):
public class CargoDestinationChangedEventHandler : IEventHandler<CargoDestinationChangedEvent>
{
private readonly IBus _bus;
public CargoDestinationChangedEventHandler(IBus bus)
{
_bus = bus;
}
public void Handle(CargoDestinationChangedEvent @event)
{
_bus.Publish(new CargoDestinationChangedMessage
{
TrackingId = @event.Cargo.TrackingId.IdString,
Origin = @event.NewSpecification.Origin.Name,
Destination = @event.NewSpecification.Destination.Name,
ArrivalDeadline = @event.NewSpecification.ArrivalDeadline
});
}
}
Jej zadaniem jest zamiana zdarzenia domenowego na komunikat NServiceBus, który jest następnie publikowany “na szynie”. Dalej dzieje się magia NServiceBus i MSMQ, która prowadzi do tego, że wiadomość zostanie obsłużona przez przeznaczony do tego obiekt znajdujący się w projekcie “Reporting.MessageHandlers”:
public class CargoDestinationChangedMessageHandler : AbstractMessageHandler<CargoDestinationChangedMessage>
{
private readonly CargoDataAccess _cargoDataAccess;
public CargoDestinationChangedMessageHandler(CargoDataAccess cargoDataAccess, ISessionFactory sessionFactory)
: base(sessionFactory)
{
_cargoDataAccess = cargoDataAccess;
}
protected override void DoHandle(CargoDestinationChangedMessage message)
{
Cargo cargo = _cargoDataAccess.Find(message.TrackingId);
cargo.UpdateRouteSpecification(message.Origin, message.Destination, message.ArrivalDeadline);
}
}
Obsługa komunikatu polega na pobraniu z bazy danych struktury reprezentującej towar, a następnie aktualizacji danych dotyczących specyfikacji trasy.
Podsumowanie
Mam nadzieję, udało mi się nie wystraszyć Was nadmierną ilością kodu. Chciałem pokazać, jak naprawdę wygląda CQRS oraz, że wbrew obiegowej opinii, nie jest to wcale coś strasznego. Nie wymaga wcale większych, niż wykorzystanie wzorca DTO, nakładów pracy, a pozwala na uzyskanie znacznie większej swobody polegającej na względnie niezależnym rozwoju podsystemów obsługi komend i zapytań.
DDDSample 0.6
Jan 29th
DDDSample.Net version 0.6 have just been released here on CodePlex. Minor changes include moving from in-memory database fakes and SQLExpress to SQLite in both tests and UI.
This finally frees the project from any external dependencies which need to be installed separately. SQLExpress caused many problems so I am very happy I managed to get rid of it. One disadvantage of SQLite is that when working in in-memory mode, it loses all data after closing connection. In thin client scenario where I want to have connection per request this is unacceptable and forced me to use SQLite in file mode. This means that, unfortunately, you have to manually add IIS account write privileges to App_Data directory in DDDSample.Net instalation folder.
The major change is including CQRS (Command and Query Responsibility Segregation) variant of the solution. CQRS is about separating application architecture into two subsystems: one for processing commands and one for processing queries. Command processing uses Domain Model to capture business logic of the problem domain. In classic solution the same model is used to populate the UI which makes it polluted with lots of UI related code. In CQRS, however, Domain Model have only one purpose — support command processing. UI queries (as well as complicated reporting queries) are processed by the other subsystem which can (and should) be optimized for reading (which could mean, for example, suing star schema).
CQRS comes in many different flavors regarding implementation. The most import ones come from Udi Dahan and Greg Young (which is best described by Mark Nijhof). My own implementation is somewhat close to Udi’s ideas (at least I hope so;-). What I haven’t done (yet) is describing commands as separate classes.
Please download the source code and compare the old (classic) implementation with the new one. Any ideas what can be done better?
DDDSample on Git
Dec 10th
Since implementation of the core features of DDDSample is almost finished I am thinking about opening the project for external contribution. In order to make it happen I am mirroring the main code repository (which is CodePlex’s TFS) with one on github. It will allow you to branch DDDSample.Net at will and publish your modifications online.
I would be very happy to post links to your modifications here and on the project main site on CodePlex. Test version of the git-ed repository is available here:
git://github.com/SzymonPobiega/DDDSample.Net.git
Please download it and try if it works for you.
DDDSample.Net 0.5
Dec 4th
Obecnie funkcjonalności biznesowe obejmują:
- Rejestrację nowego towaru do spedycji
- Wyznaczenie (niekoniecznie) optymalnej trasy transportu
- Rejestrację nowego “zdarzenia” transportowego (np. odebrano towar, załadowano towar)
- Śledzenie postępu transportu
Interfejs użytkownika zbudowany jest w oparciu o ASP.NET MVC. Komendy użytkownika przekazywane są, za pośrednictwem warstwy aplikacji dokonującej translacji żądań, do Modelu Domeny. Model zawiera całość logiki aplikacji. Podzielony jest na 3 główne obszary (zwane agregatami):
- Location – zawierający logikę związaną z lokalizacją – jest to właściwie generyczna poddomena.
- Cargo – zawierający logikę związaną z rejestracją, wyznaczaniem trasy oraz śledzeniem postępu transportu
- Handling – zawierający logikę związaną z rejestracją zdarzeń transportowych
Począwszy od wersji 0.5 komunikacja pomiędzy dwoma ostatnimi agregatami zrealizowana jest asynchronicznie, z wykorzystaniem NServiceBus. Co nam to daje? Podstawowe wykorzystanie agregatu Cargo to odczyt danych (na potrzeby śledzenia postępu). Podstawowe wykorzystanie agregatu Handling to zapis danych (na potrzeby rejestracji zdarzeń). Dzięki oddzieleniu tych dwóch możemy zastosować różne sposoby skalowania w tych dwóch obszarach systemu. A to tylko jeden z wielu przykładów…
Zapraszam do ściągnięcia DDDSample.Net i poeksperymentowania. Projekt dostępny jest w formie instalatora MSI (w sekcji downloads) lub w formie źródeł. MSI nie instaluje źródeł, a jedynie aplikację ASP.NET.




