Коли припинити спадщину?


9

Колись тому я задав запитання на стек Overflow про спадщину.

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

public abstract class Piece
{
    public void MakeMove();
    public void TakeBackMove();
}

public abstract class Pawn: Piece {}

public class WhitePawn :Pawn {}

public class BlackPawn:Pawn {}

Програмісти знайшли мій дизайн трохи над інженером, і запропонували видалити кольорові класи шматочків і утримати колір шматка як члена властивості, як показано нижче.

public abstract class Piece
{
    public Color Color { get; set; }
    public abstract void MakeMove();
    public abstract void TakeBackMove();
}

Таким чином П'єс може пізнати його колір. Після впровадження я бачив, як моя реалізація йде як нижче.

public abstract class Pawn: Piece
{
    public override void MakeMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }

    public override void TakeBackMove()
    {
        if (this.Color == Color.White)
        {

        }
        else
        {

        }
    }
}

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

У такому випадку ви мали б такі класи, як WhitePawn, BlackPawn, або ви б пішли на дизайн, зберігаючи властивість Color?

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

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

Я насправді просто запитую, чи не призведе використання властивості Color, якщо в заявах краще використовувати спадкування?


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

11
Я думаю, що ваше основне неправильне уявлення - не усвідомлювати, що в шахах "колір" - це справді атрибут гравця, а не п'єса. Ось чому ми кажемо "негритяни рухаються" і т. Д. Розмальовки є, щоб визначити, якому гравцю належить твір. Пішак дотримується тих самих правил руху та обмежень, незалежно від того, якого кольору він є єдиним варіантом, який напрямок "вперед".
Джеймс Андерсон

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

1
Більш складним питанням є створення підкласу для кожного типу твору чи ні. Тип шматка визначає більше поведінкових аспектів, ніж колір шматка.
NoChance

3
Якщо ви спокусилися почати розробку за допомогою ABC (Анотація базових класів), нам все користь, а не ... Випадки, коли ABC насправді корисні, дуже рідкісні, і навіть тоді корисніше використовувати інтерфейс. замість цього.
Еван Плейс

Відповіді:


12

Уявіть, що ви розробляєте програму, яка містить транспортні засоби. Ви створюєте щось подібне?

class BlackVehicle : Vehicle { }
class DarkBlueVehicle : Vehicle { }
class DarkGreenVehicle : Vehicle { }
// hundreds of other classes...

У реальному житті колір - це властивість предмета (або автомобіля, або шахової фігури), і повинен бути представлений властивістю.

Існують MakeMoveі TakeBackMoveрізні для чорних і білих? Зовсім не: правила однакові для обох гравців, а це означає, що ці два методи будуть абсолютно однаковими для чорно-білих , а це означає, що ви додаєте умову там, де вона вам не потрібна.

З іншого боку, якщо у вас є WhitePawnі BlackPawn, я не здивуюсь, якщо незабаром чи пізніше ви напишете:

var piece = (Pawn)this.CurrentPiece;
if (piece is WhitePawn)
{
}
else // The pice is of type BlackPawn
{
}

або навіть щось на кшталт:

public abstract class Pawn
{
    public Color Player
    {
        get
        {
            return this is WhitePawn ? Color.White : Color.Black;
        }
    }
}

// Now imagine doing the same thing for every other piece.

4
@Freshblood Я думаю, що було б краще мати directionвластивість і використовувати це для переміщення, ніж використовувати властивість кольору. Колір в ідеалі повинен використовуватися лише для візуалізації, а не для логіки.
Gyan aka Gary Buyn

7
Крім того, шматки рухаються в тому ж напрямку, вперед, назад, вліво, вправо. Різниця полягає в тому, з якої сторони дошки вони починаються. По дорозі автомобілі на протилежних смугах обидва рухаються вперед.
ковзання

1
@Blrfl Ви можете мати рацію, що directionвластивість булева. Я не бачу, що в цьому погано. Що мене збентежило питання до коментаря Freshblood, зазначеного вище, чому він хотів би змінити логіку руху на основі кольору. Я б сказав, що це важче зрозуміти, оскільки колір не має сенсу впливати на поведінку. Якщо все, що ви хочете зробити, це розрізнити, якому гравцеві належить, який твір ви могли б скласти їх у дві окремі колекції та уникати необхідності у власності чи спадку. Самі п'єси можуть бути блаженно не знаючи, якому гравцю вони належать.
Gyan aka Gary Buyn

1
Я думаю, що ми дивимось на одне й те саме з двох різних точок зору. Якби дві сторони були червоно-синіми, чи відрізнялася б їх поведінка від чорно-білих? Я б сказав «ні», тому я кажу, що колір не є причиною поведінкових відмінностей. Це лише тонка відмінність, але саме тому я використовував би directionабо, можливо, playerвластивість замість colorодного в логіці гри.
Gyan aka Gary Buyn

1
@Freshblood white castle's moves differ from black's- як? Ви маєте на увазі кастинг? Як королівська, так і королева на 100% симетрична для чорно-білих.
Конрад Моравський

4

Що ви повинні запитати у себе, це чому вам справді потрібні ці твердження у вашому класі пішаків:

    if (this.Color == Color.White)
    {

    }
    else
    {
    }

Я впевнений, що буде достатньо мати невеликий набір функцій на кшталт

public abstract class Piece
{
    bool DoesPieceHaveSameColor(Piece otherPiece) { /*...*/   }

    Color OtherColor{get{/*...*/}}
    // ...
}

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

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

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


