Nazwa klasy i jej zakres

W projektach często można spotkać się z klasami o szerokim zakresie funkcjonalnym, np. CustomerService, BookingService. Te klasy zwykle zawierają implementację wymagań biznesowych. Przy ciągłym rozwijaniu aplikacji takie klasy mogą szybko przybierać na wadze. Ich zakres odpowiedzialności może rosnąć, w zasadzie w nieskończoność. Przyczyną tego może być zbyt ogólna nazwa klasy, która nie definiuje precyzyjnie zakresu jej odpowiedzialności. Przyjrzyjmy się definicji klasy CustomerService:

class CustomerService 
{
    public IEnumerable<Customer> GetCustomers()
    {
        /* zwróć obiekty klasy Customer */
    }

    public Customer GetCustomer(int id)
    {
        /* zwróć obiekt klasy Customer */
    }

    public int AddCustomer(Customer customer)
    {
        /* dodaj nowy obiekt klasy Customer */
    }

    public void UpdateCustomer(Customer customer)
    {
        /* zaaktualizuj obiekt klasy Customer */
    }

    public void RemoveCustomer(int id)
    {
        /* usuń obiekt klasy Customer */
    }
}

Z powyższej definicji klasy można się domyśleć, że pozwala ona na zarządzanie obiektami typu Customer. Jednak sama nazwa mało nam mówi o jej granicach odpowiedzialności. Gdy powstanie potrzeba dodania nowych operacji związanych z typem Customer, to istnieje zagrożenie, że klasa CustomerService zostanie rozbudowana o nowe metody. Taki scenariusz może się powtarzać przez całą fazę konstrukcji oprogramowania. Efektem końcowym może być ciężka do utrzymania klasa, która łamie większość zasad projektowania poprawnego i czystego kodu.

W celu uniknięcia zagrożenia można podzielić klasę na mniejsze części i nadać im bardziej szczegółowe nazwy:

class CustomerReader 
{
    public IEnumerable<Customer> GetCustomers()
    {
        /* zwróć obiekty klasy Customer */
    }

    public Customer GetCustomer(int id)
    {
        /* zwróć obiekt klasy Customer */
    }
}

class CustomerWriter 
{
    public int AddCustomer(Customer customer)
    {
        /* dodaj nowy obiekt klasy Customer */
    }

    public void UpdateCustomer(Customer customer)
    {
        /* zaaktualizuj obiekt klasy Customer */
    }

    public void RemoveCustomer(int id)
    {
        /* usuń obiekt klasy Customer */
    }
}

Dla przykładu typ CustomerService został podzielony na dwie mniejsze klasy: CustomerReader i CustomerWriter. Jak nazwy wskazują pierwszy służy do pobierania obiektów, zaś drugi do modyfikowania zbioru obiektów typu Customer. Takim prostym zabiegiem zwiększyliśmy wartość granularności klas czyli zawierają one mniej funkcjonalności. Jednakże zagrożenie zostało tylko zmniejszone, bowiem podzielone klasy wciąż mogą zwiększać swój zakres. Maksymalną wartość granularności możemy uzyskać stosując wzorzec CQRS. Wtedy za każdą operację będzie odpowiedzialna oddzielna klasa:

class GetCustomersQueryHandler
{
    public QueryResult Execute(GetCustomersQuery command)
    {
        /* zwróć obiekty klasy Customer */
    }
}

class GetCustomerQueryHandler
{
    public QueryResult Execute(GetCustomerQuery command)
    {
        /* zwróć obiekt klasy Customer */
    }
}

class AddCustomerCommandHandler
{
    public CommandResult Execute(AddCustomerCommand command)
    {
        /* dodaj nowy obiekt klasy Customer */
    }
}

class UpdateCustomerCommandHandler
{
    public CommandResult Execute(UpdateCustomerCommand command)
    {
        /* zaaktualizuj obiekt klasy Customer */
    }
}

class RemoveCustomerCommandHandler
{
    public CommandResult Execute(RemoveCustomerCommand command)
    {
        /* usuń obiekt klasy Customer */
    }
}

Tym sposobem otrzymane zostały klasy o zdefiniowanym zakresie. Nazwy klas opisują precyzyjnie za co odpowiadają. Nowe funkcjonalności będą trafiać do nowych typów eliminując ryzyko rozbudowy istniejących. Oczywiście to rozwiązanie nie jest idealne i tworzy inne problemy, np. utrzymywanie dużej ilości klas czy wiele styków końcowych (ang. endpoints).

There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton

Podsumowując, nazewnictwo klas nie jest wcale trywialnym zadaniem na jakie wygląda. Poprawna nazwa powinna precyzyjnie opisywać zakres klasy. Gdy istnieje ryzyko zwiększęnia się zakresu klasy, wtedy powinna zapalić się przy niej pomarańczowa lampka z napisem „zmień mnie, niebezpiecznie rosnę!”.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *