What Is the Single Responsibility Principle (SRP)
Sri Priya
Posted On: July 18, 2024
199872 Views
17 Min Read
Single Responsibility Principle (SRP) is one of the SOLID design principles you must follow while designing, developing, and maintaining an automation framework. The SOLID design Principles help us create a maintainable, reusable, and flexible automation framework that helps update the code and deliver with less time.
In test automation projects with several WebElements and nested pages, updating and testing class files can be cumbersome if the framework isn’t well-developed with proper design principles. This is where the Single Responsibility Principle helps keep the code maintainable and easy to update, regardless of the programming language.
TABLE OF CONTENTS
What are SOLID Design Principles?
In the Object-Oriented Programming (OOP) world, many patterns, principles, and framework designs exist. In all those, five principles or patterns are called SOLID principles. In simple words, design principles help us to create more efficient, effective, maintainable, flexible, and understandable code.
If you’re preparing for a technical interview, understanding SOLID principles is crucial, and practicing oops interview questions can greatly enhance your readiness.
The main goal of the SOLID principles is to reduce dependencies to change a part of code without impacting the other part. Additionally, they’re intended to make designs easier to understand and maintain.
SOLID principles resemble the microservices. They are loosely coupled, and each service handles a dedicated function within a large-scale application. For example, the shopping cart, billing, and user profile will be individual microservices. Updating the code in the shopping cart will not impact the other two, which helps in easy maintenance.
Now, let us see what the five principles are, which are abbreviated for SOLID.
S | Single Responsibility Principle |
O | Open-Closed Principle |
L | Liskov Substitution Principle |
I | Interface Segregation Principle |
D | Dependency Inversion Principle |
The SOLID principles are the best practices used to reduce dependencies. The purpose of SOLID principles is to allow developers to build better solutions. Maintaining and updating the code effectively in case of any requirement changes will be easier. Additionally, SOLID principles help us to make designs easier to understand, maintain, and extend. Each of these principles describes a specific design pattern.
When we use all the SOLID principles in a combined way, we start writing the code effectively and efficiently. Using these design principles makes it easier to avoid issues and to build adaptively, & effectively.
Here are some of the benefits of using SOLID principles:
- The code written using SOLID principles will help to extend the code in an easy, sustainable, manageable, and efficient way.
- Using SOLID principles, classes can be extended easily without modifying the existing code base.
- SOLID principles help us reduce complex modules into smaller ones by reducing coupling and improving modularity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// Without SRP class Order { void calculateTotal() { // code to Calculate total price } void placeOrder(){ //code to place order } void sendConfirmationEmail() { // code to Send confirmation email } } // Using SRP class Order { void calculateTotal() { // code to Calculate total price } } class PlaceOrder{ void placeOrder(){ //code to place order } } class EmailService { void sendConfirmationEmail(Order order) { // code to Send confirmation email } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// without Using OCP class Vehicle { void drive() { } } // Using OCP abstract class Vehicle { abstract void drive(); } class Car extends Vehicle { void drive() { } } class Bike extends Vehicle { void drive() { } } |
What is the Single Responsibility Principle?
The Single Responsibility Principle states, “A class should have only responsibility or reason to change.” Each class should have a single responsibility or functionality and should not be affected by changes in other parts of the framework of the project.
It is one of the design patterns that states that every module or class should have only one responsibility or reason to change. A class or a module should have a single, well-defined purpose or responsibility. Applying the Single Responsibility Principle to the automation testing framework can help ensure the test code is maintainable and easy to understand.
For example, suppose you have a class that handles both login functionality and searching the product. In that case, you are violating the Single Responsibility Principle, as any change in login functionality will require modifying the same class.
The best approach would be to separate these two methods into different classes, each with its responsibility.
Benefits of the Single Responsibility Principle
In the Single Responsibility Principle, classes are highly cohesive and loosely coupled, which helps ease maintenance and makes them more flexible. When a class is highly coupled with more responsibilities, any slight change could easily lead to many changes that are more challenging to maintain. The Single Responsibility Principle helps to write decoupled code, where each class has its job and encapsulates responsibilities to other classes. If the specification of one class changes, we need to change and test only that particular class.
Below are some benefits of the Single Responsibility Principle:
- When there are multiple classes, each of them following this principle improves maintainability. It is easier to understand and make changes without difficulty in case of any requirement change.
- Classes with lesser responsibility are smaller, and it is easier to understand the code, which makes it easier to fix the defects.
- Classes with fewer responsibilities are often more reusable because they are loosely coupled to other classes and have fewer dependencies.
- Whenever any issue arises, it’s easier to identify the code causing problems when classes have a single responsibility. This helps in smoother debugging and troubleshooting.
Let us understand how to apply the Single Responsibility Principle in the test automation framework.
Applying the Single Responsibility Principle
Applying the Single Responsibility Principle can help us ensure that the test code is maintainable and easy to understand for modification.
We can apply SRP to test the automation framework by below methods:
- Separate test action and validation methods
- Test data handling
- Create modular test execution classes
- Interaction with Application Under Test (AUT)
- Utility functions
We need to create separate classes for each module and each test method. This will make the test code easier to read and understand. It helps to reuse the methods in other tests.
Encapsulating the data in separate data objects or files helps it be read and understood easily. It will help to update and maintain the test data easily. For example, a test data provider can be encapsulated for easy readability.
Break the test logic into smaller and modular methods, each having a single responsibility. This will make code easier to understand, maintain, and reuse the test logic in other tests.
Each page class should have a defined class for interacting with the application. This ensures that any UI changes are confined to a specific class.
Utility modules or classes for common functions reused across the framework should be class-specific. It helps to update easily. For example, Wait conditions and Garbage collection methods.
By following the Single Responsibility Principle, you can create a modular and maintainable test automation framework in which each class will have a single responsibility. It makes developing, maintaining, and updating automated tests in any programming language easier. This principle helps in front-end testing by fostering cohesion and less coupling.
This principle fosters cohesion, implying that a component should be responsible for a single part of the functionality provided by the software, and its services should be narrowly aligned with that responsibility.
Code Demonstration Without Using SRP
Let us understand with an example code. Below is the project without using the Single Responsibility Principle in test automation.
Project Structure:
Implementation:
Below is the code snippet for the test class, which has the following test flow:
|
Here, the LoginPage class has multiple responsibilities. It represents WebElements, methods for interaction, and handling navigation. This class has various responsibilities and hence multiple reasons to change. If the URL of the application changes, the login method needs to be updated. If login credentials change, we need to update and test the entire class, which is tedious.
As the LoginPage class has many responsibilities, we have created a more difficult class to understand and maintain because of tight coupling. To follow the Single Responsibility Principle, we should separate the responsibilities into separate classes and methods.
The class above violates the Single Responsibility Principle.
Code Walkthrough:
This LoginPage class has four responsibilities – enterEmailId, enterPassword, clickOnLogin, and goToURL. The code above will work perfectly but will lead to some challenges. We cannot make this code reusable for other classes or objects. The class has a lot of interconnected logic, so we would have difficulty fixing errors. And as the codebase grows, so does the logic, making it even harder to understand.
Imagine a new developer joining a team with this logic and a codebase of about two thousand lines of code all congested into one class; it will lead to a lot of confusion about where to update the changes, if any.
Code Demonstration Using SRP
Let us understand with an example code. Below is the project using the Single Responsibility Principle in test automation.
Project Structure:
We have separated the responsibilities of the page object class and the test class into separate classes.
Here, the TestClass is the class that has two test methods. LambdaTestHomePage is the main page class that gives navigation to other page component classes. The NavigationBar class consists of code related only to the navigation bar on the home page. MyAccountsPage class only contains the account login code. The SearchProduct class has code for search functionality, and the SpecialLinkBar class has code related to the special link bar module.
If we want to update the login credentials, we can just update the MyAccountsPage class and test only that flow. There is no need to test any other classes as we are not updating anything in other classes. The MyAccountsPage class has only one responsibility, which is account login.
Implementation:
Below is the code snippet for the test class, which has the following test scenario:
Test Scenario 1:
|
Test Scenario 2:
|
Test Scenario 1 uses the NavigationBar class only for interaction, whereas Test Scenario 2 uses both the NavigationBar and MyAccountsPage classes. Using the Single Responsibility Principle, we can also achieve loose coupling and reusability.
Code Walkthrough:
The lambdaTestHomePage class has navigation to all the other page component classes, such as SearchProduct, NavigationBar, SpecialLinkBar, and MyAccountPage classes. Each page component class has a single responsibility and reason to change. It is essential to consider the responsibilities of page object classes and ensure they are easily maintained.
Here, we will be running the tests on the LambdaTest platform. It is an AI-powered testing platform that offers a reliable and scalable infrastructure, supporting over 3,000 browsers and operating systems. This way, you can perform automation testing across different browsers and operating systems to ensure your websites and web apps work as expected.
Learning about the Single Responsibility Principle is key for creating strong and easy-to-maintain software. If you’re getting ready for an interview or want to know more, our detailed guide on Operating System Interview Questions can help you get better at it.
Before the tests run, set the environment variables LT_USERNAME & LT_ACCESS_KEY from the terminal. The account details are available on your LambdaTest Password & Security page.
For Windows:
set LT_USERNAME=LT_USERNAME
set LT_ACCESS_KEY=LT_ACCESS_KEY
For macOS:
export LT_USERNAME=LT_USERNAME
export LT_ACCESS_KEY=LT_ACCESS_KEY
For Linux:
export LT_USERNAME=LT_USERNAME
export LT_ACCESS_KEY=LT_ACCESS_KEY
To demonstrate, this is the project which I have created.
Step 1: Import the packages of JavaScriptExecutor and other methods.
Step 2: Create the RemoteWebDriver reference. RemoteWebDriver is a class that implements the WebDriver interface to execute the tests on the RemoteWebDriver server on a remote machine. It is implemented under the package below:
Step 3: The method implemented in @BeforeSuite annotation in TestNG sets the browser’s capabilities. A RemoteWebDriver instance is created with the desired browser capabilities, with the Selenium Grid URL set to the cloud-based Selenium Grid on LambdaTest [@hub.lambdatest.com/wd/hub].
Step 4: All the test navigation steps are implemented under the @Test annotation. There are two tests – Test 1 and Test 2. In @BeforeTest annotation, we are creating an object of the LambdaTestHomePage class, as it is the main page component class with navigation to all other page component classes.
Step 5: All the WebElements and methods related to the Navigation Bar module are in NavigationBar classes. The @FindBy annotation is used in page objects to specify the object location strategy for a WebElement. Using the PageFactory, these WebElements are initialized when a page object is created.
Step 6: The isDisplayed() method checks whether the WebElement is displayed or not using lambda expression for wait.
Step 7: Close the driver instance using the quit() method.
Test Execution:
The test was successfully run, and the LambdaTest Web Automation Dashboard indicates the status of the test execution.
Other SOLID Design Principles
We have already learned about the Single Responsibility Principle and its practical implementation through code. However, SRP is just the beginning. There are additional SOLID design principles that play a crucial role in creating robust software applications.
These principles collectively aim to enhance the efficiency, effectiveness, maintainability, flexibility, and understandability of our code. By adhering to these principles, developers can ensure that their software is easier to manage and scale, reducing the likelihood of encountering issues as the project evolves.
Let’s delve into these other design principles to understand how they contribute to creating high-quality, resilient software.
Open-Closed Principle
The Open-Closed Principle states, “Software entities like classes, modules, and functions should be open for extension but closed for modification.” Any entity should be able to add new features or methods without changing the existing code.
For example, you must calculate the total amount while placing the order. You will have a method to calculate the total amount paid based on the delivery location and charge. A class that calculates the total amount should not be modified every time you change the delivery location.
The best approach would be to use polymorphism or inheritance to create a subclass for the delivery location code and override each subclass’s total amount calculation method.
Liskov Substitution Principle
The Liskov Substitution Principle states, “Objects of a subclass should be able to replace objects of a superclass without breaking the code of functionality methods of the framework.” A class should be designed in such a way that it adheres to the contract or specification of the base class.
You have a base class, “Page Object,” representing a web page. It also has a load method that loads the page and returns the driver instance. You have two other subclasses: “Login Page” and “Search Product Page”. The LoginPage class returns the driver instance to the Search Product Page.
Using the Liskov Substitution Principle, you can ensure that classes are easily maintainable and can be extended and reused.
Interface Segregation Principle
The Interface Segregation Principle states, “Clients should not be forced to depend upon interfaces they do not use.” In the automation framework, we can apply the Interface Segregation Principle to break the larger interfaces into smaller ones by making them more specific as per the needs of individual clients.
We have an interface, IWebElement, which represents a web element. The IWebElement interface has methods click(), getAttribute(), findElement(), sendKeys(), clear(), and submit().
For example, a class submit represents a Submit button element on the web page. The Submit button class implements the IWebElement interface, but it should implement only the submit() method; there is no need to implement any other methods.
According to the Interface Segregation Principle, clients should not be forced to depend on methods they are not using. The Submit button clients only need to call submit() and do not need to depend on other methods.
Dependency Inversion Principle
The Dependency Inversion Principle states, “High-level modules should not depend upon low-level modules; they should depend on abstractions. Secondly, abstractions should not depend upon details; details should depend upon abstractions.” In the automation framework, the Dependency Inversion Principle can be applied using interfaces or abstract classes to define the dependencies between modules.
In the automation framework, we use the WebDriver interface variable driver instance whenever we want to interact with a web browser by creating a method. We can use this same method to interact with any browser, such as FirefoxDriver, ChromeDriver, and InternetExplorerDriver, which implements the WeDriver interface using abstract classes.
Final Thoughts
The Single Responsibility Principle is just a recipe for great and maintainable code. Even though the Single Responsibility Principle can be easily understood, we must apply the design principle correctly.
With the above example, we have now understood how to apply the Single Responsibility Principle. The Single Responsibility Principle helps us reduce complexity and makes it easier to maintain the code.
The Single Responsibility Principle is important and useful because it is easier to maintain, enhances flexibility, increases modularity, and reduces complexity. Following the Single Responsibility Principle, we can create easier code to understand, maintain, and update with minimal time.
I walked you through the Single Responsibility Principle in Selenium WebDriver with Java in this blog.
I hope you found this blog very useful and informative.
Please feel free to share this blog with your friends and colleagues.
Happy Testing…!!!
Frequently Asked Questions (FAQs)
What is a good example of the single responsibility principle?
A good example of the Single Responsibility Principle (SRP) can be demonstrated with a class that handles employee information in a software application. According to SRP, a class should have only one reason to change, meaning it should have only one responsibility.
Let’s consider an example of managing employee data and calculating their salary. Here’s a breakdown of a poor design that violates SRP and then a refactored design that adheres to SRP.
Consider that the Employee class has multiple responsibilities:
- Calculating the salary.
- Saving employee details to a database.
- Generating a report for the employee.
We can refactor the code by splitting these responsibilities into separate classes:
- Employee Class
- SalaryCalculator Class
- EmployeeRepository Class
- EmployeeReportGenerator Class
By adhering to the Single Responsibility Principle, we achieve the following benefits:
- Maintainability: Each class has a single responsibility, making the code easier to understand and maintain.
- Flexibility: Changes in one responsibility do not affect the other functionalities, making the code more flexible to changes.
- Testability: Each class can be tested independently, enhancing the testability of the code.
What is the single responsibility principle in API?
The Single Responsibility Principle (SRP) in the context of an API states that each API endpoint should have one specific responsibility or perform one specific action. This principle helps ensure that the API is easy to understand, maintain, and extend.
Got Questions? Drop them on LambdaTest Community. Visit now