Praktycznie każdy jako-tako przemyślany system z jaką-taką architekturą zawiera “klasy z logiką”. Przez “jako-tako przemyślaną architekturę” rozumiem fakt, że cała logika nie siedzi bezpośrednio w kontrolerach czy innym tego typu bycie, a w dedykowanych… “miejscach”.
W naszym projekcie jednym z ważniejszych konceptów domenowych jest numer telefonu. Można go zareprezentować na przykład w taki sposób:
public class PhoneNumber { public int Id { get; set; } public int OwnerId { get; set; } public string Number { get; set; } public string OriginalNumber { get; set; } public bool IsDisabled { get; set; } public bool IsDeleted { get; set; } public DateTime? LastDialingTime { get; set; } // more data and methods...
Podczas działania systemu niekiedy trzeba znaleźć duplikaty numerów na podstawie wartości, czyli “Number”. W projekcie, który ssie, byłoby to zaszyte w jakimś kontrolerze. U nas, jako że ssących projektów nie budujemy:), logika wyszukiwania duplikatów jest zaimplementowana w dedykowanym komponencie. Nazwijmy go IFindDuplicatePhoneNumbers.
Standardowym i przychodzącym właściwie momentalnie do głowy API takiego komponentu jest coś takiego:
public interface IFindDuplicatePhoneNumbers { IDictionary<PhoneNumber, PhoneNumber[]> FindDuplicates(IEnumerable<PhoneNumber> numbers); }
Jednak to jest rozwiązanie moim zdaniem nienajlepsze. Po pierwsze: do znalezienia duplikatów nie potrzebujemy informacji o czasie ostatniego połączenia ani całej masy innych danych dostępnych w klasie PhoneNumber. Po drugie: zmiana w klasie PhoneNumber mogłaby wpłynąć na działanie naszego “odszukiwacza duplikatów”. Po trzecie wreszcie: zarówno wykorzystanie tego API w kodzie jak i w testach mogłoby prowadzić do “skracania” sobie drogi i przekazywania obiektów niepoprawnych, na przykład mających wypełnione tylko pola Id i Number.
Jak można temu zaradzić? A rozszerzyć ten komponent o dodatkowe klasy dokładnie określające kontrakt wymagany do wykonania żądanej operacji, czyli struktury reprezentujące “input” i “output”:
public interface IFindDuplicatePhoneNumbers { DuplicationFindingResult FindDuplicates(IEnumerable<DuplicateCandidate> numbers); } public class DuplicateCandidate { public int Id { get; set; } public string Number { get; set; } } public class DuplicatedNumber { public int Id { get; set; } public string Number { get; set; } public int[] DuplicateIds { get; set; } } public class DuplicationFindingResult { public DuplicatedNumber[] Duplicates { get; set; } public bool DuplicatesFound { get { return Duplicates != null && Duplicates.Length > 0; } } }
Czy coś w ten deseń. Pełna dowolność w zależności od przedstawionych wymagań.
Co nam to daje?
Podczas implementacji tej funkcjonalności nie zastanawiamy się jakie obiekty mamy do dyspozycji. Po prostu definiujemy te obiekty, a klient (czyli kod wywołujący tworzoną logikę) ma za zadanie odpowiednie dane dostarczyć. Możemy tutaj dowolnie napawać się swobodą stosując TDD w czystej postaci, gdzie nie ogranicza nas dotychczasowy kształt systemu – robimy co mamy do zrobienia i już. Zalet jest więcej. Odrywamy się od klasy będącej bezpośrednim mapowaniem na bazę danych. Tworzymy niezależny kawałek softu, który możemy nawet wystawić jako osobny serwis. Mamy wolną rękę jeśli chodzi o pisany kod.
Oczywiście, powoduje to mnożenie się klas w projekcie. Ale czy to źle? Nie.
Dodatkowo można zamiast klasy DuplicateCandidate zrobić sam interfejs IDuplicateCandidate i zaimplementować go w naszym “głównym” PhoneNumber, ale… ja osobiście wolę klasę. Między innymi dlatego, że mogę do niej dodać metody charakterystyczne dla tego scenariusza.
Od dłuższego czasu stosuję takie rozwiązanie w praktyce i sprawdza się to znakomicie. Zachęcam do spróbowania i oderwania się do reprezentowania każdej składowej systemu w jeden-li tylko sposób. Reprezentujmy dane tak, jak wymaga tego aktualny, konkretny, najwęższy możliwy kontekst. Prawdopodobnie ciąg dalszy nastąpi…
O mikro-kontraktach | Maciej Aniserowicz o programowaniu…
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…
A ja myślałem, że będzie o mikro-kontraktach z klientami, a tu niespodzianka. :)
Wracając do tematu. Stosuję takie rozwiązanie od dłuższego czasu i potwierdzam, że dobrze się sprawdza. :)
Drugą stroną medalu jest niestety utrzymywanie jakiegoś tam mapowania między tymi obiektami, akurat tutaj wiele tego nie ma.
No, ale chyba lepszej opcji nie ma,
powoli zaczynam używać czegoś takiego w controllerach w mvc
https://github.com/abenedykt/appEngine
controller praktycznie wygląda wtedy app.Execute(request) a projektując request (w tym przypadku) samo się narzuca RequestDuplicatesFor i tutaj nr tel czy ewentualnie to id
Mic,
Z mojego doswiadczenia poki co wynika ze mapowanie nie jest problemem. I żadnego automappera nie uzywam, bo sprawial mi na dluzsza mete wiecej problemow niz dawal korzysci.
No tutaj akurat nie. Natomiast jak pewno zdążyłeś zauważyć, obiekty domenowe często nie są potrzebne w calości do rozwiązania danego problemu ( tak jak tu dałeś w swoim przykładzie ). Teraz pytanie brzmi , czy wolimy implementować rozwiązanie dla tego typu danych ( w Twoim wypadku dla PhoneNumber ) czy dla dedykowanego problemowi input type ( tak jak to zrobiłeś ). Wszystko , jak zwykle, zależy od tego, czy potencjalnie simplifikacja input type się może opłacić ( być może w wypadku specyfikacji Twojego projektu się to oplaca – w końcu sam numer to dużo mniej niż wiele innych danych w Twoim modelu domenowym ) w przyszłości :D
Dobry artykuł overall, będe obserwowal ten blog. ^^
@rek,
Też mam coś podobnego, tyle że autofac mi to konstruuje. Pseudo-cqrs z icommand/ievent i handlerami do nich, kontrolery w mvc czy akcje w nancy maja wtedy zwykle 1-5 linijek. Tyle że w tym przypadku znajdowanie duplikatow moze byc nie requestem, a tylko częścią przetwarzania requesta. Wtedy “handler” ma swoje api (komendę), a dodatkowo korzysta z api mechanizmu do wykrywania duplikatow. Separacja na dwoch poziomach.
podeślij gista, może coś ciekawego wyczytam. Generalnie budowanie może być zrzuconen na autofaca czy cokolwiek, generlanie mi chodzi o samo mapowanie requesta na handler-a. Co do wrzucania api do handlera to chyba jakaś inna koncepcja :)
true, stosuje od kilku lat takie rozwiazanie i jestem z niego bardzo zadowolony :)
@rea,
Gista chyba to nie bardzo moge, ale może jakoś to dostosuję do “publicznego pokazania” i zrobię o tym posta. Idea podobna jak tutaj, ale w nowym projekcie trochę zmodyfikowane po doświadczeniach sprzed lat: http://www.maciejaniserowicz.com/tag/application-events/ .
A przez API rozumiem:
1) komendę wysyłaną do handlera
2) kontrakt komponentu szukającego duplikatów
Handler to “mikroserwis” do którego można się dostać wysyłając komendę (więc to jego API), a duplicatefinder to kolejny “mikroserwis” z którego się korzysta tworząc wymagane struktury (więc to jego API).
Dal mnie to trochę jak wykorzystywanie koparki wprost z kamieniołomu do przekopania ogródka 2mx3m przed blokiem. Przynajmniej dla takich przypadków jak ten zaprezentowany. Przecie te 3 klasy kontraktu i 1 interfejs wraz z jego implementacją opakowywują najzwyklejszego w świecie GroupBy’ja.
Marcin,
Bo to jest banalna implementacja na potrzeby posta. Ale nawet gdyby nie była to group by /join też bym zamknął za interfejsem – bo to ważna część domeny i nawet jeśli jest “prosta” to powinna być jawna moim zdaniem.