Kontekst: Używam C #
Zaprojektowałem klasę. Aby ją odizolować i ułatwić testowanie jednostek, przekazuję wszystkie jej zależności; nie tworzy wewnętrznej instancji obiektu. Jednak zamiast odwoływać się do interfejsów w celu uzyskania potrzebnych danych, odwołuję się do Funcs ogólnego przeznaczenia zwracających wymagane dane / zachowanie. Kiedy wstrzykuję jej zależności, mogę to zrobić za pomocą wyrażeń lambda.
Wydaje mi się, że jest to lepsze podejście, ponieważ nie muszę robić żmudnych szyderstw podczas testów jednostkowych. Ponadto, jeśli implementacja otaczająca ma zasadnicze zmiany, będę potrzebowała jedynie zmiany klasy fabrycznej; żadne zmiany w klasie zawierającej logikę nie będą potrzebne.
Jednak nigdy wcześniej nie widziałem, aby IoC działało w ten sposób, co sprawia, że myślę, że istnieją pewne potencjalne pułapki, których mogę przegapić. Jedyne, co mogę wymyślić, to niewielka niezgodność z wcześniejszą wersją C #, która nie definiuje Func, i to nie jest problem w moim przypadku.
Czy są jakieś problemy z używaniem ogólnych funkcji delegatów / funkcji wyższego rzędu, takich jak Func dla IoC, w przeciwieństwie do bardziej szczegółowych interfejsów?
źródło
Func
ponieważ można nazwać parametry, w których można określić ich zamiary.Odpowiedzi:
Jeśli interfejs zawiera tylko jedną funkcję, nie więcej, i nie ma ważnego powodu, aby wprowadzić dwie nazwy (nazwa interfejsu i nazwa funkcji w interfejsie), użycie
Func
zamiast tego może uniknąć niepotrzebnego kodu wzorcowego i jest w większości przypadków zalecane - podobnie jak kiedy zaczynasz projektować DTO i rozpoznajesz, że potrzebuje on tylko jednego atrybutu członka.Myślę, że wiele osób jest bardziej przyzwyczajonych do korzystania
interfaces
, ponieważ w czasie, gdy upowszechnianie wstrzykiwania zależności i IoC stawało się popularne, nie było prawdziwego odpowiednikaFunc
klasy w Javie lub C ++ (nie jestem nawet pewien, czyFunc
w tym czasie był dostępny w C # ). Tak więc wiele samouczków, przykładów lub podręczników nadal preferujeinterface
formę, nawet jeśli użycieFunc
byłoby bardziej eleganckie.Możesz zajrzeć do mojej poprzedniej odpowiedzi na temat zasady segregacji interfejsów i podejścia Flow Design Ralfa Westphala. Ten paradygmat implementuje DI z
Func
parametrami wyłącznie z dokładnie tego samego powodu, o którym wspomniałeś już sam (i kilka innych). Jak więc widzisz, twój pomysł nie jest naprawdę nowy, wręcz przeciwnie.I tak, sam zastosowałem to podejście, do kodu produkcyjnego, do programów, które musiały przetwarzać dane w postaci potoku z kilkoma pośrednimi krokami, w tym testami jednostkowymi dla każdego z nich. Dzięki temu mogę dać ci doświadczenie z pierwszej ręki, które może działać bardzo dobrze.
źródło
Jedną z głównych zalet, które znalazłem w IoC, jest to, że pozwolę mi nazwać wszystkie moje zależności poprzez nazewnictwo ich interfejsu, a pojemnik będzie wiedział, który z nich dostarczyć konstruktorowi, dopasowując nazwy typów. Jest to wygodne i pozwala na znacznie bardziej opisowe nazwy zależności niż
Func<string, string>
.Często też stwierdzam, że nawet z jedną, prostą zależnością, czasami musi mieć więcej niż jedną funkcję - interfejs pozwala zgrupować te funkcje razem w sposób, który sam się dokumentuje, w porównaniu z wieloma parametrami, które wszystkie czytają podobnie
Func<string, string>
,Func<string, int>
.Zdecydowanie są chwile, w których przydatne jest po prostu delegowanie, które jest przekazywane jako zależność. Jest to wezwanie do osądu, kiedy używasz delegata vs. mając interfejs z bardzo małą liczbą członków. O ile nie jest naprawdę jasne, jaki jest cel tego argumentu, zwykle popełniam błąd po stronie tworzenia samodokumentującego się kodu; to znaczy. pisanie interfejsu.
źródło
Nie całkiem. Func jest własnym rodzajem interfejsu (w języku angielskim, nie w języku C #). „Ten parametr jest czymś, co dostarcza X, gdy zostanie o to poproszony”. Zaletą Func jest leniwe dostarczanie informacji tylko w razie potrzeby. Robię to trochę i polecam z umiarem.
Co do wad:
T
a niektóre sąFunc<T>
.źródło
Weźmy prosty przykład - być może wstrzykujesz sposób logowania.
Wstrzykiwanie klasy
Myślę, że to całkiem jasne, co się dzieje. Co więcej, jeśli używasz kontenera IoC, nie musisz nawet wstrzykiwać niczego jawnie, po prostu dodajesz do katalogu głównego kompozycji:
Podczas debugowania
Worker
programista musi tylko skonsultować się z katalogiem głównym kompozycji, aby ustalić, która konkretna klasa jest używana.Jeśli programista potrzebuje bardziej skomplikowanej logiki, ma cały interfejs do pracy z:
Wstrzyknięcie metody
Po pierwsze należy zauważyć, że parametr konstruktora ma dłuższą nazwę teraz
methodThatLogs
. Jest to konieczne, ponieważ nie możesz powiedzieć, coAction<string>
ma zrobić. Z interfejsem było to całkowicie jasne, ale tutaj musimy uciekać się do polegania na nazywaniu parametrów. Wydaje się to z natury mniej niezawodne i trudniejsze do wyegzekwowania podczas kompilacji.Jak wstrzyknąć tę metodę? Kontener IoC nie zrobi tego za ciebie. Pozostaje ci to jawnie wstrzykiwać, gdy tworzysz instancję
Worker
. Rodzi to kilka problemów:Worker
Worker
stwierdzą, że trudniej jest ustalić, jak wywoływana jest konkretna instancja. Nie mogą po prostu skonsultować się z katalogiem głównym kompozycji; będą musieli prześledzić kod.A może potrzebujemy bardziej skomplikowanej logiki? Twoja technika ujawnia tylko jedną metodę. Teraz przypuszczam, że możesz upiec skomplikowane rzeczy w lambda:
ale kiedy piszesz testy jednostkowe, jak testujesz to wyrażenie lambda? Jest anonimowy, więc środowisko testów jednostkowych nie może go bezpośrednio utworzyć. Może uda ci się wymyślić jakiś sprytny sposób, ale prawdopodobnie będzie to większa PITA niż przy użyciu interfejsu.
Podsumowanie różnic:
Jeśli wszystko jest w porządku, możesz podać tylko tę metodę. W przeciwnym razie sugeruję trzymać się tradycji i wstrzykiwać interfejs.
źródło
Worker
zależności na raz, pozwalając na wstrzykiwanie każdej lambda niezależnie, dopóki wszystkie zależności nie zostaną spełnione. Jest to podobne do częściowego zastosowania w FP. Ponadto użycie lambdas pomaga wyeliminować stan niejawny i ukryte sprzężenie między zależnościami.Rozważ następujący kod, który napisałem dawno temu:
Pobiera względną lokalizację do pliku szablonu, ładuje go do pamięci, renderuje treść wiadomości i składa obiekt e-mail.
Możesz spojrzeć
IPhysicalPathMapper
i pomyśleć: „Jest tylko jedna funkcja. To może byćFunc
”. Ale tak naprawdę problem polega na tym, żeIPhysicalPathMapper
nawet nie powinien istnieć. O wiele lepszym rozwiązaniem jest po prostu parametryzacja ścieżki :Rodzi to wiele innych pytań w celu ulepszenia tego kodu. Np. Może powinien po prostu zaakceptować
EmailTemplate
, a następnie może zaakceptować wstępnie renderowany szablon, a może po prostu powinien być wbudowany.Dlatego nie lubię odwracania kontroli jako wszechobecnego wzorca. Ogólnie uważa się to za boskie rozwiązanie do tworzenia całego kodu. Ale w rzeczywistości, jeśli używasz go w sposób wszechobecny (w przeciwieństwie do oszczędnego), znacznie pogorszy to twój kod , zachęcając cię do wprowadzenia wielu niepotrzebnych interfejsów, które są używane całkowicie wstecz. (Cofając się w tym sensie, że wywołujący powinien być naprawdę odpowiedzialny za ocenę tych zależności i przekazywanie wyniku zamiast wywoływania wywołania przez samą klasę).
Interfejsy powinny być używane oszczędnie, a odwracanie kontroli i wstrzykiwania zależności również powinno być stosowane oszczędnie. Jeśli masz ich duże ilości, twój kod staje się znacznie trudniejszy do odczytania.
źródło