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:
- 12Nó gây ra sự bất động.
- 12Thay đổi code rất tốn kém.
- 12Sẽ dễ dàng thêm nhiều độ phức tạp hơn là làm cho phần mềm đơn giản hơn.
- 12Không thể quản lý được code.
- 12Phải mất rất nhiều thời gian để một developer có thể hiểu được cách hoạt động của các chức năng của nó.
- 12Thay đổi một phần của phần mềm thường phá vỡ phần kia và chúng ta không thể dự đoán những vấn đề mà một thay đổi có thể mang lại.
In the article Design Principles and Design Patterns , list the common symptoms of software that are difficult to change:
- 12Rigidity: Rất khó để thay đổi code mà không gây ra sự cố, vì việc thực hiện các thay đổi trong một phần sẽ thúc đẩy nhu cầu thực hiện thay đổi trong các phần khác của code.
- 12Fragility: Thay đổi code thường phá vỡ hoạt động của phần mềm. Nó thậm chí có thể phá vỡ các bộ phận không liên quan trực tiếp đến sự thay đổi.
- 12Immobility: Mặc dù một số phần của ứng dụng phần mềm có thể có hành vi tương tự, chúng tôi không thể sử dụng lại mã và phải sao chép chúng.
- 12Viscosity: Khi phần mềm khó thay đổi, chúng tôi tiếp tục tăng thêm độ phức tạp cho phần mềm thay vì làm cho nó tốt hơn.
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:
- 12Single Responsibility Principle
- 12Open/Closed Principle
- 12Liskov Substitution Principle
- 12Interface Segregation Principle
- 12Dependency Inversion Principle
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.
12 A class should have one, and only one reason to change - Robert C Martin
Here is sample code where all functions are in a single class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token variable">@month</span> <span class="token operator">=</span> month <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate_payslip</span></span> <span class="token comment"># Code to read from database,</span> <span class="token comment"># generate payslip</span> <span class="token comment"># and write it to a file</span> <span class="token keyword">self</span> <span class="token punctuation">.</span> send_email <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">send_email</span></span> <span class="token comment"># code to send email</span> employee <span class="token punctuation">.</span> email month <span class="token keyword">end</span> <span class="token keyword">end</span> |
To create a pay slip and send it to the user, we can instantiate the class and call the payslip creation method:
1 2 3 4 | month <span class="token operator">=</span> <span class="token number">11</span> user <span class="token operator">=</span> <span class="token constant">User</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> user <span class="token punctuation">.</span> generate_payslip |
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:
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 | <span class="token keyword">class</span> <span class="token class-name">PayslipGenerator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token variable">@month</span> <span class="token operator">=</span> month <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate_payslip</span></span> <span class="token comment"># Code to read from database,</span> <span class="token comment"># generate payslip</span> <span class="token comment"># and write it to a file</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">PayslipMailer</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">send_mail</span></span> <span class="token comment"># code to send email</span> employee <span class="token punctuation">.</span> email month <span class="token keyword">end</span> <span class="token keyword">end</span> |
Next, we can instantiate these two classes and call their methods:
1 2 3 4 5 6 7 8 | month <span class="token operator">=</span> <span class="token number">11</span> <span class="token comment"># General Payslip</span> generator <span class="token operator">=</span> <span class="token constant">PayslipGenerator</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> generator <span class="token punctuation">.</span> generate_payslip <span class="token comment"># Send Email</span> mailer <span class="token operator">=</span> <span class="token constant">PayslipMailer</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> mailer <span class="token punctuation">.</span> send_mail |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">class</span> <span class="token class-name">PayslipGenerator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token variable">@month</span> <span class="token operator">=</span> month <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate_payslip</span></span> <span class="token comment"># Code to read from database,</span> <span class="token comment"># generate payslip</span> <span class="token keyword">if</span> employee <span class="token punctuation">.</span> contractor <span class="token operator">?</span> <span class="token comment"># generate payslip for contractor</span> <span class="token keyword">else</span> <span class="token comment"># generate a normal payslip</span> <span class="token keyword">end</span> <span class="token comment"># and write it to a file</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
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:
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 | <span class="token keyword">class</span> <span class="token class-name">ContractorPayslipGenerator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token variable">@month</span> <span class="token operator">=</span> month <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to read from the database,</span> <span class="token comment"># generate payslip</span> <span class="token comment"># and write it to a file</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">FullTimePayslipGenerator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token variable">@month</span> <span class="token operator">=</span> month <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to read from the database,</span> <span class="token comment"># generate payslip</span> <span class="token comment"># and write it to a file</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Make sure they have the same method name. Now, change the PayslipGenerator class to use these classes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <span class="token constant">GENERATORS</span> <span class="token operator">=</span> <span class="token punctuation">{</span> <span class="token string">'full_time'</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token constant">FullTimePayslipGenerator</span> <span class="token punctuation">,</span> <span class="token string">'contractor'</span> <span class="token operator">=</span> <span class="token operator">></span> <span class="token constant">ContractorPayslipGenerator</span> <span class="token punctuation">}</span> <span class="token keyword">class</span> <span class="token class-name">PayslipGenerator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token variable">@employee</span> <span class="token operator">=</span> employee <span class="token variable">@month</span> <span class="token operator">=</span> month <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate_payslip</span></span> <span class="token comment"># Code to read from database,</span> <span class="token comment"># generate payslip</span> <span class="token constant">GENERATORS</span> <span class="token punctuation">[</span> employee <span class="token punctuation">.</span> type <span class="token punctuation">]</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> employee <span class="token punctuation">,</span> month <span class="token punctuation">)</span> <span class="token punctuation">.</span> generate <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token comment"># and write it to a file</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
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:
- 12Có cùng tên
- 12Lấy cùng một số đối số với cùng một kiểu dữ liệu
- 12Trả về cùng một kiểu dữ liệu
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.
1 2 3 4 5 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
The subclass we created in the open / close rule example has no base class. We modify it to have the base User class:
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">ContractorPayslipGenerator</span> <span class="token operator"><</span> <span class="token constant">User</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to generate payslip</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">FullTimePayslipGenerator</span> <span class="token operator"><</span> <span class="token constant">User</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to generate payslip</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
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.
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">User</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token keyword">raise</span> <span class="token string">"NotImplemented"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <span class="token keyword">class</span> <span class="token class-name">Generator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token keyword">raise</span> <span class="token string">"NotImplemented"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">ContractorPayslipGenerator</span> <span class="token operator"><</span> <span class="token constant">Generator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to generate payslip</span> <span class="token keyword">end</span> <span class="token keyword">end</span> <span class="token keyword">class</span> <span class="token class-name">FullTimePayslipGenerator</span> <span class="token operator"><</span> <span class="token constant">Generator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to generate payslip</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
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.
12 A high-level module should not depend on a low-level module; both should depend on abstraction.
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:
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">UserReader</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">get</span></span> <span class="token keyword">raise</span> <span class="token string">"NotImplemented"</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Now, let’s say we want this to read data from Postgres. We create a subclass of UserReader for this purpose:
1 2 3 4 5 6 | <span class="token keyword">class</span> <span class="token class-name">PostgresUserReader</span> <span class="token operator"><</span> <span class="token constant">UserReader</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">get</span></span> <span class="token comment"># Code to read data from Postgres</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
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.
1 2 3 4 5 6 7 8 9 10 11 | <span class="token keyword">class</span> <span class="token class-name">FullTimePayslipGenerator</span> <span class="token operator"><</span> <span class="token constant">Generator</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> datasource <span class="token punctuation">)</span> <span class="token variable">@datasource</span> <span class="token operator">=</span> datasource <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">generate</span></span> <span class="token comment"># Code to generate payslip</span> data <span class="token operator">=</span> datasource <span class="token punctuation">.</span> get <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
The caller can now pass PostgresUserReader as a dependency:
1 2 3 | datasource <span class="token operator">=</span> <span class="token constant">PostgresUserReader</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> <span class="token punctuation">)</span> <span class="token constant">FullTimePayslipGenerator</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> datasource <span class="token punctuation">)</span> |
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.
1 2 3 4 5 6 7 8 9 10 11 12 | <span class="token keyword">class</span> <span class="token class-name">PostgresUserReader</span> <span class="token operator"><</span> <span class="token constant">UserReader</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">initialize</span></span> <span class="token punctuation">(</span> config <span class="token punctuation">)</span> config <span class="token operator">=</span> config <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">get</span></span> <span class="token comment"># initialize DB with the config</span> <span class="token keyword">self</span> <span class="token punctuation">.</span> config <span class="token comment"># Code to read data from Postgres</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Provided configuration by caller:
1 2 3 4 | config <span class="token operator">=</span> <span class="token punctuation">{</span> url <span class="token punctuation">:</span> <span class="token string">"url"</span> <span class="token punctuation">,</span> user <span class="token punctuation">:</span> <span class="token string">"user"</span> <span class="token punctuation">}</span> datasource <span class="token operator">=</span> <span class="token constant">PostgresUserReader</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> config <span class="token punctuation">)</span> <span class="token constant">FullTimePayslipGenerator</span> <span class="token punctuation">.</span> <span class="token keyword">new</span> <span class="token punctuation">(</span> datasource <span class="token punctuation">)</span> |
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.