Zwei widersprüchliche Definitionen des Prinzips der Schnittstellensegregation - welche ist richtig?

14

Beim Lesen von Artikeln über ISP scheint es zwei widersprüchliche Definitionen von ISP zu geben:

Nach der ersten Definition (siehe 1 , 2 , 3 ) sollten ISP-Klassen, die die Schnittstelle implementieren, nicht gezwungen werden, Funktionen zu implementieren, die sie nicht benötigen. Also fette SchnittstelleIFat

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

class MyClass: IFat
{ ... }

sollte in kleinere Schnittstellen ISmall_1und aufgeteilt werdenISmall_2

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

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

class MyClass:ISmall_2
{ ... }

da auf diese Weise meiner MyClassLage ist, nur die Methoden zu implementieren , es braucht ( D()und C()), ohne auch Dummy - Implementierungen für bietet gezwungen werden A(), B()und C():

Gemäß der zweiten Definition (siehe 1 , 2 , Antwort von Nazar Merza ) gibt der ISP an, dass beim MyClientAufrufen MyServicevon Methoden MyServicenicht berücksichtigt werden sollte, welche Methoden er nicht benötigt. Mit anderen Worten, wenn MyClientnur die Funktionalität von C()und benötigt wird D(), dann statt

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

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

Wir sollten MyService'sMethoden in kundenspezifische Schnittstellen unterteilen:

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();

Daher besteht das Ziel von ISP in der ersteren Definition darin, " das Leben von Klassen, die die IFat-Schnittstelle implementieren, zu vereinfachen ", während das Ziel von ISP darin besteht, " das Leben von Clients, die Methoden von MyService aufrufen, zu vereinfachen ".

Welche der beiden unterschiedlichen Definitionen von ISP ist tatsächlich korrekt?

@MARJAN VENEMA

1.

Wenn Sie also IFat in eine kleinere Schnittstelle aufteilen, sollten Sie anhand der Kohäsion der Mitglieder entscheiden, welche Methoden in welcher ISmall-Schnittstelle landen.

Während es sinnvoll ist, zusammenhängende Methoden innerhalb derselben Schnittstelle zu platzieren, dachte ich, dass bei einem ISP-Muster die Bedürfnisse des Kunden Vorrang vor der "Kohäsivität" einer Schnittstelle haben. Mit anderen Worten, ich dachte, mit dem ISP sollten wir die Methoden, die von bestimmten Clients benötigt werden, innerhalb derselben Schnittstelle zusammenfassen, auch wenn dies bedeutet, dass diese Methoden aus Gründen der Kohäsivität auch innerhalb derselben Schnittstelle weggelassen werden sollten.

Wenn es also viele Kunden waren , die sich immer nur auf Anruf benötigt wird CutGreens, aber auch nicht GrillMeat, dann ISP Muster haften sollten wir nur setzen CutGreensinnen ICook, aber auch nicht GrillMeat, auch wenn die beiden Methoden sind hochkohäsiv ?!

2.

Ich denke, Ihre Verwirrung rührt von der versteckten Annahme in der ersten Definition her: Die implementierenden Klassen folgen bereits dem Prinzip der einheitlichen Verantwortung.

Beziehen Sie sich mit "Implementieren von Klassen, die SRP nicht folgen" auf Klassen, die implementieren, IFatoder auf Klassen, die ISmall_1/ implementieren ISmall_2? Ich nehme an, Sie beziehen sich auf Klassen, die implementieren IFat? Wenn ja, warum nehmen Sie an, dass sie SRP nicht bereits folgen?

Vielen Dank

EdvRusj
quelle
4
Warum kann es nicht mehrere Definitionen geben, für die dasselbe Prinzip gilt?
Bobson
5
Diese Definitionen widersprechen sich nicht.
Mike Partridge
1
Nein, natürlich hat das Bedürfnis des Kunden keinen Vorrang vor der Kohärenz einer Schnittstelle. Sie können diese "Regel" zu weit führen und überall einzelne Methodenschnittstellen finden, die absolut keinen Sinn ergeben. Hören Sie auf, Regeln zu befolgen, und denken Sie an die Ziele, für die diese Regeln erstellt wurden. Bei "Klassen, die SRP nicht folgen" habe ich in Ihrem Beispiel nicht über bestimmte Klassen gesprochen oder dass sie SRP nicht bereits folgen. Lies erneut. Die erste Definition führt nur zum Aufteilen einer Schnittstelle, wenn die Schnittstelle nicht ISP folgt und die Klasse SRP folgt.
Marjan Venema
2
Die zweite Definition kümmert sich nicht um die Implementierer. Sie definiert Schnittstellen aus Sicht der Aufrufer und nimmt keine Annahmen darüber vor, ob bereits Implementierer vorhanden sind oder nicht. Es wird wahrscheinlich davon ausgegangen, dass Sie, wenn Sie dem ISP folgen und diese Schnittstellen implementieren, beim Erstellen natürlich dem SRP folgen.
Marjan Venema
2
Woher wissen Sie im Voraus, welche Kunden es gibt und welche Methoden sie benötigen? Das kannst du nicht. Was Sie vorher wissen können, ist, wie zusammenhängend Ihre Benutzeroberfläche ist.
Tulains Córdova

