Спадщина проти композиції для шахових фігур


9

Швидкий пошук цієї ставки обміну показує, що загалом склад вважається більш гнучким, ніж спадкування, але, як завжди, це залежить від проекту тощо. І є випадки, коли успадкування є кращим вибором. Я хочу зробити 3D-шахову гру, де кожен твір має сітку, можливо, різні анімації тощо. У цьому конкретному прикладі здається, що ви можете аргументувати справи для обох підходів, чи я помиляюся?

Спадщина виглядатиме приблизно так (з належним конструктором тощо)

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
 }

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

ЕДИТ: Я думаю, що я навчився чомусь з кожної відповіді, тому мені шкода лише вибрати один. Моє остаточне рішення отримає натхнення з декількох публікацій тут (все ще над цим працюю). Дякуємо всім, хто знайшов час для відповіді.


Я вважаю, що обидва можуть існувати пліч-о-пліч, ці двоє мають різну мету, але вони не є винятковими один для одного .. Шахи складаються з різних шматків і дощок, а шматки різного типу. Ці частини поділяють основні властивості та поведінку, а також мають специфічну властивість та поведінку. Тож, на мою думку, композицію слід наносити на дошку та шматки, тоді як успадкування слід дотримуватися над типами штук.
Хітеш Гаур

Хоча у вас можуть з’являтись люди, які стверджують, що всі спадкування погані, я думаю, що загальна ідея полягає в тому, що успадкування інтерфейсу є хорошим / прекрасним і що успадкування реалізації корисне, але може бути проблематичним. На моєму досвіді все, що перевищує єдиний рівень успадкування, сумнівне. Це може бути все гаразд, але зняти його не просто, не заплутавшись.
JimmyJames

Шаблон стратегії поширений в іграх програмування чомусь. var Pawn = new Piece(howIMove, howITake, whatILookLike)мені здається набагато простішим, більш керованим і більш досяжним, ніж ієрархія спадщини.
Мураха P

@AntP Це хороший момент, дякую!
CanISleepYet

@AntP Зробіть це відповіддю! ... У цьому є багато заслуг!
svidgen

Відповіді:


4

На перший погляд, ваша відповідь робить вигляд, що рішення "складу" не використовує спадкування. Але я думаю, ви просто забули додати це:

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

(і більше цих похідних, по одному для кожного з шести типів штук). Тепер це більше схоже на класичну схему "Стратегії", і вона також використовує спадщину.

На жаль, у цього рішення є недолік: кожен Pieceтепер містить інформацію про тип надлишково вдвічі:

  • всередині змінної члена type

  • всередині movementComponent(репресовано підтипом)

Ця надмірність - це те, що може насправді призвести до неприємностей - простий клас, як, наприклад, Pieceповинен містити "єдине джерело істини", а не два джерела.

Звичайно, ви можете спробувати зберегти інформацію про тип лише в type, а також створити не дочірні класи MovementComponent. Але ця конструкція, швидше за все, призведе до величезного switch(type)твердження під час впровадження "" GetValidMoveSquares. І це, безумовно, сильний показник того, що спадщина є кращим вибором тут .

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

Щодо "просування": я тут з @svidgen, я вважаю аргументи, викладені у відповіді @ TheCatWhisperer, дискусійною.

Тлумачення "просування" як фізичний обмін штуками замість того, щоб інтерпретувати її як зміну типу тієї самої п'єси, мені здається цілком природнішим. Тому реалізація цього способу аналогічним чином - обмінюючись одним твором на інший іншого типу - швидше за все, не викличе великих проблем - принаймні, не для конкретного шахового випадку.


Дякую, що сказали мені назву шаблону, я не зрозумів, що це називається стратегія. Ви також маєте рацію, що я пропустив у своєму питанні компонент спадкування руху. Чи справді тип є зайвим? Якщо я хочу висвітлити всі королеви на дошці, здасться дивним перевірити, який саме компонент руху вони мають, плюс мені потрібно було б передати компонент руху, щоб побачити, що це за тип, який би був супер негарним ні?
CanISleepYet

@CanISleepYet: Проблема із запропонованим вами полем "type" полягає в тому, що воно може розходитися з підтипом movementComponent. Дивіться мою редакцію, як безпечно забезпечити поле типу в дизайні "спадкування".
Док Браун

4

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

