[Coingecko] SOLID Design Principle in Ruby
The SOLID design principle is made up of five important principles that help us to build code with low maintenance cost, code that doesn’t demands a lot of time and people for fixes and improvements. We will quickly dive into these five principles with some code examples and how we can refactor these code by applying SOLID design principle.
Single Responsibility Principle
This principle states that any class, module, etc. should have single responsibility over the function of a program. A class should only have one reason to change. That’s easy to say, but it can actually be a tough principle to implement. Defining a single responsibility within the context of a program can get pretty contextual and abstract. The main idea behind the principle is to keep classes light, focused, and, therefore, easier to understand for others reading the code. Let’s look at the example below on how we usually violate the Single Responsibility Principle
#violation of Single Responsibility Principle
class InvoiceProcessor
def initialize(sales, agent)
@invoice = invoice
@agent = agent
end
def process
Commission.create(amount: calculate_commission, agent: @agent)
mark_invoice_as_processed
end
private
def mark_invoice_as_processed
@invoice.processed = true
@invoice.save!
end
def calculate_commission
@invoice.amount * 0.1
end
end
In the example above class we have a InvoiceProcessor
class that processes invoice payments of sales made by the agents. As we can see, if we want to made any changes on the way or rules that the commission is being calculated we would need to change InvoiceProcessor
class which means this class has more than one responsibility i.e., process the invoice and calculating agent’s commission. We could introduce new commission rules or strategies that would cause our calculate_commission
method to change. As such, this signals a violation of the Single Responsibility Principle.
Now, let’s refactor the code above to make it compliance with Single Responsibility Principle
#correct use of Single Responsibility Principle in Ruby
class InvoiceProcessor
def initialize(invoice)
@invoice = invoice
end
def process
CommissionCalculator.new.create_commission(invoice, agent)
mark_invoice_as_processed
end
private
def mark_deal_processed
@deal.processed = true
@deal.save!
end
end
class CommissionCalculator
def create_commission(deal, agent)
Commission.create(deal: deal, amount: deal.amount * 0.1, agent: agent)
end
end
After the refactor above, we will now have two smaller classes that handle two different or specific tasks, in compliance with Single Responsibility Principle. First, we have InvoiceProcessor
class that is responsible for processing the invoice and our calculator CommissionCalculator
class that will calculate and create new commission data for given invoice and agent.
Open / Closed Principle
This principle requires states that classes or methods should be open for extension, but closed for modification. This require us to design our code in a way that make it possible for us to change the behaviours of the system without making modifications to the classes themselves. The idea here again is to avoid overly stuffed classes filled with line after line of changes and modifications, instead handling that through separation or modules/inheritance. To have a better understanding on this principle, let’s look at an example of some code that is violating the Open/Closed Principle:-
#violation of Single Responsibility Principle
class SalaryCalculator
def initialize(employee, position)
@employee = employee
@postion = position
end
def get_salary
case @employee.monthly_salary
when :operator
get_operator_salary
when :sales_person
get_sales_person_salary
end
@employee.last_payment = Time.now
@employee.save!
end
private
def get_operator_salary
1000
end
def get_sales_person_salary
5000
end
end
As we can see from the code above, we need to modify SalaryCalculator class when we want to add a new position in the company. This violates the Open/Closed Principle. Let’s take a look at how we can refactor this code to make it compliance to open / closed principle:-
class SalaryCalculator
def initialize(employee, position)
@employee = employee
@position = position
end
def get_salary(employee_position)
@employee.monthly_salary = employee_position.get_salary
@employee.last_payment = Time.now
@employee.save!
end
end
class Operator
def get_salary
1000
end
end
class SalesPerson
def get_salary
5000
end
end
After the changes above, it now possible to add new position in the company without changing any code in SalaryCalculator class. Any additional behaviour will only require the addition of a new handler.
Liskov Substitution Principle
According to Liskov Substitution Principle, you should be able to replace any instances of a parent class with an instance of one of its children without creating any unexpected or incorrect behaviours. This ensures that abstractions are correct, and helps developers achieve more reusable code and better organized class hierarchies.
Let’s look at an example below that clearly violate the Liskov Substitution Principle:-
# Violation of the Liskov Substitution Principle in Ruby
class PublisherStats
def initialize(user)
@user = user
end
def posts
@user.blog.posts
end
end
class AdminAuthorStats < PublisherStats
def posts
user_posts = super
string = ''
user_posts.each do |post|
string += "title: #{post.title} author: #{post.author}n" if post.popular?
end
string
end
end
In the example above, we are implementing user statistics. There are two classes: a base class (PublisherStats) and its child class (AdminAuthorStats). The child class violates the LSP principle since it completely redefines the base class by returning a string with filtered data, whereas the base class returns an array of posts.
Now let’s see how we refactored the code so it conforms to the Liskov substitution principle:
# Correct use of the Liskov Substitution Principle in Ruby
class PublisherStats
def initialize(user)
@user = user
end
def posts
@user.blog.posts
end
end
class AdminAuthorStats < PublisherStats
def posts
user_posts = super
user_posts.select { |post| post.popular? }
end
def formatted_posts
posts.map { |post| "title: #{post.title} author: #{post.author}" }.join("n")
end
end
Interface segregation Principle
This principle states that no client should be forced to depend on methods it does not use. This principle doesn’t really apply to Ruby or other duck typing languages, which rely on methods and properties of objects, not strictly their type, to determine suitability.
Dependency Inversion Principle
The Dependency Inversion Principle has to do with high-level (think business logic) objects not depending on low-level (think database querying and IO) implementation details. This can be achieved with duck typing and the Dependency Inversion Principle. Often this pattern is used to achieve the Open/Closed Principle that we discussed above. To demonstrate this principle, we can even reuse that same example as a demonstration of this principle:-
class SalaryCalculator
def initialize(employee, position)
@employee = employee
@position = position
end
def get_salary(employee_position)
@employee.monthly_salary = employee_position.get_salary
@employee.last_payment = Time.now
@employee.save!
end
end
class Operator
def get_salary
1000
end
end
class SalesPerson
def get_salary
5000
end
end
As you can see, our high-level object, the SalaryCalculator, does not depend directly on an implementation of a lower-level object, Operator and SalesPerson. The only thing that is required for an object to be used by our high-level class is that it responds to the get_salary. This decouples our high-level functionality from low-level implementation details and allows us to easily modify what those low-level implementation details are. Having to write a separate usage file parser per file type would require lots of unnecessary duplication.
The post SOLID Design Principle in Ruby appeared first on CoinGecko Blog.
>> View on Chainlink
Join us on Telegram