Kiedy używać interfejsów (testy jednostkowe, IoC?)

17

Podejrzewam, że popełniłem tutaj błąd ucznia i szukam wyjaśnienia. Wiele klas w moim rozwiązaniu (C #) - śmiem twierdzić, że większość - skończyłem pisać odpowiedni interfejs. Np. Interfejs „ICalculator” i klasa „Calculator”, która go implementuje, nawet jeśli nigdy nie zastąpię tego kalkulatora inną implementacją. Ponadto większość z tych klas znajduje się w tym samym projekcie co ich zależności - tak naprawdę muszą być internal, ale ostatecznie okazały się publicefektem ubocznym implementacji ich interfejsów.

Myślę, że ta praktyka tworzenia interfejsów do wszystkiego wynikała z kilku kłamstw:

1) Początkowo myślałem, że interfejs jest niezbędny do tworzenia próbnych testów jednostkowych (używam Moq), ale od tego czasu odkryłem, że klasa może być wyśmiewana, jeśli jej członkowie są virtual, i ma konstruktor bez parametrów (popraw mnie, jeśli Nie mam racji).

2) Początkowo myślałem, że interfejs jest niezbędny do zarejestrowania klasy w frameworku IoC (Castle Windsor), np

Container.Register(Component.For<ICalculator>().ImplementedBy<Calculator>()...

kiedy w rzeczywistości mógłbym po prostu zarejestrować konkretny typ dla siebie:

Container.Register(Component.For<Calculator>().ImplementedBy<Calculator>()...

3) Zastosowanie interfejsów, np. Parametrów konstruktora do wstrzykiwania zależności, powoduje „luźne sprzężenie”.

Więc oszalałem na punkcie interfejsów ?! Zdaję sobie sprawę ze scenariuszy, w których „normalnie” używałbyś interfejsu, np. Ujawniając publiczny interfejs API lub do takich rzeczy, jak „podłączalna” funkcjonalność. Moje rozwiązanie ma niewielką liczbę klas, które pasują do takich przypadków użycia, ale zastanawiam się, czy wszystkie inne interfejsy są niepotrzebne i czy powinny zostać usunięte? Czy w związku z pkt 3) nie naruszyłbym „luźnego sprzężenia”, gdybym to zrobił?

Edycja : - Właśnie gram z Moq i wydaje się, że wymaga to metod publicznych i wirtualnych oraz publicznego konstruktora bez parametrów, aby móc się z nich kpić. Więc wygląda na to, że nie mogę mieć klas wewnętrznych?

Andrew Stephens
źródło
Jestem prawie pewien, C # nie wymaga, aby upublicznić klasy w celu wykonania interfejsu, nawet jeśli interfejs jest publiczny ...
vaughandroid
1
Nie należy testować prywatnych członków . Można to postrzegać jako wskaźnik anty-wzorca klasy boga. Jeśli czujesz potrzebę jednostkowego testowania prywatnej funkcjonalności, zwykle dobrym pomysłem jest zamknięcie tej funkcjonalności we własną klasę.
Spoike,
@Spoike, o którym mowa, to projekt interfejsu użytkownika, w którym wydaje się naturalne, aby wszystkie klasy były wewnętrzne, ale nadal wymagają one testów jednostkowych? Czytałem gdzie indziej o tym, że nie muszę testować wewnętrznych metod, ale nigdy nie zrozumiałem powodów, dla których to zrobiłem.
Andrew Stephens,
Najpierw zadaj sobie pytanie, jaka jest testowana jednostka. Czy to cała klasa czy metoda? Jednostka ta musi być publiczna, aby mogła zostać poprawnie przetestowana . W projektach interfejsu użytkownika zwykle rozdzielam testowalną logikę na kontrolery / modele widoków / usługi, które wszystkie mają metody publiczne. Rzeczywiste widoki / widżety są podłączone do kontrolerów / modeli widoków / usług i testowane poprzez regularne testy dymu (tj. Uruchom aplikację i kliknij kliknięcie). Możesz mieć scenariusze, które powinny przetestować podstawową funkcjonalność, a przeprowadzenie testów jednostkowych w widżetach prowadzi do kruchych testów i należy to zrobić ostrożnie.
Spoike,
@Spoike, więc czy upubliczniasz swoje metody maszyn wirtualnych, aby były dostępne dla naszych testów jednostkowych? Być może właśnie tam się mylę, starając się, aby wszystko było wewnętrzne. Myślę, że moje ręce mogą być związane z Moq, co wydaje się wymagać publicznych (nie tylko metod) klas, aby się z nich kpić (zobacz mój komentarz do odpowiedzi Telastyna).
Andrew Stephens,

