Code Review: IoC oraz zbyt wiele parametrów w konstruktorze

Dzisiejszy wpis dotyczy wszystkich klas, jednak zostanie on zaprezentowany na przykładzie ViewModel znanego z MVVM. W moim projekcie używam MVVM i dlatego jest to dla mnie naturalne. W poście zaprezentuje bardzo sztuczne przykłady, dlatego proszę nie skupiać się na nazewnictwie czy na zaprezentowanej funkcjonalności. Przedstawiony problem jest jednak bardzo częsty w realnych projektach i sam mam\miałem  z nim do czynienia w codziennej pracy.

Powiedzmy, że napisaliśmy ViewModel prezentujący jakąś część logiki w naszej aplikacji:

class EmployeeViewModel
{
    public void PrintReport()
    {
        EmployeeValidator validator=new EmployeeValidator();
        
        if(validator.IsValid(Employee))
        {
            DataManager dataManager = new DataManager();
            dataManager.Save(Employee);
            
            ReportPrinter reportPrinter = new ReportPrinter();
            reportPrinter.Print(Employee);
        }
    
    }
}

Powyższy kod ma wiele wad. Zacznijmy jednak od zalet. Dobrą praktyką jest, że logika nie została umieszczona bezpośrednio w ViewModel a została oddelegowana do osobnych klas typu ReportPrinter. ViewModel nie powinien stanowić implementacji wszystkich dostępnych funkcjonalności na danej formatce.

Przejdźmy do wad – powyższego kodu nie da się przetestować. Aby to umożliwić możemy spróbować wstrzyknąć implementacje w konstruktorze:

class EmployeeViewModel
{
    private readonly IEmployeeValidator _validator;
    private readonly IDataManager _dataManager;
    private readonly IReportPrinter _reportPrinter;
    
    public EmployeeViewModel(IEmployeeValidator validator, IDataManager dataManager, IReportPrinter reportPrinter)
    {
        _validator = validator;
        _dataManager = dataManager;
        _reportPrinter = reportPrinter;
    }
    
    public void PrintReport()
    {
        if(_validator.IsValid(Employee))
        {
            _dataManager.Save(Employee);
            _reportPrinter.Print(Employee);
        }
    }
}

Dużo lepiej ponieważ programujemy teraz z użyciem interfejsów a nie konkretnych implementacji. Jeśli chcemy mieć większą kontrolę kiedy dokładnie klasy są tworzone możemy skorzystać z wzorca factory:

class EmployeeViewModel
{
    private readonly Func<IEmployeeValidator> _validatorFactory;
    private readonly Func<IDataManager> _dataManagerFactory;
    private readonly Func<IReportPrinter> _reportPrinterFactory;
    
    public EmployeeViewModel(Func<IEmployeeValidator> validatorFactory, Func<IDataManager> dataManagerFactory, Func<IReportPrinter> reportPrinterFactory)
    {
        _validatorFactory = validatorFactory;
        _dataManagerFactory = dataManagerFactory;
        _reportPrinterFactory = reportPrinterFactory;
    }
    
    public void PrintReport()
    {
        IEmployeeValidator validator = _validatorFactory();
        
        if(validator.IsValid(Employee))
        {
            IDataManager dataManager=_dataManagerFactory();
            dataManager.Save(Employee);
            
            IReportPrinter printer = _reportPrinterFactory();
            printer.Print(Employee);
        }
    }
}

Bardzo często, szczególnie jednak w przypadku ViewModel, zachodzi potrzeba wykonywania różnych operacji. Po jakimś czasie, konstruktor takiego ViewModel, mógłby wyglądać następująco:

public EmployeeViewModel(IEmployeeValidator validator, IDataManager dataManager, IReportPrinter reportPrinter, IWebService webService,IService1 service2,IService2, IService3 service3 )
{
    // proste przypisania tutaj...
}

Za bardzo nie mam pomysłów na wymyślenie przykładu więc po prostu użyłem nazw IService1, IService2 itd. Bardzo często używając IoC (szczególnie w połączeniu z ViewModel), kończymy ze skomplikowanymi konstruktorami ponieważ każdą klasę jako chcemy utworzyć wewnątrz obiektu, musimy przekazać przez konstruktor.

