Наследование против композиции для шахматных фигур

9

Быстрый поиск этого стека обмена показывает, что в целом состав обычно считается более гибким, чем наследование, но, как всегда, это зависит от проекта и т. Д., И бывают случаи, когда наследование является лучшим выбором. Я хочу создать трехмерную шахматную игру, в которой каждая фигура имеет сетку, возможно, различные анимации и так далее. В этом конкретном примере кажется, что вы могли бы аргументировать оба подхода, я не прав?

Наследование будет выглядеть примерно так (с правильным конструктором и т. Д.)

class BasePiece
{
    virtual Squares GetValidMoveSquares() = 0;
    Mesh* mesh;
    // Other fields
}

class Pawn : public BasePiece
{
   Squares GetValidMoveSquares() override;
}

который, безусловно, подчиняется принципу «есть», тогда как композиция будет выглядеть примерно так

class MovementComponent
{
    virtual Squares GetValidMoveSquares() = 0;
}

class PawnMovementComponent
{
     Squares GetValidMoveSquares() override;
}

enum class Type
{
     PAWN,
     BISHOP, //etc
}


class Piece
{
    MovementComponent* movementComponent;
    MeshComponent* mesh;
    Type type;
    // Other fields
 }

Это больше вопрос личных предпочтений или один подход явно более разумный, чем другой здесь?

РЕДАКТИРОВАТЬ: Я думаю, что я узнал кое-что из каждого ответа, поэтому я чувствую себя плохо, выбирая только один. Мое окончательное решение будет вдохновлено несколькими постами здесь (все еще работаем над этим). Спасибо всем, кто нашел время, чтобы ответить.

CanISleepYet
источник
Я считаю, что оба могут существовать бок о бок, эти два предназначены для разных целей, но они не являются исключительными друг для друга. Шахматы состоят из разных фигур и досок, и фигуры разных типов. Эти части имеют некоторые основные свойства и поведение, а также имеют определенные свойства и поведение. Так что, по моему мнению, композицию следует наносить поверх доски и фигур, а наследование - по типам фигур.
Hitesh Gaur
Хотя некоторые люди могут утверждать, что все наследование плохое, я думаю, что общая идея заключается в том, что наследование интерфейса хорошо / хорошо и что наследование реализации полезно, но может быть проблематично. По моему опыту, все, что находится за пределами одного уровня наследования, сомнительно. С этим может быть все в порядке, но это не так просто, не создавая запутанного беспорядка.
JimmyJames
Паттерн стратегии является распространенным явлением в программировании игр. var Pawn = new Piece(howIMove, howITake, whatILookLike)мне кажется намного проще, более управляемым и более обслуживаемым, чем иерархия наследования.
Ant P
@AntP Это хороший момент, спасибо!
CanISleepYet
@AntP Сделай ответ! ... В этом есть много достоинств!
svidgen

Ответы:

4

На первый взгляд ваш ответ делает вид, что «композиция» решения не использует наследование. Но я думаю, вы просто забыли добавить это:

class PawnMovementComponent : MovementComponent
{
     Squares GetValidMoveSquares() override;
}

(и больше из этих производных, один для каждого из шести типов частей). Теперь это больше похоже на классический паттерн «Стратегия» и также использует наследование.

К сожалению, у этого решения есть недостаток: Pieceтеперь каждый из них дважды сохраняет свою информацию о типе:

  • внутри переменной-члена type

  • внутри movementComponent(представленный подтипом)

Избыточность - это то, что действительно может доставить вам неприятности - простой класс, подобный a, Pieceдолжен обеспечивать «единый источник правды», а не два источника.

Конечно, вы можете попытаться хранить информацию о типе только в typeдочерних классах и не создавать их MovementComponent. Но этот дизайн, скорее всего, приведет к огромному « switch(type)» заявлению в реализации GetValidMoveSquares. И это, безусловно, убедительный признак того, что наследование является лучшим выбором здесь .

Обратите внимание на «наследство» дизайн, довольно легко обеспечить поле «тип» в нерезервированной образом: добавить виртуальный метод GetType()в BasePieceи реализовать его соответствующим образом в каждом базовом классе.

Что касается «продвижения»: я нахожусь здесь с @svidgen, я считаю спорные аргументы, представленные в ответе @ TheCatWhisperer .

Интерпретация «продвижения» как физического обмена произведений вместо того, чтобы интерпретировать его как смену типа одного и того же произведения, кажется мне более естественной. Поэтому реализация этого аналогичным образом - путем обмена одной фигуры на другую другого типа - скорее всего, не вызовет каких-либо огромных проблем - по крайней мере, не для конкретного случая шахмат.