Odpowiedzi:

7

Ogólnie rzecz biorąc, jeśli tworzysz interfejs, który ma tylko jeden implementator, piszesz rzeczy dwa razy i marnujesz swój czas. Same interfejsy nie zapewniają luźnego sprzężenia, jeśli są ściśle powiązane z jedną implementacją ... To powiedziawszy, jeśli chcesz kpić z tych rzeczy w testach jednostkowych, jest to zwykle świetny znak, że nieuchronnie potrzebujesz więcej niż jednego implementatora w rzeczywistości kod.

Uważam, że to trochę zapach kodu, jeśli prawie wszystkie twoje klasy mają interfejsy. Oznacza to, że prawie wszyscy pracują ze sobą w taki czy inny sposób. Podejrzewam, że klasy robią za dużo (ponieważ nie ma klas pomocniczych) lub że za dużo abstrahujesz (och, chcę interfejsu dostawcy grawitacji, ponieważ to może się zmienić!).

Telastyn
źródło
1
Dziękuję za odpowiedź. Jak wspomniano, istnieje kilka błędnych powodów, dla których zrobiłem to, co zrobiłem, chociaż wiedziałem, że większość interfejsów nigdy nie miałaby więcej niż jednej implementacji. Część problemu polega na tym, że używam frameworku Moq, który jest świetny do kpienia z interfejsów, ale aby kpić z klasy, musi ona być publiczna, mieć publiczną parametryczną ctr, a metody, które mają być kpowane, muszą być „publicznie wirtualne”).
Andrew Stephens,
Większość klas, które testuję jednostkowo, ma charakter wewnętrzny i nie chcę ich upubliczniać tylko po to, aby były testowalne (chociaż można się spierać, po to właśnie napisałem te wszystkie interfejsy). Jeśli pozbędę się moich interfejsów, prawdopodobnie będę musiał znaleźć nową ramkę próbną!
Andrew Stephens,
1
@AndrewStephens - nie wszystko na świecie wymaga kpin. Jeśli klasy są wystarczająco solidne (lub ściśle ze sobą powiązane), aby nie potrzebować interfejsów, są wystarczająco solidne, aby można było je wykorzystać w testach jednostkowych w stanie „jak jest”. Czas / złożoność, która je przesuwa, nie są warte korzyści z testowania.
Telastyn
-1, ponieważ często nie wiesz, ile implementatorów potrzebujesz przy pierwszym pisaniu kodu. Jeśli używasz interfejsu od samego początku, nie musisz zmieniać klientów podczas pisania dodatkowych implementatorów.
Wilbert
1
@wilbert - jasne, mały interfejs jest bardziej czytelny niż klasa, ale nie wybierasz jednego lub drugiego, masz klasę lub klasę i interfejs. I możesz czytać za dużo na moją odpowiedź. Nie twierdzę, że nigdy nie należy tworzyć interfejsów dla pojedynczej implementacji - większość powinna w rzeczywistości, ponieważ większość interakcji powinna wymagać takiej elastyczności. Ale przeczytaj to pytanie jeszcze raz. Stworzenie interfejsu dla wszystkiego to po prostu hodowla ładunków. IoC nie jest powodem. Drwiny nie są powodem. Wymagania są powodem do wprowadzenia tej elastyczności.
Telastyn
4

1) Nawet jeśli konkretne klasy są możliwe do wyśmiewania, nadal wymaga to utworzenia członków virtuali zapewnienia konstruktora bez parametrów, który może być natarczywy i niepożądany. Ty (lub nowi członkowie zespołu) wkrótce przekonasz się, że systematycznie dodajesz virtualkonstruktorów bez parametrów i bez parametrów w każdej nowej klasie bez zastanowienia, tylko dlatego, że „tak to działa”.

Interfejs jest IMO o wiele lepszym pomysłem, gdy trzeba kpić z zależności, ponieważ pozwala ona odroczyć implementację do momentu, gdy naprawdę jest to potrzebne, i stanowi miły jasny kontrakt zależności.

3) Jak to jest fałsz?

Uważam, że interfejsy świetnie nadają się do definiowania protokołów przesyłania wiadomości między współpracującymi obiektami. Mogą zdarzyć się przypadki, gdy ich potrzeba jest dyskusyjna, na przykład w przypadku podmiotów domeny . Jednak wszędzie tam, gdzie masz stabilnego partnera (tj. Wstrzykniętą zależność w przeciwieństwie do przejściowego odniesienia), z którym musisz się komunikować, ogólnie powinieneś rozważyć użycie interfejsu.

