Prinzip der Schnittstellentrennung: Was tun, wenn sich die Schnittstellen erheblich überschneiden?

9

Aus agiler Softwareentwicklung, Prinzipien, Mustern und Praktiken: Pearson New International Edition :

Manchmal überschneiden sich die von verschiedenen Gruppen von Clients aufgerufenen Methoden. Wenn die Überlappung gering ist, sollten die Schnittstellen für die Gruppen getrennt bleiben. Die gemeinsamen Funktionen sollten in allen überlappenden Schnittstellen deklariert werden. Die Serverklasse erbt die allgemeinen Funktionen von jeder dieser Schnittstellen, implementiert sie jedoch nur einmal.

Onkel Bob spricht über den Fall, wenn es geringfügige Überschneidungen gibt.

Was sollen wir tun, wenn es erhebliche Überschneidungen gibt?

Sagen wir, wir haben

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

Class UiIterface : public UiInterface1, public UiInterface2, public UiInterface3{};

Was sollen wir tun, wenn es signifikante Überschneidungen zwischen UiInterface1und gibt UiInterface2?

q126y
quelle
Wenn ich auf stark überlappende Schnittstellen stoße, erstelle ich eine übergeordnete Schnittstelle, die die allgemeinen Methoden gruppiert und dann von dieser gemeinsamen erbt, um Spezialisierungen zu erstellen. ABER! Wenn Sie nie möchten, dass jemand die gemeinsame Schnittstelle ohne Spezialisierung verwendet, müssen Sie sich tatsächlich für die Codeduplizierung entscheiden, denn wenn Sie die gemeinsame gemeinsame Schnittstelle einführen, können die Benutzer diese verwenden.
Andy
Die Frage ist für mich etwas vage, man könnte sie je nach Fall mit vielen verschiedenen Lösungen beantworten. Warum ist die Überlappung gewachsen?
Arthur Havlicek

Antworten:

1

Casting

Dies wird mit ziemlicher Sicherheit eine vollständige Tangente an den Ansatz des zitierten Buches sein, aber eine Möglichkeit, sich besser an ISP anzupassen, besteht darin, eine Casting-Denkweise in einem zentralen Bereich Ihrer Codebasis mithilfe eines QueryInterfaceCOM-ähnlichen Ansatzes zu entwickeln.

Viele der Versuchungen, überlappende Schnittstellen in einem reinen Schnittstellenkontext zu entwerfen, entstehen häufig aus dem Wunsch, Schnittstellen "autark" zu machen, anstatt nur eine präzise, ​​scharfschützenähnliche Verantwortung zu übernehmen.

Zum Beispiel mag es seltsam erscheinen, Client-Funktionen wie folgt zu entwerfen:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `position` and `parenting` parameters should point to the 
// same object.
Vec2i abs_position(IPosition* position, IParenting* parenting)
{
     const Vec2i xy = position->xy();
     auto parent = parenting->parent();
     if (parent)
     {
         // If the entity has a parent, return the sum of the
         // parent position and the entity's local position.
         return xy + abs_position(dynamic_cast<IPosition*>(parent),
                                  dynamic_cast<IParenting*>(parent));
     }
     return xy;
}

... sowie ziemlich hässlich / gefährlich, da wir die Verantwortung verlieren, fehleranfälliges Casting über diese Schnittstellen in den Client-Code durchzuführen und / oder dasselbe Objekt als Argument mehrmals an mehrere Parameter desselben zu übergeben Funktion. Daher möchten wir häufig eine verwässerte Schnittstelle entwerfen, die die Anliegen von IParentingund IPositionan einem Ort konsolidiert , wie IGuiElementoder so etwas, das dann anfällig für Überschneidungen mit den Anliegen von orthogonalen Schnittstellen wird, für die ebenfalls versucht sein wird, mehr Mitgliedsfunktionen zu haben der gleiche Grund der "Selbstversorgung".

Verantwortlichkeiten mischen vs. Casting

Beim Entwerfen von Schnittstellen mit einer völlig destillierten, ultra-singulären Verantwortung besteht die Versuchung häufig darin, entweder ein Downcasting zu akzeptieren oder Schnittstellen zu konsolidieren, um mehrere Verantwortlichkeiten zu erfüllen (und daher sowohl ISP als auch SRP zu betreten).

