ninjajosh5
ninjajosh5

Reputation: 29

Object Oriented Programming: Subclasses or Enum

I am making a simple turn-based farming simulator game where players make choices whether to buy land or crops and have turn-count based growing. Different crops grow for different times, and have different purchase prices and sale values. Objective is to be the first to reach a dollar amount.

My question is how to develop these crops programmatically. I currently have each crop variation as a subclass of a Crop, however, this leads to a lot of redundancy (mostly different field/attribute values, or image paths). Since these objects are very similar save for some values, should they be subclasses, or would it be better practice to make one class Crop with an Enum of type and use logic to determine the values it should have?

Superclass Crop

Or

Crop.Type = CropType.Wheat

if(this.Type == CropType.Wheat) { return StockMarket.Wheat_Sell_Value; }

else if(this.Type == CropType.Corn) { return StockMarket.Corn_Sell_Value; }

Upvotes: 1

Views: 194

Answers (2)

Oswin Noetzelmann
Oswin Noetzelmann

Reputation: 9555

The answer depends on the difference in functionality between the crop types. The general rule is to avoid unnecessary complexity where possible and inheritance should be used sparingly because it introduces hard dependencies.

So if all crops are functionally similar and only differ by their attribute values then you would want to use a single class for crop, but if your game logic demands the crop types to behave very differently and/or carry very different sets of data, then you may want to consider creating separate structures.

If inheritance would be the best choice (in case you need separate structures) cannot be answered without knowing the exact details of your game world either. Some alternatives you could consider are:

  1. interfaces (or another type of mix-in), which allows you to re-use behavior or data across multiple types, e.g. if crops can be harvested, maybe forests can be harvested as well.
  2. structs (or data-classes), which only define the data structure and not the behavior. This is generally more efficient and forces you to do a simpler design with less abstractions.
  3. a functional programming approach where the crops only exist as primitives being passed around functions. This has all the benefits of functional programming, such as no side effects, less bugs, easier to write tests, easier concurrency designs which can help your game scale larger.

Upvotes: 2

Adrian K
Adrian K

Reputation: 10215

If you make a single crop class it risks becoming very large and unwieldly, especially if you want to add a new crop type you'll have to update the 100's of if statements littered through your code (e.g. if(this.Type == CropType.Wheat) { return StockMarket.Wheat_Sell_Value; }).

To echo @oswin's answer, use inheritance sparingly. You are probably ok using a base-class with a few "dumb" properties, but be especially careful when adding anything that implements "behaviour" or complexity, like methods and logic; i.e. anything that acts on CropType within Crop is probably a bad idea.

One simple approach is if crop types all have the same properties, but just different values; and so crop instances just get acted on by processes within the game, see below. (Note: If crops have different properties then I would probably use interfaces to handle that because they are more forgiving when you need to make changes).

// 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

The crop type values help drive logic later on. You also (I assume) need crop instance:

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

Then you simply have other parts of your program that act on instances of Crop, e.g:

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;
}

Note you can still add logic that does different things to different crops, but based on their values, not their types:

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

Update - Interfaces:

I was thinking about this some more. The assumption with code like double CurrentValue(CropInstance crop) is that it assumes you only deal in crop instances. If you were to add other types like Livestock that sort of code could get cumbersome.

E.g. If you know for certain that you'll only ever have crops then the approach is fine. If you decide to add another type later, it will be manageable, if you become wildly popular and decide to add 20 new types you'll want to do a re-write / re-architecture because it won't scale well from a maintenance perspective.

This is where interfaces come in, imagine you will eventually have many different types including Crop (as above) and Livestock - note it's properties aren't the same:

// 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; }
}

So how would interfaces come to the rescue? Crop and herd specific logic is located in the relevant instance code:

// 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;
    }
}

You can then do things with objects that implement 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);
    }

You might recall that above I said:

...but be especially careful when adding anything that implements "behaviour" or complexity, like methods and logic; i.e. anything that acts on CropType within Crop is probably a bad idea.

...which seems to contradict the code just above. The difference is that if you have one class that has everything in it you won't be able to flex, where as in the approach above I have assumed that I can add as many different game-asset types as I like by using the [x]Type and [x]Instance architecture.

Upvotes: 2

Related Questions