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…














