Why i hate singleton

Why singleton is anti pattern often? You can find various reasons for considering that singleton is evil, for example overusing this pattern, or using in inappropriate way (one of the examples is my recent legacy project, where Person have Documents and Document is a singleton while almost every person have a lot of documents).

But it is isn’t point, any pattern can be used in wrong way. Point, or wickedness of the day is that almost ever unable write unit test for code that use singleton. When created, most singletons try connect to database, or message broker, or send email message or write logs in real files etc and you can’t mock it without using some reflections library like AspectMock. And i don’t like use such tools because i agree with Michael C. Feathers that this is just hides from us fact that our code smells:

In many OO languages newer than C++, we can use reflection and special permissions to access private variables at runtime. Although that can be handy, it is a bit of a cheat, really. It is very helpful when we want to break dependencies, but I don’t like to keep tests that access private variables around in projects. That sort of subterfuge really prevents a team from noticing just how bad the code is getting. It might sound kind of sadistic, but the pain that we feel working in a legacy code base can be an incredible impetus to change.

So it is better to use DI or maybe your framework/application some component mechanism in case if it lightweight enough.

For example in Yii2 framework it can be components mechanism. Assume somewhere in method that we should test we have singleton that use database in getInstance method. Fastest solution is that we can replace this singleton with component and replace in tests this component with anonymous class extending component’s class and redefining constructor.

\Yii::$app->set(Dependency::COMPONENT_NAME, new class extends Dependency {
    public function __construct()
    {
    }
    /*
    * mocking methods
    */
});

After that we can test our method and creating instance of DependencyComponent shouldn’t fail our tests due to failed database connection.

Or in Symfony case we can replace singleton with autowired service and pass it with redefined constructor to tested class in phpunit tests.

class ExampleTest extends TestCase
{
    public function testSomething()
    {
        $exampleDependency = new class extends ExampleDependency {
            /**
             *  redefined constructor without database usage
             */
            public function __construct()
            {
            }
            /*
             * mocking methods
             */
        };
        $example = new Example($exampleDependency);
        $this->assertTrue($example->returnTrue());
    }
}

If you have some amount of untested legacy code it is examples of how you can begin test this code as fast as possible. And when legacy covered with tests you can finally refactor it.

Leave a Reply

Your email address will not be published. Required fields are marked *