fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
4 minut

DI: IoC & explicit dependencies & interfaces


12.06.2014

OK – mamy niektóre “odpowiedzialności” wyjęte do osobnych klas. Ale co dalej? Czy UsersController powinien sam, ot, tak sobie, tworzyć nowe instancje których aktualnie potrzebuje?

Nie!

Pójdę o krok dalej i powiem więcej: użycie słowa kluczowego “new” w kodzie aplikacji uznaję za anti-pattern. Howgh, rzekłem.

Jeżeli klasa potrzebuje innej klasy do działania, to ta zależność powinna być jawnie wyeksponowana w kodzie. Najlepiej przez parametry konstruktora. A jak już coś staje się zależnością, to “wypada” nałożyć na to coś interfejs (zaraz zobaczymy czemu). Po zmianach (tag demo3-start) kod wygląda następująco:

public interface IEmailValidator
{
    bool Validate(string email);
}

public class EmailValidator : IEmailValidator
{
    // ...

(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/EmailValidator.cs)

public interface IActivationLinkGenerator
{
    string GenerateLink(string token, string email);
}

public class ActivationLinkGenerator : IActivationLinkGenerator
{
    // ...

(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/ActivationLinkGenerator.cs)

public class UsersController
{
    private readonly IEmailValidator _emailValidator;
    private readonly IActivationLinkGenerator _activationLinkGenerator;

    public UsersController(
        IEmailValidator emailValidator
        , IActivationLinkGenerator activationLinkGenerator
    )
    {
        _emailValidator = emailValidator;
        _activationLinkGenerator = activationLinkGenerator;
    }

(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/UsersController.cs)

Co ta zmiana oznacza? Że odpowiedzialność za tworzenie instancji klas wykorzystanych podczas przetwarzania żądania ląduje tam gdzie powinna: w kodzie infrastruktury!

public class WebServer
{
    public void RegisterUser(string email)
    {
        var emailValidator = new EmailValidator();
        var activationLinkGenerator = new ActivationLinkGenerator();
        var controller = new UsersController(emailValidator, activationLinkGenerator);
        controller.RegisterUser(email);
    }
}

(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/WebServer.cs)

Przejdźmy do taga demo3.1, bo teraz… po co te interfejsy?

Trochę uproszczę. Między innymi po to, żebyśmy mogli przetestować naszą aplikację bez uruchamiania jej – dla mnie jest to niezmiernie istotne. Z pierwotnym kodem fakt wywalenia błędu w momencie podania niewłaściwego adresu e-mail najwygodniej byłoby przetestować… startując system i ręcznie wklepując adres, który WIEM że jest zły. To oznacza, że muszę pamiętać jakie reguły są stosowane przy walidacji oraz że muszę mieć jakiś UI. Teraz mogę napisać:

public class UserController_RegisterUser_Tests
{
    readonly UsersController _controller;
    readonly IEmailValidator _emailValidator;
    string _email;

    public UserController_RegisterUser_Tests()
    {
        _emailValidator = Substitute.For<IEmailValidator>();
        var linkGenerator = Substitute.For<IActivationLinkGenerator>();

        _controller = new UsersController(_emailValidator, linkGenerator);

        _email = "email";
    }

    void execute()
    {
        _controller.RegisterUser(_email);
    }

    [Fact]
    public void throws_when_email_not_valid()
    {
        _emailValidator.Validate(_email).Returns(false);

        Assert.Throws<ArgumentException>(
            () => execute()
        );
    }
}

(https://github.com/maniserowicz/di-talk/blob/demo3.1/src/app.Tests/UserController_RegisterUser_Tests.cs)

Zamockowałem sobie walidację, na tym poziomie nie obchodzą mnie reguły świadczące o poprawności adresu.

Ale walidacja e-maila to nie największy problem. Problemem większym jest przetestowanie faktu poprawnego wysłania (bądź nie) wiadomości tym kanałem. Z obecnymi instrukcjami byłbym zmuszony do faktycznego wysłania maila i sprawdzania skrzynki, co niesie za sobą konieczność posiadania już na tym etapie odpowiedniej konfiguracji SMTP (lub postawienia sobie lokalnie jakiejś “fałszywki”). Ale teraz, widząc jakie to proste, mogę wysyłacza wiadomości schować za interfejsem, przekazać jako zależność i zamockować. Wsio.

Gdzie jesteśmy? W punkcie, w którym oprócz testów czysto jednostkowych możemy zacząć pisać również testy interakcji pomiędzy poszczególnymi komponentami. Tutaj należy zachować szczególną ostrożność, bo mockowanie jest często oznaką nieidealnej architektury (chociaż zauważcie, że nigdzie nie twierdziłem, że przedstawiany kod jest “dobrą architekturą”) lub gorszego niż perfekcyjne rozplanowania modelu i odpowiedzialności między klasami. Dodatkowo do rozważenia pozostaje kwestia “korzystać z bibliotek do mockowania czy nie”, ale to dyskusja na inny raz.

0 0 votes
Article Rating
12 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
trackback
9 years ago

DI: IoC & explicit dependencies & interfaces | Maciej Aniserowicz o programowaniu…

Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…

fex
fex
9 years ago

Dlaczego ” mockowanie jest często oznaką nieidealnej architektury” ? Przecież predzej czy później cos wymockować trzeba – wkońcu, żeby trzymać się SRP trzeba część logiki wyrzucić do innej klasy/modułu (tak mi się wydaje :P).

markone
markone
9 years ago

Maćku, dlaczego przeniosłeś odpowiedzialność za rozwiązanie zależności do klasy Web Server, zamiast użyć jakiejś biblioteki do DI?

Piotr Perak
9 years ago

Czy, aby na pewno trzeba tutaj wstrzykiwać EmailValidator? Nie będą istnieć inne walidatory i bardzo łatwo sprawić, aby w teście prawdziwy Validator zwracał true/false. Wystarczy przekazać poprawny/błędny email, a wiemy jakie to są. (EmailValidator jest testowany, więc wiemy, że działa poprawnie) Pewnie kiedyś użyłbym stuba, jak Ty, ale ostatnio się zastanawiam. Beck i Fowler mówili, że nie używają mock’ów/stubów.

Piotr Perak
9 years ago

W jaki sposób zmiana implementacji walidatora miała by zepsuć testy? Jedynie w przypadku zmiany interfejsu musisz zmodyfikować testy. Ale to jest konieczne, czy używasz stub-a, czy nie. A jeżeli ten kontroler ma N metod, a tylko ta jedna wymaga walidatora? Nadal wstrzykujesz?

Mad
Mad
9 years ago

Piotr, rozważ taką systuację. Nie wstrzykujemy walidatora, a w teście tworzymy odpowiedni mail tak jak napisałeś. W przypadku zmiany logiki walidacji maila (lub refactoringu, np. wprowadzenie błędu) nie przeszłyby dwa testy:
– test dla EmailValidator,
– test dla UsersController.

Tutaj coś jest nie tak. Dlaczego test kontrolera nie przechodzi, skoro działa on prawdiłowo? Kożystając z fake-owego walidatora (jak w przykładzie na tym blogu), takiej sytuacji nie będzie.

Sprzet
Sprzet
9 years ago

Ja mam inne pytanie:
Dlaczego właściwie EmailValidator nie jest klasą statyczną? Przecież on nie posiada czegoś takiego jak stan. Czy odpowiedzią jest “bo miał być prosty przykład obiektu”, czy to też byłby jakiś anty- pattern, o którym nie wiem?

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również