Deklarowanie interfejsu w tym samym pliku co klasa podstawowa, czy to dobra praktyka?

29

Aby być wymiennymi i testowalnymi, zwykle usługi logiczne muszą mieć interfejs, np

public class FooService: IFooService 
{ ... }

Pod względem projektowym zgadzam się z tym, ale jedną z rzeczy, która przeszkadza mi w tym podejściu, jest to, że w przypadku jednej usługi musisz zadeklarować dwie rzeczy (klasę i interfejs), aw naszym zespole zwykle dwa pliki (jeden dla klasy i jeden dla interfejsu). Innym dyskomfortem jest trudność w nawigacji, ponieważ użycie „Idź do definicji” w IDE (VS2010) wskaże interfejs (ponieważ inne klasy odnoszą się do interfejsu), a nie rzeczywistą klasę.

Myślałem, że napisanie IFooService w tym samym pliku, co FooService, zmniejszy powyższą dziwność. W końcu IFooService i FooService są bardzo powiązane. Czy to dobra praktyka? Czy istnieje dobry powód, dla którego IFooService musi znajdować się we własnym pliku?

Louis Rhys
źródło
3
Jeśli masz tylko jedną implementację określonego interfejsu, interfejs nie jest potrzebny. Dlaczego nie skorzystać bezpośrednio z tej klasy?
Joris Timmermans,
11
@MadKeithV za luźne połączenie i testowalność?
Louis Rhys,
10
@MadKeithV: masz rację. Ale kiedy piszesz testy jednostkowe dla kodu opartego na IFooService, zazwyczaj udostępniasz MockFooService, który jest drugą implementacją tego interfejsu.
Doc Brown,
5
@DocBrown - jeśli masz wiele implementacji, to oczywiście komentarz zniknie, ale także użyteczność bezpośredniego przejścia do „pojedynczej implementacji” (ponieważ są co najmniej dwie). Następnie pojawia się pytanie: jakie informacje są w konkretnej implementacji, które nie powinny już znajdować się w opisie / dokumentacji interfejsu w miejscu, w którym korzystasz z interfejsu?
Joris Timmermans,
1
Rażąca samo-reklama: stackoverflow.com/questions/5840219/…
Marjan Venema

Odpowiedzi:

24

Nie musi to znajdować się we własnym pliku, ale Twój zespół powinien zdecydować się na standard i trzymać się go.

Masz również rację, że „Przejdź do definicji” prowadzi do interfejsu, ale jeśli masz zainstalowany Resharper , wystarczy jedno kliknięcie, aby otworzyć listę pochodnych klas / interfejsów z tego interfejsu, więc nie jest to wielka sprawa. Dlatego trzymam interfejs w osobnym pliku.

 

Scott Whitlock
źródło
5
Zgadzam się z tą odpowiedzią, w końcu IDE powinno dostosować się do naszego projektu, a nie na odwrót, prawda?
marktani
1
Poza tym bez Resharpera, F12 i Shift + F12 właściwie robią to samo. Nawet kliknięcie prawym przyciskiem myszy :) (szczerze mówiąc, zabierze Cię tylko do bezpośredniego korzystania z interfejsu, nie jestem pewien, co robi wersja Resharper)
Daniel B
@DanielB: W Resharper Alt-End idzie do implementacji (lub przedstawia listę możliwych implementacji do przejścia).
StriplingWarrior
@StriplingWarrior, wygląda na to, że dokładnie to samo robi Vanilla VS. F12 = przejdź do definicji, Shift + F12 = przejdź do implementacji (jestem prawie pewien, że działa bez Resharpera, chociaż mam go zainstalowaną, więc trudno to potwierdzić). To jeden z najbardziej przydatnych skrótów nawigacyjnych, po prostu rozpowszechniam słowo.
Daniel B
@DanielB: Domyślnie Shift-F12 to „znajdź użycie”. jetbrains.com/resharper/webhelp/…
StriplingWarrior
15

Myślę, że powinieneś przechowywać je jako osobne pliki. Jak powiedziałeś, celem jest pozostanie testowalnym i wymiennym. Umieszczając interfejs w tym samym pliku, co implementacja, wiążesz interfejs z konkretną implementacją. Jeśli zdecydujesz się utworzyć próbny obiekt lub inną implementację, interfejs nie będzie logicznie oddzielony od FooService.

Brad S.
źródło
10

Według SOLID, nie tylko należy utworzyć interfejs, i nie tylko powinien on znajdować się w innym pliku, powinien znajdować się w innym zestawie.

