All about Object Oriented Programming

That appeared to do nothing.

1 Like

What about if you change

Class.__index = function(self, Index)
	return self.BackingTable[Index]
end

to

Class.__index = function(self, Index)
	return self.BackingTable[Index] and self.BackingTable[Index] or Class
end
1 Like

Still nil.

Did a test and removed Class.__index = blah from the .new function and calling either PropaneTank:Destroy() or self:Destroy() would work, so its something going on in with that __index. ā€¦Is it overwriting the Tool metatableā€™s method? or something?

1 Like

@magnalite - Iā€™ve gone ahead and fixed the formatting of your post to work with Discourse :slight_smile: I kept the same formatting and content, just made it work with Discourseā€™s formatting which doesnā€™t support [center] BBCode. Iā€™ve also moved it to the Tutorials section. Thank you for this wonderful tutorial!

7 Likes

@Lilly_S Thanks! Much appreciated :slight_smile:

@GuestCapone

The way .__index works is it is called when an index does not exist. In this case when you create an object you create a new empty table with data about the objects in. It has none of the methods in it. To allow your object to have the appropriate methods called on it a metatable with its .__index pointing to the class table is assigned to the table with the objects data in it. When .__index returns a table lua looks into that table to see if the index exists in that too. So what actually happens when you call a method on an object is this.

Object:DoSomething()
Object[ā€œDoSomethingā€] is nil, does its metatable have an .__index set?
It does, .__index returned SomeClass.
Does SomeClass[ā€œDoSomethingā€] exist?
It does! call it!

This allows you to assign the methods without duplicating them into every single object you make.
In your case you are making .__index return a value from BackingTable and so it never has a chance to see if the method exists in the Class table.

Iā€™m not sure why my previous suggestion wouldnā€™t work though :frowning:

12 Likes

Probably because there was no Class[Index], just Class.
And by doing;

Class.__index = function(self, Index)
		return self.BackingTable[Index] or Class[Index]
end

I was able to get the :Destroy() function to work. ^.^ Thanks!

@Lilly_S Thanks for updating and moving the thread!

2 Likes

^-^ Btw I find it helps if I keep my classes in separate module scripts and require them when I need them.

2 Likes

Thanks for this tutorial! I am trying to learn OOP and I have a quick question for anyone who can answer it.

I have an object that has events connected to it (and some values like Power, Agility, etc.)

Is this all that is necessary for a :Delete() function?

function Thing:Delete() for i,v in pairs(self) do if tostring(v) == "Connection" then v:disconnect() end end self = nil end

Or do I also have to iterate through all the values and set them to nil, like self.Power = nil, or is just setting self = nil fine?

1 Like

This is incredibly helpful! Iā€™ve always been vaguely aware of OOP and have used objects once or twice, but never really understood how it worked or how to properly utilize it. This cleared up a lot of questions!

3 Likes

No, that is sufficient. It will GC (garbage collect) the members when all references are lost to them (and if all references are lost to the object, youā€™re good to go!)

I like to call setmetatable(self, nil) on my code too. Itā€™s a good defensive programming practice, as it means dead objects donā€™t have methods to call.


Youā€™ll notice the snippet you used is going to be a pain to type out. My solution is to use Maid. In my deconstructors Iā€™ll just deconstruct Maid which cleans up my signals. Hereā€™s an example:

function CameraControls:Disable()
	if self.Enabled then
		self.Enabled = false
		
		self.Maid:DoCleaning()
		self.Maid = nil
		
		self.LastMousePosition = nil
		UserInputService.MouseBehavior = "Default"
	end
end

Source

13 Likes

-snip-

i have found a solution to my question

2 Likes

Would you recommend having multiple Module Scripts for different things?

So thereā€™s gonna be a lot of functions for weapons in my game; same for AI. Would it be worth having two Module scripts. One called Weapon and one called AI? Or should I just use one?

Only advantage I can think of for using 2 is organisation; but I think thatā€™s important ( to me atleast! ).

1 Like

You should split the code up as much as reasonably possible. Each class (module) should also try to keep to itself as much as possible (ie not directly editing objects from other classes and be as self containing as possible).

This idea is known as ā€œencapsulationā€ which is one of the ā€œthree pillars of oopā€.

9 Likes

For those looking for an easy way to implement replication triggered by setting object properties, here is a sample of a simple way to do this:


Car = {}
function Car.new(position, driver, model)
    local newcar = {}
    newcar.Speed = 0

    local mt = {}
    function mt.__index(self, index)
        return Car[index] or newcar[index]
    end
    function mt.__newindex(self, index, value)
        self:Replicate(index)
        newcar[index] = value
    end
    local proxy = {}
    setmetatable(proxy, mt)
    return proxy
end

function Car:Replicate(property)
    --RemoteEvent:FireAllClients()
end

function Car:Boost()
    self.Speed = self.Speed + 5
end

I know nothing about efficiency, so I couldnā€™t tell you if this is fast or not, but it seems to do the job.

The key difference with this vs. the method outlined in the OP is that custom metamethods are defined for each object. These metamethods are called whenever someone reads or writes a value in the proxy table. The table is nominally empty because any reads are caught by __index and sent to Car or newcar and any writes are sent to newcar. The __index method is technically backwards from luaā€™s implementation ā€“ by default if the key trying to be access in the table is nil it falls back to the table. It means you canā€™t overwrite anything defined in Car in the instance, but it seems like a questionable practice to do this regardless.

8 Likes

Know this is old, but looking for some advice.

So I have my own object, called machine. Things can transfer resources into a machine. So it makes sense to create a method
machine:Transfer(resource)
However there are many types of machine, and so machine:Transfer() has different functionality depending on the machine type. Similar situations arise with other methods for the machine. What is the best way to handle this?

Currently I have a table of machine types, that looks something like this
table[Id] = {Name = ā€œBlablablaā€, TransferFunction = AnotherTable.Machine1TransferFunction}
So the machine object looks like this

function machine:Transfer(resource)
   table[self.Id].TransferFunction(self,resource)
end

But this certainly feels wrong.

2 Likes

You should be able to just override Transfer in your extended class, no?

1 Like

Sorry, but I have no idea what that means. I think Iā€™ve still got a lot of OOP to learn!

1 Like

assuming machine is the superclass, you should have an extended class (sub-class) for each type of machine you have, that all inherit from the superclassā€¦ and in those sub-classes, you should be able to override the behavior or Transfer.

2 Likes

As in, if your original class is Machine, and you extend it with CoolMachine, and you want to change the behavior of :Transfer, you should be able to justā€¦

function CoolMachine:Transfer(resource)
    --code
end

And itā€™ll override the default Machine:Transfer() behavior.

4 Likes

Ah, I get it all now, thanks!

ā€¦jeeze how did I go so long without knowing this stuff?

2 Likes