Zastanówmy się, co jest przyczyną tego. Ktoś mógłby powiedzieć, że jest to po prostu natura IoC i każda implementacja musi być przekazywana w tym samym miejscu (konstruktor).

Moim zdaniem jest to ogromna zaleta IoC, że łamanie zasady pojedynczej odpowiedzialności jest tak dobitnie pokazane. Gdybyśmy nie korzystali z IoC, inicjalizacje klas byłyby ukryte wewnątrz metod – na pierwszy rzut oka klasa wyglądałaby w porządku. Dzięki IoC, otrzymujemy konstruktor z np. 10 parametrami i wiemy, że klasa łamie zasadę pojedynczej odpowiedzialności i musi być zrefaktoryzowana.

Jeśli jakaś metoda lub konstruktor mają 5 lub więcej parametrów, wtedy zdecydowanie muszą być zrefaktoryzowane. Idealnie jak mają do 3 parametrów wejściowych i tylko jeden wyjściowy. Jak poradzić sobie zatem z konstruktorami w przypadku IoC? Oczywiście musimy dążyć do utrzymania pojedynczej odpowiedzialności poprzez stworzenie nowych klas.

Wystarczy tak naprawdę pogrupować parametry w logiczne, spójne komponenty. Jeśli do wydrukowania raportu potrzebujemy 3 innych klas, wtedy możemy stworzyć nową usługę:

class EmployeeReportProcessor: IEmployeeReportProcessor
{
    private readonly IEmployeeValidator _validator;
    private readonly IDataManager _dataManager;
    private readonly IReportPrinter _reportPrinter;
    
    public EmployeeReportProcessor(IEmployeeValidator validator, IDataManager dataManager, IReportPrinter reportPrinter)
    {
        _validator = validator;
        _dataManager = dataManager;
        _reportPrinter = reportPrinter;
    }
    
    public void Print(Employee employee)
    {
        if(_validator.IsValid(employee))
        {
            _dataManager.Save(employee);
            _reportPrinter.Print(employee);
        }
    }

}

Trzy parametry to idealna sytuacja. EmployeeProcessor nie będzie miał więcej odpowiedzialności i dotyczy tylko jednej operacji, jaką może wykonać użytkownik. Następnie możemy zaktualizować ViewModel:

public EmployeeViewModel(IEmployeeReportProcessor employeeReportProcessor, IWebService webService,IService1 service2,IService2, IService3 service3 )
{
    // proste przypisania tutaj...
}

Po prostej operacji agregacji, udało się zastąpić 3 parametry jednym. To samo robimy z kolejnymi parametrami. Staramy się wyszukać grupy o podobnej funkcjonalności i dodajemy nowa klasę agregującą. Jeśli wszystkie parametry stanowią kompletnie odrębną od siebie logikę wtedy znaczy, że takowy ViewModel jest błędnie napisany i powinien zostać rozdzielony na kilka. Często zaczynamy od jednego VM na formatkę co nie zawsze jest dobrym pomysłem. Czasami widok, musi być rozdzielony na kilka VM. Powyższe rozwiązanie, dotyczące agregacji usług dotyczy wyłącznie sytuacji, gdzie faktycznie ViewModel funkcjonalnie dotyczy tego samego obszaru w aplikacji, ale ze względu na dużą liczbę różnych usług, jego konstruktor jest ogromny.

W niektórych sytuacjach, pojedyncza agregacja nie wystarczy i np. należy stworzyć kilka hierarchii agregatorów a następnie przekazać ten na samej dole do VM.

One thought on “Code Review: IoC oraz zbyt wiele parametrów w konstruktorze”

  1. Po pierwsze bardzo fajny post (instersujący pod względem architektonicznym). Mam tylko jedno pytanko.. w jaki sposób można rejestrować obiekty Func w kontenerze IoC? Z góry dzięki za odpwoiedź.

Leave a Reply

Your email address will not be published.