Private scope for module classes?

  1. What do you want to achieve? Keep it simple and clear!
    I am writing a module/class script and I would like to disable the ability for users to directly set the value of properties. Without any other variable scopes besides global or local I am unable to do this. Is there another way of forcing users to use setter/getter methods instead of directly accessing the properties?

  2. What is the issue? Include screenshots / videos if possible!
    In some cases it is fitting or necessary to run data checks or run extra code following the setting of a property value. When users can bypass setter/getter methods they also bypass this extra code.

  3. What solutions have you tried so far?
    I have searched around a bit but haven’t found anything talking about this. Wondering if anyone else came across this issue and has a workaround.

Example Module code:

local myModule = {}

myModule.__index = myModule
myModule.ClassName = "Test Module"
myModule.Version = "1.0"
myModule.Author = "Emskipo"


function myModule.new()
	
	local new = {}
	setmetatable(new, myModule)
	
	new.HardProperty = "I am a hard coded property!"
	
	return new
end


function myModule:setNewProperty(value)
	if(typeof(value) == "string")then
		self.SoftProperty = value
	else
		error("New Property value must be a string")
	end
	
end


function myModule:printProperties()
	print(self.HardProperty)
	
	if(self.SoftProperty)then
		print(self.SoftProperty)
	end
end

return myModule

Example Script Code:

local myModule = require(game:GetService("ServerScriptService"):WaitForChild("MyModule"))

newModule = myModule.new()

newModule:printProperties()

newModule:setNewProperty("I am a new dynamic property!")

newModule:printProperties()

newModule.SoftProperty = 451 --I have now bypass the requirement that this value be a string

newModule:printProperties()

Example Output:

I am a hard coded property! (x2)
I am a new dynamic property!
I am a hard coded property!
451
2 Likes

Keep a seperate table for the properties and use the __index and __newindex metamethods to reas and write to your property table, while handling private property checks inside of your __newindex handler. I am currently on mobile, so I cannot write a code sample.

4 Likes

This seems like it will be the solution to this issue. But I need a better understanding of these methamethods before I can implement it. Ty. If you could post an example, I would def. appreciate that.

1 Like

Could you explain a possible use case for this? Might help make it a bit more clear to those that don’t understand.

I sort of did already. When you want to enforce a property value to be a certain data type such a string vs a number. But another situation might be when you want to say set a listener for a event such as a touch event on a part that a property value will hold. If you just set the part using the property itself then the listening event never gets implemented. I’m doing something similar for my case and its easier to enforce the user to use a function to set the value than it is to implement a complex mechanism to check if there is a value set or not and if the touch event was created. You can also enforce patterns in the data such as an array of a certain length or verify that other data has been set before hand that you might need to work with the property currently being set.

It is kind of like making sure you have exactly the right ingredients before you start cooking and that each of those ingredients was properly prepared before you use them. Otherwise you either use the ingredients as they were given and get totally unexpected results if they weren’t right or you check every ingredient as you go and if something isn’t right, you do a bunch of workarounds to fix the problem. Its more efficient and produces better results when you make sure your ingredients are perfect before you start.

2 Likes

So as was mentioned this is going to require some jigging of the __index and __newindex meta-methods. I’d also personally recommend the rare usage of weak tables for memory management, but they’re not necessarily needed.

So let’s get explaining:

The __index meta method follows the following flow chart:

d1

The __newindex meta method can also be shown as a flow chart:

d2

So keeping that in mind we can use these two meta methods to work with our class to get what you’re looking for.

-- This is for example a class module with its own scope
local MyClass = {}
local MyClass_mt = {}

local MyClass_Getters = {}
local MyClass_ReadOnly = setmetatable({}, {__mode = "k"})

-- Meta methods

function MyClass_mt.__index(self, key)
	if (MyClass[key]) then
		return MyClass[key]
	elseif (MyClass_ReadOnly[self][key]) then
		return MyClass_ReadOnly[self][key]
	elseif (MyClass_Getters[key]) then
		return MyClass_Getters[key](self)
	else
		error(key .. " is not a valid member of MyClass")
	end
end

function MyClass_mt.__newindex(self, key, value)
	if (MyClass_ReadOnly[self][key]) then
		warn(key .. " is a read only property and cannot be changed")
	end
end

