<

[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

Follow us on Twitter

Follow us on Facebook

You might also like

LATEST NEWS

LASTEST NEWS