Principe de séparation des interfaces: que faire si les interfaces se chevauchent considérablement?


9

Extrait du développement logiciel Agile, principes, modèles et pratiques: Pearson New International Edition :

Parfois, les méthodes invoquées par différents groupes de clients se chevauchent. Si le chevauchement est faible, les interfaces des groupes doivent rester distinctes. Les fonctions communes doivent être déclarées dans toutes les interfaces qui se chevauchent. La classe de serveur héritera des fonctions communes de chacune de ces interfaces, mais elle ne les implémentera qu'une seule fois.

Oncle Bob, parle du cas où il y a un chevauchement mineur.

Que devons-nous faire en cas de chevauchement important?

Disons que nous avons

Class UiInterface1;
Class UiInterface2;
Class UiInterface3;

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

Que devons-nous faire en cas de chevauchement important entre UiInterface1et UiInterface2?


Lorsque je rencontre des interfaces fortement chevauchantes, je crée une interface parent, qui regroupe les méthodes courantes, puis hérite de celle-ci pour créer des spécialisations. MAIS! Si vous ne voulez jamais que quelqu'un utilise l'interface commune sans la spécialisation, vous devez en fait opter pour la duplication de code, car si vous introduisez l'interface commune parrent, les gens pourraient l'utiliser.
Andy

La question est un peu vague pour moi, on pourrait y répondre avec de nombreuses solutions différentes selon les cas. Pourquoi le chevauchement s'est-il accru?
Arthur Havlicek

Réponses:


1

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 QueryInterfaceapproche 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 IParentinget IPositionen un seul endroit, comme IGuiElementou 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 QueryInterfacepartie), 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 parentméthode devrait retourner le même Entitytype, 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 IPositionPlusParentingqui dérivent des deux IPositionetIParenting(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.


11

Il s'agit d'un jugement que vous devez faire, au cas par cas.

Tout d'abord, rappelez-vous que les principes SOLIDES ne sont que cela ... des principes. Ce ne sont pas des règles. Ce n'est pas une solution miracle. Ce ne sont que des principes. Ce n'est pas pour leur enlever leur importance, vous devriez toujours vous pencher pour les suivre. Mais la seconde où ils introduisent un niveau de douleur, vous devez les abandonner jusqu'à ce que vous en ayez besoin.

Dans cet esprit, réfléchissez à la raison pour laquelle vous séparez vos interfaces en premier lieu. L'idée d'une interface est de dire "Si ce code consommateur nécessite un ensemble de méthodes à implémenter sur la classe consommée, je dois définir un contrat sur l'implémentation: si vous me fournissez un objet avec cette interface, je peux travailler avec ça."

Le but du FAI est de dire "Si le contrat dont j'ai besoin n'est qu'un sous-ensemble d'une interface existante, je ne devrais pas appliquer l'interface existante sur les futures classes qui pourraient être transmises à ma méthode."

Considérez le code suivant:

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

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

Maintenant, nous avons une situation où, si nous voulons passer un nouvel objet à ConsumeX, il doit implémenter X () et Y () pour correspondre au contrat.

Devrions-nous donc changer le code, maintenant, pour ressembler à l'exemple suivant?

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

public interface B
{
    void X();
}

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

Le FAI suggère que nous le fassions, nous devons donc nous pencher vers cette décision. Mais, sans contexte, il est difficile d'être sûr. Est-il probable que nous étendions A et B? Est-il probable qu'ils s'étendent indépendamment? Est-il probable que B implémentera jamais des méthodes dont A n'a pas besoin? (Sinon, nous pouvons faire dériver A de B.)

C'est le jugement que vous devez faire. Et, si vous n'avez vraiment pas assez d'informations pour effectuer cet appel, vous devriez probablement prendre l'option la plus simple, qui pourrait bien être le premier code.

Pourquoi? Parce qu'il est facile de changer d'avis plus tard. Lorsque vous avez besoin de cette nouvelle classe, créez simplement une nouvelle interface et implémentez les deux dans votre ancienne classe.


1
"Tout d'abord, rappelez-vous que les principes SOLIDES ne sont que ces ... principes. Ce ne sont pas des règles. Ils ne sont pas une solution miracle. Ce ne sont que des principes. Ce n'est pas pour enlever leur importance, vous devriez toujours vous pencher à les suivre. Mais à la seconde où ils introduisent un niveau de douleur, vous devez les abandonner jusqu'à ce que vous en ayez besoin. ". Cela devrait être sur la première page de chaque livre de modèles / principes de conception. Un rappel devrait également apparaître toutes les 50 pages.
Christian Rodriguez
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.