Switching from chain inheritance to composition? OOP

I have a building game where you can build objects into your shop. All objects inherit object functions like move and destroy. Then I have a category for certain objects like doors and windows. All the doors will inherit from the door subclass functions and all the windows would inherit from the window subclass function.

The issue here is what if I want a door and a window to have the turn pink ability? It’d be gross to rewrite the code.

I’m aware of composition but I’m not so sure on how I’d achieve this with it? Should I have a folder of modules called Lock, Destroy, Move, etc and have each object grab the functions they need? What would that look like in code?

local Door = {}
Door.__index = Door 

function Door.New()
	local Object = {}
	setmetatable(Object, Door)

	return Object
end

return Door

More specifically, where in this code could I add some sort of inheritance? I’ve considered doing this:

Door.Lock = path.to.module

But this wouldn’t allow me to use the self variable.

My second idea is to split up objects? I could have a birch door object that has this inside of it:

Object.Door = Door.new() --> Allows the open/close methods
Object.Handle = Handle.new() --> Allows the lock methods
Object.Special = Special.new() --> special object that can turn stuff pink, following my example

return Object

Is this how composition is done?

This is an inheritance problem that has not yet been solved.
Using multi-inheritance could solve the problem by making both special door and special window additionally inherit from another class that implements the :turnpink() method.
Interfaces don’t make much sense in a dynamically typed language.

With composition the behaviours (sets of methods) are implemented separately.
First you have to separate the things that change from the things that don’t change. For example :move() and :destroy() never change so they stay in the base class. Then we apply inheritance as usual.

-- base class
local Obj = {}
Obj.__index = Obj 

function Obj.New()
    local Object = {}
    setmetatable(Object, Obj)

    return Object
end

function Obj:move()
    print("move")
end

function Obj:destroy()
    print("destroy")
end

-- sub class
local Door = {}
setmetatable(Door, Obj)
Door.__index = Door 

function Door.New()
    local Object = {}
    setmetatable(Object, Door)

    return Object
end

function Door:open()
    print("open")
end

function Door:close()
    print("close")
end

Then we implement the things that change. Strictly speaking we would have to create a base class from which all the changing behaviours inherit, each behaviour would be a subclass. But because luau is dynamically typed, one function per behaviour is enough.

-- Color Behavior set (only one behavior)
function turnpink(self)  -- self is to call the method with colon
    print("pink")
end

So we implement the special class

local SpecialDoor = {}
setmetatable(SpecialDoor, Door)
SpecialDoor.__index = SpecialDoor 

function SpecialDoor.New()
    local Object = {}
    setmetatable(Object, SpecialDoor)
    
    Object.turnpink = turnpink  -- <----

    return Object
end

It’s in the constructor that we can add the changing behaviours.

-- test
local specialDoor = SpecialDoor.New()
specialDoor:turnpink()
specialDoor:move()
specialDoor:destroy()
specialDoor:open()
specialDoor:close()
pink  -  Server - Script:44
move  -  Server - Script:13
destroy  -  Server - Script:17
open  -  Server - Script:33
close  -  Server - Script:37

It’s a lot of theory for something so simple.

3 Likes

I believe C++ has the ability to allow an object to inherit from multiple parent classes, but I’m unsure.

Thank you for the code examples and explanation.

I’m a bit curious what’s stopping me from just grabbing all the functions I need like you did here?

Object.turnpink = turnpink
Object.open = open
Object.close = close

Will this increase memory? I’m sure it’s unorthodox, but is it bad practice?

EDIT:
After printing (self) it looks like it stores the turnpink method whereas any method defined outside of creating the class, like open and close, aren’t shown. I assume this means it will increase memory if I did the above solution for all my methods?

OOP related organization and structure has sure kept me up at night.

In practice there is no problem. Moreover, it is a bit more efficient because you are not using the metatable mechanism to find the methods (one less step).

You are also not using extra memory. Remember that functions in lua/luau are data passed by reference (no turnpink copies are ever created). All instances would point to the same function.

As to whether this is a bad practice, it depends on how you look at it. From a purely OOP point of view it is, because you are breaking a pattern (only behaviours that always change should be composed). However, almost nobody uses the purely OOP style, as it is too much theory to be used in practice. So every developer takes his liberties, as long as he is consistent and doesn’t mess up his code. Only experience can help you make a good decision.

“This Is the Way”

1 Like

Final question, do I have to declare Object.turnpink inside of the .New function? I declared it outside of the function like SpecialDoor.turnpink = turnpinkmodule and it works the same, just wondering if there’s any drawbacks.

I didn’t think about it before, lua/luau is so flexible that this still works.
Now the SpecialDoor class is using the metatable mechanism to find turnpink. In this case it would no longer be using the composition.

1 Like

For those wondering:

I decided to use a mix of composition and inheritance (might be getting my terminology wrong here) and also the Object.turnpink = turnpink declaration in rare use cases where two nested objects need to share a behavior.

Thank you, su0.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.