Durch die Verwendung eines COM- QueryInterfaceähnlichen Ansatzes (nur des Teils) spielen wir mit dem Downcasting-Ansatz, konsolidieren jedoch das Casting an einer zentralen Stelle in der Codebasis und können etwas Ähnliches tun:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should implement `IPosition` and optionally `IParenting`.
Vec2i abs_position(Object* obj)
{
     // `Object::query_interface` returns nullptr if the interface is
     // not provided by the entity. `Object` is an abstract base class
     // inherited by all entities using this interface query system.
     IPosition* position = obj->query_interface<IPosition>();
     assert(position && "obj does not implement IPosition!");
     const Vec2i xy = position->xy();

     IParenting* parenting = obj->query_interface<IParenting>();
     if (parenting && parenting->parent()->query_interface<IPosition>())
     {
         // If the entity implements IParenting and has a parent, 
         // return the sum of the parent position and the entity's 
         // local position.
         return xy + abs_position(parenting->parent());
     }
     return xy;
}

... natürlich hoffentlich mit typsicheren Wrappern und allem, was Sie zentral erstellen können, um etwas sichereres als rohe Zeiger zu erhalten.

Dadurch wird die Versuchung, überlappende Schnittstellen zu entwerfen, häufig auf das absolute Minimum reduziert. Es ermöglicht Ihnen, Schnittstellen mit sehr einzigartigen Verantwortlichkeiten (manchmal nur eine Mitgliedsfunktion im Inneren) zu entwerfen, die Sie mischen und anpassen können, ohne sich um den ISP kümmern zu müssen, und die Flexibilität der Pseudo-Enten-Eingabe zur Laufzeit in C ++ zu erhalten (obwohl natürlich mit den Kompromiss von Laufzeitstrafen, um Objekte abzufragen, um festzustellen, ob sie eine bestimmte Schnittstelle unterstützen). Der Laufzeitteil kann beispielsweise in einer Einstellung mit einem Software Development Kit wichtig sein, in der die Funktionen nicht über die Informationen zur Kompilierungszeit von Plugins verfügen, die diese Schnittstellen im Voraus implementieren.

Vorlagen

Wenn Vorlagen eine Möglichkeit sind (wir haben die erforderlichen Informationen zur Kompilierungszeit im Voraus, die nicht verloren gehen, wenn wir ein Objekt erreichen, dh), können wir dies einfach tun:

// Returns the absolute position of an entity as the sum
// of its own position and the position of its ancestors.
// `obj` should have `position` and `parent` methods.
template <class Entity>
Vec2i abs_position(Entity& obj)
{
     const Vec2i xy = obj.xy();
     if (obj.parent())
     {
         // If the entity has a parent, return the sum of the parent 
         // position and the entity's local position.
         return xy + abs_position(obj.parent());
     }
     return xy;
}

... in einem solchen Fall parentmüsste die Methode natürlich denselben EntityTyp zurückgeben. In diesem Fall möchten wir wahrscheinlich Schnittstellen direkt vermeiden (da sie häufig Typinformationen verlieren möchten, um mit Basiszeigern zu arbeiten).

Entity-Component-System

Wenn Sie den COM-ähnlichen Ansatz unter dem Gesichtspunkt der Flexibilität oder Leistung weiter verfolgen, erhalten Sie häufig ein System mit Entitätskomponenten, das den in der Branche geltenden Spiele-Engines ähnelt. An diesem Punkt werden Sie völlig senkrecht zu vielen objektorientierten Ansätzen gehen, aber ECS könnte auf das GUI-Design anwendbar sein (ein Ort, an dem ich ECS außerhalb eines szenenorientierten Fokus in Betracht gezogen habe, es aber zu spät in Betracht gezogen habe sich für einen COM-ähnlichen Ansatz zu entscheiden, um es dort zu versuchen).

Beachten Sie, dass diese Lösung im COM-Stil in Bezug auf GUI-Toolkit-Designs vollständig verfügbar ist und ECS sogar noch mehr, sodass sie nicht durch viele Ressourcen unterstützt wird. Auf diese Weise können Sie jedoch die Versuchungen, Schnittstellen mit überlappenden Verantwortlichkeiten zu entwerfen, auf ein absolutes Minimum reduzieren, was häufig zu einem Problem führt.

Pragmatischer Ansatz

