Объектно-ориентированное программирование: Подклассы или перечисление

#oop #inheritance #architecture #game-development

Вопрос:

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

Мой вопрос в том, как программно развивать эти культуры. В настоящее время у меня есть каждый вариант урожая как подкласс урожая, однако это приводит к большому количеству избыточности (в основном разные значения полей/атрибутов или пути к изображениям). Поскольку эти объекты очень похожи, за исключением некоторых значений, должны ли они быть подклассами, или было бы лучше обрезать один класс с перечислением типа и использовать логику для определения значений, которые он должен иметь?

Урожай Суперкласса

  • подкласс Пшеница
  • подкласс Кукуруза
  • подкласс Ячмень

Или

Урожай.Тип = Тип культуры.Пшеница

если(это.Тип == Тип культуры.Пшеница) { вернуть фондовый рынок.Значение Wheat_Sell_Value; }

иначе, если(это.Тип == Тип культуры.Кукуруза) { вернуть фондовый рынок.Значение Corn_Sell_Value; }

Ответ №1:

Если вы создадите один класс обрезки, он рискует стать очень большим и громоздким, особенно если вы хотите добавить новый тип обрезки, вам придется обновить 100 операторов if, заполненных вашим кодом (например if(this.Type == CropType.Wheat) { return StockMarket.Wheat_Sell_Value; } ).

Чтобы повторить ответ @oswin, используйте наследование экономно. Вероятно, вы в порядке, используя базовый класс с несколькими «немыми» свойствами, но будьте особенно осторожны при добавлении чего-либо, что реализует «поведение» или сложность, например, методы и логику; т. Е. все, что действует CropType внутри Crop , вероятно, плохая идея.

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

 // Crop Types - could he held in a database or config file, so easy to add new types.
// Water, light, heat are required to grow and influence how crops grow.
// Energy - how much energy you get from eating the crop.
Name:Barley, growthRate:1.3, water:1.3, light:1.9, heat:1.3, energy:1.4
Name:Corn, growthRate:1.2, water:1.2, light:1.6, heat:1.2, energy:1.5
Name:Rice, growthRate:1.9, water:1.5, light:1.0, heat:1.4, energy:1.8
 

Значения типа обрезки помогают в дальнейшем управлять логикой. Вам также (я предполагаю) нужен экземпляр обрезки:

 class CropInstance
{
    public CropType Crop { get; set; }
    public double Size { get; set; }
    public double Health { get; }
}
 

Тогда у вас просто есть другие части вашей программы, которые действуют на экземпляры Crop, например:

 void ApplyWeatherForTurn(CropInstance crop, Weather weather)
{
  // Logic that applies weather to a crop for the turn.
  // E.g. might under or over supply the amount of water, light, heat 
  // depending on the type of crop, resulting in 'x' value, which might 
  // increase of decrease the health of the crop instance.

  double x = crop.WaterRequired - weather.RainFall;
  // ...

  crop.Health = x;
}

double CurrentValue(CropInstance crop)
{
  return crop.Size * crop.Health * crop.Crop.Energy;
}
 

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

 double CropThieves(CropInstance crop)
{
  if(crop.health > 2.0 amp; crop.Crop.Energy > 2.0)
  {
    // Thieves steal % of crop.
    crop.Size = crop.Size * 0.9;
  }
}
 

Обновление — Интерфейсы:

Я еще немного подумал об этом. Предположение с подобным кодом double CurrentValue(CropInstance crop) состоит в том, что он предполагает, что вы имеете дело только с экземплярами обрезки. Если бы вы добавили другие типы, такие как домашний скот, такой код мог бы стать громоздким.

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

Вот где появляются интерфейсы, представьте, что в конечном итоге у вас будет много разных типов, включая Crop (как указано выше) и Livestock — обратите внимание, что свойства не совпадают:

 // growthRate - how effectively an animal grows.
// bredRate - how effectively the animals bred.
Name:Sheep, growthRate:2.1, water:1.9, food:2.0, energy:4.6, bredRate:1.7
Name:Cows, growthRate:1.4, water:3.2, food:5.1, energy:8.1, breedRate:1.1

class HerdInstance
{
    public HerdType Herd { get; set; }
    public int Population { get; set; }
    public double Health { get; }
}
 

Так как же интерфейсы придут на помощь? Логика, специфичная для урожая и стада, находится в соответствующем коде экземпляра:

 // Allows items to be valued
interface IFinancialValue
{
  double CurrentValue();
}

class CropInstance : IFinancialValue
{
  ...

    public double CurrentValue()
    {
        return this.Size * this.Health * this.Crop.Energy;
    }
}

class HerdInstance : IFinancialValue
{
  ... 

    public double CurrentValue()
    {
        return this.Population * this.Health * this.Herd.Energy - this.Herd.Food;
    }
}
 

Затем вы можете выполнять действия с объектами, реализующими IFinancialValue:

     public string NetWorth()
    {
        List<IFinancialValue> list = new List<IFinancialValue>();
        list.AddRange(Crops);
        list.AddRange(Herds);
        double total = 0.0;

        for(int i = 0; i < list.Count; i  )
        {
            total = total   list[i].CurrentValue();
        }

        return string.Format("Your total net worth is ${0} from {1} sellable assets", total, list.Count);
    }
 

Возможно, вы помните, что выше я сказал:

…но будьте особенно осторожны при добавлении всего, что реализует «поведение» или сложность, например, методы и логику; т. Е. все, что действует на тип культуры внутри культуры, вероятно, является плохой идеей.

…что, по-видимому, противоречит приведенному выше кодексу. Разница в том, что если у вас есть один класс, в котором есть все, вы не сможете сгибаться, где, как и в описанном выше подходе, я предположил, что могу добавить столько различных типов игровых активов, сколько мне нравится, используя тип [x]и архитектуру экземпляра [x].

Ответ №2:

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

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

Если наследование будет лучшим выбором (в случае, если вам нужны отдельные структуры), также невозможно ответить, не зная точных деталей вашего игрового мира. Некоторые альтернативы, которые вы могли бы рассмотреть, это:

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