Organizing classes and objects

I’ve just recently started doing research on how I can implement OOP into my workflow. I’ve always used a single script architecture approach.

In my main script I would have 2 tables, “Shared” and “Modules”. The Shared table holds all modules which are shared between the client and server and the Modules table holds the client’s modules or server’s modules and are both passed in to the .Init function in every ModuleScript which allows for communication between all scripts in the game. So I expanded on this idea and added a third table in the main script called “Classes”. This table holds all classes and is again passed into the .Init function of all ModuleScript’s.

Here’s what that looks like:

-- Server script but looks the exact same on Client
local Modules = {}
local Classes = {}
local Shared = {}

local SharedFolder = game.ReplicatedStorage:WaitForChild("Common")
local ClassesFolder = script:WaitForChild("Classes")
local ModulesFolder = script:WaitForChild("Modules")

local LoadedPlayers = {}

for _, Module in ipairs(ModulesFolder:GetChildren()) do
	if Module:IsA("ModuleScript") then
		Modules[Module.Name] = require(Module)
	end
end

for _, Module in ipairs(SharedFolder:GetDescendants()) do
	if Module:IsA("ModuleScript") then
		Shared[Module.Name] = require(Module)
	end
end

for _, Module in ipairs(ClassesFolder:GetDescendants()) do
	if Module:IsA("ModuleScript") then
		Classes[Module.Name] = require(Module)
	end
end

for _, Module in pairs(Modules) do
	if Module.Init then
		coroutine.wrap(function()
			Module.Init(Modules, Shared, Classes)
		end)()
	end
end

for _, Player in ipairs(game.Players:GetPlayers()) do
	table.insert(LoadedPlayers, Player.Name)
	for _, Module in pairs(Modules) do
		if Module.PlayerAdded then
			coroutine.wrap(function()
				Module.PlayerAdded(Player, Modules, Shared)
			end)()
		end
	end
end

game.Players.PlayerAdded:Connect(function(Player)
	if not table.find(LoadedPlayers, Player.Name) then
		table.insert(LoadedPlayers, Player.Name)
		for _, Module in pairs(Modules) do
			if Module.PlayerAdded then
				coroutine.wrap(function()
					Module.PlayerAdded(Player, Modules, Shared)
				end)()
			end
		end
	end
end)

game.Players.PlayerRemoving:Connect(function(Player)
	if table.find(LoadedPlayers, Player.Name) then
		table.remove(LoadedPlayers, table.find(LoadedPlayers, Player.Name))
	end
	for _, Module in pairs(Modules) do
		if Module.PlayerRemoving then
			coroutine.wrap(function()
				Module.PlayerRemoving(Player, Modules, Shared)
			end)()
		end
	end
end)

Is this the best way to manage classes? I can’t find any examples of how to actually organize classes in your code. Should I be requiring all classes and passing them into each ModuleScript? Or is that inefficient and should manually require the classes I need? I’ve heard the term “Composition over Inheritance” but what should my hierarchy look like if I’m using composition? For inheritance I’d imagine you place each subclass under it’s superclass and have a hierarchy of ModuleScripts. But with composition would I just throw all ModuleScripts into one Folder?

I’ve been looking for an answer to these questions for hours and can’t find anything. Nobody seems to give examples on what your codebase should actually look like when using OOP and I can only find examples of simple classes and objects.

Not sure how helpful this is, but I try to just code the thing sequentially and then organize later. Are you sure this classes organization approach is the best way to do your work?

That’s exactly what I’m trying to ask. I’m trying to figure out how other people structure their code when using OOP. This is all pretty confusing to me so I just want to make sure that I’m moving somewhat in the right direction.

Most people just put a class inside its own ModuleScript, rather than having a big table full of everything. I do the same but I typically don’t use classes like that to return from a ModuleScript, just functions

That’s exactly what I’m doing. In my main script I’m requiring all classes and putting them into a single table then passing that table into the .Init function of all other ModuleScripts. That way inside of the .Init function I can access a class by doing:

function Module.Init(Modules, Shared, Classes)
	local Object = Classes.SomeClass.new()
	Object:Method()
	print(Object.Property)
end

But I’m not sure if this is the best approach.

1 Like

This thread should have some screenshots on script organization

https://devforum.roblox.com/t/fussing-over-code-organisation-leads-me-to-be-unproductive-unmotivated-and-frustrated-is-this-something-worth-the-attention-i-give-it/758200/34?u=dthecoolest

Consider ECS, going full composition will take the pain of thinking for inheritance scenarios away.

https://devforum.roblox.com/t/fussing-over-code-organisation-leads-me-to-be-unproductive-unmotivated-and-frustrated-is-this-something-worth-the-attention-i-give-it/758200/47?u=dthecoolest