Best practices for organizing inheritance with MiddleClass

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
Gravy
Citizen
Posts: 80
Joined: Sun Jan 22, 2012 10:15 pm
Location: CA, USA

Best practices for organizing inheritance with MiddleClass

Post by Gravy »

Hi all,

I'm working on a game that has a bunch of different enemy types. Right now, I have a base class, 'Enemy', and several subclasses, some of which have their own subclasses. Some of the subclasses almost completely overwrite their inherited class functions, and I've started to find myself having to repeat code fairly often as I make changes. So my question is: What ways have you found to organize a complex class tree so that you minimize code repetition and errors?

I can think of a few different methods:
-Kitchen sink: Try to everything into the main class.
-Use special functions to overwrite individual variables rather than the entire inherited function.
-Mixins?

The correct answer will probably differ based on the individual program, but I'm just looking for some guidelines.

Thanks!
User avatar
Omnivore
Prole
Posts: 18
Joined: Fri Jun 28, 2013 12:51 am
Location: West Coast

Re: Best practices for organizing inheritance with MiddleCla

Post by Omnivore »

It's all about the public interface of your classes/objects. Too many methods and/or externally accessed instance vars and it is real easy to get confused about which does what and who does what to whom. Too few and you end up with 85000 classes, which is yet another kind of confusion.

With lua there's another issue; name collision. The more you rely upon inheritance and mixins the bigger the chance you will inadvertently overwrite something you shouldn't. It can be a bit of a hassle to debug those types of problems because their effects are so surprisingly unexpected.

Still, object oriented programming offers a great way to manage/hide the complexity of your code and allows for better overall code, in my opinion at least. One alternative to inheritance or mixins is the use of component aggregation. It allows you to customize your objects yet avoids most of the danger of name collisions since each contained object only requires one instance variable in your main object.

Another point to be wary of is using classes in places you should be using modules. if you have a number of related functions that do not share much if any data and are not tightly coupled functionally, it may well be an indication you need a module instead of a class. Another dead giveaway is when your class ends up being a singleton.

Personally I like to keep the majority of my classes relatively small and lightweight, if I can't describe a class in less than a few hundred lines of code, I probably need to refactor. I aim for somewhere around five to seven 'main' public methods at most per class, and lately have been making rather heavy use of component aggregation to keep my class inheritance trees short. I avoid both multiple inheritance and mixins like the plague unless there is absolutely no other reasonable way to express something. I guess in the end I'm saying KISS - Keep It Simple and Stupid.

Hope this helps,
Omnivore
Lua lou aye, ah no its, lua louie
User avatar
CaptainMaelstrom
Party member
Posts: 161
Joined: Sat Jan 05, 2013 10:38 pm

Re: Best practices for organizing inheritance with MiddleCla

Post by CaptainMaelstrom »

This is something I've been struggling with, too. Omnivore, your post makes a lot of sense.
User avatar
baconhawka7x
Party member
Posts: 491
Joined: Mon Nov 21, 2011 7:05 am
Location: Oregon, USA
Contact:

Re: Best practices for organizing inheritance with MiddleCla

Post by baconhawka7x »

I'm slightly confused, could you give me and example of the code?
User avatar
Omnivore
Prole
Posts: 18
Joined: Fri Jun 28, 2013 12:51 am
Location: West Coast

Re: Best practices for organizing inheritance with MiddleCla

Post by Omnivore »

I'll try an example, taken from my current project which has both good and bad examples :) Much work remaining to be done. However, one good example in it is the way the container related components work together to reduce the overall number of specialized classes. I have a need for at least three types of containers working with the same (or very similar) family of objects. Each type manages its contents in a slightly yet importantly different way.

The base class for the container half of the components involved is the Container class, its interface is:

Code: Select all

local Container   = class(Component)
function Container:init(owner, container_name)
function Container:addObject(object)
function Container:insertFirst(object)
function Container:removeObject(object)
function Container:clearAll()
function Container:asList()
function Container:serialize()
function Container:deserialize(sdata, factory)
function Container:onDeserialize(object)
Note that I'm making no attempt to be a 'full-featured' class here, just defining and implementing what I need for current requirements. The container class manages a simple list. Don't worry about the Component class yet its mainly there for syntactic sugar in attaching components to 'real' objects. Looking at the init function you can see how it attaches, when I use a raw Container component to add list management to an object I just do:

Code: Select all

local someObj...
Container(someObj, 'name_of_the_list_member_variable_to_create_and_manage')
I can add multiple Container instances to a single class. This comes in handy when dealing with the two derived classes: Inventory and EquippableGear used with my Avatar class (mobile actor or entity).

The Inventory class is intended to be used with objects that have an Item component. In this case my Inventory class has a restriction of only 26 items maximum. So it redefines the addObject method to enforce that limit. The EquippableGear class is more involved. For one thing it uses an associative array instead of a list as the storage, yet since every piece of Equipment (a component class derived from Item) can be in an Inventory or can be equipped, it still makes sense to derive it from Component. For one thing it allows reuse of the serialize method but it helps in other ways as well.

Code: Select all

local EquippedGear  = class(Container)
function EquippedGear:init(owner)
function EquippedGear:_slot_check(slot)
function EquippedGear:addObject(gear)
function EquippedGear:removeObject(gear)
function EquippedGear:swapHands()
function EquippedGear:asList()
function EquippedGear:onDeserialize(object)
The last two pieces of this component family are the components that attach to and enhance individual items, helping them to interface with the containers. The Item component class, derives from Component, once again primarily for syntactic sugar and a bit of DRY-ness. Its interface looks like this:

Code: Select all

local Item = class(Component)
function Item:init(owner, name, use_func)
function Item:setContainer(container)
function Item:drop()
function Item:use()
-- an example of a use_func:
function Item:heal()
When Item's use function is called it performs whatever operation the use_func(tion) implements. If the use_func is set to Item.heal then when the item is use'd the Avatar owning the collection the Item is in will be healed by some amount. A use_func can be used in other ways, for Equipment items, it removes them from one container and places them in another. Equipment class looks like:

Code: Select all

local Equipment = class(Item)
Equipment.VALID_SLOTS = {
  two_handed      = true,
  either_hand     = true,
  primary_hand    = true,
  secondary_hand  = true,
  armor           = true
}
function Equipment:init(owner, name, slot)
function Equipment:_toggle_equipped()
I left the VALID_SLOTS table in the above just to better show how the items are associated. In the case of a piece of Equipment the use_func doesn't have to be specified as it is set by the initializer to the _toggle_equipped method. There are similar component approaches to providing additional behaviors to Avatars, including such things as AI, combat capabilities, even temporary attachables which modify the attached object for the duration are quite possible.

Each of the above component classes has a small and well defined public interface and is easily unit testable. The number of possible name conflicts is reduced by a factor of five or so, and the implementation code is very DRY (Don't Repeat Yourself).

For a bad example, see my underground.model class in Snapshot - it has way too many methods and roles still even though I've refactored some out. In fact, if it weren't for some longer term project goals, the model class in Snapshot would be a prime example of something that might well should instead be a module.

Hope this helps,
Omnivore
Lua lou aye, ah no its, lua louie
Bobbias
Prole
Posts: 36
Joined: Sat Jun 29, 2013 1:26 pm

Re: Best practices for organizing inheritance with MiddleCla

Post by Bobbias »

I avoided going for a class hierarchy by going for a simple component based entity system using FEZ which, while apparently abandoned, is still a great piece of code to work from. Not much documentation but it has nice clean code and a handy example that makes use of it's features (except changing how something works at runtime, which is pretty easy).

It seems complicated at first, but once you get to a point where you're making things by mixing and matching Attributes (FEZ's term for Components) you realize how much less you have to worry about.

FEZ makes use of middleclass a bit, and also contains a handy event system where you can register callbacks on arbitrary events, filter them by tags if necessary, pass various arguments to the callback, etc.

I used to hate trying to figure out what my damned class hierarchy should look like. I'm enjoying working with FEZ quite a bit. It's not a lot of code, but it's pretty damn powerful for it's size.
Post Reply

Who is online

Users browsing this forum: lenlenlL6 and 52 guests