Варіант успадкування - менша кількість рядків коду та менша складність. Кожен тип шматка має "незмінне" відношення 1 на 1 зі своїми руховими характеристиками. (На відміну від репрезентації, яка від 1 до 1, але необов'язково незмінна - ви можете запропонувати різні "скіни".)

Якщо ви розірвете це незмінне відношення 1 на 1 між елементами та їх поведінкою / рухом на кілька класів, ви, ймовірно, лише додаєте складності - і не багато іншого. Тож, якби я переглядав чи успадковував цей код, я би сподівався побачити добрі, задокументовані причини складності.

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


Я не вважаю, що рішення про спадкування - це "менше рядків коду". Можливо, в цьому одному класі, але не загалом.
JimmyJames

@JimmyJames Дійсно? ... Просто порахуйте рядки коду в ОП ... зізнаємось, не ціле рішення, але не поганий показник, яке рішення має більше LOC та складність у підтримці.
svidgen

Я не хочу ставити надто тонку точку з цього приводу, тому що в цьому пункті все розумно, але так, я дійсно так вважаю. Я стверджую, що цей біт коду мізерно малий порівняно з усією програмою. Якщо це спрощує реалізацію правил (спірне питання), ви можете зберегти десятки чи навіть сотні рядків коду.
JimmyJames

Дякую, я не думав про інтерфейс, але ви, можливо, будете праві, що це найкращий шлях. Простіше майже завжди краще.
CanISleepYet

2

Справа в шахах полягає в тому, що правила гри та п'єси прописані та закріплені. Будь-який дизайн, який працює добре - використовуйте все, що вам подобається! Експериментуйте та спробуйте їх усі.

У діловому світі, однак, нічого не так строго прописано, як це - правила та вимоги бізнесу змінюються з часом, і програми повинні змінюватися з часом, щоб пристосувати їх. Ось де дані - це проти має - відносно даних мають значення. Тут простота робить складні рішення простішими в обслуговуванні з часом і зміною вимог. Загалом, у бізнесі нам також доводиться мати справу з наполегливістю, яка може включати і базу даних. Отже, у нас є такі правила, як не використовувати декілька класів, коли один клас зробить роботу, і не використовуйте спадкування, коли склад достатній. Але всі ці правила орієнтовані на те, щоб зробити кодом доцільний доступ у довгостроковій перспективі, зважаючи на зміни правил та вимог - що з шахами просто не так.

Що стосується шахів, найімовірніший шлях тривалого обслуговування - це те, що ваша програма повинна бути розумнішою та розумнішою, що з часом означає, що оптимізація швидкості та зберігання буде домінувати. Для цього вам, як правило, доведеться робити компроміси, які приносять шкоду читанню для продуктивності, і тому навіть найкращий дизайн OO з часом піде убік.


Зауважте, що було багато успішних ігор, які беруть концепцію, а потім кидають щось нове в суміш. Якщо ви, можливо, захочете це зробити в майбутньому за допомогою своєї шахової гри, вам слід врахувати можливі майбутні зміни.
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 Як ти впораєшся, коли ти фізично граєш у гру? (Сподіваємось, ви замінили шматок - пішак не будете повторно вирізати, чи не так?)
svidgen

0

що, безумовно, підкоряється принципу "є-а"

Правильно, значить, ви користуєтеся спадщиною. Ви все ще можете складати композицію в межах класу Piece, але принаймні у вас буде хребет. Склад гнучкіший, але в цілому гнучкість завищена, і, звичайно, в цьому випадку, оскільки шанси когось представити новий вид твору, який вимагає від вас відмовитися від жорсткої моделі - нульові. Гра в шахи не скоро зміниться. Спадщина дає вам орієнтир, модель, що також стосується, навчальний код, напрям. Складу не так багато, найважливіші доменні сутності не виділяються, це безхребетність, вам потрібно зрозуміти гру, щоб зрозуміти код.


дякую за те, що додав, що зі складом складніше міркувати про код
CanISleepYet

0

Будь ласка, не використовуйте тут спадщину. Хоча в інших відповідях, безумовно, є багато дорогоцінних каменів мудрості, використання спадщини тут, безумовно, буде помилкою. Використання композиції не тільки допомагає вам вирішувати проблеми, яких ви не передбачаєте, але я вже бачу тут проблему, що спадщина не може вирішуватись витончено.

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

Тепер, що стосується вашого дизайну композиції у вашому питанні, я думаю, що це зайво складне. Я не підозрюю, що вам потрібно мати окремий компонент для кожної дії і міг би дотримуватися одного класу на тип штуки. Енум типу також може бути непотрібним.

Я б визначив Pieceклас (як у вас уже є), а також PieceTypeклас. У PieceTypeкласі визначає всі методи , які пішак, ферзь, ЕСТ повинен визначити, такі як, CanMoveTo, GetPieceTypeName, ЕСТ. Тоді класи Pawnі Queen, ect практично наслідували б PieceTypeклас.


3
Проблеми, які ви бачите з просуванням та заміною, є дрібницею, IMO. Розділення проблем викликає велику кількість обов'язків, щоб таким "відстеженням за штуку" (або будь-яким іншим) в будь-якому разі керувати незалежно . ... Будь-які потенційні переваги, які пропонують ваші рішення, затьмарюють уявні проблеми, які ви намагаєтеся вирішити, IMO.
svidgen

5
Для уточнення: Безперечно, є деякі достоїнства запропонованого вам рішення. Але у вашій відповіді їх важко знайти, яка зосереджена насамперед на проблемах, які насправді дуже легко вирішити в "рішенні про спадщину". ... Цей вид заважає (як би мені не було) від будь-яких заслуг, які ви намагаєтеся донести до своєї думки.
svidgen

1
Якщо Pieceце не віртуально, я згоден з цим рішенням. Хоча дизайн класу може здатися трохи складнішим на поверхні, я думаю, що реалізація буде в цілому простішою. Основні речі, які були б пов'язані з, Pieceа не є, PieceType- це ідентичність та потенційно її історія переміщення.
JimmyJames

1
@svidgen Реалізація, яка використовує складений Piece, і реалізація, яка використовує фрагмент, заснований на спадщині, буде виглядати в основному однаково, окрім деталей, описаних у цьому рішенні. Цей розчин композиції додає єдиний рівень непрямості. Я не бачу цього як чогось, чого варто уникати.
JimmyJames

2
@JimmyJames Правильно. Але історію ходу декількох творів потрібно відстежувати і співвідносити - не те, за що має відповідати жодна композиція, IMO. Це "окрема стурбованість", якщо ви вживаєте подібні речі SOLID.
svidgen

0

З моєї точки зору, за наданою інформацією не розумно відповідати, чи має бути спадщина чи склад.

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

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

Крім того, у вашому другому дизайні факт, що у вашому Pieceкласі є typeполе, чітко показує, що тут є проблема дизайну, оскільки сама деталь може бути декількох типів. Чи не здається, що це, ймовірно, має використовувати якесь спадщину, де саме шматок є типом?

Тож, бачите, дуже важко сперечатися щодо свого рішення. Важливо не те, чи використовували ви спадщину чи склад, а чи відображає ваша модель точно область проблеми, і якщо корисно міркувати про неї та забезпечити її реалізацію.

Для правильного використання спадкування та композиції потрібен певний досвід, але вони є абсолютно різними інструментами, і я не вірю, що можна «замінити» одне іншим, хоча я можу погодитись, що система могла бути розроблена цілком за допомогою просто композиції.


Ви добре зазначаєте, що саме модель є важливою, а не інструментом. Що стосується надмірного типу, чи наслідування насправді допомагає? Наприклад, якщо я хочу виділити всі пішаки або перевірити, чи був твір королем, чи не знадобиться мені кастинг?
CanISleepYet

-1

Ну, я не пропоную жодного.

Хоча орієнтація на об'єкти є приємним інструментом і часто допомагає спростити речі, це не єдиний інструмент в інструментальному інструменті.

Розглянемо, замість цього, застосувати клас дошки, що містить простий байт-масив.
Інтерпретація її та обчислення речей на ній виконується у функціях-членах, використовуючи бітове маскування та комутацію.

Використовуйте 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); }
};

Цікаво ... і спритно, враховуючи це питання C ++. Але, не будучи програмістом на C ++, це не буде моїм першим (або другим чи третім ) інстинктом! ... Це, можливо, найбільше відповіді на С ++ тут!
svidgen

7
Це мікрооптимізація, якої не повинно слідувати жодна реальна програма.
Лежать Раян

@LieRyan Якщо у вас є OOP, все пахне предметом. І чи справді це "мікро" - це теж цікаве питання. Залежно від того, що ви робите з цим, це може бути досить значним.
Deduplicator

Хех ... що сказав , C ++ є об'єктно - орієнтованим. Я вважаю , що є причина ФП з використанням C ++ замість просто C .
svidgen

3
Тут я менш стурбований відсутністю OOP, а скоріше затуманив код бітним подвійністю. Якщо ви не пишете код для 8-бітового Nintendo або не пишете алгоритми, що вимагають роботи з мільйонами копій стану плати, це передчасна мікрооптимізація і недоцільна. У звичайних сценаріях, швидше за все, це не буде швидше, тому що нестандартний бітовий доступ вимагає додаткових бітових операцій.
Лежать Раян
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.