Too many managers?

Started by
18 comments, last by reenigne 1 week, 4 days ago

frob said:
The Singleton pattern generally has a private constructor and is explicitly not virtual nor derived from a base with virtual methods, so it can't be subclasssed nor instantiated.

Jikoob said:
Default unity singleton implementation handles its own creation which may be troublesome when you deal with access via interface.

To answer both these points - subclassing of singleton is given as a specific example in “Design Patterns” by GoF - a Singleton as an abstract base class, which selects a specific subclass when instantiating. From my experience, it's sometimes used when you have platform-specific functionality, for example, “StoreManager” singleton which actually will be “AndroidStoreManager” or “SteamStoreManager”.

Jikoob said:
For example I can't imagine how AchievementsManager could not be hidden behind interface since achievements API calls are different on all platforms.

Agreed, I think it's one of the simplest approaches. I just kinda refrain from creating interfaces for eg “GameManager” that will specifically have just one implementing class - but I can see the appeal in that, I kinda like how C++ separates class definition vs implementation, so I can view source of definition and don't care about implementation details. And yeah, I agree that you can view it as a consistent approach.

For example, if an Enemy needs a reference to IAudioManager, then:
Engine.CreateManager IAudioManager → Engine.CreateManager IEnemyManager(IAudioManager) → IEnemyManager.SpawnEnemy(IAudioManager) → Enemy.Initialize(IAudioManager)

Yeah, this approach would be an example of a dependency injection. Though as you mentioned it creates a bit of a boilerplate, and I guess it's one of the things that DI injection frameworks want to fix.
And I guess I share the sentiment about DI frameworks, not sure if I like them - personally, Singletons with their implicit dependencies were good enough for me, so I didn't have a reason to use DI injection. Maybe if I would need to write more unit tests, I would change my opinion.

Advertisement

Kaosumaru said:
a Singleton as an abstract base class, which selects a specific subclass when instantiating. From my experience, it's sometimes used when you have platform-specific functionality, for example, “StoreManager” singleton which actually will be “AndroidStoreManager” or “SteamStoreManager”.

Yeah that would work in case of polymorphism.

Kaosumaru said:
Maybe if I would need to write more unit tests, I would change my opinion.

I've never seen a unity project with unit test and I'm very curious to see one - From my experience it would be quite hard to introduce unit testing to a game environment since it would multiply the amount of code tremendously trying to unitize every aspect of the game making development process much more stretched, and it also seems impossible in such a complex system.

One smartass once said: "One of the things you find in complex systems like games, is that bugs are not isolated to units usually, they happen in the more complex parts of the code where these so called units come together, and that's not very unitizable".

Of course such unit test could help you track the bug easier but no one ever been able to convince me to write unit tests for unity game - unless it would be some 100% deterministic turn based chess type game then I would consider.

None

Jikoob said:
So the project is almost at the end of development, it's gonna get shipped in matter of 2-3 months, everything is working quite fine and the amount of “Manager” classes are now up to 24 - is it too many? What would be alternative to this? I like small classes rather than having GameManager with 10k lines and I didnt really encounter any issue with it along 2 years of project development with this approach. What's your thoughts?

If this has not been a pain point, I wouldn't spend time worrying about it… In my experience, best architecture tends to come about when it is built around avoiding actual concrete pain points, not from following “best practices” just because they're best practices 🙂

I also think it's best to try out different architectures in a fresh project, rather than trying to refactor an existing large project, unless there's a good reason to do otherwise. You don't want to get into a refactoring hell type situation, where the project loses all momentum, and ends up never getting finished.

Using many singletons doesn't necessarily mean that a project is doomed to run into huge issues... things like the scale of the project, the way that the singletons are initialized, whether scripts are isolated into separate assemblies to enforce decoupling etc. can all affect the robustness and scalability of the codebase.

Some possible pain points with over-use of singletons are:

  • Execution order issues - code in one singleton throws an exception, because another singleton isn't fully setup and ready to be used yet.
  • Hidden dependencies - Being able to access singletons any time, anywhere, means it's easy to do so in contexts where it makes little sense - like in the ToString method of a value object, or within a utility class. This can result in exceptions getting thrown from seemingly very innocent looking things, like logging code, because some method is secretly coupled to a bunch of singletons.
  • Tight coupling of everything - If singletons are accessed all over the place willy-nilly, it can result in a spaghetti codebase, where nothing is decoupled. An issue in one place can cause things to break in a completely different place, making debugging difficult. It becomes impossible to extract useful code into another project, because there are so many dependencies to various singletons.

If all singletons are always guaranteed to be fully ready for use, from the moment that the instance accessor is used, I think that can get help avoid something like 98% of all issues. But the moment that even a single manager gets some asynchronous setup logic added to it, it can cause bugs to start cropping up way more easily.