Док Браун
источник
Спасибо, что сообщили мне название паттерна, я не понимал, что он называется «Стратегия». Вы также правы, я пропустил наследование компонента движения в моем вопросе. Действительно ли тип избыточен? Если я хочу осветить все королевы на доске, было бы странно проверить, какой компонент движения у них был, плюс мне нужно было бы разыграть компонент движения, чтобы увидеть, какой это тип, который будет супер уродливым нет?
CanISleepYet
@CanISleepYet: проблема с вашим предложенным полем типа заключается в том, что оно может отличаться от подтипа movementComponent. Посмотрите мое редактирование, как обеспечить поле типа безопасным способом в дизайне «наследования».
Док Браун
4

Вероятно, я бы предпочел более простой вариант, если у вас нет явных, четко сформулированных причин или серьезных проблем с расширяемостью и обслуживанием.

Опция наследования состоит в меньшем количестве строк кода и меньшей сложности. Каждый тип фигуры имеет «неизменное» соотношение 1 к 1 с его характеристиками движения. (В отличие от его представления, которое является 1-к-1, но не обязательно неизменным - вы можете предложить различные «скины».)

Если вы разбиваете эти неизменные отношения «один к одному» между фигурами и их поведением / движением на несколько классов, вы, вероятно, только добавляете сложность - и не намного больше. Итак, если бы я просматривал или наследовал этот код, я бы ожидал увидеть хорошие, документированные причины сложности.

Все это говорит о том, что еще более простой вариант , IMO, - создать Piece интерфейс, который реализуют ваши отдельные пьесы . В большинстве языков это на самом деле очень отличается от наследования , потому что интерфейс не будет ограничивать вас от реализации другого интерфейса . ... В этом случае вы просто не получаете никакого поведения базового класса бесплатно: вы бы хотели поместить общее поведение в другое место.

svidgen
источник
Я не куплюсь на то, что решение по наследованию - это «меньше строк кода». Возможно, в этом классе, но не в целом.
JimmyJames
@JimmyJames Правда? ... Просто посчитайте строки кода в OP ... по общему признанию, не целое решение, но неплохой показатель того, какое решение более простое и сложное в обслуживании.
свидген
Я не хочу говорить об этом слишком хорошо, потому что это все условно, но да, я действительно так думаю. Я утверждаю, что этот фрагмент кода незначителен по сравнению со всем приложением. Если это упрощает реализацию правил (спорно), то вы можете сэкономить десятки или даже сотни строк кода.
JimmyJames
Спасибо, я не думал об интерфейсе, но вы можете быть правы, что это лучший способ. Проще почти всегда лучше.
CanISleepYet
2

С шахматами дело в том, что правила игры и правила фигуры прописаны и зафиксированы. Любой дизайн, который работает хорошо - используйте все, что вам нравится! Экспериментируйте и попробуйте их все.

В деловом мире, однако, ничто так строго не предписано, как это - бизнес-правила и требования меняются со временем, и программы должны меняться со временем, чтобы приспособиться. Вот где разница между данными и данными. Здесь простота упрощает сопровождение сложных решений в течение долгого времени и меняющихся требований. В целом, в бизнесе нам также приходится иметь дело с постоянством, которое также может включать базу данных. Итак, у нас есть правила, например, не использовать несколько классов, когда один класс будет выполнять работу, и не использовать наследование, когда композиции достаточно. Но все эти правила направлены на то, чтобы сделать код обслуживаемым в долгосрочной перспективе перед лицом изменяющихся правил и требований - что не так в шахматах.

Что касается шахмат, наиболее вероятный путь долгосрочного обслуживания заключается в том, что ваша программа должна становиться все умнее и умнее, что в конечном итоге означает, что оптимизация скорости и хранилища будет доминировать. Для этого вам, как правило, придется идти на компромиссы, которые жертвуют удобочитаемостью и производительностью, поэтому даже самый лучший дизайн ОО в конечном итоге уйдет на второй план.

Эрик Эйдт
источник
Обратите внимание, что было много успешных игр, в которых используется концепция, а затем добавляются новые вещи. Если вы захотите сделать это в будущем с вашей игрой в шахматы, вы должны принять во внимание возможные будущие изменения.
Flater
2

Я хотел бы начать с некоторых замечаний о проблемной области (то есть правила шахмат):

  • Набор допустимых ходов для фигуры зависит не только от типа фигуры, но и от состояния доски (пешка может двигаться вперед, если клетка пуста, или может двигаться по диагонали при захвате фигуры).
  • Некоторые действия включают удаление существующей фигуры из игры (продвижение / захват фигуры) или перемещение нескольких фигур (рокировка).

Это неловко моделировать как ответственность самой пьесы, и ни наследование, ни композиция не чувствуются здесь великолепно. Было бы более естественно смоделировать эти правила как первоклассные граждане:

