Preface
For any given project, the answer to this question will likely differ. This is simply a result of structure and overall philosophy. It may be easy and straightforward in some instances, but extremely difficult and complicated in others.
However, if this is a difficult problem, that is a very strong code smell: something is likely quite wrong with your project. That being said, we've all been there, and we often don't have time to rewrite the entire codebase. Still, to start: what's the right way to solve this?
The Right Way
IoC (Inversion of Control) is a design pattern and principle that has steadily gained traction. It basically states that instead of objects building what they need, they request an instance of what they need from some IoC container, which can give them the appropriate "thing".
In this case, we could do something like the following:
interface SystemAdapterInterface
{
public function sleep($duration);
}
class WakefulSystemAdapter implements SystemAdapterInterface
{
public function sleep($duration)
{
sleep($duration);
pcntl_signal_dispatch();
}
}
class SleepySystemAdapter implements SystemAdapterInterface
{
public function sleep($duration)
{
sleep($duration)
}
}
class SleepingEntity
{
public function __construct(SystemAdapterInterface $system)
{
$this->system = $system
}
public function mayReceiveSignal()
{
$this->system->sleep(100000);
}
}
// Register the system and sleeping entity with the IoC Container
// Using the LoEP Container for IoC
$container = new League\Container\Container;
$container->add('SystemAdapterInterface', 'WakefulSystemAdapter ');
$container
->add('SleepingEntity')
->withArgument('SystemAdapterInterface');
//Now we can easily get an instance of sleeping entity.
$container->get('SleepingEntity');
In this example, we use an IoC
container to manage what System
SleepingEntity
uses. My recommendation for an IoC
is Container
, from the League of Extraordinary Packages.
This solution keeps our SleepingEntity
decoupled from our System
. Depending on where we use SleepingEntity
, we can simply configure our IoC Container
differently. It's nice because it's relatively simple, easy to test, and allows for expansion in the future.
Unfortunately, this doesn't work particularly well if you already have a whole lot of code with sleeps (or similar global function calls), and you need to modify their behavior. So what can we do about that?
The Not Quite as Right Way
Ok, so you want to do The Right Thing, but the code is poorly documented, has dependencies intertwined every which-way, and it is an incredibly heavy lift to go through and make sure you've passed your IoC
every which way down the tree until it is used to instantiate the right object, every time.
The best thing to do is to be forward-looking. If you want to make the codebase better, more decoupled, take the first step with this addition.
Let's say you have the following:
class DoSomethingAndSleep
{
public function foo($a, $b, $c)
{
$a->thing();
$b->thing();
sleep(5);
$c->thing();
}
}
Now let's pretend that DoSomethingAndSleep
is used 1,000 different places by all kinds of different code. In fact, it is sometimes instantiated anonymously through an class variable, so finding those 1,000 places is hard. Really, really hard.
Fine. Let's just move the decoupling goodness into DoSomethingAndSleep
. Hopefully you'll have time to refactor things one by one in the future.
class DoSomethingAndSleep
{
public function __construct(SystemAdapterInterface $system=null)
{
if ($system=== null)
{
$system = App::getIoC()->get('SystemAdapterInterface');
}
$this->system = $system;
}
public function foo($a, $b, $c)
{
$a->thing();
$b->thing();
$this->system->sleep(5);
$c->thing();
}
}
Ok, so what's going on here? Well, there is firstly a recognition that we don't have the right infrastructure to pass the the IoC
into DoSomethingAndSleep
, or to use it to construct the DoSomethingAndSleep
, so instead we're going to bypass all of that and get the "standard" IoC
from our App
. This means that upon application startup, we have to:
- instantiate the IoC Container
- register the SystemAdapterInterface with the IoC Container
- register the IoC Container with the App
This is acceptable. As we have a default argument in our DoSomethingAndSleep
constructor, we can spend time moving forward refactoring things to use the IoC Container
without breaking backwards compatibility.
We still will need to move to (hopefully) using the Container
to manage objects and instances, but at least we're at a point where we are registering the SystemAdapterInterface
and using the registered one instead of directly instantiating it.
If you are using the League Container
, we can even register DoSomethingAndSleep
with our IoC Container
using this method, and leverage the Auto Wiring and the Reflection Delegate to take care of this new dependency for DoSomethingAndSleep
.
$container = new League\Container\Container;
// register the reflection container as a delegate to enable auto wiring
$container->delegate(
new League\Container\ReflectionContainer
);
$doSomethingAndSleep = $container->get('DoSomethingAndSleep');
This should make our future refactoring go even more smoothly.
Conclusion
It is always difficult to figure out how to take an old, monolithic codebase and slowly make it better, more maintainable. You can't break backwards compatibility, you need to move forward with new features, and you want to make sure the additions improve things. That's tough. I think the best suggestion is this: if you can't write tests for it, you will have a terrible time maintaining it. So every time you make a change, make sure you can write a unit test against that change.
In this case, using IoC
accomplishes this with very little effort. Even if your project isn't structured to use it, you can introduce it initially and only use it in one place, and then over time you can refactor your code until eventually everything is using DI and is pleasantly decoupled.
Best Answer
OpenGL didn't 'embrace' global state so much as it was one of few sensible solutions to a technical problem. It's expensive to communicate with a video card. Flipping bits in video memory is considerably more expensive than flipping bits in ordinary memory. As an example it's so significant that later versions of OpenGL got rid of immediate mode, because setting up buffers (i.e. retained mode), then rendering from those buffers was so dramatically more efficient it was worth the more complicated setup. It would be impractical to "pass" the entire configuration of the rendering system on every frame because of how much communication would have to take place.
In contrast, in many web server situations, it's not uncommon to pass around a massively complex data structure, encompassing the entire state of the request and whatnot, because all you're really actually passing is an 8 byte pointer.
So, the problem with global state is it can be hard to know who's modifying it and when, and so it can be tricky to reason what it's state is. So, the thing to do is to setup your application to only modify that state in well defined ways, with well defined processes, so it is easy to reason what is happening.
Just compare the rendering state with e.g. a database driven application. In many respects, the database is a big global state. However, the interface to that DB, and often the DB itself, enforces when and how it is modified, and enforces what the data looks like inside it.
One simple way you can implement something similar is to have an interface which keeps track within your application what the state of the renderer is. You know you need the renderer setup a particular way, so you can consult your interface which would know, without needing to ask the graphics card. Likewise, it can also make the decision what has to change to get the renderer into the right state in the most efficient manner.
Building 3d graphics engines is an enormously complicated and broad subject, so I can't give precise pointers or code samples. But the basic gist is you need to come up with a way to sensibly merge the global state of the 3d card with a limited scope programming model on the CPU side, rather than having your application use all global variables.
In your specific example, when you say the other devs are defining several singletons and passing them around I get highly suspicious, but without seeing more of what's going on I can't really comment on that in more detail.