The Single Responsibility Principle
Key Takeaways
- Robust software systems should be designed with objects that have tightly defined, specific responsibilities, avoiding the creation of classes that perform too many tasks.
- The Single Responsibility Principle states that a class should never have more than one reason to change, meaning it should only be concerned with a single functionality.
- Violations of the Single Responsibility Principle often occur when a class is assigned multiple unrelated tasks, as seen in the Active Record Pattern or the Singleton.
- To adhere to the Single Responsibility Principle, responsibilities can be distributed across multiple layers, for example, by using a data mapper to separate data access logic from domain logic.
- The Single Responsibility Principle, while sometimes challenging to implement, can lead to cleaner, more maintainable code and is a valuable guideline for software design.
There should never be more than one reason for a class to change.What the principle attempts to promote is that classes must always be designed to expose only one area of concern and the set of operations they define and implement must be aimed at fulfilling that concern in particular and nothing else. In other words, a class should only change in response to the execution of those semantically-related operations. If it ever needs to change in response to another, totally unrelated operation, then it’s clear the class has more than one responsibility. Let’s move along and code a few digestible examples so that we can see how to take advantage of the principle’s benefits.
A Typical Violation of the Single Responsibility Principle
For obvious reasons, there’s plenty of situations where a seemingly-cohesive set of operations assigned to a class actually scope different unrelated responsibilities, hence violating the principle. One that I find particularly instructive is the Active Record Pattern because of the momentum it has gained in the last few years. Let’s pretend we’re a blind worshiper of the pattern and of the so-called database model and want to appeal to its niceties for creating a basic entity class which should model the data and behavior of generic users. Such a class, along with the contract that it implements, could be defined as follows:<?php
namespace Model;
interface UserInterface
{
public function setId($id);
public function getId();
public function setName($name);
public function getName();
public function setEmail($email);
public function getEmail();
public function getGravatar();
public function findById($id);
public function insert();
public function update();
public function delete();
}
<?php
namespace Model;
use LibraryDatabaseDatabaseAdapterInterface;
class User implements UserInterface
{
private $id;
private $name;
private $email;
private $db;
private $table = "users";
public function __construct(DatabaseAdapterInterface $db) {
$this->db = $db;
}
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The user ID has been set already.");
}
if (!is_int($id) || $id < 1) {
throw new InvalidArgumentException(
"The user ID is invalid.");
}
$this->id = $id;
return $this;
}
public function getId() {
return $this->id;
}
public function setName($name) {
if (strlen($name) < 2 || strlen($name) > 30) {
throw new InvalidArgumentException(
"The user name is invalid.");
}
$this->name = $name;
return $this;
}
public function getName() {
if ($this->name === null) {
throw new UnexpectedValueException(
"The user name has not been set.");
}
return $this->name;
}
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
"The user email is invalid.");
}
$this->email = $email;
return $this;
}
public function getEmail() {
if ($this->email === null) {
throw new UnexpectedValueException(
"The user email has not been set.");
}
return $this->email;
}
public function getGravatar($size = 70, $default = "monsterid") {
return "http://www.gravatar.com/avatar/" .
md5(strtolower($this->getEmail())) .
"?s=" . (integer) $size .
"&d=" . urlencode($default) .
"&r=G";
}
public function findById($id) {
$this->db->select($this->table,
["id" => $id]);
if (!$row = $this->db->fetch()) {
return null;
}
$user = new User($this->db);
$user->setId($row["id"])
->setName($row["name"])
->setEmail($row["email"]);
return $user;
}
public function insert() {
$this->db->insert($this->table, [
"name" => $this->getName(),
"email" => $this->getEmail()
]);
}
public function update() {
$this->db->update($this->table, [
"name" => $this->getName(),
"email" => $this->getEmail()],
"id = {$this->id}");
}
public function delete() {
$this->db->delete($this->table,
"id = {$this->id}");
}
}
As one might expect from a typical implementation of Active Record, the User
class is pretty much a messy structure which mingles chunks of business logic such as setting/getting usernames and email addresses, and even generating some nifty Gravatars
on the fly, with data access. Is this a violation of the Single Responsibility Principle? Well, unquestionably it is, as the class exposes to the outside world two different responsibilities which by no means have a true semantic relationship with each other.
Even without going through the class’ implementation and just scanning its interface, it’s clear to see that the CRUD methods should be placed in the data access layer completely insulated from where the mutators/accessors live and breath. In this case, the result of executing the findById()
method for instance will change the state of the class while any call to the setters impact the CRUD operations as well. This implies there are two overlapped responsibilities coexisting here which makes the class change in response to different requirements.
Of course, if you’re anything like me you’ll be wondering how to turn User
into an Single Responsibility Principle-compliant structure without too much hassle during the refactoring process. The first modification that should be introduced is to keep all the domain logic within the class’ boundaries while moving away the one that deals with data access… yes, to the data access layer. There are a few nifty ways to accomplish this, but considering that the responsibilities should be sprinkled across multiple layers, the use of a data mapper is an efficient approach that permits to do this in a fairly painless fashion.
Putting Data Access Logic in a Data Mapper
The best way to keep the class’ responsibilities (the domain-related ones, of course) isolated from the ones that deal with data access is via a basic data mapper. The one below does a decent job when it comes to dividing up the responsibilities in question:<?php
namespace Mapper;
use ModelUserInterface;
interface UserMapperInterface
{
public function findById($id);
public function insert(UserInterface $user);
public function update(UserInterface $user);
public function delete($id);
}
<?php
namespace Mapper;
use LibraryDatabaseDatabaseAdapterInterface,
ModelUserInterface,
ModelUser;
class UserMapper implements UserMapperInterface
{
private $db;
private $table = "users";
public function __construct(DatabaseAdapterInterface $db) {
$this->db = $db;
}
public function findById($id) {
$this->db->select($this->table, ["id" => $id]);
if (!$row = $this->db->fetch()) {
return null;
}
return $this->loadUser($row);
}
public function insert(UserInterface $user) {
return $this->db->insert($this->table, [
"name" => $user->getName(),
"email" => $user->getEmail()
]);
}
public function update(UserInterface $user) {
return $this->db->update($this->table, [
"name" => $user->getName(),
"email" => $user->getEmail()
],
"id = {$user->getId()}");
}
public function delete($id) {
if ($id instanceof UserInterface) {
$id = $id->getId();
}
return $this->db->delete($this->table, "id = $id");
}
private function loadUser(array $row) {
$user = new User($row["name"], $row["email"]);
$user->setId($row["id"]);
return $user;
}
}
Looking at the mapper’s contract, it’s easy to see how nice the CRUD operations that polluted the User
class’ ecosystem before have been placed inside a cohesive set which is now part of the raw infrastructure instead of the domain layer. This single modification should let us refactor the domain class and turn it into a cleaner, more distilled structure that conforms to the principle:
<?php
namespace Model;
interface UserInterface
{
public function setId($id);
public function getId();
public function setName($name);
public function getName();
public function setEmail($email);
public function getEmail();
public function getGravatar();
}
<?php
namespace Model;
class User implements UserInterface
{
private $id;
private $name;
private $email;
public function __construct($name, $email) {
$this->setName($name);
$this->setEmail($email);
}
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The user ID has been set already.");
}
if (!is_int($id) || $id < 1) {
throw new InvalidArgumentException(
"The user ID is invalid.");
}
$this->id = $id;
return $this;
}
public function getId() {
return $this->id;
}
public function setName($name) {
if (strlen($name) < 2 || strlen($name) > 30) {
throw new InvalidArgumentException(
"The user name is invalid.");
}
$this->name = $name;
return $this;
}
public function getName() {
return $this->name;
}
public function setEmail($email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException(
"The user email is invalid.");
}
$this->email = $email;
return $this;
}
public function getEmail() {
return $this->email;
}
public function getGravatar($size = 70, $default = "monsterid") {
return "http://www.gravatar.com/avatar/" .
md5(strtolower($this->email)) .
"?s=" . (integer) $size .
"&d=" . urlencode($default) .
"&r=G";
}
}
It could be said that User
now has a better designed implementation as the batch of operations it performs not only are pure domain logic, but they’re semantically bound to each other. In Single Responsibility Principle parlance, the class has only one well-defined responsibility which is exclusively and intimately related to handling user data. No more, no less.
Of course, the example would look half-backed if I don’t show you how to get the mapper doing all the data access stuff while keeping the User
class persistence agnostic:
<?php
$db = new PdoAdapter("mysql:dbname=test", "myusername",
"mypassword");
$userMapper = new UserMapper($db);
// Display user data
$user = $userMapper->findById(1);
echo $user->getName() . ' ' . $user->getEmail() .
'<img src="' . $user->getGravatar() . '">';
// Insert a new user
$user = new User("John Doe", "john@example.com");
$userMapper->insert($user);
// Update a user
$user = $userMapper->findById(2);
$user->setName("Jack");
$userMapper->update($user);
// Delete a user
$userMapper->delete(3);
While the example is unquestionably trivial, it does show pretty clearly how the fact of having delegated the responsibility for executing the CRUD operations to the data mapper permits us to deal with user objects whose sole area of concern is to handle exclusively domain logic. At this point, the objects’ tasks are principle-compliant, nicely distilled, and narrowed to setting/retrieving user data and rendering the associated gravatars instead of being focused additionally on persisting that data in the storage.
Closing Remarks
Perhaps just a biased opinion based on my own experience as developer (so take it at face value), I’d dare to say the Single Responsibility Principle’s worst curse and certainly the reason why it’s so blatantly ignored in practice is the pragmatism of reality. Obviously it’s a lot easier “to get the job done” and struggle with tight deadlines by blindly assigning a bunch of roles to a class, without thinking if they’re semantically related to each other. Even in enterprise environments, where the use of contracts for outlining explicitly the behavior of application components isn’t just a luxury but a must, it’s pretty difficult to figure out how to group together cohesively a set of operations. Even though, it’s doesn’t hurt to take some time and design cleaner classes that don’t mix up unnecessarily heaps of unrelated responsibilities. In that sense, the principle is just a guideline that will assist you in the process, but certainly a very valuable one. Image via FotoliaFrequently Asked Questions about the Single Responsibility Principle
What is the Single Responsibility Principle (SRP) in PHP Laravel?
The Single Responsibility Principle (SRP) in PHP Laravel is a design principle that suggests that a class should have only one reason to change. This means that a class should only have one job or responsibility. This principle is part of the SOLID principles, a set of five principles used in object-oriented design to make software designs more understandable, flexible, and maintainable.
How can I apply the Single Responsibility Principle in PHP Laravel?
Applying the SRP in PHP Laravel involves breaking down your classes into smaller, more manageable parts, each with a single responsibility. For instance, if you have a class that handles user registration and login, you could split this into two separate classes – one for registration and one for login. This way, each class has a single responsibility, making your code easier to maintain and understand.
What are the benefits of using the Single Responsibility Principle?
The SRP offers several benefits. Firstly, it makes your code more readable and maintainable, as each class or module has a single responsibility. Secondly, it reduces the risk of bugs, as changes to one class or module won’t affect others. Lastly, it makes it easier to test your code, as each class or module can be tested independently.
Can you provide a real-world example of the Single Responsibility Principle in PHP?
Sure, let’s consider a simple example. Suppose you have a class ‘Order’ that handles both the creation of an order and the printing of the order details. According to the SRP, these two responsibilities should be separated into two classes. So, you could create a ‘CreateOrder’ class for creating an order and a ‘PrintOrder’ class for printing the order details.
How does the Single Responsibility Principle relate to other SOLID principles?
The SRP is the first principle in the SOLID acronym. It lays the foundation for the other principles, which are: Open-Closed Principle, Liskov Substitution Principle, Interface Segregation Principle, and Dependency Inversion Principle. All these principles aim to make your code more understandable, flexible, and maintainable.
What happens if I don’t follow the Single Responsibility Principle?
Not following the SRP can lead to code that is difficult to maintain and understand. Changes to one part of your code could have unintended consequences on other parts, leading to bugs. It can also make your code harder to test, as each class or module may have multiple responsibilities.
Is the Single Responsibility Principle applicable only to PHP?
No, the SRP is a general principle of object-oriented design and can be applied to any object-oriented programming language, not just PHP. It’s a principle that helps ensure your code is well-structured and easy to maintain, regardless of the specific language you’re using.
How does the Single Responsibility Principle improve code testing?
By ensuring that each class or module has a single responsibility, the SRP makes your code easier to test. You can test each class or module independently, which simplifies the testing process and makes it easier to identify and fix bugs.
Can the Single Responsibility Principle be applied to functions or methods?
Yes, the SRP can also be applied to functions or methods. Just like with classes, each function or method should have a single responsibility. This makes your functions or methods easier to understand, test, and maintain.
Are there any drawbacks to using the Single Responsibility Principle?
While the SRP has many benefits, it’s not without its drawbacks. One potential drawback is that it can lead to a larger number of smaller classes or modules, which can be harder to manage. However, with good organization and naming conventions, this can be mitigated.
Alejandro Gervasio is a senior System Analyst from Argentina who has been involved in software development since the mid-80's. He has more than 12 years of experience in PHP development, 10 years in Java Programming, Object-Oriented Design, and most of the client-side technologies available out there.