Czemu? Ponieważ każda zmiana pliku źródłowego, który kompiluje się w zestawie, wymaga ponownej kompilacji zestawu, a każda zmiana w zestawie wymaga ponownej kompilacji dowolnego zestawu zależnego. Tak więc, jeśli twoim celem, opartym na SOLID, jest możliwość zastąpienia implementacji A implementacją B, podczas gdy klasa C zależna od interfejsu I nie musi znać różnicy, musisz upewnić się, że zestaw z I nie zmienia się, chroniąc w ten sposób zwyczaje.

„Ale to tylko rekompilacja”. Słyszę, że protestujesz. Może i tak, ale w aplikacji na smartfony, co jest łatwiejsze w przypadku przepustowości danych użytkowników; pobieranie jednego pliku binarnego, który się zmienił, czy pobieranie tego pliku binarnego i pięciu innych z zależnym od niego kodem? Nie każdy program jest napisany do użytku przez komputery stacjonarne w sieci LAN. Nawet w takim przypadku, gdy przepustowość i pamięć są tanie, mniejsze wydania poprawek mogą mieć wartość, ponieważ są trywialne w przekazywaniu do całej sieci LAN za pośrednictwem Active Directory lub podobnych warstw zarządzania domeną; Twoi użytkownicy będą czekać tylko kilka sekund, aż zostanie zastosowany przy następnym logowaniu, zamiast kilku minut, aby cała instalacja została ponownie zainstalowana. Nie wspominając o tym, że im mniej zestawów należy ponownie skompilować podczas budowania projektu, tym szybciej będzie on budował,

