Я пытаюсь смоделировать карточную игру, в которой у карт есть два важных набора функций:
Первый эффект. Это изменения состояния игры, которые происходят при игре на карте. Интерфейс для эффекта выглядит следующим образом:
boolean isPlayable(Player p, GameState gs);
void play(Player p, GameState gs);
И вы можете считать карту пригодной для игры, если и только если вы можете удовлетворить ее стоимость, и все ее эффекты являются играбельными. Вот так:
// 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;
}
Ладно, пока все довольно просто.
Другой набор функций на карте - это способности. Эти способности - это изменения состояния игры, которые вы можете активировать по своему желанию. Когда я придумал интерфейс для них, я понял, что им нужен метод для определения, можно ли их активировать или нет, и метод для реализации активации. Это в конечном итоге
boolean isActivatable(Player p, GameState gs);
void activate(Player p, GameState gs);
И я понимаю, что за исключением того, что он называется «активировать», а не «играть», Ability
и Effect
иметь точно такую же подпись.
Плохо ли иметь несколько интерфейсов с одинаковой подписью? Должен ли я просто использовать один и иметь два набора одного и того же интерфейса? Как так:
Set<Effect> effects;
Set<Effect> abilities;
Если да, то какие шаги по рефакторингу я должен предпринять в будущем, если они станут неидентичными (по мере того, как будет выпущено больше функций), особенно если они расходятся (т. Е. Они оба приобретают то, чего не должен другой, в отличие от одного выигрыша) а другой является полным подмножеством)? Я особенно обеспокоен тем, что объединение их будет неустойчивым, как только что-то изменится.
Мелкий шрифт:
Я признаю, что этот вопрос порожден разработкой игр, но я чувствую, что это та проблема, которая может так же легко всплыть в неигровой разработке, особенно при попытке разместить бизнес-модели нескольких клиентов в одном приложении, как это происходит почти с каждый проект, который я когда-либо делал, имел более чем одно влияние на бизнес ... Кроме того, используемые фрагменты - это фрагменты Java, но это также легко можно применить ко множеству объектно-ориентированных языков.
источник
Ответы:
Тот факт, что два интерфейса имеют одинаковый контракт, не означает, что они имеют одинаковый интерфейс.
Принцип замещения Лискова гласит:
Или, другими словами: все, что верно для интерфейса или супертипа, должно быть верно для всех его подтипов.
Если я правильно понимаю ваше описание, способность - это не эффект, а эффект - это не способность. Если один из них изменит свой контракт, маловероятно, что другой изменится вместе с ним. Я не вижу веской причины пытаться связать их друг с другом.
источник
Из Википедии : « Интерфейс часто используется для определения абстрактного типа, который не содержит данных, но демонстрирует поведение, определенное как методы ». На мой взгляд, интерфейс используется для описания поведения, поэтому, если у вас другое поведение, имеет смысл использовать разные интерфейсы. Читая ваш вопрос, у меня сложилось впечатление, что вы говорите о разном поведении, поэтому разные интерфейсы могут быть лучшим подходом.
Еще один момент, который вы сказали, это то, что если одно из этих поведений изменится. Что происходит, когда у вас есть только один интерфейс?
источник
Если в правилах вашей карточной игры проводится различие между «эффектами» и «способностями», вам необходимо убедиться, что это разные интерфейсы. Это предотвратит случайное использование одного из них там, где требуется другой.
Тем не менее, если они чрезвычайно похожи, может иметь смысл вывести их из общего предка. Рассмотрим внимательно: у вас есть основания полагать , что «эффект» и «способности» всегда обязательно иметь один и тот же интерфейс? Если вы добавите элемент к
effect
интерфейсу, ожидаете ли вы того же элемента, добавленного кability
интерфейсу?Если это так, то вы можете поместить такие элементы в общий
feature
интерфейс, из которого они оба являются производными. Если нет, то вам не следует пытаться унифицировать их - вы будете тратить время на перемещение между базовым и производным интерфейсами. Однако, поскольку вы не намерены использовать общий базовый интерфейс для чего-либо, кроме «не повторяйте себя», это может не иметь большого значения. И, если вы придерживаетесь этого намерения, я предполагаю, что, если вы сделаете неправильный выбор в начале, рефакторинг, чтобы исправить это позже, может быть относительно простым.источник
То, что вы видите, является в основном артефактом ограниченной выразительности систем типов.
Теоретически, если ваша система типов позволила вам точно определить поведение этих двух интерфейсов, тогда их сигнатуры будут другими, потому что их поведение отличается. Практически, выразительность систем типов ограничена фундаментальными ограничениями, такими как проблема остановки и теорема Райс, поэтому не каждый аспект поведения может быть выражен.
Системы разных типов имеют разную степень выраженности, но всегда будет что-то, что не может быть выражено.
Например: два интерфейса с разным поведением ошибок могут иметь одинаковую сигнатуру в C #, но не в Java (потому что в Java исключения являются частью сигнатуры). Два интерфейса, поведение которых отличается только своими побочными эффектами, могут иметь одинаковую сигнатуру в Java, но иметь разные сигнатуры в Haskell.
Таким образом, всегда возможно, что у вас будет одна и та же подпись для разных типов поведения. Если вы считаете, что важно различать эти два поведения не только на номинальном уровне, то вам нужно переключиться на более (или другую) систему выразительных типов.
источник