SOLID: Single Responsibility Principle… simply explained

SOLID: Single Responsibility Principle… simply explained

All programs are composed of functions and data structures and the SOLID principles introduced by Robert C. Martin, help us group our code into modules and advise how to interconnect them.

The goal of the SOLID principles is to build software structures like Lego blocks that can be easy to change, understand and swap out.

There are 5 principles that make up the acronym are:

In this post, I will attempt to explain the SRP (Single Responsibility Principle).

In OO, classes are used to define the template for objects, while modules are used to organise and reuse code. In most (modern) languages, classes can also be used as modules where it is defined in a separate file and then imported and used in another file. Class and module will be used interchangeably from now on.

The Single Responsibility Principle is all about creating modules with one and only one role. You would know to use the Single Responsibility Principle (SRP) when you find yourself working on a module that has multiple roles or that is difficult to understand, test, and maintain.

Some signs that a module may be violating the SRP include:

  • The module is large and complex
  • The module has multiple responsibilities that are not closely related
  • Changes to the module have unexpected side effects in other parts of the system

Let's illustrate this with an example. Let's create a user repository class to be used throughout our application.

1 2//model 3interface User { 4 id: number 5 name: string 6 emailAddress: string 7}
1//contract 2interface IUserRepository { 3 getAllUsers(): User[] 4 createUser(user: User): void 5 deleteUser(id:number): void 6 updateUser(id: number, data: User): void 7 sendEmailTo(user: User, message: string, subject: string): void 8}
1//implementation 2class UserRepository implements IUserRepository { 3 constructor(userDataSource, emailService){ 4 this.userDataSource = userDataSource 5 this.emailService = emailService 6 } 7 8 getAllUsers(){ 9 return this.userDataSource.getAll() 10 } 11 12 createUser(user: User){ 13 this.userDataSource.create(user) 14 } 15 16 deleteUser(id: number){ 17 this.userDataSource.delete(id) 18 } 19 20 updateUser(id: number, data: User){ 21 this.userDataSource.update(id, data) 22 } 23 24 sendEmailTo(user: User, message:string, subject: string){ 25 this.emailService.send(user.emailAddress, message, subject) 26 } 27}

Although this may seem fine, it violates the SRP. The module has two roles: data source communication and sending emails. For this purpose, we should create another interface and implementation class which will handle communication.

1//model 2interface User { 3 id: number 4 name: string 5 emailAddress: string 6}
1//contract 2interface IUserRepository { 3 getAllUsers(): User[] 4 createUser(user: User): void 5 deleteUser(id:number): void 6 updateUser(id: number, data: User): void 7}
1//contract 2interface IMessageRepository { 3 sendEmailTo(user: User, message: string, subject: string): void 4}
1//implementation 2class UserRepository implements IUserRepository { 3 constructor(userDataSource){ 4 this.userDataSource = userDataSource 5 } 6 7 getAllUsers(){ 8 return this.userDataSource.getAll() 9 } 10 11 createUser(user: User){ 12 this.userDataSource.create(user) 13 } 14 15 deleteUser(id: number){ 16 this.userDataSource.delete(id) 17 } 18 19 updateUser(id: number, data: User){ 20 this.userDataSource.update(id, data) 21 } 22 23}
1//implementation 2class MessageRepository implements IMessageRepository { 3 4 constructor(emailService){ 5 this.emailService = emailService 6 } 7 8 sendEmailTo(user: User, message:string, subject: string){ 9 this.emailService.send(user.emailAddress, message, subject) 10 } 11}

In Conclusion,

Through the SRP your code now becomes easier to understand, maintainable and flexible. Complex code can be broken into smaller focused pieces. When a module needs to change it is less likely to affect other parts of code unrelated to it's role.