@quenty/servicebag
v11.10.0
Published
Service providing mechanisms for Nevermore
Downloads
3,385
Maintainers
Readme
ServiceBag
Service providing mechanisms for Nevermore
Installation
npm install @quenty/servicebag --save
Goals
- Remove requirement for many services to be loaded
- Make installing new modules really easy
- Make testing easier
- Reduce maintaince costs
- Explicitly declare service pattern
- Force declaration of service usage
- Make it easy to trace service dependencies
Requirements
- Initialize dependent services using
:Init()
- Start dependent services using
:Start()
- Retrieve services is easy
- Don't have to list off every service in the game
- Services don't load until we ask them to
- Circular dependencies allowed
Like-to-have
- Dependency injection for tests
- Works with type system
GetService<IService>()
- Async initialization (return promises instead of blocking)
- Dependency graph safe (i.e. recursive service requirement )
- Services are protected from another service erroring
Stretch goals
- Handles services for actors (remove global code)
Ideas
- Inject a ServiceBag
Rules
- Each service will be identified by its module
- Each service will be initialized once
- If we require a service, we will not have to declare subservices
-- Main script
local serviceBag = require("ServiceBag").new()
serviceBag:AddService(require("TransparencyService"))
serviceBag:Init()
serviceBag:Start()
-- Other script
local TransparencyService = require("TransparencyService") -- force declaration at top
local TestClass = {}
TestClass.ClassName = "TestClass"
TestClass.__index = TestClass
function TestClass.new(serviceBag)
local self = setmetatable({}, TestClass)
self._serviceProvider = assert(serviceBag, "No serviceBag")
self._transparencyService = self._serviceProvider:GetRequiredService(TransparencyService)
return self
end
return TestClass
local DialogPane = setmetatable({}, BasicPane)
DialogPane.ClassName = "DialogPane"
DialogPane.__index = DialogPane
function DialogPane.new(serviceBag)
local self = setmetatable(BasicPane.new(serviceBag:GetService(DialogTemplatesClient):Clone("DialogPaneTemplate")), DialogPane)
self._theme = Instance.new("StringValue")
self._theme.Value = "Light"
self._maid:GiveTask(self._theme)
self._dialogInput = DialogInput.new()
self._maid:GiveTask(self._dialogInput)
Classes versus singletons
Right now services and classes aren't the same. There's no contract to transform a class with lifetime into a service. However, because some of our services actually implement a .new() method (i.e. service definitions can be classes), we can't differentiate easily.
The issue is around life-cycle.
- Service.new() can either be a constructor which establishes just data for the service or...
- Service.new() can actually establish state data.
Other service providers solve this issue by doing the following...
- Separating out the service identifier from the actual service definition (interfaces)
- Separating out the addition of services from the actual services
We inherently don't want to separate out interfaces yet because we don't know what the actors or tie-interfaces for hot reloading will even look like.
It's important that we don't define this yet because there's a good chance separation at the service layer will be very important (i.e. we'll want to observe service state existing).
Potential solution: Interface provision
We could establish a contract that providing an interface will be allowed. That is, a service can return a value it'd like to use as an interface instead. We may need to wait until we resolve these other problems first.
Major changes in the future
- Require-by-name
- Interfaces as definition
- Hot reloading
- Async interfaces