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); // ...
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ć.
Gdy robi się skomplikowanie, odłóż rozwiązanie na poniedziałek! :)
I w ten sposób cliffhangery podbiły krainę blogów programistycznych ;)
Teraz tylko czekać na mikropłatności jeszcze: “Aby dowiedzieć się jak sobie z tym poradzić…” ;P
Thaven,
Aż musiałem sprawdzić co to cliffhanger :).
DI: gdy robi się skomplikowanie… | Maciej Aniserowicz o programowaniu…
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…
Jakiego kontenera DI masz zamiar użyć w następnych częściach? :)
Jarzyn,
Znasz PoorMansContainer? Nowość na rynku, ma szansę zrewolulcjonizować podejście do pisania i używania kontenerów ;)
Znam ;) Czyli rozumiem póki co bez żadnego konkretnego API? ;)
Z bardzo konkretnym, tyle że nie “zewnętrznym”.
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ł ;)
(prawie) wszystko o czym będzie mowa znajduje się na podlinkowanym githubie na masterze, więc sekretów żadnych tutaj nie chowam :)
https://github.com/maniserowicz/di-talk/tree/master/src
to się cieszę że się wstrzeliłem
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…)
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,
Dla mnie interfejs z jedną implementacją nie jest problemem i nie wiem czemu miałby być. Większość moich interfejsów ma jedną implementację, spora część – tylko jedną metodę. Chciałbym, aby na pytanie “co robi twój system i z jakich komponentów się składa?” odpowiedź brzmiała “zobacz jakie mam interfejsy”. Tak po prostu lubię, a wad nie widzę.
Co do mock vs stub – nie rozgraniczam tych pojęć. Wszystko nazywam mockiem niezależnie od tego czy jest to mock, stub, fake, mole, dummy czy jeszcze coś innego.
Wstrzykiwanie wszystkiego zawsze robię w konstruktorze, niezależnie od tego ile metod danej zależności potrzebuje. Jeżeli konstruktor robi się zbyt duży to oznacza że klasę trzeba podzieilć na mniejsze, bardziej sfocusowane na konkretnych zadaniach.
A co do tego, czy email service powinien robić jedno czy drugie, to tutaj nie ma żadnego znaczenia :). Akurat w głowie w miejscu “// send email…” widziałem coś w stylu “_emailSender.Send()”, czyli dokładnie to o czym piszesz. Ale skoro nie ma żadnej implementacji to też nie ma się co w jej szczegóły zagłębiać.
Wszystko w tym cyklu jest jednym dużym przykładem, żaden kawałek tego kodu nie pochodzi z produkcji.
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.
Stefan,
Transaction Script staram się unikać i dzięki takiemu rozbiciu na małe klasy/interfejsy jestem zwykle w stanie zamknąć go w jednym miejscu, jakimś “entry point” do logiki, i niech tam sobie siedzi.
Lech,
Albo licytację, odpowiedź dostaje 100 osób które zapłaciły najwięcej, a kasa przepada wszystkim ;)
“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
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?
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?
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,
Nie stosuję interfejsów “ewangelicznie”. Stosuję je raczej “konsekwentnie”. I póki co dobrze mi to służy, nie ma trudnej do ogarnięcia mnogości bytów (?) ani plików (interfejs i jego implementację trzymam zwykle w tym samym pliku).
Akurat co to walidacji e-mail to interfejs jak najbardziej MA sens. Mój walidator (regex) jest bardzo prosty, podczas gdy istnieją dedykowane usługi sprawdzające nie tylko poprawność, ale też fakt ISTNIENIA konta e-mail u danego providera. I to byłby świetny kandydat na inną implementację interfejsu IEmailValidator.
Jednak jest to nieważne, ponieważ nie piszę interfejsów z myślą o wielokrotnej ich implementacji.
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.
Stefan,
Zgoda, oczywiście że można wstrzykiwać zależności używając klas a nie interfejsów. Po prostu ja przedstawiam tutaj swoje praktyki, które u mnie działają. I to nie jest duplikacja, bo interfejs pełni u mnie inną rolę niż klasa.
“Jedna klasa z jedną jednolinijkową metodą plus interfejs” – miałem takie coś, nie widzę nic złego, chociażby dlatego, że często w momencie definiowania interfejsu nie wiem co będzie go implementowało i w ilu linijkach.
Jeśli chodzi o konkretny przypadek EMailService to zaproponowane przez Ciebie rozwiązanie (tworzenie i wysyłanie maili obok siebie, w jednej klasy, rozbite na dwie metody) jest dla mnie nie do przyjęcia. Bo to dwie różne funkcje. A więc: dwa różne interfejsy… chociaż może inaczej: to po prostu dwa interfejsy, a czy implementują je dwie klasy czy jedna to już mnie mniej interesuje.
Właściwie to cała ta dyskusja bardziej pasuje pod poprzedni wpis: http://www.maciejaniserowicz.com/2014/06/12/di-ioc-explicit-dependencies-interfaces/ :)
Jarzyn,
“Puste” interfejsy (tzw marker interfaces) to używany na co dzień mechanizm. Ja akurat wolę go od atrybutów w .NET.
Marcin,
Rozbicie odpowiedzialności między klasy od strony architektury właściwie zupełnie nie jest tematem tego cyklu. Szczerze mówiąc nawet nie zastanawiałem się nad jakąkolwiek “domeną” pisząc przykłady :). Thx, Twoja propozycja ma sens jak najbardziej, chociaż ten kod ja osobiście bym i tak wyrzucił z kontrolera.
unodgs,
Ano haha faktycznie zapomniałem że w Javie jest tak a nie inaczej, masakra :).
Moje rozwiązanie ma pewnie jakieś wady, chociaż póki co żadnej nie napotkałem. Takie “zaśmiecanie” mi nie przeszkadza, ale może to kwestia… gustu? Dobrze prawisz, że mniej kodu == lepiej, ale kod w interfejsie to żaden kod – buga tam nie będzie, refactoruje się automatycznie…
Tak jak napisałem, nie mam interfejsów planując wiele implementacji (YAGNI rulz).
No i na koniec – faktycznie to chyba dobry temat na osobny wpis, dodam sobie tą całą dyskusję do szkiców i zobaczymy co z tego wyjdzie, może zaproszę do kontynuacji :).
[…] 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. […]