Using services to create custom services for organization benefits

I was reading over an existing post about making your own “instances” (or really services) and I really liked the concept but realized that the implementation needed to be expanded quite a bit to actually be practical. Briefly, what the post is about is using existing service instances as a foundation and then expanding on them with metaprogramming; however, the implementation provided in the post doesn’t function as it’s laid out there anymore assuming it ever did. Here I’m going to provide what I believe to be, in my opinion, a succinct implementation of the design pattern laid out in that original post, as well as some worked examples, and hopefully I will have convinced you of the utility of this pattern by the end.

1. The Problem

As a game’s code base begins to grow it becomes more and more unwieldy because of lua’s relatively primitive module system in also working with a different organization method, Roblox’s ServiceProvider, and the :WaitForChild(...) problem that exists in dealing with folders as a organizational tool.

2. The Solution

We design modules our game will be using in a similar fashion to how the Roblox API designs it’s services so that from a engineering perspective, we are at least working in a uniform API. This solves the issue of heterogeneity but we’re still left require and a increasing, possibly redundant list of :WaitForChild(...) calls.

These last two issues are solved by wrapping the ServiceProvider with a metatable who’s indices includes the modules which we would like to behave similarly to services like so:

Service.lua
local ClientServices = ... -- Path to a top level folder which contains our module scripts as described below

local Services = {}

function Services:GetService(serviceName)
  local modules     = PlayerServices:FindFirstChild(serviceName)
  local service     = game:GetService(serviceName)
  local subservices = {}
  local metaService = {}

  if modules then
    for ix, subservice in pairs(modules:GetChildren()) do
      subservices[subservice.Name] = require(subservice)
    end

    return setmetatable(subservices, {
      __index = function(tbl, key)
        if type(service[key]) == "function" then
          return function(_, ...)
            return service[key](service, ...)
          end
        else
          return service[key] or subservices[key]
        end
      end
    })
  else
    return service
  end
end

return Services

Using this and working directory like the one below where the ModuleScript named Service contains the snippet above.

The Service Directory

image

We have now wrapped Service instances in a way which is indistinguishable to an existing pattern in the Roblox API. Just as one might do retrieve the HumanoidController with

local ControllerService  = game:GetService("ControllerService")
local HumanoidController = ControllerService:FindFirstChildOfClass("HumanoidController")

we are able to retrieve our own modules with

local ServiceProvider   = require(script.Parent:WaitForChild("Services").Service)

local ControllerService = ServiceProvider:GetService("ControllerService")
local CameraService     = ControllerService.CameraService

3. Why This is Useful

For large games, it tends to be the case that the first 100 lines can just be incorporating things we need into the script for later use whether it be with require, :GetService(...), :FindService(...), local variables which hold a reference to the result of :WaitForChild(...) or what have you. Compressing that all down to something which is not only compatible with :FindFirstChild(...) and :WaitForChild(...) but removes the need to strictly call require keeps things uniformly organized. It furthermore encourages that you program in a way that is compliant to how Roblox’s API is already designed. I’d even go so far as to say that this is absolutely how you should structure things on the server side and to an extent the client as well although it tends to be a little more chaotic there. Lastly, this solution is extremely lightweight only demanding a single new module.

4. What can be Improved

I think the brevity of a single function with a single utility is appropriate for discussion here but a few extensions to the implementation I’ve provided could make it much more flexible.

  1. Add caching so that only one wrapper per service is in use.
  2. Add a Service macro or base class so that service design is structurally consistent.
    2a. Add inheritable methods connecting to ServiceAdded and ServiceRemoving.
  3. Wrap FindService
8 Likes