Jump to content

  • Log In with Google      Sign In   
  • Create Account


Alternative to singleton


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • This topic is locked This topic is locked
65 replies to this topic

#21 Antheus   Members   -  Reputation: 2397

Like
0Likes
Like

Posted 19 March 2012 - 08:24 AM

This is the code base you inherit, I don't see how it's material to this discussion. Someone needed to load a texture, LoadTexture was called. We have to work within the constraints we're given, which means you don't always get to design a system from scratch.


Hence the discussion here. To make those that start from scratch not default to big ball of mud design.

So what do you do under these constraints, do you just dump it in the too hard basket, because you can't make your change as cleanly as you'd like?


You do nothing. Suck it up, do the job.

And when someone asks how to design a system, you denounce the common mistakes, such as using global namespaces, global state or excessive coupling without thought.

Sponsor:

#22 davepermen   Members   -  Reputation: 1007

Like
0Likes
Like

Posted 19 March 2012 - 10:46 AM

How long am I on gamedev now? I just love the singleton threads.

Took me quite a while to understand why they're totally bad, but once I did, i felt really ashamed to never have noticed it.

It was around the time where render to texture came up to do realtime reflections and shader effects. I realized at that point how much opengl was essentially a hidden big singleton object, and how I was so used to it that I couldn't really refactor my engine to render to multiple targets.

