Custom Service Module

Hello! So basically I want some feedback on my custom service module which is pretty basic but I think I could have made it better. Tips would be appreciated. (I am also fully aware that I used “:” unnecessarily. I just wanted to make it look like roblox’s default game:GetService(). I normally don’t use “:” for stylistic purposes)

local function returnService(request)
	local success, res = pcall(function()
		return game:GetService(request);
	end);
	return res;
end;

local customService = {};

function customService:GetService(requestedService)
	local awaitedResult = returnService(requestedService);
	if not string.match(awaitedResult, ('%s'):format(requestedService)) then
		return awaitedResult;
	end;
	
	for _, mockService in ipairs(script:GetDescendants()) do
		if mockService.Name == requestedService then
			return require(mockService);
		end;
	end;
end;

return customService;

– Requiring The Module

local using = require(game:GetService('ReplicatedStorage'):WaitForChild('Require'));
local customService = using {'CustomService'};

local stateService = customService:GetService('StateService');
stateService.AddState('Darth'); -- prints 'Darth'

The code works perfectly fine.

2 Likes

I think that this solution is a little overkill. GetService is extremely robust and the purpose of GetService is to create / wait for the server to be loaded.

well if you were talking about this “customService:GetService”, then thats incorrect since that is not using roblox’s default :GetService (I called my table function “GetService” to resemble the default roblox one. And i’m not sure what you mean. Every programmer beginner or expert I have seen uses :GetService to grab a default roblox service. If there is an alternative please suggest it.

Okay since this post for whatever reason still gets a bit of attention every blue moon, do not do this. They have pitfalls that I do not have the energy to explain. Just use GetService. MrAsync already mentioned this and since the beginning I have consistently believed that to be the best option.

For people who like being extra for no reason:

local GetService = setmetatable({}, {
    __index = function(self, i) 
        local service = game:GetService(i)
        if service ~= nil then 
            rawset(self, i, service)
        end 
        return service
    end
})

This if you want a more “elegant” way of getting a Roblox service. Otherwise, if you were to make a custom service getter. You have missed the mark a bit. Simply requiring it would have been easier, and you could make a wrapper around require like this: an import code inspired by JavaScript.

local ROOT = script.Parent

local environments = setmetatable({
	["Server"] = game:GetService("RunService"):IsServer() and game:GetService("ServerScriptService").Server or nil,
	["Client"] = game:GetService("RunService"):IsClient() and game:GetService("Players").LocalPlayer.PlayerScripts.Client or nil,
	["Shared"] = game:GetService("ReplicatedStorage").Shared,
}, {
	__index = function() 
		return ROOT
	end
})

local function import(path, environment)
	local current = environments[environment]
	
	for _, n in pairs(string.split(path, "/")) do
		current = current:WaitForChild(n)
	end

	if current:IsA("ModuleScript") then
		current = require(current)
	else
		return current
	end

	return current
end

return import 
6 Likes

Wow that is really good, couldn’t have done it better, especially for a fancy programmer like me. But just a tip, instead of iterating through a path to grab the modules, it would be a lot more efficient to cache them in a dictionary for faster access. Although, I like how you made a good use of string patterns and branchless programming, though im not completely familiar with the concept. I’m curious what this line of branchless code translates to.

	["Server"] = game:GetService("RunService"):IsServer() and game:GetService("ServerScriptService").Server or nil

I appreciate your help!

1 Like
local function returnService(request)
	local success, res = pcall(function()
		return game:GetService(request);
	end);
	return res
end

Why are you calling a function and inside it calling another one? Instead you can call the function directly into the pcall. Also, only return res if the pcall was successful because you don’t seem to print any errors judging from your code.

local success, res = pcall(game.GetService, game, request)

return success and res

The reason for game.GetService is that you can’t use : inside arguments and therefore you need to pass self as game.

Numerical loops are faster when using them with arrays.

local t = script:GetDescendants()

for i = 1, #t do
     if (t[i].Name == requestedService) then
        return require(mockService)
    end
end

Numerical for loops are not faster in the VM when used akin to a generic for loop (t[i]).

I appreciate your input on my old code. I wasn’t aware that you could use a pcall like that. 3 things I would like to mention:

  1. Your suggestion on returning ‘success and res’ seems pretty pointless and in fact inefficient considering the value of success doesn’t matter. Your method would also error since string.match can’t take a nil datatype as one if its arguments.
local function returnService(request)
     local success, res = pcall(game.GetService, game, request)

     return success and res 
end

In your scenario, if success is false it automatically returns nil since a function automatically returns nil if it returns nothing. And string.match can’t accept a nil datatype as one of its arguments.

Also, you using short - circuit programming is extra steps.

  1. It’s better to use generic for loop over numerical looping since if you wanted to retrieve the value of a key, you have to do array[i]. Indexing a key obviously does take time unlike the generic which returns both the key and value and even with 200 + elements the speed of ipairs and numerical loops are almost the exact same.
local Array = {'One', 'Two', 'Three'}
print(Array[1]) -- Slower

local CachedValue = Array[1]; -- ipairs caches the index and the value
print(CachedValue) -- Faster
  1. You don’t even have to iterate through the children of the module to retrieve the service. All you have to do is have a cache storing all the services. If you want to improve Reuseability in other projects (assuming that you have different modules in each project and that the values in the cache are going to be different). It’s better to just have a .init function that initializes during the beginning of runtime which iterates through the CustomService module and adds those modules to a cache and use a good friend of mine (memoization). If the modules are going to be needed at the beginning of the game you can have an :await method which yields until all the modules are stored and loaded in the cache.

This is a lot more practical than having to iterate through the descendants of a module each time you want to retrieve a module which is inefficient.

1 Like

That’s right, it only returns an error if the function returns an error but it doesn’t. And yes, caching is really efficient and fast.

Here’s a snippet of my service module.
It works by creating and caching the services.
You can never get unused services with this, the downside is that it dosen’t come with autocomplete.

local Services = {} do
	local ServicesModule = {}

	function ServicesModule:GetService(serviceName)
		if not Services[serviceName] then
			Services[serviceName] = game:GetService(serviceName)
		end
		return Services[serviceName]
	end

	setmetatable(Services, {__index = ServicesModule})
end
1 Like