I am attempting to model a card game where cards have two important sets of features:
The first is an effect. These are the changes to the game state that happen when you play the card. The interface for effect is as follows:
boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);
And you could consider the card to be playable if and only if you can meet its cost and all its effects are playable. Like so:
// 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;
}
Okay, so far, pretty simple.
The other set of features on the card are abilities. These abilities are changes to the game state that you can activate at-will. When coming up with the interface for these, I realized they needed a method for determining whether they can be activated or not, and a method for implementing the activation. It ends up being
boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);
And I realize that with the exception of calling it “activate” instead of “play”, Ability
and Effect
have the exact same signature.
Is it a bad thing to have multiple interfaces with an identical signature? Should I simply use one, and have two sets of the same interface? As so:
Set<Effect> effects;
Set<Effect> abilities;
If so, what refactoring steps should I take down the road if they become non-identical (as more features are released), particularly if they’re divergent (i.e. they both gain something the other shouldn’t, as opposed to only one gaining and the other being a complete subset)? I’m particularly concerned that combining them will be non-sustainable as soon as something changes.
The fine print:
I recognize this question is spawned by game development, but I feel it’s the sort of problem that could just as easily creep up in non-game development, particularly when trying to accommodate the business models of multiple clients in one application as happens with just about every project I’ve ever done with more than one business influence… Also, the snippets used are Java snippets, but this could just as easily apply to a multitude of object oriented languages.
2
Just because two interfaces have the same contract, doesn’t mean that they’re the same interface.
Liskov Substitution Principle states:
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
Or, in other words: Anything that is true of an interface or supertype should be true of all its subtypes.
If I’m understanding your description properly, an ability is not an effect and an effect is not an ability. Should either one change its contract, it’s unlikely the other one will change with it. I can see no good reason to try to bind them to each other.
From Wikipedia: “interface is often used to define an abstract type that contains no data, but exposes behaviors defined as methods“. In my opinion an interface is used to describe a behavior, so if you have different behaviors it makes sense to have different interfaces. Reading your question the impression that I got is that you are talking about different behaviors so different interfaces may be the best approach.
Another point that yourself said is that if one of those behaviors change. Then what happens when you have just one interface?
If the rules of your card game make a distinction between “effects” and “abilities”, you need to make sure that they are different interfaces. This will keep you from accidentally using one of them where the other is required.
That said, if they are extremely similar, it may make sense to derive them from a common ancestor. Consider carefully: do you have reason to believe that “effects” and “abilities” will always necessarily have the same interface? If you add an element to the effect
interface, would you expect to need the same element added to the ability
interface?
If so, then you can place such elements in a common feature
interface from which they are both derived. If not, then you shouldn’t try to unify them — you will waste your time moving stuff between base and derived interfaces. However, since you don’t intend to actually use the common base interface for anything but “don’t repeat yourself”, it may not make that much difference. And, if you stick to that intention, my guess is that if you make the wrong choice at the start, refactoring to fix it later may be relatively simple.
What you are seeing is basically an artifact of the limited expressiveness of type systems.
Theoretically, if your type system allowed you to accurately specify the behavior of those two interfaces, then their signatures would be different because their behaviors are different. Practically, the expressiveness of type systems is limited by fundamental limitations such as the Halting Problem and Rice’s Theorem, so not every facet of behavior can be expressed.
Now, different type systems have differing degrees of expressiveness, but there will always be something which cannot be expressed.
For example: two interfaces which have different error behavior may have the same signature in C#, but not in Java (because in Java exceptions are part of the signature). Two interfaces whose behavior only differs in their side-effects may have the same signature in Java, but will have different signatures in Haskell.
So, it is always possible that you will end up with the same signature for different behaviors. If you think it is important to be able to distinguish between those two behaviors on more than just a nominal level, then you need to switch to a more (or different) expressive type system.