Breaking the Single Responsibility Principle

« »

Whenever I give lessons on the Single Responsibility Principle, I always teach that object creation is a job, and that it should be separate from classes that are using the objects. So instead of this:

<?php

class TestClass
{
    public function myFunction()
    {
        $a = new Object();
    }
}

We use this:

<?php

class TestClass
{

    protected $objectFactory;

    public function __construct(ObjectFactory $objectFactory)
    {
        $this->objectFactory = $objectFactory;
    }

    public function myFunction()
    {
        $a = $this->objectFactory->createObject();
    }
}

The goal here is to abstract away the object creation process, so that we can create the object as needed (since our factory might create many different types of object), while still following the various principles, including the Single Responsibility Principle.

But this always leads to an interesting question, which is, should I be writing code that looks like this?

<?php

class TestClass
{

    protected $datetimeFactory;

    public function __construct(DateTimeFactory $datetimeFactory)
    {
        $this->datetimeFactory = $datetimeFactory;
    }

    public function myFunction()
    {
        $a = $this->datetimeFactory->createDateTime('now');
    }
}

After all, this seems a little onerous. Why not just instantiate DateTime? Let’s explore.

The concept of the “local object”

In my programming, I use the concept of a “local object”. Sometimes it just doesn’t make sense to use a factory, or to inject an object. Sometimes you need a specific object at a specific time. In those cases it’s okay to create a local object. A local object is an object that exists in one of two ways:

  1. A fully internal object used to maintain state or facilitate execution of the containing object; or
  2. An object that has no other object dependencies, and whose creation will not overtly change the application state.

Fully internal objects

Nobody has a problem creating an array inside an object, but as soon as we move to a collection object, most people see that as a violation of the SRP if we instantiate it when we need it. For me, creating an empty collection object is no more harmful than creating an array, especially if the object is going to be used internally for iteration or sorting.

DateTime is another perfect example. If I’m doing calendar math, instantiating a new DateTime object to compare against another DateTime object is a perfectly valid use case, even if it’s a technical violation of the Single Responsibility Principle.

Objects without dependencies

In cases where an object has no dependencies, creating it locally is less of a concern. For example, a DateTime object has no object dependencies (unless you specify timezone, but that’s another matter entirely).

Where you run into problems is when you inject a bunch of objects in order to create another object. This violates the Law of Demeter, and is generally best avoided. But when you have an object that’s responsible for very little and has very few dependencies, creating it when needed is acceptable.

Guidelines, not rules.

The SOLID principles are closer to guidelines than rules; they’re impossible to follow 100% of the time, and yet, adherence to 90% of the principles will ultimately serve to create a far superior application. In that way, it’s okay to break the Single Responsibility Principle in this way, and I do without feeling guilty.

Brandon Savage is the author of Mastering Object Oriented PHP and Practical Design Patterns in PHP

Posted on 5/13/2015 at 8:00 am
Categories: Uncategorized, PHP

Avi Block wrote at 5/13/2015 8:52 am:

You’re absolutely right (and in some languages all datatypes are objects…so you wouldn’t inject an Int or a Boolean)…

However, this is not SRP, this is the Dependency Inversion Principal.

Matt (@mattwells64) wrote at 5/13/2015 10:33 am:

The advantage of using a factory it would be much easier to change from say a DateTime to a Carbon object. Same with collections, PHP provides a couple of SPL collections or event your own ArrayAccess based class.

Also if you instance inside of your method you will find if much harder to mock. I came across this problem recently when I tried doing unit testing on a method that instanced an SPL FilesystemIterator.

Brandon Savage (@brandonsavage) wrote at 5/13/2015 11:45 am:

Avi, we’re BOTH right here.

Since we aren’t inverting the dependency, we do break that principle. But I’m focused on object creation as a responsibility – and creating an object in another object that has other jobs violates that rule as well.

So yes, you and I are both right. :)

Avi Block wrote at 5/13/2015 11:50 am:

Just because an object is created another object to do something, doesn’t mean that it’s violated SRP.

The usual definition of SRP is that there is more than one reason to change. This is more about the public API of your object than how it gets dependencies.

« »

Copyright © 2023 by Brandon Savage. All rights reserved.