Dwie sprzeczne definicje zasady segregacji interfejsu - która z nich jest poprawna?

14

Podczas czytania artykułów o ISP wydaje się, że istnieją dwie sprzeczne definicje ISP:

Zgodnie z pierwszą definicją (patrz 1 , 2 , 3 ), ISP stwierdza, że ​​klasy implementujące interfejs nie powinny być zmuszane do implementacji funkcji, których nie potrzebują. Tak więc interfejs tłuszczuIFat

interface IFat
{
     void A();
     void B();
     void C();
     void D();
}

class MyClass: IFat
{ ... }

powinny być podzielone na mniejsze interfejsy ISmall_1iISmall_2

interface ISmall_1
{
     void A();
     void B();
}

interface ISmall_2
{
     void C();
     void D();
}

class MyClass:ISmall_2
{ ... }

ponieważ w ten sposób mój MyClassjest w stanie zaimplementować tylko te metody, których potrzebuje ( D()i C()), nie będąc zmuszonym do zapewnienia również implementacji fikcyjnych A(), B()oraz C():

Ale zgodnie z drugą definicją (patrz 1 , 2 , odpowiedź Nazara Merzy ), ISP stwierdza, że MyClientwywołanie metod MyServicenie powinno być świadome metod MyService, których nie potrzebuje. Innymi słowy, jeśli MyClientpotrzebuje tylko funkcji C()i D(), a następnie zamiast

class MyService 
{
    public void A();
    public void B();
    public void C();
    public void D();
}

/*client code*/      
MyService service = ...;
service.C(); 
service.D();

powinniśmy segregować MyService'smetody na interfejsy specyficzne dla klienta :

public interface ISmall_1
{
     void A();
     void B();
}

public interface ISmall_2
{
     void C();
     void D();
}

class MyService:ISmall_1, ISmall_2 
{ ... }

/*client code*/
ISmall_2 service = ...;
service.C(); 
service.D();

Tak więc, zgodnie z pierwszą definicją, celem ISP jest „ ułatwienie życia klasom implementującym interfejs IFat ”, podczas gdy w drugim przypadku celem ISP jest „ ułatwienie życia klientom wywołującym metody MyService ”.

Która z dwóch różnych definicji dostawcy usług internetowych jest rzeczywiście poprawna?

@MARJAN VENEMA

1.

Kiedy więc zamierzasz podzielić IFat na mniejszy interfejs, jakie metody ostatecznie decydują o wyborze ISmallinterface na podstawie spójności elementów.

Chociaż sensowne jest umieszczenie spójnych metod w tym samym interfejsie, pomyślałem, że przy wzorcu ISP potrzeby klienta mają pierwszeństwo przed „spójnością” interfejsu. Innymi słowy, pomyślałem, że z ISP powinniśmy skupić w tym samym interfejsie metody potrzebne konkretnym klientom, nawet jeśli oznacza to pominięcie tego interfejsu tych metod, które ze względu na spójność powinny być również umieszczone w tym samym interfejsie?

Tak więc, jeśli było wielu klientów, którzy będą musieli tylko zadzwonić CutGreens, ale nie również GrillMeat, to aby zastosować się do wzorca ISP, powinniśmy tylko umieścić w CutGreensśrodku ICook, ale nie również GrillMeat, mimo że te dwie metody są bardzo spójne ?!

2)

Myślę, że twoje zamieszanie wynika z ukrytego założenia z pierwszej definicji: że klasy wdrażające przestrzegają już zasady pojedynczej odpowiedzialności.

Przez „wdrażanie klas nie przestrzegających SRP” masz na myśli te klasy, które implementują IFatlub klasy, które implementują ISmall_1/ ISmall_2? Zakładam, że masz na myśli klasy, które implementują IFat? Jeśli tak, to dlaczego zakładasz, że jeszcze nie przestrzegają SRP?

dzięki