Antworten:

6

Beide sind richtig

So wie ich es lese, besteht der Zweck von ISP (Interface Segregation Principle) darin, Schnittstellen klein und fokussiert zu halten: Alle Schnittstellenmitglieder sollten einen sehr hohen Zusammenhalt haben. Beide Definitionen sollen "Alleskönner" -Schnittstellen vermeiden.

Schnittstellentrennung und SRP (Single Responsibility Principle) verfolgen dasselbe Ziel: Sicherstellung kleiner, sehr zusammenhängender Softwarekomponenten. Sie ergänzen sich. Die Schnittstellentrennung stellt sicher, dass die Schnittstellen klein, fokussiert und in hohem Maße zusammenhängend sind. Die Befolgung des Grundsatzes der einheitlichen Verantwortung stellt sicher, dass die Klassen klein, konzentriert und in hohem Maße zusammenhängend sind.

Die erste Definition, die Sie erwähnen, konzentriert sich auf Implementierer, die zweite auf Clients. Was ich im Gegensatz zu @ user61852 als Benutzer / Aufrufer der Schnittstelle und nicht als Implementierer betrachte.

Ich denke, Ihre Verwirrung rührt von der versteckten Annahme in der ersten Definition her: Die implementierenden Klassen folgen bereits dem Prinzip der einheitlichen Verantwortung.

Für mich ist die zweite Definition mit den Clients als Aufrufern der Schnittstelle ein besserer Weg, um zum beabsichtigten Ziel zu gelangen.

Trennung

In Ihrer Frage stellen Sie fest:

Auf diese Weise kann MyClass nur die Methoden implementieren, die es benötigt (D () und C ()), ohne auch Dummy-Implementierungen für A (), B () und C () bereitstellen zu müssen:

Aber das stellt die Welt auf den Kopf.

  • Eine Klasse, die eine Schnittstelle implementiert, schreibt nicht vor, was sie in der von ihr implementierten Schnittstelle benötigt.
  • Die Schnittstellen bestimmen, welche Methoden eine implementierende Klasse bereitstellen soll.
  • Die Aufrufer einer Schnittstelle bestimmen tatsächlich, welche Funktionalität die Schnittstelle für sie bereitstellen soll und was ein Implementierer daher bereitstellen soll.

Wenn Sie also IFatin kleinere Benutzeroberflächen aufteilen , ISmallsollten Sie anhand der Kohäsion der Mitglieder entscheiden , welche Methoden in welcher Benutzeroberfläche landen .

Betrachten Sie diese Schnittstelle:

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

Welche Methoden würden Sie einsetzen ICookund warum? Würden Sie mit CleanSinkzusammenarbeiten, GrillMeatnur weil Sie zufällig eine Klasse haben, die genau das und ein paar andere Dinge tut, aber nichts mit einer der anderen Methoden zu tun hat? Oder würden Sie es in zwei zusammenhängende Schnittstellen aufteilen, wie zum Beispiel:

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

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

Hinweis zur Schnittstellendeklaration

Eine Schnittstellendefinition sollte vorzugsweise in einer separaten Einheit für sich sein, aber wenn sie unbedingt mit einem Aufrufer oder Implementierer zusammenleben muss, sollte sie wirklich mit dem Aufrufer sein. Andernfalls erhält der Aufrufer eine unmittelbare Abhängigkeit vom Implementierer, was den Zweck von Schnittstellen insgesamt zunichte macht. Siehe auch: Deklarieren der Schnittstelle in derselben Datei wie die Basisklasse. Ist dies eine gute Vorgehensweise? auf Programmierer und warum sollten wir Schnittstellen mit Klassen platzieren, die sie verwenden, anstatt solche, die sie implementieren? auf StackOverflow.

