O mikro-kontraktach

12

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…

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

12 Comments

  1. Pingback: dotnetomaniak.pl

  2. 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. :)

  3. 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,

  4. 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

  5. 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. ^^

  6. @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 :)

  7. @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).

  8. 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.