Mam interfejs o nazwie IContext
. W tym celu tak naprawdę nie ma znaczenia, co robi, oprócz następujących czynności:
T GetService<T>();
Metoda ta polega na sprawdzeniu bieżącego kontenera DI aplikacji i próbie rozwiązania zależności. Myślę, że dość standardowy.
W mojej aplikacji ASP.NET MVC mój konstruktor wygląda tak.
protected MyControllerBase(IContext ctx)
{
TheContext = ctx;
SomeService = ctx.GetService<ISomeService>();
AnotherService = ctx.GetService<IAnotherService>();
}
Więc zamiast dodawania wielu parametrów konstruktora dla każdej usługi (bo to będzie naprawdę denerwujące i czasochłonne dla programistów Rozszerzenie stosowania) Używam tej metody, aby uzyskać usług.
Teraz jest źle . Jednak w tej chwili usprawiedliwiam to w mojej głowie - mogę to wyśmiewać .
Mogę. IContext
Testowanie kontrolera nie będzie trudne . W każdym razie musiałbym:
public class MyMockContext : IContext
{
public T GetService<T>()
{
if (typeof(T) == typeof(ISomeService))
{
// return another mock, or concrete etc etc
}
// etc etc
}
}
Ale jak powiedziałem, wydaje się to złe. Wszelkie myśli / nadużycia są mile widziane.
źródło
public SomeClass(Context c)
. Ten kod jest dość jasny, prawda? To stwierdza,that SomeClass
zależy odContext
. Err, ale czekaj, to nie! Zależy tylko od zależności,X
którą uzyskuje z kontekstu. Oznacza to, że za każdym razem, gdy wprowadzasz zmianyContext
, może się zepsućSomeObject
, nawet jeśli zmieniłeś tylkoContext
sY
. Ale tak, wiesz, że tylko zmienioneY
nieX
, więcSomeClass
jest w porządku. Ale pisanie dobrego kodu nie polega na tym , co wiesz, ale na tym, co nowy pracownik wie, kiedy patrzy na twój kod po raz pierwszy.Odpowiedzi:
Posiadanie jednego zamiast wielu parametrów w konstruktorze nie jest problematyczną częścią tego projektu . Tak długo, jak twoja
IContext
klasa jest niczym innym, jak tylko fasada usługi , szczególnie w celu zapewnienia zależności używanych wMyControllerBase
całym kodzie, a nie ogólny lokalizator usług używany w całym kodzie, ta część twojego kodu jest w porządku.Twój pierwszy przykład może zostać zmieniony na
nie byłaby to znacząca zmiana projektu
MyControllerBase
. To, czy ten projekt jest dobry czy zły, zależy tylko od tego, czy chceszTheContext
,SomeService
iAnotherService
zawsze wszystko zainicjowany z makiety obiektów, lub wszystkie z nich z rzeczywistych obiektówZatem użycie tylko jednego parametru zamiast 3 w konstruktorze może być w pełni uzasadnione.
Problemem jest publiczne
IContext
ujawnienieGetService
metody. IMHO powinieneś tego unikać, zamiast tego trzymaj się wyraźnie „metod fabrycznych”. Więc czy będzie w porządku zaimplementować metodyGetSomeService
iGetAnotherService
z mojego przykładu za pomocą lokalizatora usług? IMHO to zależy. Tak długo, jak długoIContext
klasa będzie po prostu tworzyła prostą abstrakcyjną fabrykę, której celem jest dostarczenie jawnej listy obiektów usług, co jest IMHO dopuszczalne. Fabryki abstrakcyjne to zazwyczaj tylko „klej” kod, który sam nie musi być testowany jednostkowo. Niemniej jednak powinieneś zadać sobie pytanie, czy w kontekście metod takich jakGetSomeService
, czy naprawdę potrzebujesz lokalizatora usług, czy też jawne wywołanie konstruktora nie byłoby prostsze.Uważajcie więc, kiedy trzymacie się projektu, w którym
IContext
implementacja jest po prostu owinięciem publicznej, ogólnejGetService
metody, pozwalającej rozwiązać dowolne zależności przez arbitralne klasy, wtedy wszystko stosuje się tak, jak napisał @BenjaminHodgson w swojej odpowiedzi.źródło
GetService
metoda ogólna . Lepsze jest refaktoryzowanie do metod jawnie nazwanych i pisanych na maszynie. Jeszcze lepiej byłobyIContext
wyraźnie określić zależności implementacji w konstruktorze.IValidatableObject
?Ten projekt jest znany jako Service Locator * i nie podoba mi się. Istnieje wiele argumentów przeciwko niemu:
Lokalizator usług łączy Cię z Twoim kontenerem . Używając regularnego wstrzykiwania zależności (gdy konstruktor wyraźnie określa zależności), możesz od razu zastąpić swój kontener innym lub powrócić do
new
wyrażeń. Z twoimIContext
tak naprawdę nie jest to możliwe.Lokalizator usług ukrywa zależności . Jako klient bardzo trudno jest powiedzieć, czego potrzebujesz, aby zbudować instancję klasy. Potrzebujesz jakiegoś rodzaju
IContext
, ale musisz także ustawić kontekst, aby zwrócić poprawne obiekty w celu wykonaniaMyControllerBase
pracy. Nie jest to wcale oczywiste z podpisu konstruktora. Dzięki zwykłemu DI kompilator mówi dokładnie to, czego potrzebujesz. Jeśli twoja klasa ma wiele zależności, powinieneś poczuć ten ból, ponieważ skłoni cię to do refaktoryzacji. Lokalizator usług ukrywa problemy ze złymi projektami.Lokalizator usług powoduje błędy w czasie wykonywania . Jeśli zadzwonisz
GetService
z parametrem złego typu, otrzymasz wyjątek. Innymi słowy, twojaGetService
funkcja nie jest funkcją całkowitą. (Funkcje całkowite są pomysłem ze świata FP, ale w zasadzie oznacza to, że funkcje zawsze powinny zwracać wartość.) Lepiej pozwolić kompilatorowi pomóc i powiedzieć, kiedy pomyliłeś zależności.Lokalizator usług narusza zasadę substytucji Liskowa . Ponieważ jego zachowanie różni się w zależności od argumentu typu, Service Locator może być postrzegany tak, jakby efektywnie dysponował nieskończoną liczbą metod w interfejsie! Ten argument został szczegółowo opisany tutaj .
Lokalizator usług jest trudny do przetestowania . Podałeś przykład podróbki
IContext
do testów, co jest w porządku, ale na pewno lepiej nie pisać tego kodu w pierwszej kolejności. Po prostu wstrzyknij swoje fałszywe zależności bezpośrednio, bez przechodzenia przez lokalizator usług.Krótko mówiąc, po prostu nie rób tego . Wydaje się, że to uwodzicielskie rozwiązanie problemu klas z wieloma zależnościami, ale na dłuższą metę po prostu sprawisz, że twoje życie będzie nieszczęśliwe.
* Definiuję Service Locator jako obiekt za pomocą ogólnej
Resolve<T>
metody, która jest w stanie rozwiązać dowolne zależności i jest używana w całej bazie kodu (nie tylko w katalogu głównym). Nie jest to to samo, co Service Facade (obiekt, który łączy jakiś niewielki znany zestaw zależności) lub Abstract Factory (obiekt, który tworzy instancje jednego typu - typ Abstract Factory może być ogólny, ale metoda nie jest) .źródło
MyControllerBase
nie jest on ani sprzężony z konkretnym kontenerem DI, ani też nie jest to „naprawdę” przykład anty-wzorca Service Locator.GetService<T>
metoda ogólna . Rozwiązywanie arbitralnych zależności to prawdziwy zapach, który jest obecny i poprawny w przykładzie PO.GetService<T>()
metoda zezwala na żądanie dowolnych klas: „Metoda ta polega na spojrzeniu na bieżący kontener DI aplikacji i próbie rozwiązania zależności. Myślę, że dość standardowa”. . Odpowiedziałem na twój komentarz na początku tej odpowiedzi. Jest to w 100% Lokalizator usługNajlepsze argumenty przeciwko anty-wzorcowi Service Locator są wyraźnie stwierdzone przez Marka Seemanna, więc nie będę się zbytnio zastanawiał, dlaczego to zły pomysł - jest to podróż edukacyjna, którą musisz poświęcić trochę czasu na zrozumienie siebie (I również polecam książkę Marka ).
OK, aby odpowiedzieć na pytanie - ponownie określmy swój rzeczywisty problem :
Istnieje pytanie, które rozwiązuje ten problem na StackOverflow . Jak wspomniano w jednym z komentarzy:
Szukasz niewłaściwego miejsca na rozwiązanie swojego problemu. Ważne jest, aby wiedzieć, kiedy klasa robi zbyt wiele. W twoim przypadku mocno podejrzewam, że nie ma potrzeby stosowania „kontrolera podstawowego”. W rzeczywistości w OOP prawie zawsze nie ma potrzeby dziedziczenia . Różnice w zachowaniu i współdzielonej funkcjonalności można osiągnąć całkowicie poprzez odpowiednie użycie interfejsów, co zwykle skutkuje lepiej faktoryzowanym i enkapsulowanym kodem - i nie ma potrzeby przekazywania zależności konstruktorom nadklasy.
We wszystkich projektach, nad którymi pracowałem, w których istnieje Kontroler bazowy, zostało to zrobione wyłącznie w celu udostępnienia dogodnych właściwości i metod, takich jak
IsUserLoggedIn()
iGetCurrentUserId()
. STOP . To jest okropne niewłaściwe wykorzystanie dziedzictwa. Zamiast tego utwórz komponent, który udostępnia te metody i weź zależność od niego tam, gdzie jest potrzebny. W ten sposób komponenty pozostaną testowalne, a ich zależności będą oczywiste.Oprócz czegokolwiek innego, używając wzorca MVC, zawsze zalecałbym chudych kontrolerów . Możesz przeczytać więcej na ten temat tutaj, ale istota wzorca jest prosta, że kontrolery w MVC powinny robić tylko jedno: obsłużyć argumenty przekazane przez środowisko MVC, delegując inne obawy na inny komponent. To znowu jest zasada pojedynczej odpowiedzialności w pracy.
Naprawdę pomocna byłaby znajomość twojego przypadku użycia, aby dokonać dokładniejszej oceny, ale szczerze mówiąc, nie mogę wymyślić żadnego scenariusza, w którym klasa podstawowa byłaby lepsza niż dobrze uwzględnione zależności.
źródło
protected
delegacji jednowierszowych do nowych komponentów. Następnie można stracić BaseController i zastąpienia tychprotected
metod z bezpośrednich połączeń z zależnościami - to uprości swój model i dokonać wszystkich zależności wyraźne (co jest bardzo dobrą rzeczą w projekcie z 100s kontrolerów!)Dodaję do tego odpowiedź na podstawie wkładu wszystkich innych. Wielkie dzięki wszystkim. Najpierw moja odpowiedź: „Nie, nie ma w tym nic złego”.
Odpowiedź doktora Browna na „Service Facade” Przyjąłem tę odpowiedź, ponieważ to, czego szukałem (jeśli odpowiedź brzmiała „nie”), to kilka przykładów lub rozwinięcie tego, co robiłem. Podał to, sugerując, że A) ma nazwę, a B) są prawdopodobnie lepsze sposoby na zrobienie tego.
Odpowiedź Benjamina Hodgsona na „Lokalizator usług” Mimo, że doceniam wiedzę, którą tutaj zdobyłem, to nie jestem „lokalizatorem usług”. Jest to „fasada usług”. Wszystko w tej odpowiedzi jest poprawne, ale nie w moich okolicznościach.
Odpowiedzi USR
Zajmę się tym bardziej szczegółowo:
Nie tracę żadnego oprzyrządowania i nie tracę żadnego „statycznego” pisania. Fasada usługi zwróci to, co skonfigurowałem w moim kontenerze DI lub
default(T)
. A to, co zwraca, jest „wpisywane”. Odbicie jest zamknięte.Z pewnością nie jest to „rzadkie”. Ponieważ używam kontrolera podstawowego , za każdym razem, gdy muszę zmienić jego konstruktor, być może będę musiał zmienić 10, 100, 1000 innych kontrolerów.
Używam zastrzyku zależności. O to chodzi.
I na koniec, komentarz Julesa do odpowiedzi Benjamina nie tracę żadnej elastyczności. To moja fasada usług. Mogę dodać tyle parametrów,
GetService<T>
ile chcę, aby rozróżnić różne implementacje, tak jak zrobiłby to podczas konfigurowania kontenera DI. Na przykład mógłbym zmienić,GetService<T>()
abyGetService<T>(string extraInfo = null)
obejść ten „potencjalny problem”.W każdym razie jeszcze raz dziękuję wszystkim. To było naprawdę przydatne. Twoje zdrowie.
źródło
GetService<T>()
Metoda jest w stanie (próba) rozstrzyga arbitralnych zależnościami . To czyni go lokalizatorem usług, a nie fasadą usług, jak wyjaśniłem w przypisie mojej odpowiedzi. Jeśli miałbyś zastąpić go małym zestawemGetServiceX()
/GetServiceY()
metod, jak sugeruje @DocBrown, byłby to Fasada.IContext
i wstrzyknąć to, a co najważniejsze: jeśli dodasz nową zależność, kod będzie nadal się kompilował - nawet jeśli testy zakończą się niepowodzeniem w czasie wykonywania