如何在卡牌游戏设计中遵循接口隔离原则且避免类型转换?
问题背景
我正在开发一个个人项目的简单卡牌游戏,规则如下:
- 卡牌分为action、item、champion三种类型
- 玩家可将卡牌置于自己的playing field
- 打出action或item卡牌会立即触发其效果
- champion卡牌进入playing field时处于untapped状态,需被tapped才能激活能力
- 回合结束时,action和item卡牌会被弃置,champion卡牌则留在场上并恢复为untapped状态
- 玩家可直接攻击对手,或攻击对手场上的champion卡牌
最初的简化设计代码:
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(); } }
在实现攻击规则时,若在Card接口中添加takeDamage()方法,ItemCard和ActionCard需实现该方法但永远不会被调用,只能抛出NotTargetableException,违反了接口隔离原则(Interface Segregation Principle)。
调整后的设计:
interface Attackable { void takeDamage(Combat combat); } class ChampionCard extends AbstractCard implements Attackable {} class Player implements Attackable {}
但此时getPlayedCards()返回Card集合,攻击时需要强制转换为Attackable,这是代码坏味道。
我的问题:如何在这种场景下遵循接口隔离原则且避免类型转换?
补充:我想到一个方案——在PlayingField中新增targetableCards()方法返回Attackable类型集合,将可被攻击的卡牌加入该集合,这是否合规?
解决方案分析
你提出的targetableCards()方案是合规且推荐的
这个方案完全契合接口隔离原则,同时从根源上消除了类型转换的需求:
- 职责分离清晰:
PlayingField作为场上卡牌的管理者,分别提供getPlayedCards()(返回所有场上卡牌)和targetableCards()(仅返回可被攻击的目标)两个方法,调用方无需自行判断类型,直接按需调用即可。 - 封装逻辑合理:哪些卡牌可被攻击的判断逻辑,放在
PlayingField内部比外部调用方处理更合适,符合单一职责原则,也避免了重复的类型检查代码。
进一步优化建议
细化PlayingField的内部卡牌管理:
在PlayingField内部拆分维护不同类型的卡牌集合,比如actionCards、itemCards、championCards,这样:getPlayedCards()可以通过合并这些集合返回所有场上卡牌targetableCards()直接返回championCards(因为只有它实现了Attackable)
内部管理更清晰,也避免了添加卡牌时的重复判断。
统一攻击目标的获取逻辑:
玩家本身也是Attackable,可以在Player类中新增getAllAttackableTargets()方法,返回包含自身和场上targetableCards()的集合,让攻击方可以一次性获取所有可攻击目标(玩家+对方场上的冠军卡牌)。增强集合的安全性:
返回targetableCards()时,可以返回不可修改的集合视图(比如Collections.unmodifiableList()),避免外部代码直接修改场上的可攻击卡牌集合。
示例优化代码
interface Card {} abstract class AbstractCard implements Card {} class ActionCard extends AbstractCard {} class ItemCard extends AbstractCard {} interface Attackable { void takeDamage(Combat combat); } class ChampionCard extends AbstractCard implements Attackable { @Override public void takeDamage(Combat combat) { // 实现冠军卡牌受伤害逻辑 } } class PlayingField { private final Collection<ActionCard> actionCards = new ArrayList<>(); private final Collection<ItemCard> itemCards = new ArrayList<>(); private final Collection<ChampionCard> championCards = new ArrayList<>(); public Collection<Card> getPlayedCards() { return Stream.concat( Stream.concat(actionCards.stream(), itemCards.stream()), championCards.stream() ).collect(Collectors.toUnmodifiableList()); } public Collection<Attackable> getTargetableCards() { return Collections.unmodifiableCollection(championCards); } public void addCard(Card card) { if (card instanceof ActionCard) { actionCards.add((ActionCard) card); } else if (card instanceof ItemCard) { itemCards.add((ItemCard) card); } else if (card instanceof ChampionCard) { championCards.add((ChampionCard) card); } } public void endTurn() { actionCards.clear(); itemCards.clear(); championCards.forEach(champion -> { // 恢复为untapped状态的逻辑 }); } } class Player implements Attackable { private final PlayingField playingField; public Player(PlayingField playingField) { this.playingField = playingField; } @Override public void takeDamage(Combat combat) { // 玩家受伤害逻辑 } public Collection<Attackable> getAllAttackableTargets() { Collection<Attackable> targets = new ArrayList<>(playingField.getTargetableCards()); targets.add(this); return Collections.unmodifiableCollection(targets); } }
内容的提问来源于stack exchange,提问作者Stefan Rendevski