Dependency injection can be used to get rid of these pain points in a pretty elegant way:

  • It can make dependencies of objects and methods crystal clear.
  • It can be used to make it impossible to construct instances without providing them with all their dependencies (constructor injection, factory method…).
  • It can be used to automatically validate that all components in all scenes and prefabs in the project have all their dependencies assigned via the Inspector.
  • Objects can be automatically initialized in optimal order based on their dependencies, to avoid initialization order issues.

But replacing singletons with dependency injection can also introduce some additional complexity, and using IoC containers can cause issues of their own (e.g. they tend to be complicated to use, compared to simple inspector drag-and-drop). So if the singletons are working for the project, I wouldn't be in hurry to spend days refactoring them for potentially no practical gain.

Jikoob said:
I've never seen a unity project with unit test and I'm very curious to see one - From my experience it would be quite hard to introduce unit testing to a game environment since it would multiply the amount of code tremendously trying to unitize every aspect of the game making development process much more stretched, and it also seems impossible in such a complex system.

I would also argue that it's not an all-or-nothing decision. Unit tests can also be used only in some situations, where the risk of human mistakes is extremely high. E.g. parsing code, or code using regex, would be great candidates for a unit test. If used like this, the creation of unit tests won't add much at all to development time, while potentially saving quite a bit of time by helping bugs get caught early on and efficiently. Nowadays AI can also be used to automatically generate some unit tests, which can make the process even cheaper.

Also, the more automated tests that a project has, the less time needs to be spend testing new builds. The larger the scale of the project, the more difficult it will be to manually test it comprehensively after every change. Imagine working on an MMO with a main questline that is hundreds of hours long and a character progression system where it takes months to unlock everything - time invested into creating a large suite of automated tests can save enormous amounts of QA resources in the long run.

One smartass once said: "One of the things you find in complex systems like games, is that bugs are not isolated to units usually, they happen in the more complex parts of the code where these so called units come together, and that's not very unitizable".

Thinking of the “unit” in a unit test as a user-facing feature, instead of a single method or a class, can help in creating unit tests that don't focus on testing small implementation details, but on ensuring that the system works overall at the edges. Tests like these don't break as easily, when changes are made to the code, because as long as the public API being tested, and its observed behaviour don't change, the tests will continue to pass.

Kaosumaru said:
subclassing of singleton is given as a specific example in “Design Patterns” by GoF - a Singleton as an abstract base class, which selects a specific subclass when instantiating.

Yeah, no. Just double-checked the book, it's pages 127-134 if you're following along at home.

The closest they get to it is point implementation issue #2 on page 130, going on to the next page, writing the key issue with an inheritance version is the necessity to write the code in a way that the instance must be initialized with the instance of the subclass, then deciding that a registry of well-known objects is a better approach there.

In either event, as the project is about to ship it isn't something to fix right now. If the “manager” classes are working with the code, functional code that's working today generally shouldn't be changed without reason.

frob said:
Yeah, no. Just double-checked the book, it's pages 127-134 if you're following along at home.

@kaosumaru is right on this one; the Design Patterns book definitely says that the singleton pattern can be used with subclassing, and in two different places:

Design Patterns: Elements of Reusable Object-Oriented Software:

Page 127:

Use the Singleton pattern when

  • there must be exactly one instance of a class, and it must be accessible to clients from a well-known access point.
  • when the sole instance should be extensible by subclassing, and clients should be able to use an extended instance without modifying their code.

Page 128:

Consequences

Permits refinement of operations and representation. The Singleton class may be subclassed, and it's easy to configure an application with an instance of this extended class. You can configure the application with an instance of the class you need at run-time.

Sisus said:
I also think it's best to try out different architectures in a fresh project, rather than trying to refactoring an existing large project

frob said:
In either event, as the project is about to ship it isn't something to fix right now.

Yes of course, that's not what I planned on doing, it would be a disaster. I just wanted to talk about an approach I would like to try out maybe in next project or just as a somewhat of a framework and i'm curious what you people think about it.

Thanks for all the replies!

None

Trying to resist derailing, but can't resist on this one.

Yes, pages 127 and 128 mention the possibility of subclassing. It continues with 130-132 covering a long list of issues with the subclassing approach, ultimately recommending against using the Singleton for a creation pattern and instead using it as a lookup into well-known instances. “It can be done” is different from “it is a good decision”.

@Jikoob If it works for you then there isn't anything really wrong with it.

Myself I use an asset manager that handles anything that can fit under the category of an asset, images, sound, fonts, models, maps, …

If you make a generic enough manager you can simply add an interface to allow new asset types for it to manage.

I don't use it as a singleton.

Advertisement