Design patterns
Design patterns are a way to structure code so it becomes more modular, easily extendable and modifiable.
Behavioral
Strategy
Strategy is a composition based pattern for defining object behavior by giving it strategy object that handles encapsulated functionality.
When to use:
An object has different ways to perform the same method. When an object has methods with many if-else clauses, it is a symptom that class contains several different behaviors that could be handled by strategies.
Example:
A sing
method that has several logical ways to perform (and is harder to test or extend):
class Bird
def initialize(type)
@type = type
end
def sing
if type == :duck
puts 'Quack'
else
puts 'I thought I taw a putty tat'
end
end
end
can be refactored to:
class QuackStrategy
def sing
puts 'Quack'
end
end
class TweetStrategy
def sing
puts 'I thought I taw a putty tat'
end
end
class Bird
def initialize(sing_strategy)
@sing_strategy = strategy
end
def sing
@sing_strategy.sing
end
end
>> Bird.new(QuackStrategy.new).sing
=> "Quack"
>> Bird.new(TweetStrategy.new).sing
=> "I thought I taw a putty tat"
Tips:
- Reek’s ControlParameter smell is an indicator that code could be refactored to use strategies.
Template method
Design pattern based on inheritance, use case similar to one of a strategy pattern.
When to use:
Prefer using the strategy pattern to template method pattern whenever possible. Only use template method pattern in cases where strategy pattern does not fit.
Example:
class Bird
def inspect
puts "This is a #{self.class.to_s}"
puts "It says #{sing}"
end
def sing
raise NotImplementedError
end
end
class Duck < Bird
def sing
puts 'Quack'
end
end
class Blackbird < Bird
def sing
puts 'Chirp'
end
end
Iterator
Iterator defines the way how an object should be traversed to access its contained objects. It can be a collection or a composite, or anything else that has a structure.
When to use:
There is a collection object that needs to be traversed in a custom way. Or there’s a complex object that needs traversing.
Example:
class NumberIterator
def initialize(numbers = [])
@numbers = numbers
end
def each_odd_number(&block)
@numbers.each do |num|
yield num if num % 2 != 0
end
end
end
>> NumberIterator.new([1, 2, 3, 4, 5]).each_odd_number { |num| puts num }
=> 1
=> 3
=> 5
Tips:
- Combine iterators with ruby
Enumerable
module to get useful enumeration methods.
Observer
In observer pattern, object maintains a list of observers and notifies them when various events happen.
When to use:
When one or many objects depend on a state of the observable object and needs to know when observable object state changes. A preferred alternative to callbacks.
Example:
class User
def initialize(name:, email:)
@name = name
@email = email
end
def email=(value)
@email = value
send_email_confirmation
end
private
def send_email_confirmation
puts "Sending email confirmation to #{@email}"
end
end
can be refactored to:
class User
def initialize(name:, email:, email_observers: [])
@name = name
@email = email
@email_observers = email_observers
end
def email=(value)
@email = value
notify_email_observers
end
private
def notify_email_observers
@email_observers.each { |observer| observer.notify(@email) }
end
end
class UserEmailObserver
def notify(email)
send_email_confirmation(email)
end
private
def send_email_confirmation(email)
puts "Sending email confirmation to #{email}"
end
end
>> user = User.new(name: 'John', email: '[email protected]', email_observers: [UserEmailObserver.new])
>> user.email = '[email protected]'
=> "Sending email confirmation to [email protected]"
Command
A design pattern that decouples the object that invokes the operation from the object that knows how to perform it.
When to use:
- Application works with operations that may be performed in any order (e. g., cut, paste, add, divide…).
- Operations may need to be persisted and/or processed later.
- Operations may need to be processed asynchronously.
- Application has to support redo/undo of operations.
Example:
The following example implements a restaurant order system. The waiter (Invoker) takes an order (Command) from a customer by writing it down on the pad. The order is delegated to the chef (Receiver), which knows exactly how to make it.
class Chef
def initialize(name)
@name = name
end
def make_caesar_salad
puts 'making Caesar salad'
end
def make_margherita_pizza
puts 'making Margherita pizza'
end
end
class Order
def initialize(chef, dish)
@chef = chef
@dish = dish
end
def execute
@chef.send("make_#{@dish}")
end
end
class Waiter
def initialize(name)
@name = name
@pad = []
end
def place_order(order)
@pad << order
order.execute
end
end
>> chef = Chef.new('John')
>> waiter = Waiter.new('Joan')
>> waiter.place_order(Order.new(chef, :margherita_pizza))
=> "making Margherita pizza"
>> waiter.place_order(Order.new(chef, :caesar_salad))
=> "making Caesar salad"
Tips:
- Composite can help implement this pattern with macro commands.
- The invoker of the command should not now about the concrete command classes. It should use only the abstract interface.
- Command pattern should not be used if the command is only a link between the receiver and the actions that carry out the request.
- Command pattern should also be avoided if the command implements everything itself and does not send anything to the receiver (only the receiver should know how to perform the operation).
Chain of responsibility
The chain of responsibility pattern creates a chain of receiver objects for a request. This pattern decouples sender and receiver of request based on type of request. In this pattern each receiver contains a reference to another receiver.
When to use:
- When the request can be processed by different handlers
Example:
class Project
attr_accessor :code, :design
def completed?
code && design
end
end
class Handler
attr_accessor :successor
def initialize(successor = nil)
self.successor = successor
end
def handle(request)
perform_actions(request)
return true if request.completed?
successor ? successor.handle(request) : false
end
end
class Designer < Handler
def perform_actions(request)
request.design = 'Project design'
end
end
class Developer < Handler
def perform_actions(request)
request.code = 'Project code'
end
end
# Usage
designer = Designer.new
developer = Developer.new
designer.successor = developer
designer.handle(Project.new)
Structural
Decorator
Decorator accepts an object in its initializer and produces a decorated object. Decorated object follows interface of the original one, delegates some methods to the original object and modifies others when needed. This allows to change object behavior or add new features without changing behavior for objects of same class.
When to use:
- We want to modify/add features to class, however, we are sure introduced features are not said class responsibility.
- There is a need for models with more functionality than data storing, validation and association logic.
Example:
Let’s say we have a Comment model and want to post a comment to a Facebook’s wall after saving it locally. Instead of adding an after_save callback to Comment model, we create a decorator that does posting to Facebook.
class Comment < ActiveRecord::Base
end
class CommentNotifier
def initialize(comment)
@comment = comment
end
def save
@comment.save && notify_receiver
end
private
def notify_receiver
puts 'Notifying comment receiver about the new comment'
end
end
Using decorated objects is pretty straight-forward:
class CommentsController < ApplicationController
def create
@comment = CommentNotifier.new(Comment.new(params[:comment]))
if @comment.save
flash[:notice] = 'Your comment was posted.'
redirect_to comments_path
else
render 'new'
end
end
end
Tips:
- Use ruby’s
SimpleDelegator
to avoid writing method delegations to original object:
class User
def first_name
'John'
end
def last_name
'Doe'
end
def full_name
"#{first_name} #{last_name}"
end
end
class UserDecorator < SimpleDelegator
def full_name
"#{last_name}, #{first_name}"
end
end
>> decorated_user = UserDecorator.new(User.new)
>> decorated_user.full_name
=> "Doe, John"
Proxy
Very similar design pattern to Decorator pattern. While primary intent of decorators is adding and extending the functionality of objects, proxies are used to modify a behavior of existing object methods. Another difference is that decorator always requires accepting decorated object in its initializer. Proxy, on the other hand, can create proxied object when needed.
When to use:
- Building object is expensive, and we want to delay it as much as possible.
- Limit access to the object (e.g. authorization).
Example:
class Message
attr_reader :owner, :body
def initialize(owner:, body:)
@owner = owner
@body = body
end
end
class MessageReader
attr_reader :user, :message
def initialize(user:, message:)
@user = user
@message = message
end
def read
message.body
end
end
class MessageReaderProxy
def initialize(user:, message:)
@reader = MessageReader.new(user: user, message: message)
end
def read
raise 'Unauthorized' unless @reader.user == @reader.message.owner
@reader.read
end
end
>> message = Message.new(owner: 'Charles', body: 'Hello, world!')
>> reader = MessageReader.new(user: 'Charles', message: message)
>> reader.read
=> "Hello, world!"
>> proxy = MessageReaderProxy.new(user: 'Charles', message: message)
>> proxy.read
=> "Hello, world!"
>> proxy = MessageReaderProxy.new(user: 'Alice', message: message)
>> proxy.read
=> RuntimeError: Unauthorized
Adapter
Code using adapter pattern is structured in a way that adapter objects provide a single interface to access methods to different underlying objects.
When to use:
When you want to produce the unified interface for objects of unrelated classes.
Example:
Say we want to post a message on a Facebook’s wall and to Twitter’s feed at the same time. We have two classes that do post to respective services:
class FacebookPoster
def self.post_message(message)
puts "Posts '#{message}' to FB wall"
end
end
class TwitterMessage
def initialize(msg)
@msg = msg
end
def send
puts "Sending '#{@msg}' to Twitter"
end
end
We can use adapters to provide a unified interface for message posting:
module CommentAdapters
module Facebook
def self.post(message)
FacebookPoster.post_message(message)
end
end
module Twitter
def self.post(message)
TwitterMessage.new(message).send
end
end
end
class Comment
ADAPTERS = [
CommentAdapters::Facebook,
CommentAdapters::Twitter
]
def initialize(message)
@message = message
end
def post_everywhere
ADAPTERS.each do |adapter|
adapter.post(@message)
end
end
end
>> Comment.new('Hello, world!').post_everywhere
=> "Posts 'Hello, world!' to FB wall"
=> "Sending 'Hello, world!' to Twitter"
Tips:
- You can pass adapter as a symbol parameter with a little of Ruby magic:
class Comment
def initialize(message)
@message = message
end
def post_to(adapter)
CommentAdapters.const_get(adapter.to_s.capitalize).post(@message)
end
end
>> Comment.new('Hello, world!').post_to(:facebook)
=> "Posts 'Hello, world!' to FB wall"
>> Comment.new('Hello, world!').post_to(:twitter)
=> "Sending 'Hello, world!' to Twitter"
Facade
Provides simplified interface to consume an object.
When to use:
When you commonly access the object in a somewhat complicated way.
Example:
require 'net/http'
class NetworkingFacade
def initialize(host)
@uri = URI.parse(host)
end
def simple_get(page)
Net::HTTP.start(@uri.host, @uri.port) { |http| http.get(page) }.body
rescue SocketError
nil
end
end
>> NetworkingFacade.new('http://www.necolt.com').simple_get('/')
=> "<!DOCTYPE html>..."
>> NetworkingFacade.new('http://www.necolt.commm').simple_get('/')
=> nil
Composite
A design pattern that allows having structures consisting of primitive objects and/or other structures and manipulate them uniformly (with a uniform interface).
When to use:
- There is a need to manipulate a hierarchical collection which may consist of “simple” or “composite” objects. Composite objects may as well consist of simple and/or composite objects. Composite objects are processed in one way and primitive objects in another.
- “The sum acts like one of the parts” situation.
Example:
This example implements elements of a tree-like structure: LeafNode
and InternalNode
. Both components have a uniform interface (method value
) which is used to manipulate the structure (in this case, to get a string representation of a node):
class LeafNode
def initialize(value)
@value = value
end
def value
"(#{@value})"
end
end
class InternalNode
def initialize(nodes:)
@nodes = nodes
end
def value
"[#{@nodes.map(&:value).join(', ')}]"
end
end
>> node = InternalNode.new(
>> nodes: [
>> LeafNode.new(5),
>> InternalNode.new(nodes: [LeafNode.new(3), LeafNode.new(2)])
>> ]
>> )
>> node.value
=> "[(5), [(3), (2)]]"
Bridge
The bridge pattern can be simply considered as a double abstraction. It is extremely useful when you need to apply a Strategy on an object that has varied behavior in certain cases. It promotes code extensibility, as new strategies and variations can be added.
When to use:
Whenever you’re applying a Strategy and notice that you have some extra complicated logic lying around that varies in certain conditions.
Example:
class MoneyInput
def execute
# steps to input money
end
end
class MoneyWithdrawal
def execute
# steps to withdraw money
end
end
class BilledOperation
def initialize(action)
@action = action
end
def execute
@action.execute
bill_operation
# some additional logic
end
def bill_operation
# steps to bill the operation
end
end
class Operation
def initialize(action)
@action = action
end
def execute
@action.execute
# some additional logic
end
end
class ATM
def initialize(billed)
@billed = billed
end
def execute_action(action)
operation.new(action).execute
end
private
def operation
@operation ||= begin
if @billed
BilledOperation
else
Operation
end
end
end
end
Tips
- It’s a great choice for building complicated internal APIs.
- Make sure you actually need it, it shouldn’t be used for simple logic. As it increases the application complexity.
Flyweight
You can think of the flyweight pattern as a modification to a conventional object factory. The modification being that rather than just creating new objects all the time the factory should check to see if it has already created the requested object and return it instead of creating it again.
When to use:
- When you want to reduce the number of objects created
Example:
class Factory
def initialize
self.items = {}
end
def item(item_type)
items[item_type] ||= create_item(item_type)
end
private
attr_accessor :items
def create_item(item_type)
# creates an item
end
end
# Usage
factory = Factory.new
circle = factory.item(:circle)
square = factory.item(:square)
Creational
Singleton
A design pattern that allows a class to instantiate only one single object and global access is provided to that object. Since singleton in lazy instantiated (i.e. instance only created on first use, and the same instance used later on), it provides performance and memory usage benefits.
When to use:
- A case when a class can only have a single instance.
- There is a need for a global state (singletons are preferred to global variables).
- Concurrent access to a shared resource (e.g. database connection).
- When creating an instance of a class is very expensive, however, one instance is sufficient enough.
Example:
class ErrorLogger
def initialize
puts 'Initializing error logger'
@stream = STDERR
end
def log(message)
@stream.puts(message)
end
def self.instance
@@instance ||= new
end
private_class_method :new
end
>> ErrorLogger.instance.log("error 1")
=> "Initializing error logger"
=> "error 1"
>> ErrorLogger.instance.log("error 2")
=> "error 2"
>> ErrorLogger.new.log("error 3")
=> NoMethodError: private method `new' called for ErrorLogger:Class
Tips:
- Include ruby’s
Singleton
module into class to make it a singleton:
require 'singleton'
class ErrorLogger
include Singleton
def initialize
puts 'Initializing error logger'
@stream = STDERR
end
def log(message)
@stream.puts(message)
end
end
Builder
A builder is an interface for constructing a part of an object’s state.
When to use:
If creating an object requires some boilerplate code, and the object is created at least in two places in the code, it makes sense to move object creation code to a builder object. It helps to avoid code duplication and possible bugs. Alternatively, object creation code should be moved to a builder object if creating object requires information or resources that should not be contained within the composed class. It consists of a director that uses builders to build an object.
Example:
# Director
class Search
def initialize(params)
@params = params
@query = User.all
end
def execute
[NameQuery, ActiveUserQuery].each do |query_builder|
@query = query_builder.new(@query).call(@params)
end
end
end
# Concrete builder
class NameQuery
def initialize(query)
@query = query
end
def call(params)
@query.where('name ILIKE :name', name: "%#{params[:name]}%")
end
end
# Concrete builder
class ActiveUserQuery
def initialize(query)
@query = query
end
def call(_params)
@query.where('current_sign_at <= :time', time: 1.day.ago)
end
end
Factory Method
This design pattern is like a Template Method for creating objects. Rather than using the new
method to create a new object instance, it advocates the usage of factory method (static or not).
When to use:
- When there is a hierarchy of objects and the class cannot anticipate the type of objects it is supposed to create.
- When a class wants its subclasses to decide which type of an object to create.
- When the newly created object is used through a common interface.
Example:
The example shows a simple document creation system. create_document
is the factory method here. Method new_document
implements all the needed logic for getting a new document but it lets the subclass MyApplication
to handle document instance creation.
class Document
attr_reader :name
def initialize(name)
@name = name
end
end
class MyDocument < Document
def open
puts "Opening the document named #{name}"
end
end
class Application
def initialize
@docs = []
end
def new_document(name)
document = create_document(name)
@docs << document
document.open
end
def report_all_documents
@docs.each { |doc| puts doc.name }
end
end
class MyApplication < Application
def create_document(name)
MyDocument.new(name)
end
end
>> application = MyApplication.new
>> application.new_document("HelloWorld.txt")
=> "Opening the document named HelloWorld.txt"
>> application.new_document("Test.html")
=> "Opening the document named Test.html"
>> application.report_all_documents
=> "HelloWorld.txt"
=> "Test.html"
Tips:
- If the code must know about the concrete class of the newly created object, then it should be considered carefully whether to use this pattern or not since it might bring unnecessary complexity to the application.
Object pool
An object pool is a set of initialized objects that are taken from a pool when needed and put back after use. Using a pool of initialized objects avoids initialization and destruction operations.
When to use:
When object initialization and/or destruction is expensive, the object is only used for a (short) period of time, and objects of a specific class are initialized often.
Example:
class Item
def initialize
puts 'Performing expensive initialization'
end
end
class ItemPool
def initialize(pool_size)
@pool_size = pool_size
@items = @pool_size.times.map { Item.new }
end
def take
@items.shift
end
def release(item)
@items << item
end
end
Tips:
- Make sure object state is reset before returning it to pool.
- Object pool only makes sense when object initialization/destruction is expensive. Otherwise, simply creating objects and letting garbage collector to clean them can be more performant than programmatically resetting object state. Object pool also is memory demanding.
- An example of popular gem using object pool:
connection_pool
.
Web application specific patterns
Query Objects
Query objects encapsulate complex SQL queries.
Example:
Simplified example that produces a query to find active users:
class ActiveUsersQuery
def initialize(relation)
@relation = relation
end
def call
relation.where(email_confirmed: true).where('current_sign_in_at > ?', 1.year.ago)
end
end
Value Objects
Value objects contain some values, and comparison results between value objects depend on contained values instead of object references (e.g. String or Time objects). Extracting Value objects promotes dry code.
Example:
class PhoneNumber
attr_reader :country_code, :number
def initialize(country_code, number)
@country_code = country_code
@number = number
end
def ==(other)
country_code == other.country_code && number == other.number
end
def to_s
"#{country_code} #{number}"
end
end
Tips:
- Use Ruby’s
Comparable
to get<
,<=
,==
,>=
,>
,between?
methods just by implementing<=>
method. - Use them to separate the application logic from the data layer, e.g. if a User has a phone number column in his table, it doesn’t mean that the phone number related code should belong to the User model.
Policy Objects
Policy objects contain read operations and expose object policies (object state, it’s abilities or attributes) to the object consumer.
Example:
class SubscriptionPolicy
def initialize(user)
@user = user
end
def subscribed?
expiration_date = @user.subscribed_until
expiration_date.present? && expiration_date <= Date.current
end
end
Service Objects (Interactors)
Service objects responsibility is to connect your exposed WEB API to your core application domain. They are named as verbs and not nouns, indicating an action that you’re requesting. Whether your action consists of multiple steps, or validations, it should end up here, preferably composing internal components.
When to use:
- A non-trivial action needs to be performed.
- Action requires interaction between two or more objects.
- Action is not model responsibility.
- Action can be performed in multiple ways (has several logical branches).
Example:
class CommentOnPost
attr_reader :message, :post, :commenter, :comment
def initialize(message:, post:, commenter:)
@message = message
@post = post
@commenter = commenter
end
def call
@comment = post.comments.new(body: message, owner: commenter)
if commenter.can_post_on?(post)
comment.save
else
comment.errors.add(:base, :unauthorized)
end
end
def success?
comment.errors.blank?
end
end
class CommentsController < ApplicationController
expose(:post) { Post.find(params[:post_id]) }
def create
action = CommentOnPost.new(message: params[:message], post: post, commenter: current_user)
action.call
if action.success?
# Handle success case
...
else
# Handle errors
...
end
end
end
Tips:
- Use
interactor-rails
gem for easier building of interactors:
class CommentOnPost
include Interactor
delegate :message, :post, :commenter, :comment, to: :context
def call
build_comment
save_comment
end
private
def build_comment
context.comment = post.comments.new(body: message, owner: commenter)
end
def save_comment
context.fail! unless commenter.can_post_on?(post)
comment.save
end
end
Form Objects
Form objects handle updating one or many ActiveRecord models. Using form objects bring a number of benefits:
- Keeps ActiveRecord model clean.
- Validation and persistence logic is isolated and easily testable.
When to use:
- Attributes must be modified before persisted in the database (e.g. stripping, sanitizing or converting input data).
- Additional steps must be taken after persisting data (e.g. sending an email after creating a new user). Better alternative to callbacks.
- Case-specific validations needed (instead of adding conditional validations to ActiveRecord models).
- Logic, that doesn’t belong to model persistence (e.g. search form - it doesn’t persist anything in the database, however, still is a form).
Example:
class OrderForm
include ActiveModel::Validations
attr_accessor :price, :shipping_address
validates :price, numericality: { greater_than: 0 }
validates :shipping_address, :presence, length: { minimum: 10, maximum: 200 }
def initialize(params = {})
self.price = params[:price]
self.shipping_address = params[:shipping_address]
end
def save
return false unless valid?
order.save
end
private
def order
Order.new(price: price, shipping_address: shipping_address)
end
end
>> order = OrderForm.new(price: 0.0)
>> order.save
=> false
>> order.errors.full_messages.to_sentence
=> "Price must be greater than 0 and Shipping address is too short (minimum is 10 characters)"
>> order = OrderForm.new(price: 100.0, shipping_address: 'Planet Earth, Solar System, Milky Way')
>> order.save
=> true
Tips:
- If logic is too complex for one form, use Service Objects / Interactors.
View objects
View objects contain logic that is needed only for displaying information and don’t belong to models.
Example:
class PostViewObject
attr_reader :post
delegate :title, :name, :user, to: :post
def initialize(post)
@post = post
end
def title_with_author
"#{title} by #{user.name} @ #{created_at}"
end
end