Why Does My Class Constructor Init Method Never Get Called Right? (OOP Inheritance)

Hey, while working with oop inheritance recently, i encountered two problems;
The Classic Utility i mentioned in post.

--init.luau
require(script.Mid).new()
--require(script.Mid)()

1. Superclass ignores overridden methods from subclass

--Base.luau
local Base = {}
Base.__index = Base

function Base.new()
    local self = setmetatable({}, Base)
    self:Init()

    return self
end

function Base:Init()
    print('Base initialized')
end

return Base
--Mid.luau
local Base = require('./Base')

local Mid = {}
Mid.__index = Mid

function Mid.new()
    return setmetatable(Base.new(), Mid)
end

function Mid:Init()
    print('Mid initialized')
end

setmetatable(Mid, Base)
return Mid

Output: Base initialized
My solution: Using classic utility by rxi (I tried triggering Init function in subclass instead of superclass just triggers more problem in my inheritance chain)


2. Overridden method called by superclass ignores changes in subclass constructor

--Base.luau
local Object = require('./classic')

local Base = Object:extend()

function Base:new()
    self.mythicValue = "Waits for being overridden"
    self:Init()
end

function Base:Init()
    print(`Base initialized mythic value is: {self.mythicValue}`)
end

return Base
--Mid.luau
local Base = require('./Base')

local Mid = Base:extend()

function Mid:new()
    Mid.super.new(self)
    self.mythicValue = 15
end

function Mid:Init()
    print(`Mid initialized mythic value is: {self.mythicValue}`)
end

return Mid

Output: Mid initialized mythic value is: Waits for being overridden
My solution: task.defer(self.Init, self) but it feels like a weird workaround just to make basic inheritance behave properly


So, while both of my solutions technically solve the issues, they feel weird when this kind of basic behavior should be simpler to implement in any object-oriented structure, does anyone have cleaner solutions for this?

1 Like

I would say just use ECS, but that has its own problems mostly just organizational (You still end up with a lot of scripts and components to organize and debug).

Problem 1, the base.new() functions calles init first, then the metatable change happens.

Solution: You can create a factory module to autoinitialize your objects for you. Not sure if this is the best but its my most immediate idea.

local autoInit(object)
local obj = object.new()
obj:Init()
end

return autoInit

Problem 2 seems to be the same root problem as problem 1 with the init function inside the constructor.

1 Like

as mentioned above, luau is a language where I would recommend composition over inheritance. Architectural patterns that benefit from Data Oriented Design such as ECS are a good fit for it, specially since metatables are our way to imitate class inheritance functionality, given we don’t actually have classes.

1 Like

I dont understand the point of it in first place?
Why cant you just initialize class inside its constructor?
Anyways metatable OOP is outdated consider using this: Strictly typed <<Object Oriented Programming>> (Better than metatable OOP)
Its more optimized

reason of not initialize class inside constructor because i want override init in my subclasses.
thanks but im not looking for optimization and classic module provides me a very good class i can get same results with typescript classes my 2. problem isn’t just about lua classes its a general oop question so im not looking for classes without metatables

2 Likes

The 2. problem isn’t just about luau classes, in my codebase i actually don’t know how can i use composition over inheritance


Just for example;
Let’s say i have/need really extensible classes for bullets
Example inheritance chains:

Base -> Hitscan -> Laser
Base -> Projectile -> Bomb
...

How can i use composition over inheritance or a data oriented design on here

This guy got a good module that solves this problem really well:

I’d say you should give it a shot.

It’s not fixes 2. problem, i already using classic for 1. problem

why dont you just have same method check what class do you use and apply initiation acordingly?
Since its metatable OOP it will end up as a better solution anyways.

The overwriting and constructor mechanism of the module I mentioned is far cleaner. I suggest giving the constructor mechanism of the 2.0 in the documentation a try. When the first constructor (base class) is called, the values in the child class already exist and has been overwritten, therefore the base class constructor will always have the object with its values already updated.

I was tried class++ like 2 days ago and results was same i dont want achieve same thing with more complexity i already have solutions i just looking for simpler/cleaner ones the 2. problem also happens in typescript classes something in logic is wrong but i can’t figure it out

Probably researching self.type (or smth for identify class) would just add more complexity than task.defer as i think

I say you should give the new version a go then. It’s not complex at all. It solves what you’re trying to do in a more sequential order, without depending on task.defer(). Though, your choice.

I think class++ just adds limitations and i dont know is it stable or not, while the classic module solves my problem with a single metamethod and one logic trick and classic module is made 11 year ago and a common stable utility for lua scripters. If I had to choose, I’d go with dthecoolest’s solution over task.defer

Your use case is actually a really good one for ECS.

In this example, what if you want to create a hitscan laser that also explodes on impact like a bomb? Under inheritance this complicates things.

Under ECS however, classes only hold data (properties) and all behavior is handled externally through “systems”

local bullet = Entity.new()
Entity.addComponent(bullet, DamageComponent.new(5)) --now deals 5 damage
Entity.addComponent(bullet, BombComponent.new(15)) --now explodes on a radius of 15
Entity.addComponent(bullet, HitScanComponent.new(50)) --now acts under hitscan

BulletSystem.fireBullet(bullet) --performs behavior based on components
code implementation example
--entity.luau
function newEntity()
	local e = {
		components = {}
	}
	return e
end

function addComponent(e, c: {[string]: any} & {type: string})
	e.components[c.type] = c
	return c
end

function getComponent(e: Entity, type: string)
	return e.components[type]
end

return {
	new = newEntity,
	getComponent = getComponent,
	addComponent = addComponent,
}
--HitscanComponent.luau
return {
	new = function(range: number)
		return {
			type = 'Hitscan',
			range = range
		}
	end
}
--ProjectileComponent.luau
return {
	new = function(velocity: number)
		return {
			type = 'Projectile',
			velocity = velocity
		}
	end
}
--BulletSystem.luau
function fireBullet(e: Entity)
	if e.components['Projectile'] then
		--create a projectile moving in Projectile.Velocity direction
	elseif e.components['Hitscan'] then
		--create a raycast using Hitscan.range
	end
end

function bulletImpact(e: Entity)
	if e.components['Bomb'] then
		--blow up in a radius and apply damage from components['Damage']
	end
end

return {
	fireBullet = fireBullet,
	bulletImpact = bulletImpact
}
1 Like

I probably will implement :Hit method of bomb to laser just it (for large codebase i would create behavior modules for store methods waiting for get implemented by classes)

I actually was used this logic but this part of system i cant understand because a projectile needs store fields and lifecycles in this logic and this pushed me to use OOP

these fields are found within the entity’s components, that’s where you may find the “state” you wish to modify/update

1 Like

Thanks i understand now. Also you mentioned composition over inheritance do you have any ideas on how to use composition over inheritance on here?

^ this process is called composition

see how bullet doesn’t truly inherit from anything? but rather the object is built from the sum of its parts?

This logic is good but about extensibility i dont know really. as an example what will happen if a bomb and some other components should move different including projectile, or bomb should move a part of bomb model on every projectile step/update