Do you like organizing your services but wish there was an easy way for services to depend on each other?
Introducing Karen, the library specifically for speaking to your managers!
Taking inspiration from C# .Net service registration and Kotlin’s Jetpack Startup library, this library allows you to explicitly register your services and their dependencies, while handling initialization order for you, access your initialized services globally, and manage dependency injection for tests.
Installation
With Wally
Add Karen to the dependencies section of your wally.toml file.
[dependencies]
Karen = "kylaaa/karen@0.1.5"
From GitHub
- Goto the Karen GitHub repo.
- Download the latest
Karen.rbxm
from the Releases section. - Drag it into Roblox Studio from your Downloads folder.
Usage
Creating singleton classes
local A = {}
A.__index = A
function A.new(dependencies : {})
-- use dependencies to initialize the service
local B = dependencies.B
return setmetatable({
-- optionally store the dependencies table for later
-- dependencies = dependencies
}, A)
end
function A:foo()
print("Hello world")
-- use dependent services inside function calls
-- self.dependencies.B:bar(123)
end
Intializing all of your singletons
At the entry point to your code, initialize all of the implementations for your singletons.
local Packages = game.ReplicatedStorage.Packages -- your path to where packages are stored
local Karen = require(Packages.Karen)
local ManagersFolder = script.Parent.Managers --your path to where you store your singleton ModuleScripts
-- initialize the singleton manager
local sm = Karen.new()
-- register each singleton by passing the ModuleScript Instance along with a list of its dependencies
sm:registerSingleton(ManagersFolder.A, {
ManagersFolder.B
})
sm:registerSingleton(ManagersFolder.B, {
ManagersFolder.C,
ManagersFolder.D
})
sm:registerSingleton(ManagersFolder.C, {})
sm:registerSingleton(ManagersFolder.D, {})
-- tell the manager to initialize all of the singletons with their dependencies
sm:initialize()
Accessing registered singletons
-- simply use the string name of the ModuleScript to access the initialized singleton
sm:get("A"):foo()
-- or
Karen.getInstance():get("A"):foo()
How Is this useful?
As projects get larger, having logic cleanly divided into testable chunks becomes more and more important. And having services with cleanly defined interfaces allows for that functionality to be easily mocked for tests.
A simple use case would be to have a LogManager whose sole job is to allow for messages to be logged at varying levels, and every service uses it instead of using print
or warn
statements. Then from a single place, you can change how these messages are displayed throughout the entire game.
Example LogManager
local LibraryRoot = script:FindFirstAncestor("TwitchBlox")
local Packages = LibraryRoot.Packages
local Signal = require(Packages.Signal)
local LogManager = {}
LogManager.__index = LogManager
LogManager.LogLevel = {
None = 0,
Error = 1,
Warning = 2,
Message = 3,
Trace = 4,
}
function LogManager.new(dependencies : {})
local cm = dependencies.ConfigurationManager
local lm = {
logLevel = cm:getValue("LOGGING_LEVEL"),
NewMessage = Signal.new(), -- (logLevel : number, ... : any) -> ()
}
setmetatable(lm, LogManager)
cm.Updated:Connect(function()
lm.LogLevel = cm:getValue("LOGGING_LEVEL")
end)
return lm
end
function LogManager:log(level : number, ...)
assert(level ~= LogManager.LogLevel.None, "level cannot be `None`")
if level <= self.LogLevel then
self.NewMessage:fire(level, ...)
end
end
function LogManager:error(...)
self:log(LogManager.Error, ...)
end
function LogManager:warn(...)
self:log(LogManager.Warning, ...)
end
function LogManager:message(...)
self:log(LogManager.Message, ...)
end
function LogManager:trace(...)
self:log(LogManager.Trace, ...)
end
return LogManager
So yeah!
Give it a shot, let me know what you think. If you run into any bugs, please feel free to create a bug report on the github repo!Happy coding!
~ItReallyIsKyler