Zastrzeżenie: Nie zawsze jest to możliwe lub wykonalne. Najłatwiej to zrobić, tworząc scentralizowany projekt „interfejsów”. Ma to swoje wady; kod staje się mniej przydatny, ponieważ do projektu interfejsu ORAZ do projektu implementacji należy odwoływać się w innych aplikacjach, ponownie wykorzystując warstwę trwałości lub inne kluczowe komponenty aplikacji. Możesz rozwiązać ten problem, dzieląc interfejsy na bardziej ciasno połączone zespoły, ale wtedy masz więcej projektów w swojej aplikacji, co sprawia, że ​​pełna kompilacja jest bardzo bolesna. Kluczem jest równowaga i utrzymanie luźno sprzężonej konstrukcji; zazwyczaj możesz przenosić pliki w razie potrzeby, więc gdy zobaczysz, że klasa będzie wymagała wielu zmian, lub że nowe implementacje interfejsu będą potrzebne regularnie (być może do połączenia z nowo obsługiwanymi wersjami innego oprogramowania,

KeithS
źródło
8

Dlaczego oddzielne pliki są niewygodne? Dla mnie jest o wiele ładniejszy i czystszy. Powszechne jest tworzenie podfolderu o nazwie „Interfejsy” i umieszczanie tam plików IFooServer.cs, jeśli chcesz zobaczyć mniej plików w Eksploratorze rozwiązań.

Interfejs jest zdefiniowany we własnym pliku z tego samego powodu, dla którego klasy są zwykle definiowane we własnym pliku: zarządzanie projektem jest prostsze, gdy struktura logiczna i struktura plików są identyczne, więc zawsze wiesz, który plik jest zdefiniowany dla danej klasy w. Może to ułatwić Ci życie podczas debugowania (ślady stosu wyjątków zwykle dają numery plików i wierszy) lub podczas scalania kodu źródłowego w repozytorium kontroli źródła.

Avner Shahar-Kashtan
źródło
6

Często dobrą praktyką jest, aby pliki kodu zawierały tylko jedną klasę lub jeden interfejs. Ale te praktyki kodowania są środkiem do celu - lepszej struktury kodu i ułatwienia pracy. Jeśli ty i twój zespół będziecie łatwiej pracować, jeśli klasy będą trzymane razem z ich interfejsami, to na pewno to zrobicie.

Osobiście wolę mieć interfejs i klasę w tym samym pliku, gdy jest tylko jedna klasa, która implementuje interfejs, na przykład w twoim przypadku.

Jeśli chodzi o problemy z nawigacją, mogę gorąco polecić ReSharper . Zawiera kilka bardzo przydatnych skrótów do bezpośredniego przejścia do metody implementującej określoną metodę interfejsu.

Pete
źródło
3
+1 za wskazanie, że praktyki zespołu powinny dyktować strukturę plików oraz za podejście sprzeczne z normą.
3

Wiele razy chcesz, aby Twój interfejs znajdował się nie tylko w osobnym pliku dla klasy, ale nawet w osobnym zestawie .

Na przykład interfejs umowy serwisowej WCF może być współdzielony zarówno przez klienta, jak i usługę, jeśli masz kontrolę nad oboma końcami przewodu. Przenosząc interfejs do własnego zestawu, będzie on miał mniej własnych zależności zestawu. To znacznie ułatwia klientowi konsumpcję, poluzowując sprzężenie z jego implementacją.

StuartLC
źródło
2

Rzadko, jeśli w ogóle, ma sens jedna implementacja interfejsu 1 . Jeśli umieścisz interfejs publiczny i klasę publiczną implementującą ten interfejs w tym samym pliku, istnieje duże prawdopodobieństwo, że nie potrzebujesz interfejsu.

Gdy klasa, którą kolokujesz z interfejsem, jest abstrakcyjna i wiesz, że wszystkie implementacje interfejsu powinny dziedziczyć tę klasę abstrakcyjną, zlokalizowanie dwóch w tym samym pliku ma sens. Nadal powinieneś przeanalizować swoją decyzję o użyciu interfejsu: zadaj sobie pytanie, czy sama klasa abstrakcyjna byłaby w porządku, i porzuć interfejs, jeśli odpowiedź jest twierdząca.

Ogólnie jednak powinieneś trzymać się strategii „jedna klasa publiczna / interfejs odpowiada jednemu plikowi”: jest łatwa do naśladowania i ułatwia nawigację w drzewie źródłowym.


1 Jednym z godnych uwagi wyjątków jest konieczność posiadania interfejsu do celów testowych, ponieważ wybór frameworku nakłada dodatkowe wymagania na kod.

dasblinkenlight
źródło
3
W rzeczywistości dość normalnym wzorcem jest posiadanie jednego interfejsu dla jednej klasy w .NET, ponieważ pozwala testom jednostkowym zastępować zależności próbnymi, stubami, spys lub innymi podwójnymi testami.
Pete,
2
@Pete Zredagowałem swoją odpowiedź, aby dołączyć to jako notatkę. Nie nazwałbym tego wzorca „normalnym”, ponieważ pozwala on testowalności dotyczyć „przecieku” do głównej bazy kodu. Nowoczesne frameworki próbne pomagają w dużym stopniu rozwiązać ten problem, ale posiadanie interfejsu zdecydowanie upraszcza wiele rzeczy.
dasblinkenlight,
2
@KeithS Klasa bez interfejsu powinna być w twoim projekcie tylko wtedy, gdy masz całkowitą pewność, że podklasę nie ma sensu i tworzysz tę klasę sealed. Jeśli podejrzewasz, że w przyszłości możesz dodać drugą podklasę, od razu umieść interfejs. PS Przyzwoity asystent refaktoryzacji może być twój po skromnej cenie jednej licencji
ReSharper
1
@KeithS I tak, wszystkie twoje klasy, które nie są specjalnie zaprojektowane do podklas, powinny być zapieczętowane. W rzeczywistości klasy życzeń były domyślnie zapieczętowane, zmuszając cię do jawnego wyznaczenia klas do dziedziczenia, w taki sam sposób, w jaki język obecnie zmusza cię do wyznaczenia funkcji do zastąpienia przez oznaczenie ich virtual.
dasblinkenlight,
2
@KeithS: Co się stanie, gdy jedna implementacja stanie się dwiema? To samo, co w przypadku zmiany wdrożenia; zmieniasz kod, aby odzwierciedlić tę zmianę. Obowiązuje tutaj YAGNI. Nigdy nie dowiesz się, co się zmieni w przyszłości, więc zrób najprostszą możliwą rzecz. (Niestety testowanie miesza się z tą maksymą)
Groky
2

Interfejsy należą do ich klientów, a nie do ich implementacji, zgodnie z definicją zwinnych zasad, praktyk i wzorców autorstwa Roberta C. Martina. Zatem połączenie interfejsu i implementacji w tym samym miejscu jest sprzeczne z jego zasadą.

Kod klienta zależy od interfejsu. Umożliwia to kompilację i wdrożenie kodu klienta i interfejsu bez implementacji. Następnie możesz mieć różne implementacje działające jak wtyczki do kodu klienta.

AKTUALIZACJA: To nie jest zasada zwinna. Gang czterech wzorców projektowych już w 94 roku mówi już o klientach stosujących się do interfejsów i programujących interfejsy. Ich pogląd jest podobny.

Patkos Csaba
źródło
1
Korzystanie z interfejsów wykracza daleko poza domenę, którą posiada Agile. Agile to metodologia programistyczna, która wykorzystuje konstrukcje językowe w określony sposób. Interfejs to konstrukcja językowa.
1
Tak, ale interfejs nadal należy do klienta, a nie do implementacji, niezależnie od tego, czy mówimy o zwinnym czy nie.
Patkos Csaba,
Jestem z tobą w tej sprawie.
Marjan Venema,
0

Ja osobiście nie lubię „ja” przed interfejsem. Jako użytkownik takiego typu nie chcę widzieć, że jest to interfejs, czy nie. Typ jest interesujący. Pod względem zależności FooService to JEDNA możliwa implementacja IFooService. Interfejs powinien znajdować się w pobliżu miejsca, w którym jest używany. Z drugiej strony wdrożenie powinno odbywać się w miejscu, w którym można je łatwo zmienić bez wpływu na klienta. Wolałbym więc dwa pliki - normalnie.

ollins
źródło
2
Jako użytkownik chcę wiedzieć, czy typ jest interfejsem, ponieważ na przykład oznacza to, że nie mogę newz nim korzystać, ale z drugiej strony mogę go używać w różnych wariantach. I Ijest uznanym konwencja nazewnictwa w .NET, więc jest to dobry powód, aby stosować się do niego, jeśli tylko pod kątem spójności z innymi rodzajami.
svick,
Rozpoczęcie Iw interfejsie było jedynie wstępem do moich argumentów dotyczących rozdzielenia dwóch plików. Kod jest lepiej czytelny i sprawia, że ​​odwracanie zależności jest bardziej wyraźne.
ollins
4
Czy ci się to podoba, czy nie, użycie „I” przed nazwą interfejsu jest de facto standardem w .NET. Nieprzestrzeganie standardu w projekcie .NET byłoby moim zdaniem naruszeniem „zasady najmniejszego zdziwienia”.
Pete,
Chodzi mi o: oddzielne w dwóch plikach. Jeden do abstrakcji i jeden do wdrożenia. Jeśli użycie Iw twoim otoczeniu jest normalne: użyj go! (Ale mi się nie podoba :))
ollins
A w przypadku P.SE przedrostek I przed interfejsem sprawia, że ​​bardziej oczywiste jest, że plakat mówi o interfejsie, a nie dziedziczy po innej klasie.
0

