Moulage
Cela va presque certainement être une tangente complète à l'approche du livre cité, mais une façon de mieux se conformer à ISP est d'adopter une mentalité de casting dans une zone centrale de votre base de code en utilisant une QueryInterface
approche de style COM.
Une grande partie des tentations de concevoir des interfaces qui se chevauchent dans un contexte d'interface pure vient souvent du désir de rendre les interfaces «autosuffisantes» plus que d'exécuter une responsabilité précise et semblable à celle d'un tireur d'élite.
Par exemple, il peut sembler étrange de concevoir des fonctions client comme celle-ci:
// 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;
}
... ainsi que assez moche / dangereux, étant donné que nous perdons la responsabilité de faire un casting sujet aux erreurs dans le code client en utilisant ces interfaces et / ou en passant plusieurs fois le même objet comme argument à plusieurs paramètres du même une fonction. Nous finissons donc souvent par vouloir concevoir une interface plus diluée qui consolide les préoccupations de IParenting
et IPosition
en un seul endroit, comme IGuiElement
ou quelque chose comme ça qui devient alors susceptible de se chevaucher avec les préoccupations des interfaces orthogonales qui seront également tentées d'avoir plus de fonctions membres pour la même raison "d'autosuffisance".
Mêler responsabilités et casting
Lors de la conception d'interfaces avec une responsabilité totalement distillée et ultra-singulière, la tentation sera souvent d'accepter une conversion descendante ou de consolider des interfaces pour remplir plusieurs responsabilités (et donc marcher sur ISP et SRP).
En utilisant une approche de style COM (juste la QueryInterface
partie), nous jouons à l'approche de downcasting mais consolidons le casting à un endroit central dans la base de code, et pouvons faire quelque chose de plus comme ceci:
// 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;
}
... bien sûr, espérons-le, avec des wrappers sûrs et tout ce que vous pouvez créer de manière centralisée pour obtenir quelque chose de plus sûr que des pointeurs bruts.
Avec cela, la tentation de concevoir des interfaces qui se chevauchent est souvent atténuée au minimum absolu. Il vous permet de concevoir des interfaces avec des responsabilités très singulières (parfois une seule fonction membre à l'intérieur) que vous pouvez mélanger et faire correspondre tout ce que vous aimez sans vous soucier du FAI, et obtenir la flexibilité de la frappe pseudo-canard à l'exécution en C ++ (bien sûr avec le compromis des pénalités d'exécution pour interroger les objets pour voir s'ils prennent en charge une interface particulière). La partie d'exécution peut être importante, par exemple, dans un paramètre avec un kit de développement logiciel où les fonctions n'auront pas à l'avance les informations de compilation des plugins qui implémentent ces interfaces.
Modèles
Si les modèles sont une possibilité (nous avons à l'avance les informations de compilation nécessaires qui ne sont pas perdues au moment où nous nous emparons d'un objet, c'est-à-dire), alors nous pouvons simplement le faire:
// 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;
}
... bien sûr dans un tel cas, la parent
méthode devrait retourner le même Entity
type, auquel cas nous voulons probablement éviter les interfaces purement et simplement (car ils voudront souvent perdre les informations de type au profit de travailler avec des pointeurs de base).
Système Entité-Composante
Si vous commencez à poursuivre l'approche de style COM plus loin du point de vue de la flexibilité ou des performances, vous vous retrouverez souvent avec un système d'entité-composant similaire à ce que les moteurs de jeu appliquent dans l'industrie. À ce stade, vous irez complètement perpendiculairement à de nombreuses approches orientées objet, mais ECS pourrait être applicable à la conception d'interface graphique (un endroit que j'ai envisagé d'utiliser ECS en dehors d'un focus orienté scène, mais je l'ai considéré trop tard après s'installer sur une approche de style COM pour y essayer).
Notez que cette solution de style COM est complètement disponible en ce qui concerne les conceptions de boîte à outils GUI, et ECS serait encore plus, donc ce n'est pas quelque chose qui sera soutenu par beaucoup de ressources. Pourtant, cela vous permettra certainement d'atténuer les tentations de concevoir des interfaces qui se chevauchent au minimum absolu, ce qui en fait souvent une préoccupation.
Approche pragmatique
L'alternative, bien sûr, est de détendre un peu votre garde ou de concevoir des interfaces à un niveau granulaire, puis de commencer à les hériter pour créer des interfaces plus grossières que vous utilisez, comme celles IPositionPlusParenting
qui dérivent des deux IPosition
etIParenting
(j'espère avec un meilleur nom que ça). Avec des interfaces pures, il ne devrait pas violer le FAI autant que les approches monolithiques hiérarchiques profondes couramment appliquées (Qt, MFC, etc., où la documentation ressent souvent le besoin de cacher les membres non pertinents étant donné le niveau excessif de violation du FAI avec ces types des conceptions), donc une approche pragmatique pourrait simplement accepter un certain chevauchement ici et là. Pourtant, ce type d'approche de style COM évite d'avoir à créer des interfaces consolidées pour chaque combinaison que vous utiliserez jamais. Le problème de "l'autosuffisance" est complètement éliminé dans de tels cas, et cela éliminera souvent la source ultime de la tentation de concevoir des interfaces qui ont des responsabilités qui se chevauchent et qui veulent se battre avec SRP et ISP.