Die Alternative besteht natürlich darin, Ihre Wache ein wenig zu entspannen oder Schnittstellen auf granularer Ebene zu entwerfen und sie dann zu erben, um gröbere Schnittstellen zu erstellen, die Sie verwenden, z. B. IPositionPlusParentingdie von beiden IPositionund abgeleitet sindIParenting(hoffentlich mit einem besseren Namen als dem). Bei reinen Schnittstellen sollte der ISP nicht so stark verletzt werden wie bei den üblicherweise angewendeten monolithischen, tief hierarchischen Ansätzen (Qt, MFC usw.), bei denen in der Dokumentation häufig die Notwendigkeit besteht, irrelevante Mitglieder zu verbergen, da ISP bei diesen Arten übermäßig häufig verletzt wird Ein pragmatischer Ansatz könnte hier und da einfach eine gewisse Überlappung akzeptieren. Diese Art von COM-Ansatz vermeidet jedoch die Notwendigkeit, konsolidierte Schnittstellen für jede Kombination zu erstellen, die Sie jemals verwenden werden. Das Problem der "Selbstversorgung" wird in solchen Fällen vollständig beseitigt, und dies beseitigt häufig die ultimative Versuchung, Schnittstellen mit überlappenden Verantwortlichkeiten zu entwerfen, die sowohl mit SRP als auch mit ISP kämpfen möchten.


quelle
11

Dies ist eine Entscheidung, die Sie von Fall zu Fall treffen müssen.

Denken Sie zunächst daran, dass SOLID-Prinzipien genau das sind ... Prinzipien. Das sind keine Regeln. Sie sind keine Silberkugel. Sie sind nur Prinzipien. Das soll ihre Bedeutung nicht beeinträchtigen, Sie sollten sich immer dazu neigen, ihnen zu folgen. Aber in der Sekunde, in der sie Schmerzen verursachen, sollten Sie sie loswerden, bis Sie sie brauchen.

Denken Sie vor diesem Hintergrund darüber nach, warum Sie Ihre Schnittstellen überhaupt trennen. Die Idee einer Schnittstelle lautet: "Wenn für diesen konsumierenden Code eine Reihe von Methoden für die konsumierte Klasse implementiert werden muss, muss ein Vertrag für die Implementierung geschlossen werden: Wenn Sie mir ein Objekt mit dieser Schnittstelle zur Verfügung stellen, kann ich arbeiten." damit."

Der Zweck des ISP besteht darin, zu sagen: "Wenn der von mir benötigte Vertrag nur eine Teilmenge einer vorhandenen Schnittstelle ist, sollte ich die vorhandene Schnittstelle nicht für zukünftige Klassen erzwingen, die möglicherweise an meine Methode übergeben werden."

Betrachten Sie den folgenden Code:

public interface A
{
    void X();
    void Y();
}

public class Foo
{
     public void ConsumeX(A a)
     {
         a.X();
     }
}

Jetzt haben wir eine Situation, in der, wenn wir ein neues Objekt an ConsumeX übergeben möchten, es X () und Y () implementieren muss, um dem Vertrag zu entsprechen.

Sollten wir jetzt den Code ändern, um wie im nächsten Beispiel auszusehen?

public interface A
{
    void X();
    void Y();
}

public interface B
{
    void X();
}

public class Foo
{
     public void ConsumeX(B b)
     {
         b.X();
     }
}

ISP schlägt vor, dass wir sollten, also sollten wir uns zu dieser Entscheidung neigen. Aber ohne Kontext ist es schwer, sicher zu sein. Ist es wahrscheinlich, dass wir A und B erweitern werden? Ist es wahrscheinlich, dass sie sich unabhängig voneinander verlängern? Ist es wahrscheinlich, dass B jemals Methoden implementiert, die A nicht benötigt? (Wenn nicht, können wir A von B ableiten lassen.)

Dies ist das Urteil, das Sie fällen müssen. Und wenn Sie wirklich nicht genug Informationen haben, um diesen Anruf zu tätigen, sollten Sie wahrscheinlich die einfachste Option wählen, die möglicherweise der erste Code ist.

Warum? Weil es einfach ist, Ihre Meinung später zu ändern. Wenn Sie diese neue Klasse benötigen, erstellen Sie einfach eine neue Schnittstelle und implementieren Sie beide in Ihrer alten Klasse.

pdr
quelle
1
"Denken Sie zunächst daran, dass SOLID-Prinzipien genau das sind ... Prinzipien. Sie sind keine Regeln. Sie sind keine Silberkugel. Sie sind nur Prinzipien. Das soll ihre Bedeutung nicht beeinträchtigen, Sie sollten sich immer lehnen Aber wenn sie ein gewisses Maß an Schmerz verursachen, sollten Sie sie loswerden, bis Sie sie brauchen. " Dies sollte auf der ersten Seite jedes Entwurfsmuster- / Grundsatzbuchs stehen. Als Erinnerung sollte es auch alle 50 Seiten erscheinen.
Christian Rodriguez