Kikito's 'middleclass' OOP Solution

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.

In a language such as C++, in the simplest case of no compiler optimizations, if you had something like that Person class with a GetName function, that function’s compiled code would exist once in memory, and each Person object would have a pointer to it (the address of the function at runtime).

That said, any number of other things can happen with compiler optimizations: small functions will often get inlined with an optimization setting that favors speed over executable size, which could completely optimize the function out of existence. Places in your source code where you have person->GetName(), your executable might just have the code for the function body itself person.first + " " + person.last, with no actual function call.

1 Like