Kikito's 'middleclass' OOP Solution

I was geeking out on the interwebs reading about an old, seemingly discontinued JIT compiler for Lua (conveniently called LuaJIT). Somehow stumbled upon a thread where this quite simple and eloquent solution for OOP popped up. It’s authored by ‘kikito’ on github and is titled ‘middleclass’.

Link

‘middleclass’ makes declaring classes, inheritance, initializing etc. super simple. IMO, this is a much easier way than setting up metatables (which I do agree is versatile, but so verbose and annoying to use)!

Example Code (from link)
local class = require(YOURDIRPATHTOMODULEHERE) -- original line of code: local class = require 'middleclass'

local Fruit = class('Fruit') -- 'Fruit' is the class' name

function Fruit:initialize(sweetness)
  self.sweetness = sweetness
end

Fruit.static.sweetness_threshold = 5 -- class variable (also admits methods)

function Fruit:isSweet()
  return self.sweetness > Fruit.sweetness_threshold
end

local Lemon = class('Lemon', Fruit) -- subclassing

function Lemon:initialize()
  Fruit.initialize(self, 1) -- invoking the superclass' initializer
end

local lemon = Lemon:new()

print(lemon:isSweet()) -- false

What do you guys think about this?

2 Likes

This saves you… no code at all? I don’t really see the point. Especially because you can’t just require 'middleclass' in Roblox.

local class = require 'middleclass'

local Fruit = class('Fruit')

function Fruit:initialize(sweetness)
  self.sweetness = sweetness
end

Fruit.static.sweetness_threshold = 5

function Fruit:isSweet()
  return self.sweetness > Fruit.sweetness_threshold
end

vs

local Fruit = {}
Fruit.__index = Fruit

function Fruit.new(sweetness)
  return setmetatable({ sweetness = sweetness }, Fruit)
end

Fruit.sweetness_threshold = 5

function Fruit:isSweet()
  return self.sweetness > Fruit.sweetness_threshold
end

Result: 9 non-blank lines in both cases.

2 Likes

There’s a middleclass.lua file that you would import (check the github)

I mean that you would have to replace that with something like:

require(game:GetService("ReplicatedStorage").middleclass)

Unless you use some sort of custom module loader, which is again more code to be dragging in.

That would be correct, didn’t catch that. Regardless, this is one line of code that makes creating objects a lot easier imo

I’ll change that the require line of code, though.

One benefit would be replacing obfuscating statements with ones that are easier to understand if you just have an OOP background (and don’t know about Lua metatables)

vs

image

Apart from that though, I’m also not really convinced it is a much better solution.

3 Likes

I’m wondering why the creator’s usage example suggests this:

function Lemon:initialize()
    Fruit.initialize(self, 1) -- invoking the superclass' initializer
end

instead of this:

function Lemon:initialize()
    Lemon.super.initialize(self, 1) -- invoking the superclass' initializer
end
1 Like

That is quite odd, considering the creator uses .super in the middleclass.lua file as well.

On second thought, maybe this wasn’t the best solution to share. Should’ve delved deeper into it!

I’ve explored middleclass, but personally I think metatables are more clear than middle class. Metatables are built into Lua and using them as a class is a “standard” pattern. I don’t think middleclass is as intuitive.

I don’t think the added dependency is worth it.

I’ve looked at midldeclass in the past. Was quite interesting, but didn’t really use it. Whenever I needed classes back then, a simple __index metatable was all I needed. and now I’ve created my own class stuff, which now that I think about it, is quite similar to middleclass in some regards

That’s the consensus here too. After much consultation with the Lua experts here (@Tiffblocks, @0xBAADF00D, and @LPGhatguy) as well as some A/B testing and pros/cons evaluation, I decided on using the metatable OOP pattern for the camera scripts refactor as well. Other Lua projects here have used it also, like the Lua Avatar editor. The whole __Index thing wasn’t natural and readable to me either, as someone who has worked mostly in Java and C++ that are designed specifically for OOP, but having a few quirky lines of metatable boilerplate is a small price to pay. In the camera modules, those lines look like this:

