Having trouble harnessing the power of Inheritance. (best practices advice requested)

Started by
19 comments, last by WozNZ 9 years, 2 months ago

Use IS-A and inheritance to model functional interfaces and abstractions that you program against, e.g. FmodAudioService IS-A(n) AudioService, and not to model your real-world taxonomies.


Another that may be more familiar to developers on the board is graphics cards.

You request a Direct3D device and the system provides it. It doesn't matter what the device is, as long as it satisfies the requirements.

He used this example:


if (theBaddie is Knight) { ... }
if (theBaddie is Wizard) { ... }
...

which in practice is extremely rare for properly written inheritance.

Imagine the rough equivalent:


if( myD3DDevice is GeForce980) {...}
if( myD3DDevice is GeForce970) {...}
if( myD3DDevice is RadeonR9295) {...}
if( myD3DDevice is RadeonR9290) {...}
... repeated for many different cards ...

About the only time you would do something like that is if you had some extremely specific bug on a card, and you'd likely be testing both the card and the firmware version. If you've done everything correctly you don't need to know any of it. You just call CreateTexture2D() or whatever you need on the device and it just works.

When it comes to inheritance in c++, the Liskov Substitution principle (already mentioned above) is very closely related to the Dependency Inversion Principle. You should be able to specify an interface and the consumers of the class should be able to do all their work only by using that interface.

I should be able to obtain a ID3D11Device and not care what the actual concrete class is; it satisfies the description and therefore I am satisfied. The actual driver may present me with many different varieties of devices that work. It might be a card that just barely meets the specification. It might be a super fancy new card. It might be a card released five years from now. It might be a software-based reference implementation. The code should not know or care, all it needs to know is that the object meets the interface requirements.

Moving the analogue back to the question:

I should be able to create a Baddie object (whatever that is) and not care what the actual concrete class is; it may be a Wizard or Knight or anything else, I don't care and I'll never bother to check, it satisfies the description and I am satisfied.

I do not know if the Baddie object is a low-level grunt or an end of level boss. I don't know if the Baddie object is a tank or a turret or a dragon or a paratrooper. All I know is that it satisfies the Baddie object interface, and I can use it in exactly the same way I use every other Baddie object.

If it becomes necessary to know details, such as if it is a Wizard or Knight or some other type, that is almost certainly a defect. It may be a design defect (you shouldn't have used inheritance) or it may be an implementation defect (you shouldn't peek inside the implementation details), but either way it stays a defect.

Advertisement

Thanks all! I've definitely learned that IS-A has a much stricter definition in programming than what I've always thought.

Right now I'm trying to decide whether to push forward with my inheritance code by fixing it, or whether to abandon it and simply have separate unrelated custom classes for each baddie type. OR the third option is to use composition, which I think I still need to read up on. Here's my high level thoughts on the 3 approaches. Comments are much appreciated!

Inheritance

I could maybe fix my current design by removing all the

if (theBaddie is Knight) { ... }
if (theBaddie is Wizard) { ... }

tests. I guess in this case I should have something like a Baddie.Update() method that each subclass can override. So the code would look more like this:

theBaddie.Update();

and any Knight or Wizard specific code would be in the overridden Knight.Update() and Wizard.Update() functions respectively. That way the calling code does not need to know the actual type of Baddie object its dealing with. This definitely seems cleaner than what I have so far, however I still have a lot of thinking to do, to see if this approach will work for all of my Baddie classes. For example, only the Wizard can cast spells, so my initial thought is that I need a Wizard.CastSpell() function, that ONLY the Wizard has. But I think this breaks the inheritance model because suddenly the Wizard is special and not exactly a Baddie anymore. So instead of a custom new CastSpell() function in the Wizard class, maybe I just add spell-casting code into the Wizard.Update() function, thus keeping with the strict IS-A rule.

Custom unrelated classes

So this one seems pretty simple. I simply make each baddie type its own class, that does not inherit from anything. This gives me maximum flexibility because there are no restrictions placed on any of the individual Baddie classes. I can have any variables, functions and behaviors I want, without having to conform to what the Base class defines. The major down side I see with this approach is there will be a ton of similar looking (almost repeated) code. I will need separate loops for each baddie type:

// update all baddies

foreach (Wizard baddie in baddieList_Wizards) {

baddie.update()

}

foreach (Knight baddie in baddieList_Knights) {

baddie.update()

if (castTimer <= 0) {

baddie.CastSpell();

castTimer = 1000;

}

}

foreach (Slimer baddie in baddieList_Slimers) {

baddie.update()

}

foreach (Snake baddie in baddieList_Snakes) {

baddie.update()

}

Composition

I think I need to read up some more on this because I dont quite understand. From what I gather, I would have a single barebones Baddie class which can contain almost any combination of behavior objects. Something like this I think:

class Baddie {

private Component_Health cHealth = null;

private Component_Location cLocation = null;

private Component_SpellCasting cSpellCasting = null;

private Component_DeathAction cDeathAction = null;

... etc

}

Then I could make a Baddie behave any way I want it to, by just giving it the right components. For example:

class Component_Health {

// defines how healthy the baddie is, and what it takes to kill him

public int hitPointsCurrent;

public int hitPointsMaximum;

public int armourCurrent; // hitpoints are not harmed until armour is reduced to zero

public int armourMaximum;

}

class Component_DeathAction {

// defines any special action that occurs when a baddie dies

public int dropGoldChance;

public int dropItemChance;

public int explodeChance;

}

So a new baddie could be created like this:

Baddie b = new Baddie();

Component_Health c = new Component_Health();

c.hitPointsCurrent = 10;

c.hitPointsMaximum = 10;

b.cHeath = c;

Component_DeathAction d = new Component_DeathAction();

d.dropGoldChance = 90;

d.dripItemChance = 15;

d.explodeChance = 1;

b.cDeathAction = d;

So now we have a baddie that has 10 hitpoints, no armor, and might drop gold, an item, or explode when you kill him.

Where I'm confused is how does the update loop code look? It seems to me this will have the same code smell that my original inheritance method did. In other words, you'll constantly have to be checking things on each object. Instead of checking the type, we'll have to check the component:

foreach (Baddie b in baddieList) {

if (b.cHealth != null ){

// do health related stuff

}

if (b.cLocation != null) {

// do location related stuff

}

if (b.cSpellCasting != null) {

// do spell casting stuff

}

if (b.cDeathAction != null) {

// do death action stuff

}

}

This doesnt look that much better than all my tests for baddie type: if (theBaddie is Wizard) { ... } , however I guess it does feel more flexible overall.

You're still missing much of it. Instead of testing for the subclass you are testing for null. You are treating your objects as plain old data.

Go read up on the Dependency Inversion Principle, linked to above.

Member functions should generally be verbs, not nouns. DoSomething(), not GetValue().

Your last code approach goes in the ECS (Entity - Component - System) direction, but not quite.

You'd have entities (like your Baddie class), but entities wouldn't be limited to enemies. They'd also include the player, items, etc..

Then you have the components (like you wrote them) to store the actual data, since entites are just containers.

What you're missing is the systems.

These would iterate over all entities they can be applied to each update loop

e.g.:


class PlayerMovementSystem
{
    void Move(Entity e)
    {
        *update player position if any movement keys are pressed*
    }
}

class RenderSystem
{
    void Render(Entity e)
    {
        Renderer.DrawSprite(e.Sprite, e.Position);
    }
}

Thanks again all.

For anyone who's interested, I've decided to fix my inheritance model and push forward with that. If I run into any more major hurdles, I'll probably read up on components and move in that direction.

However for now my original idea of using inheritance seems to be working. The main change I made was going from this:

foreach {Baddie theBaddie in _allBaddies) {

if (theBaddie is Knight) { ... }
if (theBaddie is Wizard) { ... }

if (theBaddie is Slime) { ... }

}

to this:

foreach {Baddie theBaddie in _allBaddies) {

theBaddie.Update();

theBaddie.Move();

theBaddie.Attack();

}