guillaume31
źródło
3

Ideą IoC jest zamiana typów betonu na wymienne. Więc nawet jeśli (teraz) masz tylko jeden kalkulator, który implementuje ICalculator, druga implementacja może zależeć tylko od interfejsu, a nie od szczegółów implementacji z Kalkulatora.

Dlatego pierwsza wersja rejestracji kontenera IoC jest poprawna i ta powinna być używana w przyszłości.

Jeśli uczynisz swoje klasy publicznymi lub wewnętrznymi, nie będzie to tak naprawdę powiązane, podobnie jak poruszanie się.

Wilbert
źródło
2

Byłbym bardziej zmartwiony faktem, że wydaje ci się, że musisz „kpić” z każdego rodzaju projektu.

Podwójne testy są najbardziej przydatne do izolowania kodu od świata zewnętrznego (biblioteki innych firm, dyskowe operacje we / wy, sieciowe operacje we / wy itp.) Lub od naprawdę drogich obliczeń. Będąc zbyt liberalnym z ramą izolacji (czy naprawdę NAPRAWDĘ tego potrzebujesz? Czy Twój projekt jest tak złożony?) Może łatwo prowadzić do testów powiązanych z implementacją i czyni projekt bardziej sztywnym. Jeśli napiszesz kilka testów, które sprawdzają, czy jakaś klasa o nazwie metoda X i Y w tej kolejności, a następnie przekazała parametry a, b i c do metody Z, czy NAPRAWDĘ otrzymujesz wartość? Czasami dostajesz takie testy podczas testowania rejestrowania lub interakcji z bibliotekami stron trzecich, ale powinien to być wyjątek, a nie reguła.

Jak tylko twoja struktura izolacji mówi ci, że musisz upublicznić rzeczy i że zawsze musisz mieć bezparametrowe konstruktory, nadszedł czas, aby uciec od uniku, ponieważ w tym momencie twoja struktura zmusza cię do pisania złego (gorszego) kodu.

Mówiąc bardziej szczegółowo o interfejsach, uważam, że są one przydatne w kilku kluczowych przypadkach:

  • Kiedy musisz wywołać wywołania metod do różnych typów, np. Gdy masz wiele implementacji (jest to oczywiste, ponieważ definicja interfejsu w większości języków jest tylko zbiorem metod wirtualnych)

  • Gdy musisz być w stanie oddzielić resztę kodu od klasy. Jest to normalny sposób oddzielania systemu plików i dostępu do sieci oraz bazy danych i tak dalej od podstawowej logiki aplikacji.

  • Gdy potrzebujesz odwrócenia kontroli w celu ochrony granic aplikacji. Jest to jeden z najpotężniejszych sposobów zarządzania zależnościami. Wszyscy wiemy, że moduły wysokiego poziomu i moduły niskiego poziomu nie powinny wiedzieć o sobie nawzajem, ponieważ będą się one zmieniać z różnych powodów, a Ty NIE chcesz mieć zależności od HttpContext w swoim modelu domeny lub od niektórych ram renderowania Pdf w bibliotece mnożenia macierzy . Zmuszenie klas granicznych do implementacji interfejsów (nawet jeśli nigdy nie są one nigdy zastępowane) pomaga rozluźnić sprzężenie między warstwami i drastycznie zmniejsza ryzyko przenikania przez warstwy zależności od typów znających zbyt wiele innych typów.

sara
źródło
1

Skłaniam się ku idei, że jeśli jakaś logika jest prywatna, to implementacja tej logiki nie powinna dotyczyć nikogo i jako taka nie powinna być testowana. Jeśli jakaś logika jest na tyle złożona, że ​​chcesz ją przetestować, prawdopodobnie ma ona odrębną rolę i dlatego powinna być publiczna. Np. Jeśli wyodrębnię kod w metodzie prywatnego pomocnika, stwierdzę, że zwykle jest to coś, co równie dobrze może zajmować osobną klasę publiczną (formater, parser, maper, cokolwiek).
Jeśli chodzi o interfejsy, pozwalam sobie na konieczność kiknięcia lub wyśmiewania się z klasy. Rzeczy takie jak repo, zazwyczaj chcę być w stanie stubować lub kpić. Maper w teście integracji, (zwykle) nie tak bardzo.

Stefan Billiet
źródło