Deux interfaces avec des signatures identiques


13

J'essaie de modéliser un jeu de cartes où les cartes ont deux ensembles de fonctionnalités importants:

Le premier est un effet. Ce sont les changements d'état du jeu qui se produisent lorsque vous jouez la carte. L'interface pour l'effet est la suivante:

boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);

Et vous pourriez considérer la carte comme jouable si et seulement si vous pouvez couvrir son coût et que tous ses effets sont jouables. Ainsi:

// in Card class
boolean isPlayable(Player p, GameState gs) {
    if(p.resource < this.cost) return false;
    for(Effect e : this.effects) {
        if(!e.isPlayable(p,gs)) return false;
    }
    return true;
}

D'accord, jusqu'à présent, assez simple.

L'autre ensemble de fonctionnalités sur la carte sont les capacités. Ces capacités sont des modifications de l'état du jeu que vous pouvez activer à volonté. En proposant l'interface pour ceux-ci, j'ai réalisé qu'ils avaient besoin d'une méthode pour déterminer s'ils peuvent être activés ou non, et d'une méthode pour implémenter l'activation. Il finit par être

boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);

Et je me rends compte qu'à l'exception de l'appeler "activer" au lieu de "jouer", Abilityet Effectavoir exactement la même signature.


Est-ce une mauvaise chose d'avoir plusieurs interfaces avec une signature identique? Dois-je simplement en utiliser un et avoir deux ensembles de la même interface? Ainsi:

Set<Effect> effects;
Set<Effect> abilities;

Dans l'affirmative, quelles étapes de refactorisation dois-je suivre si elles deviennent non identiques (à mesure que davantage de fonctionnalités sont publiées), en particulier si elles sont divergentes (c'est-à-dire qu'elles gagnent toutes les deux quelque chose que l'autre ne devrait pas, par opposition à une seule gagnant et l'autre étant un sous-ensemble complet)? Je suis particulièrement préoccupé par le fait que leur combinaison ne sera pas durable dès que quelque chose changera.

Les petits caractères:

Je reconnais que cette question est engendrée par le développement de jeux, mais je pense que c'est le genre de problème qui pourrait tout aussi facilement surgir dans le développement non-jeu, en particulier lorsque vous essayez d'adapter les modèles commerciaux de plusieurs clients dans une application comme cela arrive avec à peu près chaque projet que j'ai jamais réalisé avec plus d'une influence commerciale ... De plus, les extraits de code utilisés sont des extraits de code Java, mais cela pourrait tout aussi bien s'appliquer à une multitude de langages orientés objet.


Suivez simplement KISS et YAGNI et tout ira bien.
Bernard

2
La seule raison pour laquelle cette question apparaît même est que vos fonctions ont trop accès à l'état, en raison des paramètres Player et GameState incroyablement larges et sans contraintes.
Lars Viklund

Réponses:


18

Le fait que deux interfaces aient le même contrat ne signifie pas qu'elles sont la même interface.

Le principe de substitution de Liskov stipule:

Soit q (x) une propriété prouvable sur les objets x de type T. Alors q (y) devrait être prouvable pour les objets y de type S où S est un sous-type de T.

Ou, en d'autres termes: tout ce qui est vrai d'une interface ou d'un supertype doit être vrai de tous ses sous-types.

Si je comprends bien votre description, une capacité n'est pas un effet et un effet n'est pas une capacité. Si l'un change son contrat, il est peu probable que l'autre change avec lui. Je ne vois aucune bonne raison d'essayer de les lier les uns aux autres.


2

De Wikipédia : "l' interface est souvent utilisée pour définir un type abstrait qui ne contient pas de données, mais expose des comportements définis comme des méthodes ". À mon avis, une interface est utilisée pour décrire un comportement, donc si vous avez des comportements différents, il est logique d'avoir des interfaces différentes. En lisant votre question, l'impression que j'ai eue est que vous parlez de comportements différents, donc différentes interfaces peuvent être la meilleure approche.

Un autre point que vous avez dit est que si l'un de ces comportements change. Que se passe-t-il alors lorsque vous n'avez qu'une seule interface?


1

Si les règles de votre jeu de cartes font une distinction entre "effets" et "capacités", vous devez vous assurer qu'il s'agit d'interfaces différentes. Cela vous évitera d'utiliser accidentellement l'un d'eux lorsque l'autre est requis.

Cela dit, s'ils sont extrêmement similaires, il peut être judicieux de les dériver d'un ancêtre commun. Réfléchissez bien: avez-vous des raisons de croire que les "effets" et les "capacités" auront toujours nécessairement la même interface? Si vous ajoutez un élément à l' effectinterface, vous attendriez-vous à ce que le même élément soit ajouté à l' abilityinterface?

Si c'est le cas, vous pouvez placer ces éléments dans une featureinterface commune dont ils sont tous deux dérivés. Sinon, vous ne devriez pas essayer de les unifier - vous perdrez votre temps à déplacer des éléments entre les interfaces de base et dérivées. Cependant, puisque vous n'avez pas l'intention d'utiliser réellement l'interface de base commune pour autre chose que "ne vous répétez pas", cela ne fera pas beaucoup de différence. Et, si vous vous en tenez à cette intention, je suppose que si vous faites le mauvais choix au début, la refactorisation pour y remédier plus tard peut être relativement simple.


0

Ce que vous voyez est essentiellement un artefact de l'expressivité limitée des systèmes de types.

Théoriquement, si votre système de type vous permettait de spécifier avec précision le comportement de ces deux interfaces, leurs signatures seraient différentes car leurs comportements sont différents. En pratique, l'expressivité des systèmes de types est limitée par des limitations fondamentales telles que le problème de l'arrêt et le théorème de Rice, de sorte que toutes les facettes du comportement ne peuvent pas être exprimées.

Maintenant, différents systèmes de types ont différents degrés d'expressivité, mais il y aura toujours quelque chose qui ne peut pas être exprimé.

Par exemple: deux interfaces qui ont un comportement d'erreur différent peuvent avoir la même signature en C #, mais pas en Java (car en Java les exceptions font partie de la signature). Deux interfaces dont le comportement ne diffère que par leurs effets secondaires peuvent avoir la même signature en Java, mais auront des signatures différentes en Haskell.

Ainsi, il est toujours possible que vous vous retrouviez avec la même signature pour des comportements différents. Si vous pensez qu'il est important de pouvoir distinguer ces deux comportements à plus qu'un niveau nominal, alors vous devez passer à un système de type expressif plus (ou différent).

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.