DEV Community

Cover image for Rails design pattern
Aleksandr Korolev
Aleksandr Korolev

Posted on

Rails design pattern

If you are a Rails-developer you know at least MVC pattern. Most of us know of course that it has some limitations in terms of structuring your code (fat controllers/models) and in some point of your developer's path you start thinking about code quality and here I mean not code style and other minor things but more general - single responsibility principal (SRP) and how we can organize code in order to make it real, Liskov’s Substitution Principle etc.

Ok, we have spotted a problem and what can we do with it?
As you can have noticed - SOLID principles and general OO design are our tools. But they are too general. Are there something more specific? Googling will provide you at least next possible patterns:

  • Builder
  • Decorator
  • Form
  • Interactor
  • Observer
  • Policy
  • Presenter
  • Query
  • Service
  • Value

I am not going to retell them, you can easily find definitions. But I want to light on next moment: how many from them you are using in yours projects? Is it straightforward which pattern you need to use?

Let take for example Service and Interactor:

The service design pattern breaks down a program into its functional constituents. Each service object completes one task, such as running payment for a customer, completing a bank transaction, etc.

You can use the interactor design pattern to split a large task into a series of small, inter-dependent steps. If any of the steps fail, the flow stops, and a relevant message appears about why the execution failed.

The definitions are different but the aim is the same - breaks down something big into small blocks. From my point of view, the interactor pattern is a special case of service.

class AuthenticateUser
  include Interactor

  def call
    if user = User.authenticate(context.email, context.password)
      context.user = user
      context.token = user.secret_token
    else
      context.fail!(message: "authenticate_user.failure")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

This excerpt was written in context of interactor gem.
and

class AuthenticateUserService < BaseService
  def perform
    if (user = User.authenticate(context.email, context.password))
      success_result(status: :success, message: nil, data: user) # this method as and error_result are implemented in BaseService
    else
      error_result(status: :error, message: user.errors, data: {})
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Does it look similar? From my perspective, they are not different patterns but one pattern with slightly different implementation.

Let's take anther one confusing couple: Decorator vs Presenter:

The Decorator pattern adds new functionality to an existing object by wrapping it with additional objects containing the new features. This helps to modify an instance of a class for extra features and not define new static subclasses in the code.

The Presenter design pattern aims to isolate view rendering logic from the controller and model and encapsulate it into a reusable component. It makes the controller and models leaner and enhances code readability.

Let's take a look into code snippets:

class PostPresenter
  def initialize(post)
    @post = post
  end

  def image
    @post.image? ? @post.image.url : 'no-image.png'
  end

  # similary define other methods with their logic

end
Enter fullscreen mode Exit fullscreen mode
class Car
  def roof
    "fixed"
  end
end

class ConvertibleDecorator < SimpleDelegator
  def roof
    "collapsible"
  end

  def self.decorate(car)
        new(car)
  end

 private

 def model
   __getobj__
 end
end

#using 
basicCar1 = Car.new()
basicCar2 = Car.new()

basicCar1 = ConvertibleDecorator.decorate(basicCar1)

puts(basicCar1.roof) # => collapsible
puts(basicCar2.roof) # => fixed
Enter fullscreen mode Exit fullscreen mode

You can argue that is different approach... but I would like to say - different implementation but the aim is the same. Both examples are trying to separate some additional(auxiliary) logic in separate place. And there are other way for do it, for example:

class Car
  def roof
    "fixed"
  end
end

module ConvertibleDecorator
  def roof
    "collapsible"
  end
end

#using 
basicCar1 = Car.new()
basicCar2 = Car.new()

basicCar1 = basicCar1.extend ConvertibleDecorator

puts(basicCar1.roof) # => collapsible
puts(basicCar2.roof) # => fixed
Enter fullscreen mode Exit fullscreen mode

Some other patterns are more straightforward and not so confusing. But the main idea of the post is:

  • try to use general principle when you structure the code (SOLID);
  • not use patterns because it is just the best practice but when you can see that it makes sense; if you cannot match you class with one of this patterns it is ok if you use something new what related to you problem (often developers see Services in a project and try to put all new classes there however it can be QueryObject or ValueObject).

The definitions of the patterns and part of examples were taken from here

Discussion (0)