EdvRusj
źródło
4
Dlaczego nie może istnieć wiele definicji obsługiwanych przez tę samą zasadę?
Bobson,
5
Te definicje nie są ze sobą sprzeczne.
Mike Partridge
1
Nie, oczywiście, potrzeba klienta nie ma pierwszeństwa przed spójnością interfejsu. Możesz przejść tę „zasadę” daleko i skończyć z interfejsami z pojedynczą metodą w całym miejscu, które nie mają absolutnie żadnego sensu. Przestań przestrzegać zasad i zacznij myśleć o celach, dla których reguły te zostały utworzone. Z „klasami nie przestrzegającymi SRP” nie mówiłem o żadnych konkretnych klasach w twoim przykładzie ani o tym, że nie przestrzegają już SRP. Przeczytaj ponownie. Pierwsza definicja prowadzi do podziału interfejsu tylko wtedy, gdy interfejs nie jest zgodny z ISP, a klasa jest zgodna z SRP.
Marjan Venema
2
Druga definicja nie dotyczy implementatorów. Definiuje interfejsy z perspektywy osób wywołujących i nie przyjmuje żadnych założeń dotyczących tego, czy implementatory już istnieją. Prawdopodobnie zakłada się, że obserwując ISP i wdrażając te interfejsy, będziesz oczywiście przestrzegać SRP podczas ich tworzenia.
Marjan Venema
2
Skąd wiesz, którzy klienci będą istnieć i jakich metod będą potrzebować? Nie możesz To, co możesz wiedzieć wcześniej, to spójność interfejsu.
Tulains Córdova

Odpowiedzi:

6

Obydwa są prawidłowe

Sposób, w jaki go czytam, ma na celu utrzymanie małych interfejsów i koncentracji interfejsów: wszyscy członkowie interfejsu powinni mieć bardzo wysoką spójność. Obie definicje mają na celu uniknięcie interfejsów typu „jack-of-all-trade-master-of-none”.

Segregacja interfejsu i SRP (zasada pojedynczej odpowiedzialności) mają ten sam cel: zapewnienie małych, wysoce spójnych komponentów oprogramowania. Uzupełniają się. Segregacja interfejsów zapewnia, że ​​interfejsy są małe, skoncentrowane i bardzo spójne. Przestrzeganie zasady pojedynczej odpowiedzialności gwarantuje, że zajęcia są małe, skoncentrowane i bardzo spójne.

Pierwsza wymieniona definicja dotyczy implementatorów, druga klientów. Co, w przeciwieństwie do @ user61852, uważam za użytkowników / wywołujących interfejs, a nie implementatorów.

Myślę, że twoje zamieszanie wynika z ukrytego założenia z pierwszej definicji: że klasy wdrażające przestrzegają już zasady pojedynczej odpowiedzialności.

Dla mnie druga definicja, w której klientami są osoby wywołujące interfejs, jest lepszym sposobem na osiągnięcie zamierzonego celu.

Segregowanie

W swoim pytaniu stwierdzasz:

ponieważ w ten sposób moja MyClass jest w stanie zaimplementować tylko te metody, których potrzebuje (D () i C ()), nie będąc zmuszonym do zapewniania również fałszywych implementacji dla A (), B () i C ():

Ale to wywraca świat do góry nogami.

  • Klasa implementująca interfejs nie określa, czego potrzebuje w interfejsie, który implementuje.
  • Interfejsy określają, jakie metody powinna zapewnić klasa implementująca.
  • Osoby wywołujące interfejs naprawdę decydują o tym, jakiej funkcjonalności potrzebują interfejs, aby je zapewnić, a tym samym to, co powinien zapewnić dostawca.

Kiedy więc zamierzasz podzielić się IFatna mniejszy interfejs, które metody ostatecznie ISmalldecydują o tym, który interfejs powinien zostać wybrany na podstawie spójności elementów.

Rozważ ten interfejs:

interface IEverythingButTheKitchenSink
{
     void DoDishes();
     void CleanSink();
     void CutGreens();
     void GrillMeat();
}

Jakie metody byś zastosował ICooki dlaczego? Czy CleanSinkpołączyłbyś to GrillMeattylko dlatego, że akurat masz klasę, która to robi i kilka innych rzeczy, ale nic podobnego do żadnej z innych metod? Czy podzieliłbyś go na dwa bardziej spójne interfejsy, takie jak:

interface IClean
{
     void DoDishes();
     void CleanSink();
}

interface ICook
{
     void CutGreens();
     void GrillMeat();
}

Uwaga dotycząca deklaracji interfejsu

