SOLID Design Principles in Ruby

Tram Ho

All software applications change from time to time. Changes made to the software can cause unexpected problems. However, change is inevitable, because we cannot build software that does not need change. Software is constantly changing throughout their lifecycle. What we can do is design our software in a fungible way. Proper software design can take time and effort at first, but in the long run, it saves time and effort. So tightly bundled software is fragile and we cannot predict what will happen to change. Here are some of the effects of poorly designed software:

In the article Design Principles and Design Patterns , list the common symptoms of software that are difficult to change:

Software should be designed in a way that can control and predict changes.

SOLID design principles help to solve these problems by separating software programs. Robert C. Martin introduced these concepts in his article titled Design Principles and Design Patterns.

SOLID design principles include the following five principles:

We’ll cover each of these principles to understand how these principles can help build well-designed software in Ruby.

Single Responsibility Principle – SRP

Suppose, for an HR management software, we need the function of creating users, adding salaries for employees and creating pay stubs. While building it, we could add these functions to a single class, but this approach causes unwanted dependencies between these functions. It’s simple when we get started, but when things change and new requirements arise, we won’t be able to predict what functions the change will break.

Here is sample code where all functions are in a single class:

To create a pay slip and send it to the user, we can instantiate the class and call the payslip creation method:

Now, there is a new request. We want to generate payslips but don’t want to send emails. We need to keep existing functionality and add a new payslip generator for internal reporting without emailing, as it’s for internal proposals. During this stage, we want to ensure that the existing pay stubs sent to the employee remain active.

For this request we cannot reuse existing code. We need to add a variable to the create_payslip method saying that if yes then send another email not. This can be done, but since it modifies existing code, it may break the exit function.

To make sure we don’t mess things up, we need to separate these logic into separate classes:

Next, we can instantiate these two classes and call their methods:

This approach separates responsibilities and ensures a predictable change. If we just need to change the mailing function, we can do it without changing the report creation. It also helps to predict any changes in functionality.

Let’s say we need to change the format of the month field in the email to November instead of 11. In this case we will modify the PayslipMailer class and this will make sure that nothing will change or break the function. features of PayslipGenerator.

Every time you write a piece of code, ask a question after that. What is this class responsible for? If you have an “and” in your answer, divide the class into layers. Smaller classes are always better than large, generic ones.

Open / Closed Principle – OCP

Bertrand Meyer initiated the open / closed principle in his book, Object-Oriented Software Construction.

The principle states, “software entities (classes, modules, functions, etc.) must be open to extend but closed for modification”. This means that we can change our behavior without changing the entity.

In the example above, we have the function to send pay stubs to an employee, but it is very common to all employees. However, a new requirement arose: make pay stubs based on employee type. We need logic to create different payrolls for full-time employees and contractors. In this case we can modify the existing PayrollGenerator and add the following functionality:

However, this is a bad stereotype. In doing so, we are modifying the existing class. If we need to add more generation logic based on the employee contract, we need to modify the existing class, but doing so violates the open / close principle. By modifying the class, we run the risk of making unintended changes. When something changes or is added, this can cause unspecified problems in existing code. These if-elses can be in more places than within the same class. So when we add a new kind of employee, we might miss out on places where if-else this is present. Finding and modifying them all can be risky and can create problems.

We can refactor this code in such a way that we can add functionality by extending the functionality but avoiding entity changes. So let’s create a separate class for each of these classes and have the same constructor for each:

Make sure they have the same method name. Now, change the PayslipGenerator class to use these classes:

Here, we have a class mapping GENERATORS constant that is called based on employee type. We can use it to determine which class to call. No, when we have to add a new function, we can simply create a new class for it and add it to the constant GENERATORS. This helps to extend the class without breaking something or thinking about existing logic. We can easily add or remove any kind of payslip generator.

Liskov Substitution Principle – LSP

The Liskov substitution principle states, “if S is a subtype of T, then objects of type T can be replaced by objects of type S”.

