Organize modules into services!

Cleanly organizing modulescript instances can be difficult, so I took a look at how roblox does things and thought maybe it would look neat to create a module to make require less of a chore.

local modules = require(game.ReplicatedStorage.ServiceModules);

modules:AddService("ExampleService", game.ReplicatedStorage.ExampleModule, { present = true });

-- Equivalent to modules.ExampleService when present = true
local ExampleModule = modules:GetService("ExampleService");

The modules are only require()'d if they are marked as present or retrieved with :GetService() and the result from require is cached to make sure they only run once, like they would normally (this might be useless since I think this how require normally works anyway)

Of course, a downside to this is the loss of type information and autocomplete (if you export types from modules you have to require them directly), which can probably be worked around but it would have to really push LuaU type system to its limits and I haven’t worked it out that far yet.

But it can definitely make using multiple modulescripts less tedious, for example:

local ServiceA, ServiceB = modules:GetServices("ServiceA", "ServiceB");

ServiceA:Func();

ServiceB:Func();

The source for the module is here:

local module = {}

Loaded = {};

-- Modules parented to this module will become services automatically.

-- present: Whether or not the service is accessible via module.[name], and loaded as soon as this module loads.
-- move: If true, the ModuleScript passed is parented to the module instead of creating a reference.
function module:AddService(name: string, service: ModuleScript, properties: { present: boolean, move: boolean })
	properties = properties or {};
	
	if script:FindFirstChild(name) then
		warn("Service module \"" .. name .. "\" already exists!");
		
		return;
	end
	
	if not service then
		error("Expecting ModuleScript for argument #2, got nil.");
	end
	
	if typeof(service) ~= "Instance" then
		error(if typeof(service) ~= "Instance" then "Expecting ModuleScript for argument #2, got " .. typeof(service) else "Expecting ModuleScript for argument #2, got " .. module.ClassName)
	end
	
	if properties.move then
		service.Name = name;
		
		service.Parent = script;
	else
		local ref = Instance.new("ObjectValue");
		
		ref.Name = name;
		
		ref.Value = service;
		
		ref.Parent = script;
	end
	
	if properties.present then
		module[name] = service;
		
		service:AddTag("Present");
	end
end

function module:GetService(name: string)
	if not name then
		error("Expecting string for argument #1, got nil");
	end
	
	if Loaded[name] then
		return Loaded[name];
	end
	
	local m = script:FindFirstChild(name);
	
	if not m then
		return;
	end
	
	-- AddService parents an ObjectValue to avoid cloning the script, so modules can still expect to stay where they are in the heirarchy.
	if m:IsA("ObjectValue") then
		m = m.Value;
	end
	
	local service = require(m);
	
	Loaded[name] = service;
	
	return service;
end

function module:GetServices(...: string)
	local names = { ... };
	
	local result = {};
	
	for i, v in pairs(names) do
		table.insert(result, module:GetService(v));
	end
	
	return unpack(result);
end

-- Load present modules.
for i,v in pairs(script:GetChildren()) do
	if (v:IsA("ModuleScript") or v:IsA("ObjectValue")) and v:HasTag("Present") then
		local service = if v:IsA("ModuleScript") then require(v) else require(v.Value);
		
		Loaded[v.name] = service;
		
		module[v.name] = service;
	end
end

return module
3 Likes

Not really.

It’s only really a chore when you don’t know what you’re doing.

Reinventing the wheel.

If only there was a built-in function that loads modules plus doesn’t have this problem.


Moral of the story: you’re reinventing Knit’s architecture (and inherent flaw).

Pro-tip - use folders to organize services:
image

I know you can use folders. The point is not to have to go through folders for every module, even if you shorten the path with a variable it can still be annoying to type out when you need to use them a lot.

Calling them services just sounds cooler but realistically this is just aiming to sprinkle sugar on require, it just happens to be the same thing Knit did(minus getting multiple at once).

Also why do people make things like Knit, there’s a similar thing for Unity called Futile.

Sure— I understand that, but at the end of the day, it’s something sweet at the cost of something sour; losing out on autocomplete and typechecking.