I am developing a simple card game, as part of a personal project. The game has a few rules, for example:
- A card can be either action, item, or champion
- A player can play a card on their "playing field"
- Playing an action or item immediately actives their effect
- Champions enter the "playing field" "untapped", and they need to get "tapped" to activate their ability
- At end of turn, actions and items are discarded, while champions remain in play, untapped.
- A player can attack either the opponent directly, or their champions in play.
A simplistic design for this would be:
interface Card {}
class AbstractCard implements Card {}
class ActionCard extends AbstractCard {}
class ItemCard extends AbstractCard {}
class ChampionCard extends AbstractCard {}
class PlayingField {
public Collection<Card> getPlayedCards() {}
}
class Player {
private final PlayingField playingField;
public Collection<Card> getPlayedCards() {
return playingField.getPlayedCards();
}
}
Since a Player
could play any of ItemCard
, ActionCard
, or ChampionCard
, I defined the method getPlayedCards()
to work with Card
. Now, in order to implement the rule that says "A player can attack either the opponent directly, or their champions in play.", I quickly realised I would need to add a method takeDamage()
to Card
. However, cards of type ItemCard
or ActionCard
would never be able to be attacked, they are not valid targets. So, adding this method on Card
would result in needless implementation in these two classes, where I would have to throw an exception, something along the lines of:
public void takeDamage(Combat combat) {
throw new NotTargetableException();
}
I was reading more about the Integration Segregation Principle, which basically says that I should avoid adding needless methods to an interface, so as not to force classes to implement methods that would/should never be invoked. Looking at my Card
s, ActionCard
and ItemCard
would never be valid targets of an attack. Furthermore, instances of these classes would not exist inside of the collection returned from getPlayedCards()
during opponent's turn, since they are discarded. So, a better approach would be to have:
interface Attackable {
void takeDamage(Combat combat);
}
class ChampionCard extends AbstractCard implements Attackable {}
class Player implements Attackable {}
But now comes my dilemma. Since Card
does not have a takeDamage
method, and getPlayingCards()
returns instances of Card
, I would have to type cast this into an Attackable
to be able to attack it. In the case of a ClassCastException
, this would have the same meaning as my previous NotTargetableException
. The general sentiment towards type casting, though, is that it is a code smell, and an indication that there's something wrong with the code design.
So, my question is. How would I achieve Interface Segregation without type casting in this case?
Edit:
Now that I wrote the question out, one simple "workaround" for this that I could think of, would be to have a method such as:
class PlayingField {
public Collection<Card> getPlayedCards() {} // same as before
public Collection<Attackable> targetableCards() {} // new method
}
And then AttackableCards
on play would be added to this collection. Would this be the "approved" approach?
CodePudding user response:
If you don't have any issue with changing the current hierarchy this can be done in the following way.
By Keeping different abstract parents for Attackable and Non-Attackable (safe) implementation.
CodePudding user response:
public class Card {
private Champion champion;
private Item item;
private Action action;
...
}
i.e. instead of a Champion card being a "kind of" card, a Card "has a" Champion. (Or "can have" in this case)
Then handle the stuff you can't do. Perhaps the default item.use() is a failure or you can't call if item is null.
The card is a container that may have one or more of these other types. (Also get some flexibility such as cards that contain both a champion and an action/item like an on tap effect)