Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
An example of inheritance woes
/* Ok lets start by expressing the intent for a vehicle to move */
public interface IVehicleMovement
{
public float Speed {get; set;}
public void Move(Vector2 direction);
}
/* This will implement the most common form of movement */
public class SimpleWheelMovement : IVehicleMovement
{
public int WheelCount {get; set;}
}
/* This will implement the 4 wheel drive logic from the previous example, but in isolation */
public class FourWheelDriveMovement : IVehicleMovement
{
public readonly int WheelCount {get; private set;} // It will always be set to 4 so no point pretending it can be otherwise
}
/* This will implement the basic movement for jets without even knowing about wheels */
public class JetMovement : IVehicleMovement {}
/* Now lets express the intent to attack things */
public interface IWeapon
{
float AttackDamage { get; set; }
void Shoot(SomeEntity target);
}
/* So lets make a simple one for a single gun */
public class SimpleGun : IWeapon {}
/* Lets make one for a cannon on a jet or a tank */
public class VulcanCannon : IWeapon {}
/* Finally lets make one to express missiles */
public class MissileLauncher : IWeapon {}
/*
Now that we have expressed all our components and intents with behaviour implementations lets see the end entities with composition,
I COMPLETELY RECOMMEND USING IoC IN REAL WORLD, for brevity I will be using explicit instantiation here.
*/
/* So lets make Mad Max again (*/
public class MadMaxCar
{
private IVehicleMovement _maxsCar = new SimpleWheelMovement(); // He will use simple car behaviour
private IWeapon _maxsGun = new SimpleGun(); // He will use simple weapon behaviour
/*
If we were to make the above components public we could remove the need for these, however although you
are writing an extra line or 2 to wrap up the underlying components you will see that in the case of a jet
it is far better to wrap up concerns internally and expose them like a facade.
*/
public void Move(Vector2 direction) { _maxsCar.Move(direction); }
public void Attack(SomeEntity target) { _maxsGun.Attack(target); }
}
/* Lets make the Subaru */
public class SubaruImpressa
{
private IVehicleMovement _fourWheelDriveMovement = new FourWheelDriveMovement();
public void Move(Vector2 direction) { _fourWheelDriveMovement.Move(direction); } // Same as above we just wrap it
}
/* Lets make the jet */
public class MigFulcrum
{
/* Notice how we have 2 weapons here */
private IVehicleMovement _jetEngines = new JetMovement();
private IWeapon _vulcanCannon = new VulcanCannon();
private IWeapon _missileLauncher = new MissileLauncher();
// I am sure we know what this does by now
public void Move(...){...};
// So lets assume you just fire everything at once, mech warrior style
public void Attack(SomeEntity target)
{
_vulcanCannon.Attack(target);
_missileLauncher.Attack(target);
}
}
/*
Now using the composition approach we have isolated concerns, so we can test our vehicle movements and weapons in isolation,
so we know for sure they work as intended and can easily add many more movement types, like if I decided to add a tank
I could add a new TankTrackMover : IVehicleMovement, so I am expressing the behaviour at a lower level allowing it to be
reused/tested/maintained far easier.
Some of the main things to point out:
- There is not that much more code being used here than the inheritance path, but it is easier to see what does what as its all split out,
and this also means that you can change each end objects behaviour independently without it effecting others.
- You may also notice that with inheritance you can reference BaseVehicle everywhere but in our example here we have no base class to use
as reference when trying to let everything else know what it exposes, which is a great point and I intentionally left that out as to
not confuse things but you may want to have an IVehicle and then an IAttackableVehicle to imply what it exposes.
- As mentioned normally you would use IoC and possibly DI to satisfy these private components, as this way you could then change the
SimpleCarMovement in MadMaxCar to use DayZPartyBusMovement and the MadMaxCar would be none the wiser, as you would then be telling it
how to behave.
- We could have easily done without the interfaces (I would not do that) and just express the logic via concrete classes, as I know some
people think of interfaces as clutter, but if you were to do this then there would be no contract for them to adhere to, making future
development a bit trickier if a new guy joins how does he know what the UnderWaterVehicleMovement should expose?
*/
/*
Ultimately I am not saying that inheritance is bad, just that composition can be a far more maintainable/testable/reuseable way to express
common logic in an encapsulated way. It also means that you can easily change behaviours of objects at any point without re-writing
the base objects. The key is that you are using interfaces as the contract between what behaviours will be exposed, and the implementations
to show how the behavours differ while still adhering to the base contract.
*/
/* So lets start off with a base vehicle, the behaviour all vehicles will want */
public abstract class BaseVehicle
{
public float Speed {get; set;}
public float Health {get; set;}
public virtual Move(Vector2 direction) { ... } // Do basic move
}
/* Now lets add some wheels to the vehicle as most vehicles have wheels right? */
public class WheeledVehicle : BaseVehicle
{
public int WheelCount {get;set;}
public override Move(Vector2 direction) { ... } // Apply wheel physics or something to alter the move method
}
/* Now lets add some guns so we can shoot stuff */
public class AttackVehicle : WheeledVehicle
{
public float AttackDamage {get;set;}
public virtual void Shoot(SomeEntity target) { ... } // Shoot something with the guns
}
/* Mad Max had a car right? our default inherited behaviour is 100% fit for him */
public class MadMaxCar : AttackVehicle
{
// { Wheel count: 4, Attack Damage: 1.0f, Speed: 70.0f, Health: 100 }
}
/*
Well I got a Subaru Impressa now, and I want it to have 4wd so it handles better,
it still is a vehicle but just needs to tweak the behaviour of the Move to apply
the 4wd logic for improved handling, no biggie.
*/
public class SubaruImpressa : WheeledVehicle
{
// { Wheel count: 4, Speed: 70.0f, Health: 100 }
public override void Move(Vector2 direction) { ... } // Lets add some 4wd improvements to our wheeled turning
}
/*
Better make a jet, it is a Vehicle and can attack but doesnt care about wheels...
Well we can ignore the wheel values as we dont care about that part of the inheritance chain.
We can also completely change the movement to just fly directly, and change attack to allow for missiles or guns.
*/
public MigFulcrum : AttackVehicle
{
// { Wheel count: 0, Attack Damage: 100.0f, Speed: 700.0f, Health: 200 }
public bool isUsingMissiles { get; set; }
public override void Move(Vector2 direction) { ... } // Lets completely remove the wheel logic and put our own flying logic in
public override void Attack(SomeEntity target) { ... } // Lets put in the logic to check if we are using missiles and AoE or not
}
/*
Now the above is a simple example which could be expressed many ways but some key things to point out:
- In most inheritance trees you account for the 95% (vehicles with wheels) and then just override behaviour in the other 5% (non wheeleed vehicles)
However this will often lead to duplication of code and lots of micro hacks to bend the base logic to the new scenarios will.
- If we want a Jet or a Tank or Train etc which are a rarity we have a wasted variable for WheelCount which is not needed but will still take up space
- For a jet we will end up ignoring half the base logic however from base classes, and even if we ignore the base.Attack/Move calls
we will still end up having those virtual method hooks floating around in the code which are not being used.
- We could make lots of further inheritance trees for like FlyingAttackVehicle and UnderwaterAttackVehicle and remove the wheeled vehicle from
the basic inheritance path, but you will eventually face the point where ideally you want multiple inheritance but dont want the diamond problem.
- Most uses of inheritance are to allow code reuse, which is a great thing. However to do it this way we are only reusing at the code/unit
level not at the component level which is where you would get far more re-use.
Anyway this is an ok example and not the worst use case for inheritance but it should give you a quick overview of the hackery that you
are probably going to end up finding as you try to express more niche concerns from a base implementation just to save duplicating code.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.