+1 для того, щоб вказати, що код, що стосується кольору, швидше за все, немає. Білі та чорні пішаки по суті ведуть себе так само, як і всі інші твори.
Конрад Моравський

2

Спадкування не настільки корисне, коли розширення не впливає на зовнішні можливості об'єкта .

Властивість Color не впливає на використання об'єкта Piece . Наприклад, всі шматки можуть закликати метод moveForward або moveBackwards (навіть якщо переміщення не є законним), але інкапсульована логіка Piece використовує властивість Color для визначення абсолютного значення, яке представляє рух вперед або назад (-1 або + 1 на "вісі"), що стосується вашого API, ваш суперклас та підкласи - це однакові об'єкти (оскільки всі вони піддають однакові методи).

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


Рух назад є незаконним лише у випадку пішаків. І вони насправді не повинні знати, чорні вони чи білі - достатньо (і логічно), щоб вони розрізняли форварди проти назад. BoardКлас (наприклад) може бути відповідальними за перетворення цих концепцій в -1 або +1 , в залежності від кольору частини.
Конрад Моравський

0

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

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

interface IMoveCalculator
{
    List<Square> GetValidDestinations();
}

class KnightMoveCalculator : IMoveCalculator;
class QueenMoveCalculator : IMoveCalculator;
class PawnMoveCalculator : IMoveCalculator; 
// etc.

class Piece
{
    IMoveCalculator move_calculator;
public:
    Piece(IMoveCalculator mc) : move_calculator(mc) {}
}

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

@WeekendWarrior - "Особисто я взагалі нічого не успадкував би Piece". Здається, П'єс відповідає за більше, ніж просто обчислення рухів, що є причиною того, щоб розділити рух як окрему проблему. Визначення того, яку частину отримує, який калькулятор буде залежним від введення залежності, наприкладvar my_knight = new Piece(new KnightMoveCalculator())
Бен Коттрелл

Це було б гарним кандидатом на модель легкої ваги. На шаховій дошці є 16 пішаків, і всі вони поділяють однакову поведінку . Така поведінка може бути легко витягнута в єдину вагу. Те саме стосується всіх інших творів.
MattDavey

-2

У цій відповіді я припускаю, що під "шаховим двигуном" автор питання означає "шаховий AI", а не просто двигун перевірки руху.

Я думаю, що використання OOP для творів та їх дійсних рухів є поганою ідеєю з двох причин:

  1. Сила шахового двигуна сильно залежить від того, наскільки швидко він може маніпулювати дошками, див., Наприклад, подання на шаховій програмі . Зважаючи на рівень оптимізації, описаний у цій статті, я не вважаю, що звичайний функціональний дзвінок є доступним. Виклик до віртуального методу додав би рівень непрямості та нарахував би ще більшу ефективність покарання. Вартість OOP там недоступна.
  2. Переваги OOP, такі як розширюваність, не потрібні для штук, оскільки шахи, швидше за все, не отримають нових штук незабаром. Доцільна альтернатива ієрархії типу, заснованої на спадщині, включає використання переліків та перемикачів. Дуже низькотехнологічний і схожий на C, але важко перемогти ефективність.

Відхилення від міркувань щодо продуктивності, включаючи колір твору в ієрархії типів, на мою думку, погана ідея. Ви опинитесь у ситуаціях, коли вам потрібно перевірити, чи є два шматки різних кольорів, наприклад для захоплення. Перевірка цієї умови легко виконується за допомогою властивості. Зробити це за допомогою is WhitePiece-tests було б гірше порівняно.

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

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


1
Можливо, не потрібно занадто швидко писати шаховий движок.
Свіжа кров

5
-1. Такі рішення, як "гіпотетично, це може стати проблемою продуктивності, тому це погано", - ІМХО майже завжди є чистою забобоною. І успадкування - це не лише "розширюваність" у тому сенсі, про який ви його згадуєте. Для різних творів застосовуються різні шахові правила, і кожен вид твору, що має різну реалізацію подібного методу, WhichMovesArePossibleє розумним підходом.
Doc Brown

2
Якщо вам не потрібна розширюваність, краще використовувати підхід на основі перемикача / перерахування, оскільки це швидше, ніж віртуальний метод відправки. Шаховий двигун є важким для процесора, і операції, які виконуються найчастіше (і, отже, критичні для ефективності), виконують рухи та скасовують їх. Лише на один рівень вище, на якому перераховані всі можливі рухи. Я очікую, що це теж буде критично часом. Справа в тому, що я програмував шахові двигуни в C. Навіть без OOP низькі рівні оптимізації в представленні на дошці були суттєвими. Який досвід у вас є, щоб звільнити мій?
Джон

2
@Joh: Я не буду зневажати ваш досвід, але я впевнений, що це не те, що було після ОП. І хоча оптимізація низького рівня може бути важливою для кількох проблем, я вважаю це помилкою зробити їх основою для дизайнерських рішень високого рівня - особливо, коли досі не виникає жодних проблем щодо продуктивності. Мій досвід полягає в тому, що Кнут був дуже правий, сказавши "Передчасна оптимізація - корінь усього зла".
Doc Brown

1
-1 Я повинен погодитися з док. Справа в тому, що цей "двигун", про який говорить ОП, може просто стати механізмом перевірки шахової гри для двох гравців. У такому випадку думка про продуктивність OOP проти будь-якого іншого стилю є абсолютно смішною, прості перевірки моментів зайняли б мілісекунди навіть на низькому кінці ARM-пристрою. Не читайте більше вимог, ніж зазначено насправді.
Тімоті Болдрідж
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.