devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
0 3 minut

DI: gdy robi się skomplikowanie…


18.06.2014

Rozważmy przez chwilę scenariusz wysyłania wiadomości e-mail. A raczej tą część procesu, w której generowana jest treść. W tagu demo3-finish mamy taki interfejs:

public interface IEmailService
{
    void RegistrationEmail(string email, string link);
}

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

Jego implementacja powinna zająć się dostarczeniem wiadomości w odpowiednie miejsce. Ale czy powinna również babrać się w stringach, razorach czy innych resxach aby skomponować tekst przekazany użytkownikowi końcowemu? Niewydajemiesie, widziałbym raczej coś takiego:

public interface IEmailTemplateGenerator
{
    string ActivationTemplate(string link);
}

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

Dzięki czemu sama czynność wysyłania e-maila będzie prosta i przejrzysta:

public class EmailService : IEmailService
{
    private readonly IEmailTemplateGenerator _templateGenerator;

    public EmailService(IEmailTemplateGenerator templateGenerator)
    {
        _templateGenerator = templateGenerator;
    }

    public void RegistrationEmail(string email, string link)
    {
        string template = _templateGenerator.ActivationTemplate(link);

        // send email...
    }
}

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

Czy powinienem zatem już teraz, od razu, rzucić się do implementacji interfejsu IEmailTemplateGenerator? Nieee, nie chcęęę, to nuuudneeee!!

I na szczęście nie muszę. Zastosowanie interfejsów daje nam wielką zaletę: nie musimy ich implementować! Możemy pisać nasz system inkrementalnie, dodając interfejsy tu i ówdzie, oznaczając miejsca do „zrobienia później”. Całość „się spina”, możemy spokojnie pisać i testować kod mimo, że nie da się go nawet uruchomić. Ale praca idzie do przodu.

Wszystko pięknie i słitaśnie, ale nagle wszystko mi się posypało. O ile w testach UserControllera nie ma problemu, bo interfejs IEmailService mogę sobie zamockować:

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

 _controller = new UsersController(_emailValidator, linkGenerator, emailService);

 // ...

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

to już w kodzie „prawdziwym” nie jest tak prosto:

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

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

Zwróćcie uwagę na linijkę tworzącą „emailService”. Co my tam mamy? „new IEmailTemplateGenerator()”. Co za kretyn, chce tworzyć instancję interfejsu, przecież to się nawet nie skompiluje! Ano właśnie… Nie mamy implementacji, więc nie mamy jak utworzyć „template generatora”. Więc nie mamy jak utworzyć „email service”. Więc nie mamy jak utworzyć „users controllera”. Więc nie mamy jak obsłużyć żądania. Ba, skoro się nie kompiluje, to nawet nie mamy jak uruchomić testów. Has the shit just hit the fan?

Niekoniecznie. Bo… bo następnym razem zobaczymy jak sobie z tym poradzić.

Nie przegap kolejnych postów!

Dołącz do ponad 9000 programistów w devstyle newsletter!

Tym samym wyrażasz zgodę na otrzymanie informacji marketingowych z devstyle.pl (doh...). Powered by ConvertKit
Powiadom o
Paweł
Paweł

Gdy robi się skomplikowanie, odłóż rozwiązanie na poniedziałek! :)

Thaven
Thaven

I w ten sposób cliffhangery podbiły krainę blogów programistycznych ;)

Lech
Lech

Teraz tylko czekać na mikropłatności jeszcze: „Aby dowiedzieć się jak sobie z tym poradzić…” ;P

trackback

DI: gdy robi się skomplikowanie… | Maciej Aniserowicz o programowaniu…

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

Jarzyn
Jarzyn

Jakiego kontenera DI masz zamiar użyć w następnych częściach? :)

Stefan
Stefan

Macieju,

używanie interfejsów tworzy warstwę abstrakcji, co później ułatwia testowanie. Dla mnie problemem jest to, że powstają interfejsy, które mają i będą miały tylko jedną implementację. Nawet w testach często używane są mocki, a nie stuby. Dlaczego nie używać klas z metodami wirtualnymi?

