Which OOP method do you like the most?

Hello! There are two OOP implementation methods I have seen in Lua. Which do you like more?
#1:

local Car = {}

function Car.new(tireNum,color)
	local Tires = tireNum
	local Color = color
	local isOn = false
	
	local Methods = {}
	Methods.turnOn = function()
		isOn = true
	end
	
	Methods.getIsOn = function()
		return isOn
	end
	
	Methods.getColor = function()
		return Color
	end
	
	Methods.getTires = function()
		return Tires
	end
	
	return Methods
	 
end

Pros:

  • fields are private (do they ever need to be, though?)
  • intelligent code completion works since there is no metatable involved (ie. easier time working with the code down the road)
    *potential to have private/public methods

Cons:

  • methods are duplicated each time an instance is made
  • not mainstream
  • more work writing accessor and mutator methods

#2:

local Car = {}
Car.__index = Car

function Car.new(tireNum,color)
	local self = setmetatable({},Car)
	self.Tires = tireNum
	self.Color = color
	self.isOn = false
        return self
end

function Car:turnOn()
	self.isOn = true
end

Pros:

  • mainstream
  • methods are not duplicated

Cons:

  • metatables = can’t use intelligent code completition

What do you guys think? Thanks.

3 Likes

My favourite is the second, just because it looks nicer and it stays more true to an actual OOP language.

8 Likes

I prefer the second since I find it easier to find the methods I’m looking for. If you have a lot of methods for the object, the new/constructor function will get super long and convoluted. Being able to collapse individual methods, including the constructor, helps me find what I’m looking for quickly. Auto-complete also rarely works for me anyway, since it doesn’t work if you don’t reference the module directly, so that’s not much of a downside.

4 Likes

I use P.O.O.P
Private
Object
Oriented
Programming

And by private, I mean that all methods and variables are private to each other? Get it? Heh. Jk.

Jokes aside I usually use #1 but when I’m making a large project that needs a lot of code to be written (my most recent one was like 40,000 lines of module scripts code) I tend to use #2 to prevent redundancy.
Honestly I wish I would have learned about meta tables earlier as they have been pretty useful for me.

I also think that the 2nd method feels a lot more like OOP in other languages.

5 Likes

You shouldn’t be using the 1st one at all. It incurs extra overhead every time the function keyword is hit because it has to make another closure to fit your needs.

Also, if you want private methods, do it the Python way and prepend them with __private__ or something. Yes Python does something like that internally.

5 Likes

Huh, you can do that in Lua? I thought that kind of stuff including _G wasn’t available in Roblox.

Usually I just use self.somemethod.

2 Likes

I typically do it the same way @Quenty does it,

self._privateMethod = "blah blah" -- private
2 Likes

I most liekly shouldn’t, but I use #1 personally.

2 Likes

So for a third option, I have adopted the RoStrap version of OOP. It has a bunch of neat features like actually private variables, built in maid/janitor class, type checking support and bunch of other really cool features, I highly recommend checking it out

Here is an example of a basic class using this

return PseudoInstance:Register("FooBar", {
	
	Internals = {
		PrivateVarible = false;
	};

	Events = {
		
	};

	Methods = {
		foo = function(self)
			print("foo");
		end;
	};

	Properties = {
		bar = Typer.AssignSignature(2, Typer.Number,function(self,Number)
			self:rawset("bar",Number);
		end);
	};

	Init = function(self, ...)
		self:superinit(...)
	end;
})
9 Likes

I myself use a mutation of #2. I prefer to keep my methods inaccessible through the library. I do this by having the methods and constructor be in different containers. This is kinda like how you cant do CFrame:Lerp but instead you have to construct an actual CFrame datatype then call :Lerp on it.
image

What I mean by that is Car:turnOn would not be a valid call. Instead you would have to create a new Car instance and then call :turnOn; Car.new():turnOn()

Example:
If I am wanting to create a Car library that can create Car instances I would do something like this.

local car = {} --// table for the library

local carMethods = {} --// table for the methods
carMethods.__index = carMethods

function car.new(tireNum, color)
    local newCar = {
        tires = tireNum,
        color = color,
        isOn = false
    }

    setmetatable(newCar, carMethods)

    return newCar
end

function car:turnOn()
    self.isOn = true
end
Other

This didn’t seem to relevant so I thought I’d put it in a spoiler / hide details drop down thing.
I prefer to encase my methods in a do end statement so I can collapse code blocks to easily skim through my code.

-- [[ Car Class Declaration ]] --

local car = {}

do
    --// Car Methods

    local carMethods = {}

    carMethods.__index = carMethods

    function car.new(tireNum, color)
        local newCar = {
            tires = tireNum,
            color = color,
            isOn = false
        }

        setmetatable(newCar, carMethods)

        return newCar
    end

    function car:turnOn()
        self.isOn = true
    end
    
end
2 Likes

For you performance junkies out there, @zeuxcg touched on this in his talk at RDC and explained the most performant way to do OOP in the new Lua VM.

8 Likes

As someone with a preference for functional programming I actually prefer the second one because the first one

  1. uses closures as state, I prefer when state is enclosed in a single well explained place. In an ideal world functions should be stateless.
  2. doesn’t work well with unit testing techniques such as replacing self with a dummy object.

The language Lua in particular however prefers the second one as well, it’s faster, more flexible, and (arguably) easier to read. While I absolutely understand the need for privacy (I wrote a whole library for Lua5.3 which adds privacy and strong typing in a much different way using interfaces), the easiest way to accomplish privacy is to simply lie in your documentation. If someone happens to stumble upon your private variables and happens to mutate them like a lunatic then either they know what they’re doing or it’s their own fault

2 Likes

I realize I didn’t add the return statement on my code, lol.

1 Like

Coming from the perspective of someone who is not very familiar with OOP, the second method looks a lot cleaner. The downside of not being able to use intelligent code completion seems pretty minor compared to it’s benefits.

3 Likes