Kodowanie interfejsów może być złe, w rzeczywistości jest traktowane jako anty-wzorzec dla niektórych kontekstów i nie jest prawem. Zwykle dobrym przykładem kodowania anty-wzorców interfejsów jest posiadanie interfejsu, który ma tylko jedną implementację w Twojej aplikacji. Innym byłoby, gdyby plik interfejsu stał się tak uciążliwy, że trzeba ukryć jego deklarację w pliku zaimplementowanej klasy.

Shivan Dragon
źródło
posiadanie tylko jednej implementacji w interfejsie niekoniecznie jest złą rzeczą; można chronić aplikację przed zmianą technologii. Na przykład interfejs technologii utrwalania danych. Miałbyś tylko jedną implementację dla obecnej technologii utrwalania danych, jednocześnie chroniąc aplikację, gdyby to się zmieniło. Tak więc w tym czasie byłby tylko jeden chochlik.
TSmith
0

Umieszczenie klasy i interfejsu w tym samym pliku nie nakłada żadnych ograniczeń na to, jak można z nich korzystać. Interfejs może być nadal używany do próbnych prób itp. W ten sam sposób, nawet jeśli znajduje się w tym samym pliku co klasa, która go implementuje. Pytanie to można postrzegać wyłącznie jako wygodę organizacyjną, w którym to przypadku powiedziałbym, że rób wszystko, co czyni twoje życie łatwiejszym!

Oczywiście można argumentować, że posiadanie dwóch w tym samym pliku może sprawić, że nieostrożni uznają je za bardziej sprzężone programowo niż są w rzeczywistości, co stanowi ryzyko.

Osobiście, gdybym natknął się na bazę kodów, która korzystała z jednej konwencji nad drugą, nie byłbym zbytnio zaniepokojony.

Holf
źródło
To nie tylko „wygoda organizacyjna” ... to także kwestia zarządzania ryzykiem, wdrażania mgmt, śledzenia kodu, kontroli zmian, szumu kontroli źródła, bezpieczeństwa mgmt. Istnieje wiele punktów widzenia na to pytanie.
TSmith