Przejdź do treści

DevStyle - Strona Główna
DI: IoC & explicit dependencies & interfaces

DI: IoC & explicit dependencies & interfaces

Maciej Aniserowicz

12 czerwca 2014

Backend

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.

Zobacz również