-- Getters

function MyClass_Getters.SomeStringCount(self)
	return #self.SomeString
end


-- Class Constructor

function MyClass.new()
	local self = {
		SomeNumber = 123,
		SomeString = "ABC"
	}
	
	MyClass_ReadOnly[self] = {
		ReadOnlyExample = "This cannot be changed externally"
	}
	
	return setmetatable(self, MyClass_mt)
end

-- Public Methods

function MyClass:MultiplyNum()
	return self.SomeNumber * 2
end

function MyClass:ChangeReadOnly(newValue)
	-- if you wanted to change the read only you could only do where MyClass_ReadOnly exists
	MyClass_ReadOnly[self].ReadOnlyExample = newValue
end

--

return MyClass

And then you could use the class like so:

local test = MyClass.new()

-- methods and normal properties work like normal
test.SomeNumber = 5
print(test:MultiplyNum())

-- getter functions return based on the class
print(test.SomeStringCount)

-- can't set read only properties directly
print(test.ReadOnlyExample)
test.ReadOnlyExample = "new value!"

-- can do it through the class tho
test:ChangeReadOnly("new value two electric boogaloo")
print(test.ReadOnlyExample)

The output is as follows:

  10
  3
  This cannot be changed externally
  20:10:48.345 - ReadOnlyExample is a read only property and cannot be changed
  new value two electric boogaloo

Hope that helps!

10 Likes

Y’know I read this over again, I think it makes more sense to specifically have to define setter behaviour and default to read only.

Here’s my revised example:

local MyClass = {}
local Properties = setmetatable({}, {__mode = "k"})

local Getters = {}
local Setters = {}

-- Meta-methods

function MyClass:__index(key)
	if (MyClass[key]) then
		return MyClass[key]
	elseif (Getters[key]) then
		return Getters[key](self)
	elseif (Properties[self][key]) then
		return Properties[self][key]
	else
		error(key .. " is not a valid member of MyClass")
	end
end

function MyClass:__newindex(key, value)
	if (Setters[key]) then
		Properties[self][key] = Setters[key](self, value)
	elseif (Properties[self][key]) then
		warn(key .. " is a read-only property of MyClass")
	end
end

function MyClass:__tostring()
	return table.concat({self.x, self.y, self.z}, ", ")
end

MyClass.__metatable = false

-- Constructors

function MyClass.new(x, y, z)
	local self = {}
	
	x, y, z = x or 0, y or 0, z or 0
	
	Properties[self] = {
		x = x,
		y = y,
		z = z,
		
		Magnitude = math.sqrt(x*x + y*y + z*z),
		testProp = "abc"
	}
	
	return setmetatable(self, MyClass)
end

-- Getters

function Getters.Unit(self)
	local props = Properties[self]
	local x, y, z = props.x, props.y, props.z
	local magnitude = props.Magnitude
	
	if (magnitude > 0) then
		return MyClass.new(x / magnitude, y / magnitude, z / magnitude)
	else
		return MyClass.new(0, 0, 0)
	end
end

-- Setters

function Setters.testProp(self, value)
	-- specifically have to define as not read-only
	return value
end

-- Public Methods

function MyClass:Dot(self2)
	return self.x*self2.x + self.y*self2.y + self.z*self2.z
end

--

return MyClass

Edit: If you want more info on weak tables and why I’m using them for the properties table check this out: What is the use of a weak table and strong table? - Scripting Helpers

14 Likes

Very thorough explanation and examples tyvm!

1 Like

I’m trying to implement this for functions specifically. Here is an example of what I am talking about in something like java:

public class thing{
 public thing(){
 }

 public void DoThingPublicly(){ //Can be called at any time because it is a public method
 }
 private void DoThingPrivately(){ //Can ONLY be called in this script or others that share the scope
 }
}

I have no clue how to implement this in roblox OOP. I found a hacky way of doing it by passing the object into the function in the module script but thats extremely tacky and I’m looking for a solution that could be more flexible. Here’s an example:

local module = {}
module.__ = module

local function thing(Obj)
	print("YEAAAA!!")
end

function module.new(character)
	local new = {}
	setmetatable(new,module)
	
	new.Character = character
	
	return new
end

function module:DoIt()
	thing(self)
end

return module

Can this be done or is this the best way to do it?