Services streamlining
See original GitHub issueFollow up issue to move the discussion from the chat to here.
I usually have two motivations to create a new separate class which we all seemed to agree to call Services now.
Motivation 1. Delay decisions
Sometimes decisions can be delayed, e.g. Networking, Asset Loading, File Storage, etc. Using a service let’s you continue with the fun stuff and implement the nasty bits later, e.g. a ViewService can use Resources.Load() but the ViewService can later be updated to use proper Asset Bundle loading without having to refactor systems.
Motivation 2. Multiple systems use same / similar logic
Example:
- You create SystemA with logic in Execute()
- You create SystemB with some duplicated code from SystemA
- You refactor your code to remove the duplicated code
Where to put this code? -> Services There are multiple different ways with pros and cons:
a) Move the duplicate code to a static class Service.cs with a static method DoSth() and call it from SystemA and SystemB Service.DoSth()
Pros:
- very simple
Cons:
- We cannot mock and potentially cannot test the code anymore (tight coupling)
- Utility functions must not have any state
- Some service might require some initialization in order to work (e.g. RandomService.Initialize(seed))
Conclusion:
Don’t do this
b) Don’t use a static class and pass the service via ctor arg into the system
Pros:
- explicit
Cons:
- Too much work, constant refactoring and service passing
- Poor manual DI
- Potentially large ctors
- not fun at all 😃
Conclusion:
Explicit, works, proceed at own risk if you love refactings 😃
c) Don’t use a static class but a static getter Service.singleton
Pros:
- very simple
- can be mocked and unit tested
- can be swapped with alternative impl (not as easy as a proper DI solution)
- explicit dependencies are easy to spot at the top of systems
- Kind of halfway DI
public sealed class SystemA : ReactiveSystem<GameEntity> {
public Service service = Service.singleton;
// ...
}
Cons:
- People tend to leave your team when they see *.singleton 😃
Conclusion
Simple, testable, works, not much overhead in development, but not very elegant
d) Add a ServiceComponent and use contexts.services.randomService.value.Int()
Pros:
- can be mocked and unit tested
- can be swapped with alternative impl
Cons
- Moving system logic to components as part of refactoring feels like a bad idea
- implicit, dependencies are harder to spot
- more components (unnecessary?)
- miss-use of components?
e) Traditional automated DI, sth like
public sealed class SystemA : ReactiveSystem<GameEntity> {
[Inject]
public Service service;
// ...
}
Pros:
- same as c)
Cons:
- needs to be implement, makes sense if we all agree on it
- Magic
Conclusion:
Looks ok to me. There are tons of DI Framework already, create a new one tailored for Entitas? Can we avoid manually wiring and use CodeGeneration to setup defaults?
f) CodeGeneration
Need to think more about this, maybe you guys have ideas. Flag Service classes wit [Service]? Have an IService interface? Conventions based on naming? What should be generated? contexts.randomService.Int()?
Pros?
???
- potentially a seamless integration to the Entitas CodeGeneration experience
Cons:
???
- implicit, dependencies are harder to spot
You can checkout Match One. It’s using approach c) atm.
Do you guys have more ideas / suggestion how to streamline service? Do you see more pros / cons. Do you have a favorit? Do you have a better solution?
Cheers 😃
Issue Analytics
- State:
- Created 6 years ago
- Reactions:4
- Comments:14 (2 by maintainers)

Top Related StackOverflow Question
My favored option is services as Interfaces existing as generated fields in the
Contextsclass.1: Define your service interface e.g.
2: Generator adds field
Contexts.timeServicewith a nice error message if it is accessed when it is null (e.g. “you forgot to provide an implementation of TimeService before you accessed it”).3: In GameController.cs (or whatever your application start point is)
Now from any system we can get _contexts.timeService.deltaTime etc.
Pros: It’s basically your fourth option above, but without involving components - so you eliminate 3 of the 4 cons straight away. No need to generate anything more than the fields in the contexts class - all the methods are part of the original interface decleration. I suppose they wouldn’t even have to be interfaces, if you wanted to do it with concreate classes that would work in the same way.
The way I currently work is to store directly onto a component, but i would prefer it to be in the contexts class if there was code-generation support for that.
For option b), one can use dependency injection library like Zenject to make things much easier. Zenject is probably too bulky to be a dependency of Entitas, but the users can choose to use them together without problems. Or Entitas can implement its own lightweight DI library. But IMO Zenject is popular and stable enough and we can just let users decide and not reinvent wheels.