Definicja interfejsu powinna najlepiej znajdować się w oddzielnej jednostce, ale jeśli absolutnie musi żyć z osobą wywołującą lub implementującą, tak naprawdę powinna być z osobą wywołującą. W przeciwnym razie program wywołujący uzyskuje bezpośrednią zależność od implementatora, co całkowicie eliminuje przeznaczenie interfejsów. Zobacz także: Deklarowanie interfejsu w tym samym pliku co klasa podstawowa, czy to dobra praktyka? o programistach i dlaczego powinniśmy umieszczać interfejsy z klasami, które ich używają, a nie tymi, które je implementują? na StackOverflow.

Marjan Venema
źródło
1
Czy widzisz aktualizację, którą dokonałem?
EdvRusj
„osoba dzwoniąca uzyskuje natychmiastową zależność od implementatora ” ... tylko wtedy, gdy naruszysz DIP (zasada inwersji zależności), jeśli wewnętrzne zmienne, parametry, zwracane wartości itp. są typem ICookzamiast typu SomeCookImplementor, jak nakazuje DIP, to nie robi tego nie musisz na tym polegać SomeCookImplementor.
Tulains Córdova
@ user61852: Jeśli deklaracja interfejsu i implementator znajdują się w tej samej jednostce, natychmiast uzyskuję zależność od tego implementatora. Niekoniecznie w czasie wykonywania, ale z pewnością na poziomie projektu, po prostu dlatego, że tam jest. Projekt nie może się już bez niego kompilować ani używać. Ponadto wstrzykiwanie zależności nie jest tym samym, co zasada odwrócenia zależności. Możesz być zainteresowany DIP na wolności
Marjan Venema
Użyłem ponownie przykładów kodu w tym pytaniu programmers.stackexchange.com/a/271142/61852 , ulepszając go po tym, jak został już zaakceptowany. Dałem ci należne uznanie za przykłady.
Tulains Córdova
Cool @ user61852 :) (i dzięki za uznanie)
Marjan Venema
14

Mylisz słowo „klient” użyte w dokumentach Gang of Four z „klientem”, jak w przypadku konsumenta usługi.

„Klient”, zgodnie z zamiarem definicji Gang of Four, to klasa implementująca interfejs. Jeśli klasa A implementuje interfejs B, oznacza to, że A jest klientem B. W przeciwnym razie wyrażenie „klienci nie powinni być zmuszani do implementowania interfejsów, których nie używają” nie miałoby sensu, ponieważ „klienci” (jak w przypadku konsumentów) nie niczego nie implementować. Fraza ma sens tylko wtedy, gdy widzisz „klient” jako „implementator”.

Jeśli „klient” oznaczał klasę, która „zużywa” (wywołuje) metody innej klasy, która implementuje duży interfejs, wówczas wywołanie dwóch metod, na których Ci zależy, i zignorowanie pozostałych, wystarczyłoby, abyś odłączył się od reszty metody, których nie używasz.

Istotą tej zasady jest unikanie, aby „klient” (klasa implementująca interfejs) musiał implementować fikcyjne metody w celu zachowania zgodności z całym interfejsem, gdy dba on tylko o zestaw powiązanych metod.

Ma również na celu jak najmniejszą ilość sprzężenia, aby zmiany dokonane w jednym miejscu powodowały mniejszy wpływ. Dzięki segregacji interfejsów zmniejszasz sprzężenie.

Problemy te pojawiają się, gdy interfejs robi zbyt wiele i ma metody, które powinny być podzielone na kilka interfejsów zamiast tylko jednego.

Oba przykłady kodu są w porządku . Tyle tylko, że w drugim zakładasz, że „klient” oznacza „klasę, która wykorzystuje / wywołuje usługi / metody oferowane przez inną klasę”.

Nie znalazłem żadnych sprzeczności w pojęciach wyjaśnionych w trzech podanych linkach.

Po prostu wyjaśnij, że „klient” jest implementatorem w rozmowie SOLID.

