Design patterns in rails


Why Use Design Patterns in Rails?

Ruby on Rails, as a framework, encourages convention over configuration, facilitating developers to follow best practices effortlessly.

However, despite Rails’ simplifications, complex problems needing elegant solutions will always exist. This is where design patterns come into play.

Design patterns in Rails serve as a standardized approach to solve common problems, promoting code reusability and maintainability.

Understanding when and how to apply these patterns can significantly optimize your application architecture.

Singleton Pattern: A Rails Example

The Singleton Pattern ensures a class has only one instance and provides a global point of access to it. In Rails, Singleton might be useful for managing a shared resource throughout the application, like a configuration settings object.

Bad Practice: Misusing Global Variables

# app/models/global_config.rb
class GlobalConfig
  @@instance = new

  def self.instance
    @@instance
  end

  private_class_method :new
end

This approach, using class variables, risks threading issues and complicates testing due to state persistence between tests.

Preferred Singleton Implementation

# app/models/configuration.rb
require 'singleton'

class Configuration
  include Singleton

  # Define configuration attributes with read-accessors
  attr_reader :api_key, :api_secret

  def initialize
    @api_key = ENV['API_KEY']
    @api_secret = ENV['API_SECRET']
  end
end

Using Ruby’s Singleton module, we sidestep potential pitfalls by ensuring thread safety and making unit tests easier to manage.

See, same object id everytime:

Configuration.send(:new) # => #<Configuration:0x00007ffca20fd998>
Configuration.send(:new) # => #<Configuration:0x00007ffca2119df0>
Configuration.send(:new) # => #<Configuration:0x00007ffca20e8c78>

Factory Pattern: Managing Object Creation in Rails

The Factory Pattern provides a way to encapsulate the instantiation of objects. This pattern is ideal when dealing with a system that must manage, maintain, or manipulate collections of objects that have common characteristics.

Example: Different User Types

Suppose your application handles different types of users, each with unique behaviors and permissions.

# app/models/user_factory.rb
class UserFactory
  def self.create(user_type, attributes)
    case user_type
    when :admin
      AdminUser.new(attributes)
    when :guest
      GuestUser.new(attributes)
    else
      StandardUser.new(attributes)
    end
  end
end

This Factory abstracts the instantiation logic and promotes a cleaner, more maintainable codebase by encapsulating the creation details.

The code above could also be refactor using Metaprogramming.

# app/models/user_factory.rb
class UserFactory
  def self.create(user_type, attributes)
    if user_type
      klass = "#{user_type.humanize}User".constantize
      klass.new(attributes)
    else
      StandardUser.new(attributes)
    end
  end
end

PS: This pattern is use in SIT. In that case your column status would be validated by an enum and you would have one model per enum value making the code above developer friendly. Metaprogramming can be a trap sometimes.

Overusing Design Patterns

While design patterns provide robust solutions, they come with the risk of overuse. Implementing a pattern where it’s not needed can overcomplicate the application, making it harder to understand and maintain. Always evaluate if a pattern adds value to your solution or if a simpler approach could be enough.

Conclusion

As a rule of thumb, here are some overkill solutions that require second thought:

  • Has and belongs to many tables
  • Multi Database connections
  • Include AWS services
  • Metaprogramming
  • Use dry.rb
  • SIT

Related posts

Free Training By Email:
Building a Rails Engine

We will send udpates but we care about your data.
No spams.

Back to homepage