DEV Community

Vlad Hilko
Vlad Hilko

Posted on

8 Design Principles that every Ruby Developer should know

In this article we'll discuss 8 design principles that every Ruby developer should know. We'll go through each principle, giving a definition, a simple example, and the benefits they give us.

So, what are they?

  • DRY
  • KISS
  • YAGNI
  • SOLID
    • Single-responsibility principle
    • Open-closed principle
    • Liskov substitution principle
    • Interface segregation principle
    • Dependency inversion principle

Let's go through them one by one.


DRY

DRY (don’t repeat yourself) means don’t write duplicate code, instead use Abstraction to abstract common things in one place.

For example, if we had the following code:

p 'hello'  # any complex logic 
p 'hello'  # any complex logic 
p 'hello'  # any complex logic 
p 'hello'  # any complex logic 
p 'hello'  # any complex logic 
p 'hello'  # any complex logic 
p 'hello'  # any complex logic 
Enter fullscreen mode Exit fullscreen mode

Then it would make sense to encapsulate it in a new method

def hello
  p 'hello' # any complex logic 
end

hello
hello
hello
hello
Enter fullscreen mode Exit fullscreen mode

Why do we need it?

  • Easier to Read/Understand
  • Easier to Change/Maintain
  • Easier to Test
  • Easier to Reuse

KISS

Keep it simple, stupid (KISS) is a design principle which states that designs and/or systems should be as simple as possible.

Why do we need it?

  • Easier to read, understand and maintain
  • Chances of producing a bug becomes less
  • Code review takes less time

YAGNI

YAGNI stands for You aren't gonna need it. This principle means you should implement only required functionalities.

Why do we need it?

  • Saves your time and company money
  • Avoids supporting DEAD code and simplifies refactoring

SOLID

SOLID is an acronym for five separate object-oriented design principles:

  • The single-responsibility principle
  • The open-closed principle
  • The Liskov substitution principle
  • The interface segregation principle
  • The dependency inversion principle

The single-responsibility principle

The basic idea of the single-responsibility principle is that a class should basically serve one purpose. As a commonly used definition, "every class should have only one reason to change".

Let's take a look at the simplest example:

def hello
  calculation = 1 + 1 + 3 # any complex logic 

  puts calculation # any complex output 
end

hello
Enter fullscreen mode Exit fullscreen mode

The code above violate Single Responsibility principle because it does 2 things at the same time ( Calculation and Printing). To fix this, we need to do the following:

def calculation # complex logic encapsulated
  1 + 1 + 3 
end

def print_calculation # complex output encapsulated
  puts calculation
end

print_calculation
Enter fullscreen mode Exit fullscreen mode

Now we have 2 methods, each with only one purpose and reason for the change.

Why do we need it?

  • Easier to Understand
  • Easier to Test
  • Easier to Maintain, Refactor or Replace
  • Code becomes reusable

The open-closed principle

The open-closed principle states that a class/object should be open for extension, but closed for modification. It means that if it is necessary to change a specific class because of new business requirements, it is better to create a new code for the changes or new functionality instead of affecting the existing one in the cases where it is possible. Open/closed principle is intended to mitigate risk when introducing new functionality.

For example if we want to change p 'hello' in the following code:

class Animal
  def hello
    p 'hello'
  end
end

Animal.new.hello
Enter fullscreen mode Exit fullscreen mode

It would be better to create a separate class, inherit everything from the base class, and change p 'hello' there:

class AnimalExtension < Animal
  def hello
    p 'Hello World!'
  end
end

AnimalExtension.new.hello
Enter fullscreen mode Exit fullscreen mode

Why do we need it?

  • Reduces chances of breaking existing logic and increases product stability
  • It reduces maintenance cost

The Liskov substitution principle

The Liskov substitution principle states that any place in the code where you can use an object of type T, you can also use an object of a subtype of T. In terms of Ruby, this means that any place in your code where you are using an instance of a class, you can also use an instance of a subclass without anything breaking.

Let's take a look at the following example:

class Animal
  def hello
    p 'Hello'
  end 
end

animal = Animal.new
animal.hello 
Enter fullscreen mode Exit fullscreen mode

We expect that the above method will always print something. To break this principle, we just need to create a subclass that does something else, like:

class Cat < Animal
  def hello
    return true
  end
end
Enter fullscreen mode Exit fullscreen mode

Or

class Dog < Animal
  def hello
    raise 'error'
  end
end
Enter fullscreen mode Exit fullscreen mode

And instead of using

animal = Animal.new
animal.hello 
Enter fullscreen mode Exit fullscreen mode

We start using subclass instance

animal = Cat.new
animal.hello 
Enter fullscreen mode Exit fullscreen mode

If our code doesn't print anything we're violating The Liskov substitution principle.

Why do we need it?

  • The code becomes predictable, so the cost of maintenance is reduced

The interface segregation principle

The interface segregation principle states that clients should not be forced to depend on methods they do not use. The goal of this principle is to reduce the side effects of using larger interfaces by breaking application interfaces into smaller ones.

For example, we have the following module:

module ManageExtensions
  def create
    p 'created'
  end 

  def destroy
    p 'destroyed'
  end
end

class CreateAnimal
  include ManageExtensions
end

class DestroyAnimal
  include ManageExtensions
end

CreateAnimal.new.create
DestroyAnimal.new.destroy
Enter fullscreen mode Exit fullscreen mode

This module contains too many methods that we don't use in the classes. To apply interface segregation principle we have to rewrite the module as follows:

module CreateExtensions
  def create
    p 'created'
  end 
end

module DestroyExtensions
  def destroy
    p 'destroyed'
  end
end

class CreateAnimal
  include CreateExtensions
end

class DestroyAnimal
  include DestroyExtensions
end

Enter fullscreen mode Exit fullscreen mode

Now the class only has access to the methods it is supposed to use.

Why do we need it?

  • Reduces unexpected bugs when the Class does not have the ability to perform an action
  • Better code organization
  • Easier to Understand and maintain

DEPENDENCY INVERSION

The dependency inversion principle states that high-level modules should not depend on low-level modules, and both high-level and low-level modules should depend on abstractions. It also states that abstractions should not depend on concrete implementations, but that concrete implementations should depend on abstractions.

For example, we have the following class that just print something:

class Animal
  def hello
    p 'Hello'
  end
end

Animal.new.hello
Enter fullscreen mode Exit fullscreen mode

But sometimes we want to change the print method for example from p 'Hello' to puts 'Hello' or print 'Hello'.

What should we do according to DEPENDENCY INVERSION principle?

We should encapsulate puts and print method into abstractions:

class Puts
  def show(text)
    puts text
  end
end

class Print
  def show(text)
    print text
  end
end
Enter fullscreen mode Exit fullscreen mode

And then pass this class as a dependency

class Animal
  attr_reader :printer

  def initialize(printer)
    @printer = printer
  end

  def hello
    printer.show('Hello')
  end
end

Animal.new(Puts.new).hello
Enter fullscreen mode Exit fullscreen mode

Why do we need it?

  • Keeps your code loosely coupled
  • Code becomes reusable and DRY

Conclusion

We've just covered the 8 most important Ruby Design Principles. I hope this will help you to avoid serious mistakes during writing complex programs.

Top comments (0)