How to separate xyManager effectively into smaller responsibility classes?

Started by
9 comments, last by frob 8 years, 11 months ago

On the Manager problem....

Typically for resources it can be broken down into four classes. There is a "pool" or "store" class, there is a "proxy" class that can be returned instantly with a placeholder even if the real version is not fully loaded yet, there is a "loader" class that does the work of getting into memory, and there is a "cache" class that helps determine the lifetime.

The term "Manager" is often a bad word. It doesn't define a single responsibility. You should be able to, after defining a single responsibility and using the dependency inversion principle, come up with an interface that does all the things you need and nothing more.

But that also leads up to the next part of the discussion.

On the bulk communications questions...

Design is important. There is an enormous difference between shared mutable values and shared immutable values.

There are many examples of shared mutable shared state gone wrong. You set a value and somewhere else in the giant code base someone else has set it back. An example from a few days back, someone had set a shared state that the UI respected 32-bit Unicode, something else inside the code changed it to say it was MBCS. Problems ensued. Any time you have mutable shared state there are difficulties in managing control over it.

But immutable shared state tends to work better. There are solutions to dramatically reduce the shared state, like using a global message bus that any system can listen to, but then you immediately have the difficulty of finding that global message bus. Logging is another situation, you don't care about the details of the log, but which log do you use?

A common solution to help with immutable shared state is a well-known instance of a service finder. It is a bit of a compromise. Note that the items are not a singleton, nor are they a static instance. You create a global pointer to a service finder which is initialized at a well-defined point early in the program's lifetime. You establish policy rules about when and how the individual services are modified. Very importantly, those services must be stable and immutable over defined times.

An example of an immutable shared state with a service finder, you might have policy in place that says the service finder instance valid after startup, and that the members are always immutable during update. So when looping over all the game objects you can write query functions like ::gServiceFinder->simulator->clock->CurrentTime(). Or you can broadcast messages with ::gServiceFinder->messageBus->SendMessage( ... ); Or you can access logging with ::gServiceFinder->log->Info(...); Note that all of these are immutable, they don't change observable state within the system.

This is usually decoupled enough that you can write unit tests (by filling in some proxy objects to the global pointer), you can replace systems as needed (they are not singletons and they are not static instances, pointers to them are not stored by anyone else in the code), and you can have strict policy rules about when those items are modified. But it also keeps things available to the wide range of systems that need access to information (no need to pass a large structure around, don't need to worry about being too deep into one system that others are inaccessible).


Just keep in mind that you are correct, mutable shared state is a source of nightmares and should be avoided.


Putting them together back with the original question.

BuildingManager is a singleton, right now it does
- Hold the vector of buildings
- Process OnClick events (i detect which building was clicked by iterating over them unless i found the clicked one)
- Update them
- Draw them
- Add building

It's maybe too much for a single class.


It is. It violates the Single Responsibility Principle.

You've got a pool or store of buildings. I presume they are on a map. You've got events and a map. You've got an "update". You've got a "draw". Lots of responsibilities.

Your game map doesn't really need to know what the items are, but it does need to find objects your mouse is over. That seems like a responsibility of the game map, not a responsibility of the building manager. And if you are following the dependency inversion principle you should be interested finding all the nearby objects that support OnClick events. So perhaps a function on your map class called FindNearby() that takes a distance, which in this case happens to be zero, and an interface you need, the objects that support OnClick, and then it returns a container of them. So perhaps: clickables = ::gServices->simulation->map->FindNearby( mouseMapLocation, 0, Clickable); would work. Then you can loop over all the results and call their OnClick function.

The nice thing about finding things by supported interfaces, or by components if you are going with that design, is that you can write much generic code. You can search for your nearest respawn point regardless of what the concrete type is. You can search for the nearest source of music. You can search for the nearest waypoint. The nearest shop. The nearest whatever. As a bonus feature you can later modify your game -- or a modder can modify your game -- and add a new concrete type of whatever the thing is, and everything will continue to just work.

As an example from The Sims, the characters could look for the nearest available "radio", which is a generic music source. It could be any of a bunch of portable stereo systems, or it could be a wall-mounted speaker. Later when we made some changes to the car, a parked car could be used as a radio. Create a new type of object as a fancy juke box, it is also tagged as a radio. Want to wire your whole house with speakers? Just add a radio component. If they had foolishly built it around requiring a concrete radio class then all those other options would be unavailable. Yay for dependency inversion. Just search for the nearby items that satisfy the interface, and if you have at least one of them, you are good to go.

For calling updates, usually everything in the game world needs an update. Same for rendering, that should be the responsibility of a rendering system instead.

For other broad events that need to be broadcast, perhaps send it to any system interested in the event: ::gServices->messageBus->SendMessage( ItemClicked, pItem ); Then anything registered with the message bus to process the ItemClicked message can handle it.

This topic is closed to new replies.

Advertisement