fbpx
devstyle.pl - Blog dla każdego programisty
devstyle.pl - Blog dla każdego programisty
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ć.

0 0 votes
Article Rating
28 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Paweł
Paweł
10 years ago

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

Thaven
Thaven
10 years ago

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

Lech
Lech
10 years ago
Reply to  Thaven

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

trackback
10 years ago

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

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

Jarzyn
Jarzyn
10 years ago

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

Jarzyn
Jarzyn
10 years ago
Reply to  procent

Znam ;) Czyli rozumiem póki co bez żadnego konkretnego API? ;)

Jarzyn
Jarzyn
10 years ago
Reply to  procent

A mógłbyś przybliżyć, o czym mowa? Bo chyba niezbyt rozumiem ;) A co do DI to stawiam w tym dopiero pierwsze kroki, więc Twój cykl mi akurat idealnie przypasował ;)

Stefan
Stefan
10 years ago

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
unodgs
10 years ago
Reply to  Stefan

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
10 years ago

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
10 years ago

“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
10 years ago

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 narzędziu pod tytułem Object Browser i potrzebowali powrotu do czasów C++ i plików nagłówkowych?

Jarzyn
Jarzyn
10 years ago

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
10 years ago

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 string Build() ….
public void Send(IEmailTransport transport) ….
}

i użycie jej w kontrolerze, który spina infrastrukturę i logikę:

var email = new EmailTemplate();
Prepare(email);
email.Send(smtpEmailTransport);

Podkreślam, że jest to tylko mój punkt widzenia. Pozdrawiam!

unodgs
unodgs
10 years ago
Reply to  procent

procent,
Fakt, zapomniałem, że w C# można mieć wiele publicznych interfejsów i klas w jednym pliku. Pisałem bardziej z punktu widzenia Javy, którą ostatnio używam a tam nie ma takiej możliwości. Przez byt rozumiem tutaj klasę albo interfejs (coś co zaśmieca przestrzeń nazw :) ). Niemniej jednak to, co napisałeś, potwierdza mój zarzut. Owszem, dla każdego interfejsu można wymyślić kilka implementacji – tylko czy w realnej aplikacji będziesz faktycznie korzystał z zewnętrznej usługi walidującej email? Nie chce przez to powiedzieć, że twoja “konsekwencja” jest zła, a raczej to, że ma wady i zalety. Ja po prostu cenię aplikacje zwięzłe (im mniej kodu tym lepiej), a tworzenie interfejsów z “automatu” niestety do tego nie prowadzi. W sumie dobry temat na osobny wpis.

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. […]

Kurs Gita

Zaawansowany frontend

Szkolenie z Testów

Szkolenie z baz danych

Książka

Zobacz również