Why global state is the devil, and how to avoid using it
You may have heard that globals are bad. This is often thrown around as programming gospel by people who don't completely understand what they're saying. These people aren't wrong, they just don't often program what they preach. I've lost track of the number of times I've had the "globals are bad" conversation with someone (and been in agreement) only to find their code is littered with statics and singletons. These people are confusing globals (as in the $GLOBALS array) and global state.
Before I go any further I must point out that you will always have some global state. PHP provides a lot of it ($_SERVER, $_GET, $_POST etc) and that isn't that bad. So why is global state a bad thing? Imagine the following (somewhat contrived) situation:
<?php
class Foo {
function __construct(){
$GLOBALS['mystring'] = 'Foo';
}
function greet(){
echo $GLOBALS['mystring'];
}
}
class Bar {
function __construct(){
$GLOBALS['mystring'] = 'Bar';
}
function greet(){
echo $GLOBALS['mystring'];
}
}
$foo = new Foo();
$foo->greet(); //Foo
$bar = new Bar();
$bar->greet(); //Bar
$foo->greet(); //Bar
Oh noes! We've changed the behaviour of our Foo
object without doing anything to it directly. If you were using classes you didn't write, that would most likely confuse you rather a lot.
Read-only global state
Ok, so what if we only use "read-only" global state? That's a little better, but it can still cause problems when it comes to testing.
<?php
class SaveFormToFile {
function save(){
file_put_contents('/save/file.csv',
implode(',', $_POST). "\\n"
);
}
}
$s = new SaveFormToFile();
$s->save();
This all looks fine, but how would you test it? You'd visit the webpage with a form in it, put in some data and submit it of course. If you think that's the best way to test your code then you've a lot to learn, sonny jim. What if you wanted to write a unit test for the SaveFormToFile
class? You'd have to spoof the $_POST array...
<?php
//Spoof the post data
$_POST['one'] = 1;
$_POST['two'] = 2;
$_POST['three'] = 3;
$s = new SaveFormToFile();
$s->save();
//Check it worked
$content = explode(',', file_get_contents('/save/file.csv'));
assert($_POST == array_values($content););
Great! That works! But what about when you want to test another class that uses data from $_POST in the same request? You've just polluted the post data, that could break things in the next class you test.
What's the solution then? How can you avoid using global state? The answer lies in the solution to the question "When is global state not global?".
When global state isn't global
Ok, I've been a little mean there; global state is always global, but it's not as global when the global scope is your current scope. Consider this reworking of the SaveFormToFile
class and its usage:
<?php
class SaveFormToFile {
function save($post_array){
file_put_contents('/save/file.csv',
implode(',', $post_array). "\\n"
);
}
}
$s = new SaveFormToFile();
$s->save($_POST);
That might not seem all that different, but it really is; the class no-longer relies on the global $_POST array. What's that I hear you cry? "But you're still using $_POST! You've just put in a different place!"? That, smarty-pants, is exactly the point. I'm using $_POST in the global scope, where it may as well be a local variable. Now when I write unit tests for it I can just pass an array straight into the save
method without worrying about poluting anything else.
Statics and creating your own dependencies
I mentioned statics and singletons earlier on. I put it to you that they are just as bad. Consider:
<?php
class Config {
static function databaseName(){
return 'production_db';
}
}
class BlogPost {
function save($post_data){
$db = new Db();
$db->selectDb(Config::databaseName());
/*...and then continue to write things into the database...*/
}
}
$blog_post = new BlogPost();
$blog_post->save($_POST);
The config class provides a static method to get the database name; how very useful! Until you want to test it, that is. We really don't want to be writing to the production database when we're running unit tests; who knows what kind of havoc that might wreak. We've got almost exactly the same problem we had before: the class we want to test relies too heavily on things it doesn't control, and we can't easily write tests for it as a result.
Before I go on to provide a solution to this problem, I want to point out another, very similar, problem. The save
method of the BlogPost
creates its very own Db
object. That seems fine, until you consider the posibility of not wanting to write to the database at all in some of your unit tests. That method uses the class known as Db
, whether you like it or not; you cannot force it into using a mock version of the Db
class.
Wait! Wait just a minute! What about singlestons?! ... Calm down. I'll get to those in a minute.
Dependency injection
So how do we crack this seemingly tough biscuit? Remember how we fixed the problem with the SaveFormToFile
class? The answer is similar, and is called "dependency injection".
<?php
class Config {
function databaseName(){
return 'production_db';
}
}
class BlogPost {
protected $db;
function __construct($db){
$this->db = $db;
}
function save($post_data){
//Use $this->db to write things to the database
}
}
$config = new Config();
$db = new Db();
$db->selectDb($config->databaseName());
$blog_post = new BlogPost($db);
$blog_post->save($_POST);
Much better! None of the classes rely on anything we can't easily change at runtime; making them nice and easy to test. We have injected the dependencies. If we want to use a mock Db object, or give a real Db object a different database name during testing, we can.
Isn't this quite a bit more code? Yes, it is more code, but it's code that's a lot easier to test; and code that's easier to test requires less work in the long run. It might take (slightly) more time to write the code initially, but if it's easy to maintain and test you will save a lot of time in the future. So what about singletons? What's the problem with them?
Singletons - just as bad
A singleton is just a regular class, but it behaves in such a way that forces you to only ever have one object of that class. A basic singleton class would look something like this:
<?php
class Singleton {
private static $instance;
public $test_string;
private function __construct(){
//A private constructor, so only this class can call it
}
public static function getInstance(){
if (!self::$instance){
self::$instance = new Singleton();
}
return self::$instance;
}
}
$foo = Singleton::getInstance();
$bar = Singleton::getInstance();
$foo->test_string = 'Foo';
echo $foo->test_string; //Foo
echo $bar->test_string; //Foo
What you have done when you create a singleton is create more global scope. The problems we have already discussed apply here too; despite the $test_string
member not being static, it has the same effect because $foo
and $bar
are one and the same.
So there you have it: singletons, statics, global state; don't use them unless your current scope is the global scope and you will be a much happier programmer as a result.
First posted: Fri, 10 Sep 2010 12:04:59 +0000