When ever you think a singleton gives you anything, just instanciate one object (globally if you want) and use that. All the features of a singleton, and the moment you need another instance, you can just create one (you won't do that by accident anyways).

now about having anything in the global namespace, and how bad that is, that's another topic :)

it's just, once you get your head around doing stuff properly, the need for those constructs instantly vanishes. it's quite liberating.



over the years, one solution i've found is to always throw away anything that you call manager. once you write a class like "TextureManager", throw it away. a manager is the worst kind of construct you have. define what you want to really have. a manager is a be all do all thing, and thus will evolve into some global singleton bloat that you don't need.

//funky fake-language alert
class Scene {
   List<Textures> textures;
   Scene(string Filename);
}
Scene::Scene(string Filename) {
    content = new Content(Filename);
    foreach(var texture in content) {
	    textures.Add(new Texture(texture));
    }
}

to put simply: just code. don't even think about it further. use stuff where you need it, and don't spread it over all your program just because you can.
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud


#23 Potatoman   Members   -  Reputation: 108

Like
0Likes
Like

Posted 21 March 2012 - 06:23 AM

So what do you do under these constraints, do you just dump it in the too hard basket, because you can't make your change as cleanly as you'd like?


You do nothing. Suck it up, do the job.


I personally consider it a cardinal sin to go in and 'fix' things, if there's no demonstrable benefit to refactoring, something that impacts the project in a tangible way either now or in the immediate future (not 'maybe' later). If it's going to help later, fix it later.

In the example cited, with texture loading, I think it clearly falls into this category of 'no demonstrable benefit of making this change'. I've seen systems like this persist for decades without a need for change. Unless you can outline how it improves your final product, given a finite budget you've necessarily lowered the quality of the end product, by neglecting higher priority systems that do have a tangible impact on your application.

#24 Potatoman   Members   -  Reputation: 108

Like
0Likes
Like

Posted 21 March 2012 - 06:24 AM

over the years, one solution i've found is to always throw away anything that you call manager. once you write a class like "TextureManager", throw it away. a manager is the worst kind of construct you have. define what you want to really have. a manager is a be all do all thing, and thus will evolve into some global singleton bloat that you don't need.


Typically your texture loader will see a texture has already been loaded, and return an appropriately reference counted instance of that texture. That 'already loaded' texture directory needs to exist somewhere, just wondering where you store this data if not a texture manager?

#25 davepermen   Members   -  Reputation: 1007

Like
1Likes
Like

Posted 21 March 2012 - 06:32 AM

in simple: it just shouldn't happen at that level. why can't a scene properly load it's textures (and all other data besides textures) without loading stuff multiple times? that's not part of a texture manager, it's just part of the scene to handle that.

scene {
std::<texturename, Texture> textures;
}

done.

there is your texturemanager, your complex global have to be thing that just does not need to.

when a level gets loaded, it loads all the data it has to, into it's own object. when the level gets unloaded, it frees it all.

with the singleton, all you do is, when a level gets loaded, it loads all the stuff into a global object. when the level gets unloaded, it frees it all from the global object (maybe).

it's the same. just, in one case, you actually handle it where the stuff belongs, instead of outsourcing it to something that artifically acts like a god to your application.


and no, there are NO issues with that. it's clean. it's simple. it just works.
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud


#26 Potatoman   Members   -  Reputation: 108

Like
0Likes
Like

Posted 21 March 2012 - 06:40 AM

I've got a few more examples I'd like to put out there, as I'm still in pursuit of a solution that makes all parties happy. Basically to elaborate on my previous example (to give a concrete situation where people can provide alternatives), in the loading routines each object is simply loaded from an XML node. You have a generic XML loader, which a graphics plugin (extension) can register custom types against. So <GraphicsEntity type="UserGraphicsEntity"/> is encountered, and the XML node is forwarded to some pre-registered load interface. The graphics engine knows nothing of what might be loaded, and thus cannot have any specific information 'passed in' to its load calls.


std::unique_ptr<IGraphicsEntity> UserGraphicsEntity::LoadGraphicsEntity(LoadContext& context)
{
  // general loading

  // resolve waveform interface
  std::unique_ptr<IWaveFormData> wfd = context.ResolveInterface<IWaveFormData>(guid1);

  // resolve temperature interface
  std::unique_ptr<ITemperatureData> td = context.ResolveInterface<ITemperatureData>(guid2);
}

Now the nice thing here is 'guid' may refer to an object that doesn't implement ITemperatureData, but 'ResolveInterface' can choose to automatically construct an agent that does the conversion for you. It might instance a TemperatureAgent object, that does whatever is required to extract temperature data from the specified object - and the source object providing the temperature need never know about this intermediary interface. It creates a well defined place where the translation between modules takes place - whether that be waveform data from the sound engine, or temperature data from your dynamics engine. Don't think the source object should ever be available to other modules? That's fine, don't have it 'publish' its information, so it's effectively hidden from its peers.

So where does one access this 'ResolveInterface' method? You can certainly pass it around in the load context as above, you could have a singleton Publishers::getInstance().ResolveInterface method. Anyone can publish itself to the directory, and since we're using GUIDs, by definition you will never need more than one directory.

Now you could have this publisher interface contained in a factory, so instead we get

std::unique_ptr<IGraphicsEntity> UserGraphicsEntityFactory::LoadGraphicsEntity(XMLNode& node)
{
  // general loading

  // resolve waveform interface
  std::unique_ptr<IWaveFormData> wfi = m_publishers.ResolveInterface<IWaveFormData>(guid1);

  // resolve temperature interface
  std::unique_ptr<ITemperatureData> ti = m_publishers.ResolveInterface<ITemperatureData>(guid2);
}

I think this would be a perfectly adequate substitute, I don't think it would require a terribly large amount of overhead to implement. It is a plausible alternative to use of a global instance that doesn't inhibit productivity, because when you add a new 'publisher', you do it in precisely one place, and you don't have to update a bunch of code to get access to this new object - it happens for free. This allows objects from any module to access data from any other module though, and I can't help but feel some will step in and say that's not acceptable. That's what a forum is for though.. The other thing is you'll get a proliferation of 'm_publishers' type entities scattered far and wide, an acceptable, but I can't help but feel a little messy, side effect of such an implementation.


One other thought is I believe people get too caught up in what is the bad part of globals (and their cousin singletons), it's not the 'how' that is the problem, meaning how you got a reference to that piece of data - but the 'why'. To illustrate
void Gun::Update()
{
  if(g_triggerActive)
  {
	Fire();
  }
}
What's wrong with the above? It's pretty evident, why would a global state, g_gunFiring, be used in a generic Gun class. Doesn't make a lot of sense. How about we get rid of the global
void Gun::Update(bool triggerActive)
{
  if(triggerActive)
  {
	Fire();
  }
}
Ignore for a moment it's a bit weird passing in the trigger state to the Update method, but that's not relevant. Now if we call
void main()
{
// ...

  gun->Update(g_triggerActive);
Well it hardly fixes a thing. This is the problem I see with so called 'solutions' to the global instance problem. Simply storing the global shared instance as a member variable doesn't change the fundamental behavior of your program. It is mathematically identical in pretty well all respects, except for cache locality related items. Most notably, it changes precisely nil in terms of thread safety when we're talking about references to global data, an often harped on about benefit of not using globals (which I've yet to understand). The problem is not so much the global, but whether what the global represents really is global - and whether referencing that global state from the code in question makes sense.

#27 Potatoman   Members   -  Reputation: 108

Like
0Likes
Like

Posted 21 March 2012 - 07:02 AM

in simple: it just shouldn't happen at that level. why can't a scene properly load it's textures (and all other data besides textures) without loading stuff multiple times? that's not part of a texture manager, it's just part of the scene to handle that.

scene {
std::<texturename, Texture> textures;
}

done.

there is your texturemanager, your complex global have to be thing that just does not need to.


Meaning things just perform the lookup by name, and the load code (what might be termed the texture manager by some, as *something* needs to facilitate on the fly load requests based on dynamic player inputs) will populate it in advance accordingly? I don't mind this approach (I presume all graphics entities have a scene passed in?), and assuming 'scene' is a class (not a namespace), the context object I refer to in earlier code is pretty much just 'scene'.

#28 davepermen   Members   -  Reputation: 1007

Like
1Likes
Like

Posted 21 March 2012 - 08:40 AM

scene is a class that i create objects from, which are then the levels that one can play.

and yes, at load time, you put the filename (+relative path) as key into the map, and the actual loaded texture into the value, if it's not yet there. when ever you load a mesh, you check it's texture filenames against that map, add if not yet there, and replace the filename in the mesh with an iterator to the instance of the texture in the map (or a smart ptr, or what ever).

mostly one or twoliners, depends a bit on the language (i'm coding in c#, i guess you work in c++).

there will be a class game, which has a level currentlevel in it, on which update and draw will be called. a new level gets loaded by new Scene(filenameofscene); and that's it. it loads all it's textures into the map, all the meshes into their map, etc. and then processes the data in update, draws it in draw.

typically, scene can't even draw itself, but i just access all it's content to draw that in the drawing routine, but that's to each his own.



the biggest helper to stop creating global god-like variables: code what you use. not code something that might solve everything. code something that solves YOUR NEED. your level needs textures, so give it textures. it needs meshes, so give it meshes. done. that simplifies code massively. especially together with typical containers (vectors, maps). learn to use them instead of creating magical managers. much simpler in the end. much less of a mess.




btw, your 'fire' code example is perfect: if you have a global, only one player can shoot/trigger. multiplayer on the same machine? impossible.

but if you refactor it to have a class player, that stores it's own trigger variable, you suddenly can. a gun is then owned by that player, and shoots, if the player "is shooting".

the advantage: you can derive players: localxbox360controllerplayer, localkeyboardandmouseplayer, networkconnectedplayer, computeraiplayer, and they can all shoot individually, with the identical gun, the identical logic. they can even pick up your gun to use it, once they killed you.

singletons and globals always take away options you haven't even imagined. but believe me, at some point, you, or someone else will. then, rewriting everything will be the only solution.
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud


#29 Antheus   Members   -  Reputation: 2397

Like
0Likes
Like

Posted 21 March 2012 - 09:27 AM

It is mathematically identical in pretty well all respects


Yes it is - as per those Turing equivalence theorems, all code is identical, regardless of language or algorithm.

void Gun::Update(bool triggerActive)
{
if(triggerActive)
{
Fire();
}
}


Enter IOC/DI...
class SceneEntity {
  GlobalFlags * pFlags;
}

class Gun : SceneEntity {
  ...
  void update() {
	 if (pFlags->triggerActive) Fire;
  }
}
The pFlags gets passed in during construction, most likely inside a factory.

Most notably, it changes precisely nil in terms of thread safety when we're talking about references to global data, an often harped on about benefit of not using globals (which I've yet to understand)


Well:
class DistributedSim {
  threadlocal<GlobalFlags> flags;
  Gun * g;
  DistributedSim() {
	g = new Gun(flags);
  }
}
Now I can run multiple simulations inside same process without any lock contention.

And if flags need to be shared, the above merely exposes just how complex of a problem that sharing is.

except for cache locality related items


If flags is shared, things get much worse fast. If shared data, something like a flag is modified frequently (millions of times vs. once per frame/step), each such change triggers cache line invalidation, stalling all cores. "I know, I'll use lockless algorithms" - now you're stalling the CPU millions of times per second. Sharing is simply expensive.

Of course, nobody knows about that since it doesn't show up in profiler. Code just runs slower than in a single thread and nobody is any wiser.

By being explicit about which parts are shared, you might notice the actual complexity of synchronizing distributed code and choose different design or algorithm, perhaps by batching updates or using rolling buffer or perhaps something else altogether.


Finally, IT industry concluded in 2002 that singletons spell death of project maintainability. And IT isn't known for being at bleeding edge of experimentation. Computational performance was never relevant to them (except for that certain bug they found in VMs), but performance and scalability of development was. During the next year, everything shifted to IOC/DI container or dynamic languages. None of this can be proven mathematically, but falls under soft observations. It just works better in practice.

#30 phantom   Moderators   -  Reputation: 7058

Like
0Likes
Like

Posted 21 March 2012 - 12:12 PM

Well it hardly fixes a thing. This is the problem I see with so called 'solutions' to the global instance problem. Simply storing the global shared instance as a member variable doesn't change the fundamental behavior of your program. It is mathematically identical in pretty well all respects, except for cache locality related items. Most notably, it changes precisely nil in terms of thread safety when we're talking about references to global data, an often harped on about benefit of not using globals (which I've yet to understand). The problem is not so much the global, but whether what the global represents really is global - and whether referencing that global state from the code in question makes sense.


The problem is your example is, once again, bad code.
In any well designed system the 'trigger state' isn't going to be a global but will be a value which has passed down a line from a few systems to get to a semantically sane meaning at a given point.

An example might be something like;

[Input subsystems queries input state]->[input state is converted to in game value]->[passed onto 'controller' object which uses the state (and maybe previous states) to cause a reaction somewhere else]

The last step is vague because it depends on the game but if you take something like Unreal the input ends up on a 'controller' and that controller then drives the entity it is attached to (with the added bonus that your 'controller' doesnt have to be local, nor human driven, to Just Work™), be it updating the camera, moving the model or passing the message onto a 'weapon controller' system which determains if a gun should shoot or not.

At the end of the day the only thing which sees a 'raw' trigger state is the input subsystem and that hangs around about as long as it takes to get converted to your 'in game' state values to drive other systems.

#31 Potatoman   Members   -  Reputation: 108

Like
-2Likes
Like

Posted 24 March 2012 - 11:09 AM

It is mathematically identical in pretty well all respects


Yes it is - as per those Turing equivalence theorems, all code is identical, regardless of language or algorithm.

I don't see how this relates to our discussion.


void Gun::Update(bool triggerActive)
{
if(triggerActive)
{
Fire();
}
}


Enter IOC/DI...
class SceneEntity {
  GlobalFlags * pFlags;
}

class Gun : SceneEntity {
  ...
  void update() {
	 if (pFlags->triggerActive) Fire;
  }
}
The pFlags gets passed in during construction, most likely inside a factory.

This illustrates what I mean. Passing in a set of global flags to a Gun class, so the Gun class now has a member pointer to a global variable, provides little or no benefit to your program. The core problem, the fact you're accessing global state in an inappropriate context (as opposed to 'inappropriate manner', by accessing that information through globals), has not been resolved

Most notably, it changes precisely nil in terms of thread safety when we're talking about references to global data, an often harped on about benefit of not using globals (which I've yet to understand)


Well:
class DistributedSim {
  threadlocal<GlobalFlags> flags;
  Gun * g;
  DistributedSim() {
	g = new Gun(flags);
  }
}
Now I can run multiple simulations inside same process without any lock contention.

You could have achieved the same outcome using threadlocal<GlobalFlags> g_flags. I'm not advising that's the way to go, I'm just saying eliminating the global instance does not, in and of itself, fix any multithreading issues. It merely prepares the framework to allow multithreaded behavior, but without actual changes to implement the thread safety, nothing changes.

Finally, IT industry concluded in 2002 that singletons spell death of project maintainability. And IT isn't known for being at bleeding edge of experimentation. Computational performance was never relevant to them (except for that certain bug they found in VMs), but performance and scalability of development was. During the next year, everything shifted to IOC/DI container or dynamic languages. None of this can be proven mathematically, but falls under soft observations. It just works better in practice.

Unsure what you mean by 'IT industry', however in terms of such a conclusion, at best, they could conclude poor use of singletons is a significant barrier to project maintainability. At worst, such a statement is an outright falsity, as some singletons never need to be removed, thus endure the complete project lifecycle.

#32 Antheus   Members   -  Reputation: 2397

Like
0Likes
Like

Posted 24 March 2012 - 06:01 PM

Use what works for you.

#33 davepermen   Members   -  Reputation: 1007

Like
1Likes
Like

Posted 27 March 2012 - 05:38 AM

a singleton in mostly every os of today: the mouse.

if it wouldn't be, we could plug in more than one mouse and use more than one cursor (similar to multitouch) since years in every os. and heck yes, i would. sadly, at some point we got limited to only one device ever, as no one ever needs more than one cursor. and the whole os' got designed around it.

now that we can have multitouch, the os' are still not designed for it (at least win7). the result: i can not, for example, drag multiple windows independently around the desktop at the same time.


it's a simple example of potential that was there since years and never got exploited just because we where all fixed on "there can be only one".
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud


#34 Olof Hedman   Crossbones+   -  Reputation: 2740

Like
0Likes
Like

Posted 27 March 2012 - 08:21 AM

a singleton in mostly every os of today: the mouse.

if it wouldn't be, we could plug in more than one mouse and use more than one cursor (similar to multitouch) since years in every os. and heck yes, i would. sadly, at some point we got limited to only one device ever, as no one ever needs more than one cursor. and the whole os' got designed around it.

now that we can have multitouch, the os' are still not designed for it (at least win7). the result: i can not, for example, drag multiple windows independently around the desktop at the same time.

it's a simple example of potential that was there since years and never got exploited just because we where all fixed on "there can be only one".


Even though I don't like singletons particularly, this is a pretty good example of that you can't predict _everything_, requirements change.
And also a pretty contrived example.

Its a pretty fringe case to have multiple mice connected (how often do you really need to move two windows independently at the same time?)
Also, with custom drivers, its no problem to connect more mice if you should need it for your special case. (controlling your game, or navigating 3d space in some editor or whatnot). The OS doesn't support it in the main UI, because it doesn't help the user experience.

Having multiple cursors isn't really similar to multitouch, multitouch is fairly more complex to handle then that, mainly because you don't know where the "cursor" is until the user touches. Even single touch is different from using a mouse.

So supporting multiple mouses and maintaining support without ever using it for the 35 years we have had mouses, would be a huge waste of time, and wouldn't really help the multi-touch case.
Every OS with multi-touch support define a new, separate API, for the multi touch case.

Though, on the other hand, the fact you can't predict everything, and that singletons is a less flexible design, is a good argument for not using singletons :)

#35 davepermen   Members   -  Reputation: 1007

Like
1Likes
Like

Posted 27 March 2012 - 08:56 AM

If you ever used a 3d app like 3d studiomax, or photoshop, you would have loved more than one mouse.

The fact that you can't even imagine the possibilities (pinch to zoom with something as accurate as a mouse? a dream. same for rotation and stuff, 3d modelling in general) just shows how far a singleton can restricts ones mind.

the example i gave in an older post was about rendering to multiple contexts in the early days of opengl to do rendertotexture style stuff. as opengl is essentially a huge singleton, i never could grasp myself around it, i was too used to that "statemachine that just does everything for me". and that's exactly what singletons evolve into: huge "everythings" that then make you go blind on possibilities you're missing.


the other example (i think in the 'are globals bad' topic) was the player singleton. a lot do that, imagining there's just one player. but even in a singleplayer, that's not true. you have enemies. imagine them being able to use the same physics, the same movements, the same weapons, the same cars and planes and stuff that you do? they are a player, just one with a different "controller" behind it.

once you move away from the player singleton, suddenly you can be anything in the game, so you could start to mind-control other characters, etc. or they could use your vehicle when you don't look for it. suddenly, the game evolves while you develop it into something much bigger.

singletons always block you at the most important part: where you're thinking. because while coding, that part is active: your brain. and if the code does not allow you to do stuff, you won't even imagine that stuff anymore.



and yes, touch and mouse are not the same. hence multitouch does not replace the power of multiple mice. i feel this the most when i'm producing music. to do it accurately, you can't use touch. you need a mouse. but more than one mouse would allow so many features we're used from multitouch, it would be great.
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud


#36 Antheus   Members   -  Reputation: 2397

Like
0Likes
Like

Posted 27 March 2012 - 09:13 AM

Even though I don't like singletons particularly, this is a pretty good example of that you can't predict _everything_, requirements change.


/dev/mouse1, /dev/mouse2, /dev/mouse3, ..., /dev/mouseN. Same for any other hardware resource.

It's "one mouse should be enough for everyone" that is unusual.

#37 davepermen   Members   -  Reputation: 1007

Like
0Likes
Like

Posted 27 March 2012 - 09:34 AM

obviously, there are multiple mice supported (at least, since usb mice exist. not sure about os' that only had support for ps2 connections). but not multiple cursors. at least, not by default (i've seen special drivers, and one can use those for apps, but they a) don't follow the mouse speed settings of the os and b) don't support the rest of the os, which is, as said, designed for singleton szenarios).

no clue about linuxs situation, of course.. i'm a windows guy.
If that's not the help you're after then you're going to have to explain the problem better than what you have. - joanusdmentia

My Page davepermen.net | My Music on Bandcamp and on Soundcloud


#38 Serapth   Crossbones+   -  Reputation: 5316

Like
0Likes
Like

Posted 27 March 2012 - 10:02 AM

I have multiple mice plugged into my computer, I have my wireless mouse, my laptop trackpad and my wacom tablet. It's technically the pointer that would be the singleton.



I'm kinda shocked at the lack of design pattern proponents in this thread. The Service Locator pattern, and/or Dependency injection are effective means of decoupling a globally available resource in a manner that is well abstracted, while still unit-testable. Although I suppose passing the interface in via parameter is an example of DI of sorts. Design patterns are your friend, as these software development problems are the same regardless to if you are writing a Forex trading system or a 3D renderer.

#39 Telastyn   Crossbones+   -  Reputation: 3726

Like
1Likes
Like

Posted 27 March 2012 - 11:19 AM

I'm kinda shocked at the lack of design pattern proponents in this thread.


Mildly blind obedience to design patterns is a significant reason that singleton overuse exists in the first place.

#40 Serapth   Crossbones+   -  Reputation: 5316

Like
0Likes
Like

Posted 27 March 2012 - 11:21 AM


I'm kinda shocked at the lack of design pattern proponents in this thread.


Mildly blind obedience to design patterns is a significant reason that singleton overuse exists in the first place.



Yes, but ignoring them is throwing the baby out with the bathwater.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS