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 columnstatus
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
Building a Rails Engine