Why design patterns in Rails
Rails leans on convention over configuration, so a lot of best practices come for free.
But hard problems still show up, and that is where design patterns earn their keep: a standard way to solve a recurring problem, so your code stays reusable and maintainable.
The trick is knowing when a pattern actually helps, and when it just adds ceremony.
Singleton
Singleton ensures a class has one instance with a global access point. In Rails it fits shared, read-mostly state, like a configuration object.
Don’t do this (class variables)
# app/models/global_config.rb
class GlobalConfig
@@instance = new
def self.instance
@@instance
end
private_class_method :new
end
Class variables invite threading issues and leak state between tests.
Do this (Ruby’s Singleton)
# app/models/configuration.rb
require 'singleton'
class Configuration
include Singleton
attr_reader :api_key, :api_secret
def initialize
@api_key = ENV['API_KEY']
@api_secret = ENV['API_SECRET']
end
end
Ruby’s Singleton module gives you thread safety and clean tests. Same object every time:
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Configuration.instance # => #<Configuration:0x00007ffca20fd998>
Factory
The Factory pattern wraps object creation. It shines when one trigger has to build several object types that share a shape.
Example: user types
Say your app has several user types, each with its own behavior 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
The factory hides the instantiation logic and keeps callers clean.
You can also write it with 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 pairs with STI. Your
statuscolumn gets validated by an enum, with one model per enum value, which makes the version above developer-friendly. Metaprogramming can still be a trap.
Don’t overdo it
Patterns are tools, not trophies. Drop one where it is not needed and you just make the app harder to read. Always ask whether the pattern earns its place, or whether a plain constructor would do.
As a rule of thumb, these deserve a second thought before you reach for them:
- Has and belongs to many tables
- Multi-database connections
- Wrapping AWS services
- Metaprogramming
- dry.rb
- STI