Marjan Venema
quelle
1
Kannst du das Update sehen, das ich gemacht habe?
EdvRusj
"Der Aufrufer bekommt eine sofortige Abhängigkeit vom Implementierer " ... nur wenn Sie das DIP (Abhängigkeitsinversionsprinzip) verletzen, wenn interne Variablen, Parameter, Rückgabewerte usw. des Aufrufers vom Typ ICookstatt vom Typ sind SomeCookImplementor, wie dies bei DIP-Mandaten der Fall ist muss sich nicht darauf verlassen SomeCookImplementor.
Tulains Córdova
@ user61852: Wenn Schnittstellendeklaration und Implementierer in derselben Unit sind, bekomme ich sofort eine Abhängigkeit von diesem Implementierer. Nicht unbedingt zur Laufzeit, aber mit Sicherheit auf Projektebene, einfach dadurch, dass es da ist. Das Projekt kann nicht mehr ohne es oder was auch immer es verwendet, kompiliert werden. Außerdem ist die Abhängigkeitsinjektion nicht dasselbe wie das Abhängigkeitsinversionsprinzip. Vielleicht interessiert dich DIP in the wild
Marjan Venema
Ich habe Ihre Codebeispiele in dieser Frage " programmers.stackexchange.com/a/271142/61852" erneut verwendet und sie verbessert, nachdem sie bereits akzeptiert wurden. Ich habe die Beispiele gebührend gewürdigt.
Tulains Córdova
Cool @ user61852 :) (und danke für die Gutschrift)
Marjan Venema
14

Sie verwechseln das Wort "Client", wie es in den Dokumenten der Vierergruppe verwendet wird, mit einem "Client", wie es beim Verbraucher einer Dienstleistung verwendet wird.

Ein "Client", wie er in den Definitionen der Vierergruppe vorgesehen ist, ist eine Klasse, die eine Schnittstelle implementiert. Wenn Klasse A Schnittstelle B implementiert, dann sagen sie, dass A ein Client von B ist. Andernfalls sollte der Ausdruck "Clients sollten nicht gezwungen werden, Schnittstellen zu implementieren, die sie nicht verwenden" nicht sinnvoll, da "Clients" (wie bei Verbrauchern) nicht funktionieren nichts implementieren. Der Satz ist nur dann sinnvoll, wenn Sie "client" als "implementor" sehen.

Wenn "client" eine Klasse bedeutet, die die Methoden einer anderen Klasse "verbraucht" (aufruft), die die große Schnittstelle implementiert, dann würde es ausreichen, die beiden Methoden aufzurufen, die Sie interessieren, und den Rest zu ignorieren, um Sie vom Rest von zu entkoppeln die Methoden, die Sie nicht verwenden.

Der Geist des Prinzips besteht darin, zu vermeiden, dass der "Client" (die Klasse, die die Schnittstelle implementiert) Dummy-Methoden implementieren muss, um die gesamte Schnittstelle zu erfüllen, wenn es nur um eine Reihe von Methoden geht, die miteinander in Beziehung stehen.

Es wird auch angestrebt, den Kopplungsaufwand so gering wie möglich zu halten, damit an einem Ort vorgenommene Änderungen weniger Auswirkungen haben. Durch die Trennung der Schnittstellen reduzieren Sie die Kopplung.

Dieses Problem tritt auf, wenn die Schnittstelle zu viel tut und Methoden hat, die in mehrere Schnittstellen anstatt nur einer aufgeteilt werden sollten.

Ihre beiden Codebeispiele sind in Ordnung . Es ist nur so, dass Sie in der zweiten davon ausgehen, dass "client" "eine Klasse, die die von einer anderen Klasse angebotenen Dienste / Methoden nutzt / aufruft".

Ich finde keine Widersprüche in den Begriffen, die in den drei von Ihnen angegebenen Links erläutert wurden.

Stellen Sie in SOLID talk nur klar, dass "client" Implementierer ist.