// pseudo-code, not pure C++

interface MovementRule {
  set<Square> getValidMoves(Board board, Square from); // what moves can I make from the given square?
  void makeMove(Board board, Square from, Square to); // update board state to reflect a specific move
}

class Game {
  Board board;
  map<PieceType, MovementRule> rules;
}

MovementRuleэто простой, но гибкий интерфейс, который позволяет реализациям обновлять состояние платы любым способом, необходимым для поддержки сложных движений, таких как рокировка и повышение по службе. Для стандартных шахмат у вас была бы одна реализация для каждого типа фигуры, но вы также можете подключить различные правила в Gameэкземпляре для поддержки разных вариантов шахмат.

Касабланка
источник
Интересный подход - хорошая пища для размышлений
CanISleepYet
1

Я думаю, что в этом случае наследование будет более чистым выбором. Композиция может быть немного более элегантной, но мне кажется, что она немного более вынужденная.

Если вы планируете разрабатывать другие игры, в которых используются движущиеся фигуры, которые ведут себя по-разному, композиция может быть лучшим выбором, особенно если вы использовали фабричный шаблон для генерации фигур, необходимых для каждой игры.

Курт Холдеман
источник
2
Как бы вы справились с продвижением по наследству?
TheCatWhisperer
4
@TheCatWhisperer Как вы справляетесь с этим, когда физически играете в игру? (Надеюсь, вы замените фигуру - не
перерезаете
0

которая, безусловно, подчиняется принципу "есть"

Правильно, значит, вы используете наследование. Вы все еще можете сочинять в своем классе Piece, но, по крайней мере, у вас будет основа. Композиция более гибкая, но гибкость, в общем, переоценена, и, конечно, в этом случае, потому что шансы того, что кто-то представит новый тип пьесы, который потребует от вас отказаться от вашей жесткой модели, равны нулю. Игра в шахматы не изменится в ближайшее время. Наследование дает вам руководящую модель, что-то, что также должно быть связано, обучающий код, направление. Композиции не так много, самые важные доменные сущности не выделяются, она бесхребетна, вам нужно разбираться в игре, чтобы понимать код.

Мартин Маат
источник
спасибо за добавление, что с композицией сложнее рассуждать о коде
CanISleepYet
0

Пожалуйста, не используйте наследование здесь. В то время как другие ответы, несомненно, имеют много драгоценных камней, использование наследования здесь, безусловно, было бы ошибкой. Использование композиции не только помогает вам решать проблемы, которые вы не ожидаете, но я уже вижу здесь проблему, которую наследование не может обработать изящно.

Повышение в должности, когда пешка превращается в более ценную фигуру, может стать проблемой наследования. Технически, вы можете справиться с этим, заменив одну часть другой, но у этого подхода есть ограничения. Одним из таких ограничений будет отслеживание статистики по частям, когда вы можете не захотеть сбрасывать информацию о деталях при продвижении (или писать дополнительный код для их копирования).

Теперь, что касается дизайна вашей композиции в вашем вопросе, я думаю, что это излишне сложно. Я не подозреваю, что вам нужно иметь отдельный компонент для каждого действия и можно придерживаться одного класса для каждого типа элемента. Тип enum также может быть ненужным.

Я бы определил Pieceкласс (как у вас уже есть), а также PieceTypeкласс. В PieceTypeклассе определяет все методы , которые пешка, ферзь, ЭСТ должен определить, такие как, CanMoveTo, GetPieceTypeName, ЭСТ. Тогда классы Pawnand Queen, ect фактически наследуются от PieceTypeкласса.

TheCatWhisperer
источник
3
Проблемы с продвижением и заменой, которые вы видите, тривиальны, ИМО. Разделение проблем в значительной степени требует, чтобы такое «отслеживание за штуку» (или что-то еще) в любом случае обрабатывалось независимо . ... Любые потенциальные преимущества, которые предлагает ваше решение, затмеваются воображаемыми проблемами, которые вы пытаетесь решить, ИМО.
svidgen
5
Чтобы уточнить: несомненно, есть некоторые достоинства в предложенном вами решении. Но их трудно найти в вашем ответе, который фокусируется в первую очередь на проблемах, которые действительно очень легко решить в «решении наследования». ... Этот вид умаляет (во всяком случае, для меня) то, что вы пытаетесь донести до вашего мнения.
svidgen
1
Если Pieceне виртуальный, я согласен с этим решением. Хотя дизайн класса на первый взгляд может показаться немного более сложным, я думаю, что реализация в целом будет проще. Главные вещи, которые могут быть связаны с, Pieceа не с, PieceType- это его личность и, возможно, его история перемещений.
JimmyJames
1
@svidgen Реализация, в которой используется составная часть, и реализация, использующая часть, основанную на наследовании, будут выглядеть в основном идентичными, за исключением деталей, описанных в этом решении. Это композиционное решение добавляет единый уровень косвенности. Я не считаю это чем-то, чего стоит избегать.
JimmyJames
2
@JimmyJames Верно. Но историю перемещений нескольких частей необходимо отслеживать и коррелировать - не то, за что должен отвечать какой-либо один элемент, ИМО. Это «отдельная забота», если вы увлекаетесь твердыми вещами.
svidgen
0

С моей точки зрения, с учетом предоставленной информации не имеет смысла отвечать, должно ли быть наследство или состав.

  • Наследование и компоновка - это всего лишь инструменты в наборе инструментов объектно-ориентированного конструктора.
  • Вы можете использовать эти инструменты для разработки множества различных моделей для предметной области проблемы.
  • Поскольку существует много таких моделей, которые могут представлять домен, в зависимости от точки зрения разработчика на требования, вы можете комбинировать наследование и композицию несколькими способами, чтобы создать функционально эквивалентные модели.

Ваши модели совершенно разные, в первой вы смоделировали концепцию шахматных фигур, во второй - концепцию движения фигур. Они могут быть функционально эквивалентными, и я бы выбрал тот, который лучше отражает область и помогает мне легче рассуждать об этом.

Кроме того, во втором дизайне тот факт, что у вашего Pieceкласса есть typeполе, ясно показывает, что здесь есть проблема дизайна, так как сама часть может быть нескольких типов. Разве это не похоже на то, что, вероятно, следует использовать какое-то наследование, где сама фигура является типом?

Итак, вы видите, очень трудно спорить о вашем решении. Важно не то, использовали ли вы наследование или композицию, а то, точно ли ваша модель отражает суть проблемы, и полезно ли ее рассуждать и обеспечивать ее реализацию.

Требуется некоторый опыт для правильного использования наследования и композиции, но это совершенно разные инструменты, и я не верю, что можно «заменить» один другим, хотя я могу согласиться с тем, что система может быть спроектирована полностью с использованием только композиции.

edalorzo
источник
Вы подчеркиваете, что важна модель, а не инструмент. Относительно избыточного типа действительно помогает наследование? Например, если я хочу выделить все пешки или проверить, была ли фигура королем, тогда мне не понадобится кастинг?
CanISleepYet
-1

Ну, я не предлагаю ни того, ни другого.

Хотя объектная ориентация является хорошим инструментом и часто помогает упростить вещи, это не единственный инструмент в наборе инструментов.

Вместо этого рассмотрите возможность реализации класса Board, содержащего простой байтовый массив.
Интерпретация и вычисление чего-либо происходит в функциях-членах с использованием битовой маскировки и переключения.

Используйте MSB для владельца, оставив 7 бит для части, хотя оставьте ноль для пустого.
Альтернативно, ноль для пустого, знак для владельца, абсолютное значение для части.

Если вы хотите, вы можете использовать битовые поля, чтобы избежать ручной маскировки, хотя они непереносимы:

class field {
    signed char data = 0;
public:
    constexpr field() = default;
    constexpr field(bool black, piece x) noexcept
    : data(x < piece::pawn || x > piece::king ? 0 : (black << 7) | x))
    {}
    constexpr bool is_black() noexcept { return data < 0; }
    constexpr bool is_white() noexcept { return data > 0; }
    constexpr bool empty() noexcept { return data == 0; }
    constexpr piece piece() noexcept { return piece(data & 0x7f); }
};
Deduplicator
источник
Интересно ... и склонный считая это вопрос C ++. Но, не будучи программистом на C ++, это не было бы моим первым (или вторым, или третьим ) инстинктом! ... Это может быть самый ответ C ++ здесь, хотя!
svidgen
7
Это микрооптимизация, которой не должно следовать никакое реальное применение.
Ложь Райан
@LieRyan Если у вас есть только ООП, все пахнет как объект. И действительно ли это «микро» - тоже интересный вопрос. В зависимости от того, что вы делаете с этим, это может быть весьма значительным.
Дедупликатор
Хех ... при этом C ++ является объектно-ориентированным. Я полагаю , что есть причина ФП с использованием C ++ вместо просто C .
svidgen
3
Здесь я менее обеспокоен отсутствием ООП, но скорее запутываю код с помощью битов. Если вы не пишете код для 8-битного Nintendo или не пишете алгоритмы, которые требуют работы с миллионами копий состояния платы, это преждевременная микрооптимизация и нецелесообразно. В нормальных сценариях это, вероятно, не будет быстрее в любом случае, потому что для выравнивания битового доступа требуются дополнительные битовые операции.
Ли Райан