Introduction to ServiceProvider - The Service pattern

To begin with, I will mention that this module is still in operation and I am trying to find a way to adequately work with generics, that is, dynamic typing of custom services.

  • In fact, this is a singleton in a wrapper, just like any service in Roblox.
    image

Open source code:

local singletonModulesFolder = script:FindFirstChild("SingletonModules")

local ServiceProvider = {}
ServiceProvider.singletons = {
    --not table.insert for types
	["Service"] = require(singletonModulesFolder.Service).new(),
	["Players"] = require(singletonModulesFolder.Players),
}

function ServiceProvider:GetService<T>(className: string): T
	if ServiceProvider.singletons[className] then
		return ServiceProvider.singletons[className]
	else
		return game:GetService(className)
	end
end

local game: DataModel & typeof(ServiceProvider) = setmetatable(ServiceProvider, { __index = game })

return game

In Roblox development, it is often necessary to create modules that provide specific functionality and are used in different parts of the game. To simplify access to these modules and make the code more organized, you can use the Service pattern.

What is ServiceProvider?
ServiceProvider is a module that simulates the Roblox service system. It allows you to register your own modules as “services” and access them from any script in the game using the familiar syntax. game:GetService("MyServiceName")

Advantages of using ServiceProvider:

Imitation of the Roblox service system: Allows you to organize the code in a familar style
Centralized access to modules: All “services" are registered in one place, which simplifies their management and reduces the number of require() in your code
Improved code readability: Instead of direct require(), you use game:GetService(“ServiceName”), which makes the code more understandable and declarative

Example: Creating a custom “service”:


In fact, initially I wanted to register a QuickTimeEventService and I almost succeeded, but I abandoned this idea because I couldn’t type all parts of the code correctly, which lost the charm of writing code in the style of MetaClass.__index = Class


Let’s create a “service” that do nothing!

  • in general, if I were you, you should write this in two different modules, but I thought it would be more readable for you to see everything in one code.
--!strict

-- ServiceName:  The table that will hold the service's methods (the "class" definition).
local ServiceName = {}

-- ServiceNameMeta: The metatable for the service, used for the singleton pattern.
local ServiceNameMeta = {}
ServiceNameMeta.__index = ServiceName

-- Class: The table holding instance methods and properties.
local Class = {}

-- ClassMeta:  Metatable for instances, enabling "inheritance" of methods from Class.
local ClassMeta = {}
ClassMeta.__index = Class

-- ServicePropertyes: A type definition for the instance's properties.
type ServicePropertyes = {
	property: string
}

-- ServiceType: A type definition combining class methods and instance properties.
type ServiceType = typeof(Class) & ServicePropertyes

-- An example method that can be called on instances of the service.
function Class:SomeFunction1(): ()
end

-- Create: Creates new instances (objects) based on ServiceName.
function ServiceName:Create(property: string): ServiceType
	local self = setmetatable({}, ClassMeta)
	self.property = property
	return self::ServiceType
end

-- ServiceNameType:  Type definition for the service singleton.
type ServiceNameType = typeof(ServiceName)

-- new: Creates the singleton instance of the service.
local function new(): ServiceNameType
	local self = setmetatable({}, ServiceNameMeta)
	return self :: ServiceNameType
end

return { new = new }

I think you’ve noticed that I’ve separated the meta and class logic. I did this so that the player could not call __index from the service, could not cyclically call new() and separate dot and colon annotations. I think this way of writing is much more consistent with the service. Let me know if I’m wrong.

next: this module also supports the ability to change services.
here is an example of adding the KickAllPlayers feature for the Players service:

local PlayersModule = {} 

type Enchant = typeof(PlayersModule)

function PlayersModule:KickAllPlayers(reason: string)
	local PlayersService = game:GetService("Players")
	for _, player in ipairs(PlayersService:GetPlayers()) do
		player:Kick(reason)
	end
end

local EnchantedPlayers = setmetatable(PlayersModule, { __index = game:GetService("Players") })

return EnchantedPlayers :: Players & Enchant

NOT TO FORGET TO TYPING!!
You just have to repeat what I did. After creating the wrapper, we type it into typeof(module) & initial service

How do I get typing with using this script?
since the module is still in the works, I have not completed the logic for better receiving services, and the ReplicatedStorage type will point us to the type itself, but will not tighten the hierarchy, so I suggest you use… A bit of a crappy way

local Save = game -- saving the game... So ugly
local game = require(game.ReplicatedStorage.ServiceProvider)

local ReplicatedStorage: typeof(Save.ReplicatedStorage) = game:GetService("ReplicatedStorage") -- this only RS, typechecker know ur hierarhy in it
local Players: typeof(game.singletons.Players) & typeof(Save.Players) = game:GetService("Players") -- its a union of datamodel players and updated players
local Service: typeof(game.singletons.Service) = game:GetService("Service") -- ur own service

Thanks for your attention, this is the end.

Good or no?
  • Good
  • Normal
  • Baaad

0 voters

1 Like

There is an easier way to do this:

local module = {}

-- Metatable for service caching
module.services = {}

setmetatable(module, {
	__index = function(t, serviceName)
		if not t.services[serviceName] then
			t.services[serviceName] = game:GetService(serviceName)
		end
		return t.services[serviceName]
	end
})

return module

Then:

local module = require(script.Parent)
module.Workspace
3 Likes