Tulains Córdova
quelle
Laut @pdr handelt es sich bei den Codebeispielen in allen Links zwar darum, dass der Client (eine Klasse, die Methoden einer anderen Klasse aufruft) nicht mehr über den Dienst informiert, sondern vielmehr darum, dass der ISP dies tut. Verhinderung, dass Clients (Implementierer) gezwungen werden, Schnittstellen zu implementieren, die sie nicht verwenden. "
EdvRusj
1
@EdvRusj Meine Antwort basiert auf den Dokumenten auf der Object Mentor-Website (Bob Martin Enterprise), die von Martin selbst verfasst wurden, als er in der berühmten Gang of Four war. Wie Sie wissen, war der Gnag of Four eine Gruppe von Software-Ingenieuren, darunter auch Martin, die das Akronym SOLID geprägt und die Prinzipien identifiziert und dokumentiert haben. docs.google.com/a/cleancoder.com/file/d/…
Tulains Córdova
Sie stimmen also nicht mit @pdr überein und finden daher die erste Definition von ISP (siehe meinen ursprünglichen Beitrag) angenehmer?
EdvRusj
@EdvRusj Ich denke beide haben recht. Aber die zweite fügt durch die Verwendung der Client / Server-Metapher unnötige Verwirrung hinzu. Wenn ich mich für einen entscheiden muss, würde ich mich für den offiziellen Gang of Four One entscheiden, der der erste ist. Was aber wichtig ist, um Kopplungen und unnötige Abhängigkeiten zu reduzieren, ist der Sinn der SOLID-Prinzipien. Es spielt keine Rolle, welcher richtig ist. Das Wichtigste ist, dass Sie die Schnittstellen entsprechend den Verhaltensweisen trennen. Das ist alles. Wenn Sie Zweifel haben, gehen Sie einfach zur Originalquelle.
Tulains Córdova
3
Ich stimme Ihrer Behauptung nicht zu, dass "Client" Implementierer in SOLID talk ist. Zum einen ist es sprachlicher Unsinn, einen Anbieter (Implementierer) als Kunden dessen zu bezeichnen, was er bereitstellt (implementiert). Ich habe auch noch keinen Artikel über SOLID gesehen, der dies zu vermitteln versucht, aber vielleicht habe ich das einfach übersehen. Am wichtigsten ist jedoch, dass der Implementierer einer Schnittstelle so konfiguriert wird, dass er entscheidet, was in der Schnittstelle enthalten sein soll. Und das ergibt für mich keinen Sinn. Die Aufrufer / Benutzer einer Schnittstelle definieren, was sie von einer Schnittstelle benötigen, und die Implementierer (Plural) dieser Schnittstelle sind verpflichtet, diese bereitzustellen.
Marjan Venema
5

Beim ISP geht es darum, den Client davon abzuhalten, mehr über den Dienst zu wissen, als er wissen muss (z. B. um ihn vor nicht zusammenhängenden Änderungen zu schützen). Ihre zweite Definition ist korrekt. Meiner Meinung nach schlägt nur einer dieser drei Artikel etwas anderes vor ( der erste ) und es ist einfach falsch. (Edit: Nein, nicht falsch, nur irreführend.)

Die erste Definition ist viel enger mit LSP verbunden.

pdr
quelle
3
In ISP sollten Clients nicht gezwungen werden, nicht verwendete Schnittstellenkomponenten zu konsumieren. In LSP sollten SERVICES nicht gezwungen werden, Methode D zu implementieren, da der aufrufende Code Methode A erfordert. Sie sind nicht widersprüchlich, sondern komplementär.
pdr
2
@EdvRusj, das Objekt, das von ClientA aufgerufene InterfaceA implementiert, ist möglicherweise genau dasselbe Objekt, das von Client B benötigtes InterfaceB implementiert. In den seltenen Fällen, in denen derselbe Client dasselbe Objekt wie verschiedene Klassen sehen muss, wird der Code nicht angezeigt normalerweise "berühren". Sie werden es als ein A für den einen Zweck und ein B für den anderen Zweck betrachten.
Amy Blankenship
1
@EdvRusj: Es könnte hilfreich sein, wenn Sie Ihre Definition der Schnittstelle hier überdenken. Es ist nicht immer eine Schnittstelle in C # / Java-Begriffen. Sie könnten einen komplexen Dienst mit einer Reihe von einfachen Klassen haben, so dass Client A die Wrapper-Klasse AX verwendet, um eine "Schnittstelle" mit Dienst X zu bilden. Wenn Sie also X auf eine Weise ändern, die A und AX betrifft, ist dies nicht der Fall gezwungen, BX und B. zu beeinflussen
pdr
1
@EdvRusj: Es wäre genauer zu sagen, dass es A und B egal ist, ob sie beide X anrufen oder einer Y und der andere Z anrufen. DAS ist der grundlegende Punkt von ISP. So können Sie auswählen, für welche Implementierung Sie sich entscheiden, und später ganz einfach Ihre Meinung ändern. ISP bevorzugt nicht die eine oder andere Route, aber LSP und SRP könnten.
pdr
1
@EdvRusj Nein, Client A könnte Service X durch Service y ersetzen, wobei beide die Schnittstelle AX implementieren würden. X und / oder Y können andere Schnittstellen implementieren, aber wenn der Client sie als AX aufruft, sind diese anderen Schnittstellen ihm egal.
Amy Blankenship