I still have all my Baddie subclasses (Knight, Slimer, Wizard etc), but they all now use virtual methods to expand on the simple/generic Update() Move() Attack() functions provided by the Base Baddie class. I no longer need to check the type of any Baddie because the virtual methods just magically do the right thing for me.

One of the big arguments I'm hearing against inheritance is that its not very flexible. "What if you wanted to create a Knight who could also cast spell? You would have to create another KnightWizard subclass for example". While I suppose thats true, I'm ok with that fact. I'm actually planning to make each new Baddie type significantly different from the rest, so its not likely that any new Baddie I'm going to design is going to be simply a merging of 2 existing Baddies. In other words I'm probably not going to create a KnightWizard because I already have a Knight and a Wizard and that would be lame.

If anything, I would maybe get rid of the Wizard class, and then add a boolean to the Knight class for canCastSpells. Now I have a Knight that can cast spells or not. Actually, could this be considered a small step in the direction of Entity/Component ? I know components are generally thought of as classes, such as SpellCastingComponent, however what if its just a boolean like canCastSpells? Isn't that a very simple implementation of components? If each Baddie has a canCastSpells option, isn't that kind of like a Spell component that I can add and remove (enable and disable)? Anyway, I'm not planning on doing much of this, just sort of thinking out loud.

I do have some very basic variables that actually let me significantly change the behavior of any specific Baddie, without having to create new custom Baddie subclasses. For example I can change the speed, damage, hitpoints, texture/color, sounds, and drop chances of a Slimer Baddie simply by tweaking its parameters. I dont need any new FastSlimer or RedSlimer or ArmourdSlimer subclasses. I can actually make it seem like there are 100 different types of enemies by just using the Slimer class.

Anyway I just wanted to post an update and see what all you experts thought. I really appreciate the comments so far and have been learning lots.

In case you have mixed classes, like that spellcasting knight, I'd recommend using Interfaces.

So you'd have for example:

ISwordsman

ISpellcaster

IArcher

etc...


In case you have mixed classes, like that spellcasting knight, I'd recommend using Interfaces.

While using interfaces like this can work, and it is certainly one method of implementing the Dependency Inversion Principle, it is not the only way to do it.

Interfaces are hard coded and they don't play well with component systems that are so popular right now.

If your system is primarily code based rather than data driven, and it is not component based, yes, that pattern can work. In fact, sometimes that pattern is used on the components themselves in an ECS.

However, that pattern tends to not be as extensible in the broader sense. It cannot be data driven. It requires hard-coding the interfaces, is difficult to modify an existing class, can lead to difficulties when multiple bases reuse a name or signature, can lead to difficult-to-resolve dependency chains, and so on.

However for now my original idea of using inheritance seems to be working. The main change I made was going from this:

foreach {Baddie theBaddie in _allBaddies) {

if (theBaddie is Knight) { ... }
if (theBaddie is Wizard) { ... }

if (theBaddie is Slime) { ... }

}

to this:

foreach {Baddie theBaddie in _allBaddies) {

theBaddie.Update();

theBaddie.Move();

theBaddie.Attack();

}

This is the correct way to handle it, let virtual routing handle the differences for you.
In OO, whenever you find you have "if" statements that acts on an object type to perform a different action you have something wrong in your code structure and should look at else you can handle it.
With your Baddie classes you should avoid creating a different Subclass unless there is actually a difference that makes it work while otherwise you will create a maintenance nightmare for yourself. If you have a Gnome and a Goblin and they are the same apart from the name and damage they inflicted then they could share the same Subclass with a Name and Damage property etc, A factory object would be responsible for creating the instance and configuring it for the Monster type
WozNZ,

Thats what I basically ended up with. There were a lot of folks saying definitely do not use inheritance for this, but I wanted to see the "why not" for myself. So far its going well. Maybe down the line if I add lots more complex baddies, I'll have to switch over to the component system that everyone is praising, but for now inheritance is working.