local BaseCamera = require(script.Parent)
local ClassicCamera = {}
ClassicCamera.__index = ClassicCamera
setmetatable(ClassicCamera, {
	__index = BaseCamera,
	__call = function(class,...)
		return class.new(...)
	end
})

After that, the rest of the class looks pretty much like it would in any other language, with class methods of the form function ClassName:MethodName()

1 Like

Thanks! I really appreciate that move.

Honestly, I’d really prefer to have the OOP look like this, although this is definitely biased towards my own style, I think it provides the most clarity…

This especially makes it clear when initialization is happening instead of assuming __call() is the constructor (Although if you come from Python or JavaScript maybe this is more clear).

Also it allows BaseCamera to construct itself! Mostly, it prevents this setmetatable() call from turning into an inlined table constructor, which I think is cleaner.

local BaseCamera = require(script.Parent)

local ClassicCamera = setmetatable({}, BaseCamera)
ClassicCamera.__index = ClassicCamera

function ClassicCamera.new()
    local self = setmetatable(BaseCamera.new(), ClassicCamera)

    -- Initialize
    self.Variable = true
    
    return self
end

function ClassicCamera:Method()
     -- Do thing...
end

If you need to add metamethods you can do

function ClassicCamera:__call(...)
    return self.new(...)
end

function ClassicCamera:__newindex(Index, Value)
    print("New Index", Index, Value)
    rawset(self, Index, Value)
end

Here’s a more full example:

4 Likes

testimony - im hooked on this like im hooked on phonics ^

1 Like

Is that bolded BaseCamera intentional, or should it be ClassicCamera?

And yeah, I could easily leave out the __Call function. It’s natural to me for a constructor to look like ClassName(), but you’re right in that it would be more consistent with the rest of Roblox Lua to explicitly call ClassName.new() to construct.

Yes, that was a mistake.

Another interesting idea is for more complex components, you can override the __index metamethod to get behavior based upon indexes.

A really useful class that came out of this was a ValueObject which works like an ObjectValue, but for any Lua object. Check out the source.

You can use classes like this for really interesting results:

...
function ActionManager.new()
	local self = setmetatable(BasicPane.new(), ActionManager)
	
	self.ActiveAction = ValueObject.new()
	self.Actions = {}
	
	self.ActionAdded = Signal.new() -- :fire(Action)
	
	self.Maid:GiveTask(ContextActionService.LocalToolEquipped:connect(function(Tool)
		self:StopCurrentAction()
	end))

	self.Maid:GiveTask(self.ActiveAction.Changed:connect(function(Value, OldValue)
		if OldValue then
			OldValue:Deactivate()
		end
	end))
	
	return self
end

function ActionManager:StopCurrentAction()
	self.ActiveAction.Value = nil -- Fires the changed event! 
end
...

This is incredibly useful as state is maintained in a single object – and it’s easy to see how these events connect. Being able to fire/mutate state based upon an internal state is really useful and makes for cleaner code.

The cost is that it’s a bit harder to follow for people not used to events (the observer pattern).

This is how I do my OOP solutions too. Exactly as you have it. Works really well!

1 Like

Metatables are really cool though.
Also classes are a poor man’s closure.

5 seconds too late

Now I’m curious :persevere:

Whoops, didn’t mean to delete that. I basically mentioned how it’s interesting that metatable pseudo-classes actually save a lot of memory (relatively), since it allows any number of instances to share the exact same methods.

Example:

local Person = {}
Person.__index = Person

function Person.new(firstName, lastName)
	local self = setmetatable({
		FirstName = firstName;
		LastName = lastName;
	}, Person)
	return self
end

function Person:GetFullName()
	return self.FirstName + " " + self.LastName
end


-- Create 10,000 people:
local people = {}
for i = 1,10000 do
	table.insert( Person.new("Bob", "Smith" + i) )
end

While there are 10,000 instances of Person, there is still only 1 method GetFullName in memory, not 10,000. The metatable will just point to the same GetFullName, passing self to give it instance intent. So, while I could have easily computed the full name and saved it as a property, I saved memory by only computing it from a method and not storing it.


I’m not sure how languages with actual classes do this, but I can only imagine that they do such optimizations.