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 QueryInterface
COM-ä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 IParenting
und IPosition
an einem Ort konsolidiert , wie IGuiElement
oder 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 parent
müsste die Methode natürlich denselben Entity
Typ 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. IPositionPlusParenting
die von beiden IPosition
und 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.