With your Baddie classes you should avoid creating a different Subclass unless there is actually a difference that makes it work while otherwise you will create a maintenance nightmare for yourself. If you have a Gnome and a Goblin and they are the same apart from the name and damage they inflicted then they could share the same Subclass with a Name and Damage property etc,

This was the key that I was originally missing. I can swap out textures and sounds, and have different hitpoints, armor, damage, speed, etc etc etc all without having to create a new subclass. A new subclass is only needed for drastically different functionality, like a whole new way of moving or attacking.


Thats what I basically ended up with. There were a lot of folks saying definitely do not use inheritance for this, but I wanted to see the "why not" for myself. So far its going well.

The trick is that what you described is probably the direction that should be avoided.

My hunch, based on your description, is that you have something like this: (Forgive errors, just typing as I go)

class Baddie {

public:

virtual void Update() {...}

virtual void Attack() {...}

virtual void Move() {...}

...

Then for each of your subclasses you build your own custom logic for each function.

This is what you see in many books that teach the language, so it is understandable.

Herb Sutter wrote about this in depth well over a decade ago in C++ and others were writing about it years before that. It applies to C++ just as well to C#, here is one such article.

Generally your interfaces -- the public methods -- should be non-virtual in the base class. The code for them may call virtual functions that modify parts of the behavior, but the overwhelming majority should rely on the base behavior.

Let me give a real life example of that.

Working on a major golfing title on it's annual release. The game has many different game modes. "Stroke Play" (traditional golf), "Match Play" (counting holes rather than strokes, slightly shorter game), "One Ball" (players take turns hitting a single ball, the one getting it in wins the hole, with rules to let you set up bad shots for your opponent), and so on. There were around 20 or so game modes that had accumulated over the years.

These game modes had a collection of functions. The behavior was modified in the way described above. The functions were pure virtual ("abstract" in C# parlance) so each subclass mostly copied the code for the functions from a game mode that was similar, then modified as necessary.

We needed to add a tiny bit of functionality to all the game modes. It was not much, basically a line of initialization, 2-3 lines to update a statistic, and a call for cleanup when done.

Unfortunately we discovered that several of the game modes --- over many years of modification by many different developers --- had been modified wildly from their initial purpose. Some specializations dropped large chunks of functionality. A few of them had copied bugs between themselves, where something was removed from one function with a comment about the removal, then nearly identical code with the same effect was present in another function with a comment to fix a bug of the missing functionality, then a comment elsewhere with with another copy of the code with a bug note that they weren't sure why it hadn't been called yet since it should have been in the early stages, but they called it again just in case. In other cases there was duplication or near-duplication between many different game modes, often a bug fix in one mode with a comment, then another fix in a different mode that wrote the bug was the same so they just copied the solution.

If this code had been implemented with those guidelines of a non-virtual public interface that does the core work and limited non-public virtual functions that slightly modify the behavior, most of those bugs could have been avoided. Each of the specializations could have been implemented and shared between modes perhaps through a query to the subclass with a protected virtual method, and the mode-specific changes could have been implemented with the base classes calling a private virtual method.

When you allow the public interface to completely replace the internals by making it virtual, the net result is that the function itself is meaningless and can be modified to do anything at all. You might have a virtual function MakeBlue(), another virtual MakeGreen(), another virtual MakeRed(). Then a derived class author comes along and decides for this object all three versions should be overridden; not only should they all be blue, but they should also open a popup dialog and also update some statistics. Then another derived class comes along and overrides the Blue and Red function to become green, and adds statistics to those two methods but not the third. Repeat for several subclasses, each one adding slightly different behavior. Before long the base class user discovers that the function names only have minimal correlation to what the functions actually do, and each subclass has diverged in functionality to the point where you start needing to know which subclass you are dealing with so you can use the base interface.

So while inheritance is a powerful tool, and inheritance represents one of the strongest relationships in these programming languages, it is very easy to misuse. Many developers are taught how to use it, but it is less common to be taught why and when to use it, and why not and when not to use it, along with patterns of using it effectively.

This topic is closed to new replies.

Advertisement