To understand this principle, let us first explore the problem. Following the open / closed principle, we design our software in an scalable way. We have created a Payslip subclass generator that does a specific job. For the caller, the class they are calling is unknown. These classes need to behave the same so that the caller cannot tell the difference. By behavior, we want to say that the methods in the class must be consistent. The methods in these classes must have the following characteristics:

Let’s see an example of the payslip generator. We have two generators, one for full-time employees and one for contractors. Now, to ensure that these payslips have consistent behavior, we need to inherit them from a base class. Let us define a base class called Users.

The subclass we created in the open / close rule example has no base class. We modify it to have the base User class:

Next, we define a set of methods that are required for any subclass that extend the User class. We define these methods in the base class. In our case, we only need one method, called create.

Here, we have defined the create method, which has an increment statement. So any subclass that inherits the base class requires a generating method. If it does not appear, this will cause the error that the method was not executed. This way we can ensure that the subclass is consistent. With this, the caller can always be sure that the create method is present.

This principle makes it easy to replace any subclass without damaging everything and without making many changes.

Interface Segregation Principle – ISP

The interface separator principle is applicable to static languages ​​and since Ruby is a dynamic language, there is no concept of interfaces. Interfaces define the rules of abstraction between classes.

The principle states clearly,

Clients should not be forced to depend upon interfaces that they don’t use. – Robert C. Martin

This means that it is better to have multiple interfaces that are a generic interface that any class can use. If we define a generic interface, the class must depend on a definition it does not use.

Ruby doesn’t have interfaces, but let’s take a look at the concept of classes and subclasses to build something similar.

In the example used for the Liskov substitution rule, we see that the FullTimePayslipGenerator subclass is inherited from the generic User class. But User is a very generic class and can contain other methods. If we had to have another function, such as Leave, it would have to be a subclass of User. Leaving doesn’t need a create method, but it will depend on this method. So instead of having a generic class, we can have a specific class for this:

This generator is intended for payroll creation and the subclass does not need to depend on the generic User class.

Dependency Inversion Principle – DIP

Dependency reversal is a principle that is applied to separate software modules.

Design, using the principles described above, guides us to the principle of the inverse of dependence. Any class with a single responsibility needs things from other classes to function. To create the payroll, we need access to the database, and we need to write to the file after the report is generated. With the principle of single responsibility, we are trying to have only one job per class. However, things like reading from the database and writing to the file need to be done in the same class.

It’s important to remove these dependencies and separate the main business logic. This will keep the code flexible during predictable changes and changes. The dependency needs to be reversed, and the module caller must have control over the dependency. In our pay stub generator, the dependency is the source of the data for the report; This code should be organized in such a way that the caller can specify the source. The dependency control needs to be reversed and the caller can easily modify it.

In our example above, the ContractorPayslipGenerator module controls the dependency, since specifying where to read the data and how to store the output is controlled by the class. To revert this, let’s create a UserReader class that reads the user data:

Now, let’s say we want this to read data from Postgres. We create a subclass of UserReader for this purpose:

Similarly, we could have a reader from FileUserReader, InMemoryUserReader or any other kind of reader we want. Now we need to modify the FullTimePayslipGenerator class so that it uses PostgresUserReader as the dependency.

The caller can now pass PostgresUserReader as a dependency:

Callers have control over dependencies and can easily change sources as needed.

The dependency reversal doesn’t just apply to classes. We also need to reverse the configurations. For example, while connecting Postgres server, we need specific configuration, such as DBURL, username and password. Instead of encrypting these profiles in the class, we need to pass them down from the caller.

Provided configuration by caller:

Callers now have full control over dependencies and make change management easier and less painful.

Conclude

SOLID design makes code separation and changes less difficult. It is important to design programs in such a way that they are decoupled, reusable, and responsive to change. All five SOLID principles are complementary and should coexist. A well-designed code base is flexible, volatile, and fun to work with. Any new developer can join in and easily understand the code.

It is really important to understand what kind of SOLID problems it solves and why we are doing this. Understanding the problem helps you to better grasp software design and design principles.

Share the news now

Source : Viblo