Druga kwestia to wstrzykujesz IEmailTemplateGenerator do EmailService. Robisz to w konstruktorze, ale czy wszystkie metody będą go potrzebowały? No i w mojej ocenie mieszasz warstwę infrastruktury z warstwą logiki biznesowej. Według mnie, IEmailService powinien mieć metodę type SendEmail(address, body), a warstwa logiki wygenerować szablon wiadomości i ją wysłać. (No chyba, że tutaj chodziło tylko o przykład…)

unodgs

Muszę zgodzić się ze Stefanem. Ewangeliczne stosowanie interfejsów prowadzi do kodu trudnego do ogarnięcia (w sensie mnogości bytów i plików). Cała trudność w tworzeniu oprogramowania to właśnie wyważenie abstrakcji. Sytuację w której powstaje dużo interfejsów dla których istnieje tylko jedna implementacja ja osobiście nazywam programowaniem przyszłości, która nigdy nie nadejdzie (nikt nigdy nie napisze drugiego walidatora emaila). Oczywiście sprawa nie jest prosta, wręcz delikatna ale wato o tym wspomnieć :)

Stefan
Stefan

Macieju,

dzięki za komentarz. Piszesz o interfejsach, które mają jedną metodę. Jak w takim razie zapatrujesz się i czy stosujesz wzorzec Transaction Script opisany przez Fowlera? Tak sobie myślę, że przydałyby się jednak funkcje globalne w C#/.NET.

Tomasz
Tomasz

„Zwróćcie uwagę na linijkę tworzącą “emailService”. Co my tam mamy? “new IEmailTemplateGenerator()”. Co za kretyn, chce tworzyć instancję interfejsu, przecież to się nawet nie skompiluje!”

http://stackoverflow.com/questions/3271223/how-to-define-the-default-implementation-of-an-interface-in-c

Stefan
Stefan

Wystawianie interfejsów na granicach warstw/zestawów ma sens. Wyciąganie interfejsów dla wszystkich klas to według mnie tworzenie duplikacji, która niczemu nie służy, bo przecież zależności można wstrzykiwać również używając klas. Poza tym taka gloryfikacja interfejsów może prowadzić do sytuacji, że bardziej zbliżamy się do tego, że mamy klasy z jedną metodą, która ma jedną linijkę + oczywiście interfejs. A przecież EMailService, jeśli jest prosty, mógłby i tworzyć, i wysyłać maile – wystarczy te dwie rzeczy rozdzielić do dwóch chronionych wirtualnych metod, co pozwoli to łatwo przetestować. Zamiast tego mamy komplikację w postaci 4 bytów i wstrzykiwania zależności. Czyżby niektórzy zapomnieli o… Czytaj więcej »

Jarzyn
Jarzyn

A co powiecie Panowie o pustych interfejsach? Tak, bez żadnej metody, używane tylko po to, by przez refleksję uchwycić wszystkie implementujące go klasy?

Marcin
Marcin

Zgadzam się z moimi przedmówcami. To co tutaj autor proponuje to klasyczny transaction script – nie mamy obiektów biznesowych tylko szereg wywoływanych metod z różnych usług, brak stanu w domenie, itp. Mieszają się tutaj odpowiedzialności. Obecnie mamy: kontroler (infrastraktura) używa usługi IEmailService (to również jest infrastruktura, gdyż odpowiada za wysyłanie wiadomości), która używa IEmailTemplateGenerator (tutaj mamy domenę bo to jest clue aplikacji – wygenerowanie maila, reszta może się zmienić). Stoi to w sprzeczności z opisem Clean Architecture (http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html), który przemawia do mnie najbardziej. Sugerowałbym wydzielenie w jakiś sposób domeny (to tak na szybko:), na przykład: public class EMailTemplate { public… Czytaj więcej »

trackback

[…] Ostatnim razem rozstaliśmy się w takim napięciu, że aż jeden z Czytelników nazwał to cliffhangerem (nauczyłem się nowego słowa!). Zanim jednak zaczniemy przyglądać się rozwiązaniu naszej niewesołej sytuacji (nie kompiluje się, buuu): chwila refleksji i nader trafnego (a jak!) porównania. […]

Moja książka „Zawód: Programista”

Facebook

Zobacz również