Tulains Córdova
źródło
Ale według @pdr, podczas gdy przykłady kodu we wszystkich linkach są zgodne z ISP, definicja ISP polega bardziej na „odizolowaniu klienta (klasy wywołującej metody innej klasy) od wiedzy o usłudze” niż o „ zapobieganie zmuszaniu klientów (implementatorów) do implementacji interfejsów, których nie używają. ”
EdvRusj
1
@EdvRusj Moja odpowiedź opiera się na dokumentach na stronie Object Mentor (przedsiębiorstwo Bob Martin), napisanych przez samego Martina, gdy był w słynnej Gangu Czterech. Jak wiecie, Gnag of Four był grupą inżynierów oprogramowania, w tym Martina, który wymyślił skrót SOLID, zidentyfikował i udokumentował zasady. docs.google.com/a/cleancoder.com/file/d/…
Tulains Córdova
Więc nie zgadzasz się z @pdr, a zatem uważasz, że pierwsza definicja ISP (patrz mój oryginalny post) jest bardziej akceptowalna?
EdvRusj
@EdvRusj Myślę, że oba mają rację. Ale drugi wprowadza niepotrzebne zamieszanie przy użyciu metafory klient / serwer. Gdybym musiał wybrać jeden, wybrałbym oficjalny gang czterech, który jest pierwszy. Ale co ważne, zmniejsza sprzężenie i niepotrzebne zależności, co jest duchem SOLIDNYCH zasad. Nie ma znaczenia, który z nich jest odpowiedni. Ważne jest, abyś posegregował interfejsy według bahaviors. To wszystko. Ale w razie wątpliwości przejdź do oryginalnego źródła.
Tulains Córdova
3
Tak więc nie zgadzam się z twoim twierdzeniem, że „klient” jest implementatorem w rozmowie SOLID. Po pierwsze, nonsensem językowym jest nazywanie dostawcy (realizatora) klientem tego, co zapewnia (wdraża). Nie widziałem też żadnego artykułu na temat SOLID, który próbowałby to przekazać, ale mogłem po prostu tego przegapić. Co najważniejsze, ustawia implementatora interfejsu jako decydującego o tym, co powinno być w interfejsie. I to nie ma dla mnie sensu. Wywołujący / użytkownicy interfejsu określają, czego potrzebują z interfejsu, a realizatorzy (w liczbie mnogiej) tego interfejsu są zobowiązani to zapewnić.
Marjan Venema
5

ISP polega przede wszystkim na odizolowaniu klienta od wiedzy na temat usługi, niż powinna wiedzieć (na przykład ochrony przed niepowiązanymi zmianami). Twoja druga definicja jest poprawna. Według mojego czytania tylko jeden z tych trzech artykułów sugeruje coś innego ( pierwszy ) i jest to po prostu błędne. (Edycja: Nie, nie jest źle, po prostu wprowadza w błąd.)

Pierwsza definicja jest znacznie ściślej związana z LSP.

pdr
źródło
3
W ISP klienci nie powinni być zmuszani do KONSUMOWANIA komponentów interfejsu, których nie używają. W LSP USŁUGI nie powinny być zmuszane do implementacji metody D, ponieważ kod wywołujący wymaga metody A. Nie są ze sobą sprzeczne, są komplementarne.
pdr
2
@EdvRusj, obiekt, który implementuje interfejs A wywoływany przez klienta A, może w rzeczywistości być dokładnie tym samym obiektem, który implementuje interfejs B wymagany przez klienta B. W rzadkich przypadkach, gdy ten sam klient musi widzieć ten sam obiekt co różne klasy, kod nie będzie zwykle „dotyk”. Będziesz patrzył na to jako A w jednym celu i B w drugim celu.
Amy Blankenship
1
@EdvRusj: Pomocne może być ponowne przemyślenie definicji interfejsu tutaj. Nie zawsze jest to interfejs w języku C # / Java. Możesz mieć złożoną usługę z kilkoma prostymi klasami owiniętymi wokół niej, tak że klient A używa klasy opakowania AX do „łączenia” się z usługą X. Zatem zmieniając X w sposób, który wpływa na A i AX, nie jesteś zmuszony wpływać na BX i B.
pdr
1
@EdvRusj: Bardziej trafne byłoby stwierdzenie, że A i B nie dbają o to, czy oboje dzwonią do X, czy jeden do Y, a drugi do Z. TO jest fundamentalny punkt ISP. Możesz więc wybrać implementację, którą chcesz zastosować, i łatwo zmienić zdanie później. ISP nie faworyzuje jednej lub drugiej trasy, ale LSP i SRP mogą.
pdr
1
@EdvRusj Nie, klient A byłby w stanie zastąpić usługę X usługą y, z których obie implementowałyby interfejs AX. X i / lub Y mogą implementować inne interfejsy, ale kiedy klient wywołuje je jako AX, nie przejmuje się tymi innymi interfejsami.
Amy Blankenship