Earlier this month, I was asked by a colleague to explain why global state is bad. It seems to me that many developers don't fully understand what global state is, or if they do, why it is labelled as negative.
This post aims to show why global state is harmful, and how you can avoid it within your code. The examples are given in PHP, but you can apply the concepts to any object-oriented language.
Here's something worth distinguishing before we continue: calling a stateless static method isn't global state, it's just tight coupling. Even though these are two different issues, they're both harmful and avoidable.
So what is global state?
Global state is essentially the same thing as insanity in this definition: a way to affect the execution of code hidden from sight, so that two apparently identical lines actually produce a different result depending on some external factor.
Source: Google chosen result
While working on a codebase, we may need access to utilities or 'helper objects' in multiple places throughout the application, or even objects that we expect to use "everywhere" like Database
or Logger
objects.
The global
keyword provides access to variables defined within the global scope - which is to say that they are accessible globally from anywhere in the application.
Most developers know now not to use global
within their code:
class MyObject
{
public function doSomething()
{
/** Access objects from the global space **/
global $logger;
global $database;
}
}
Instead, developers now believe they're playing it safe by using static calls to retrieve these objects intead:
/** Well, it's in a class, it MUST be safe... **/
class Resources
{
public static $database;
public static $logger;
}
/** Here's our object that uses the db and logger **/
class MyObject
{
public function doSomething()
{
Resources::$database->query(/** Get some data **/);
Resources::$logger->debug(/** Log all the things **/);
}
}
The above code is effectively the same as using global
. But why is this bad? Because anyone, or any object, can change this state at any time, and therefore you can't rely on it having the state you expect in your own objects.
Why is this important? When you're working in a team of more than one person, it's imperitive that every tool (object) you are using has a reliable state you can safely expect to work with every time you use that object.
When you open an object to the world (i.e. the rest of your codebase), anything can alter it at any time. If one object triggers an unexpected side-effect within the global state, you can no longer rely on the starting state of that object throughout all of it's uses in your codebase. Therefore, the object is inherently untestable.
This may not seem like a big issue in small codebases, but as soon as more than one developer gets involved, the problems become apparent. This isn't the only issue with global state.
Inherently untestable code is bad code. It doesn't actually matter whether or not you write unit tests with 100% coverage - if your code is testable, then it's likely much better than code that is not.
Using statics or calls to external objects at most hooks you into the global state so you can't re-use the code easily anywhere else, and at least hooks you into the framework you're using so you are actually unable to take your code anywhere else.
Using global state or static calls introduces a class dependency that isn't explicitly announced in your codebase - meaning that future developers will have to go off and find this newly introduced class to figure out where it came from and why you're randomly pulling it in half-way through your code.
What an object needs to be built and to function should be available from that object's constructor / method signatures - including any primitives, objects or interfaces that are typehinted for. Simply making static calls in random places within a file means more work for future developers who will have to read your code line-by-line just to see if there are any dependencies required by the object they are looking at to make it work.
This might be okay for you because you know exactly why you're doing adding static calls and what the objects you are pulling in are used for. However, the next developer with the responsibility of maintaining all of your code will hate you, and you won't be able to easily use this code anywhere else. You can also forget about open-sourcing it as a standalone library.
As a result of using a static call, you have now coupled the object you're coding to the object you're pulling in with the static methods available within it. This means that to use this code anywhere else, you're going to have to make sure that the static object you're calling exists, possibly in the exact same location, for the rest of time.
What's the problem with that? You can't use your object in any other way than the way you're currently designing. For example, if you couple your database abstraction to your custom logger by making static calls to that logger everywhere, you can't use that database class in another project without also copying over what might be an outdated and terribly written logger or heavily modifying it - which means it isn't re-usable.
Good developers dont write legacy code so don't make your code legacy right from the beginning by coupling your objects with static calls.
Dependency injection is a software design pattern that implements inversion of control and allows a program design to follow the dependency inversion principle. The term was coined by Martin Fowler. An injection is the passing of a dependency (a service) to a dependent object (a client).
Source: Wikipedia
Use Dependency Injection, which is the technical term for basically passing in objects as constructor or method parameters into other objects, meaning that no business objects (ignoring factories / builders) are directly responsible for instantiating other objects.
class Database
{
protected $logger;
public function __construct(Logger $logger)
{
$this->logger = logger;
}
}
Although I am an advocate of using the Decorator Pattern to add additional functionality to objects, such as logging, I will go into this in a future post. For now the point to take in is that passing objects into other objects is a good way of decoupling your code.
Global state ensures you are writing Legacy code. If you hook multiple objects together with global state, your architecture is brittle, difficult to maintain, a nightmare for future developers and can make it nearly impossible for object re-use, never mind testability.
Don't make static calls. Don't use Laravel's "façades" (which are completely different from the actual Façade Pattern that hook you into using the same framework for the rest of time. Don't listen to those who try and convince you that global state is occasionally okay.
Be responsible with your codebase and put in the initial effort of avoiding global state to make refactoring a significantly easier thing to do in the future.
You might be conflating state and methods. Scala - a popular no-global-state culture still has singleton/static objects.