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