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…