Когда прекратить наследование?

9

Однажды я задал вопрос о переполнении стека о наследовании.

Я сказал, что проектирую шахматный движок в ООП-моде. Таким образом, я наследую все свои части от абстрактного класса 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
        {

        }
    }
}

Теперь я вижу, что свойство color вызывает операторы if в реализациях. Это заставляет меня чувствовать, что нам нужны определенные кусочки цвета в иерархии наследования.

В таком случае у вас будут классы, такие как 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свойство и использовать его для перемещения, чем использовать свойство color. Цвет в идеале должен использоваться только для рендеринга, а не для логики.
Gyan aka Gary Buyn
7
Кроме того, фигуры движутся в одном направлении: вперед, назад, влево, вправо. Разница в том, с какой стороны доски они стартуют. На дороге автомобили на встречных полосах движутся вперед.
Слипсет
1
@Blrfl Вы можете быть правы, что directionсвойство является логическим. Я не вижу, что с этим не так. Что меня смутило в этом вопросе до комментария Фрешблада выше, так это то, почему он хотел бы изменить логику движения в зависимости от цвета. Я бы сказал, что это сложнее понять, потому что цвет не имеет смысла влиять на поведение. Если все, что вы хотите сделать, это определить, какой игрок владеет какой частью, вы можете поместить их в две отдельные коллекции и избежать необходимости в собственности или наследовании. Сами фигуры могут блаженно не знать, к какому игроку они принадлежат.
Gyan aka Gary Buyn
1
Я думаю, что мы смотрим на одно и то же с двух разных точек зрения. Если бы две стороны были красными и синими, было бы их поведение отличаться от того, когда они были черно-белыми? Я бы сказал нет, поэтому я говорю, что цвет не является причиной каких-либо поведенческих различий. Это только тонкое различие, но именно поэтому я бы использовал directionили, возможно, playerсвойство вместо colorлогики в игре.
Gyan aka Gary Buyn
1
@ Свежая кровь white castle's moves differ from black's- как? Вы имеете в виду рокировку? Рокировка на стороне короля и на стороне королевы на 100% симметричны для черного и белого.
Конрад Моравский
4

Вы должны спросить себя, почему вам действительно нужны эти утверждения в вашем классе Pawn:

    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

Наследование не так полезно, когда расширение не влияет на внешние возможности объекта .

Свойство Цвет не влияет , как объект Кусок используется . Например, все фигуры могут вызывать метод moveForward или moveBackwards (даже если перемещение не разрешено), но инкапсулированная логика пьесы использует свойство Color для определения абсолютного значения, которое представляет движение вперед или назад (-1 или + 1 по оси y), что касается вашего API, ваш суперкласс и подклассы являются идентичными объектами (так как все они предоставляют одни и те же методы).

Лично я бы определил некоторые константы, представляющие каждый тип фрагмента, а затем использовал бы оператор switch для перехода к частным методам, которые возвращают возможные перемещения для экземпляра «this».

Лео
источник
Движение назад запрещено только в случае пешек. И им на самом деле не нужно знать, черные они или белые - этого достаточно (и логично), чтобы заставить их различать вперед и назад. 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

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

Я думаю, что использование ООП для фигур и их правильных движений - плохая идея по двум причинам:

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

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

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

При этом, если вы пишете первый шахматный движок, я бы определенно посоветовал придерживаться принципов чистого программирования и избегать приемов оптимизации, описанных автором Crafty. Разобраться со спецификой шахматных правил (рокировка, повышение по службе, en-passant) и сложными техниками искусственного интеллекта (хеш-доски, вырезка альфа-бета) достаточно сложно.

Джох
источник
1
Возможно, не стоит писать шахматный движок слишком быстро.
Freshblood
5
-1. Суждения типа «гипотетически это может стать проблемой производительности, поэтому это плохо», ИМХО почти всегда чистое суеверие. И наследование - это не просто «расширяемость» в том смысле, в котором вы ее упоминаете. Разные шахматные правила применяются к разным фигурам, и каждый вид фигуры, имеющий различную реализацию подобного метода, WhichMovesArePossibleявляется разумным подходом.
Док Браун
2
Если вам не нужна расширяемость, предпочтителен подход, основанный на switch / enum, потому что он быстрее, чем отправка виртуальным методом. Шахматный движок сильно загружен процессором, и операции, которые выполняются чаще всего (и, следовательно, критично для эффективности), выполняют движения и отменяют их. На один уровень выше перечисляются все возможные ходы. Я ожидаю, что это также будет критично ко времени. Дело в том, что я запрограммировал шахматные движки на C. Даже без ООП низкоуровневые оптимизации в представлении на доске были значительными. Какой опыт вы должны уволить мой?
Joh
2
@Joh: Я не буду неуважительно относиться к твоему опыту, но я уверен, что это не то, что было после ОП. И хотя низкоуровневые оптимизации могут быть важны для нескольких проблем, я считаю ошибкой делать их основой для высокоуровневых проектных решений - особенно когда до сих пор не возникает проблем с производительностью. Мой опыт показывает, что Кнут был очень прав, когда говорил: «Преждевременная оптимизация - корень всего зла».
Док Браун
1
-1 Я должен согласиться с Доком. Дело в том, что этот «движок», о котором говорит ОП, может быть просто средством проверки для шахматной игры для 2 игроков. В этом случае думать о производительности ООП по сравнению с любым другим стилем совершенно нелепо, простая проверка моментов займет миллисекунды даже на младшем ARM-устройстве. Не читайте больше в требованиях, чем фактически заявлено.
Тимоти Болдридж