Last active
March 17, 2025 21:12
-
-
Save AhmedElSharkasy/8220770 to your computer and use it in GitHub Desktop.
Rails 3 AntiPatterns, Useful snippets and Recommended Refactoring. Note: Some of them are collected from different online resources/posts/blogs with some tweaks.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Rails as it has never been before :) | |
Rails 3 AntiPatterns, Useful snippets and Recommended Refactoring. | |
Note: Some of them are collected from different online resources/posts/blogs with some tweaks. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# If the last parameter in a method definition is prefixed with an ampersand, any associated block | |
# is converted to a Proc object and that object is assigned to the parameter. | |
# It must be the last argument in the parameter list | |
def do_math_operation(amount, &block) | |
block.call(amount) # OR yield(amount) | |
end | |
result = do_math_operation(5) {|amount| amount * 2} | |
=> 10 | |
# A proc is a reusable object oriented block , a block is actually a proc that can't be saved | |
block.class => Proc | |
# The same above can be done using proc | |
def do_math_operation(amount, proc) | |
proc.call(amount) # yield won't work! | |
end | |
multiply_by_2 = Proc.new do |n| | |
n * 2 | |
end | |
result = do_math_operation(5, multiply_by_2) | |
=> 10 | |
# The same above can be done using lambda | |
def do_math_operation(amount, lambda) | |
lambda.call(amount) # yield won't work! | |
end | |
multiply_by_2 = lambda{ |n| n * 2 } | |
result = do_math_operation(5, multiply_by_2) | |
=> 10 | |
# Differences between Lambda and Procs | |
# 1- Lambda checks for number of parameters , throw an exception if less or more parameters were passed | |
# ex: | |
lambda = lambda{ |str1, str2| "#{str1}, #{str2}"} | |
lambda.call('str1', 'str2') | |
=> str1, str2 | |
lambda.call('str2','str2','str3') | |
ArgumentError: wrong number of arguments (3 for 2) | |
proc = Proc.new{ |str1, str2| "#{str1}, #{str2}" } | |
proc.call("str1") | |
=> str1, | |
# 2- Return in Lambda block will return the value to the calling method and the method continues normally, | |
# while return in Proc block stops the calling method execution | |
def proc_return | |
# create a proc | |
p = Proc.new {|n| return "(proc: #{ n })"} | |
# call the proc | |
output = p.call(2) | |
# method return (will not be reached) | |
"proc_return: #{ output }" | |
end | |
def lambda_return | |
# create a lambda | |
l = lambda {|n| return "(lambda: #{ n })"} | |
# call the lambda | |
output = l.call(2) | |
# method return | |
"lambda_return: #{ output }" | |
end | |
# return in proc causes return in calling method | |
puts proc_return | |
# => "(proc: 2)" | |
# return in lambda returns control to calling method | |
puts lambda_return | |
# => "lambda return: (lambda: 2)" | |
# Note proc can't have a return in it , while lambda can | |
# (if you wish for the calling method to continue) | |
# The same can be done using method objects | |
def do_math_operation(amount, method) | |
method.call(amount) # yield won't work! | |
end | |
def multiply_by_2(n) | |
n * 2 | |
end | |
result = do_math_operation(5, method(:multiply_by_2)) | |
=> 10 | |
# Method objects will act like lambda , only lambda is anonymous |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/models/concerns/users/csv_conversion.rb | |
class User | |
module CsvConversion | |
extend ActiveSupport::Concern | |
included do | |
def to_csv(options = {}) | |
CSV.generate(options) do |csv| | |
csv << %w[id username email] | |
all.each do |user| | |
csv << [user.id, user.username, user.email] | |
end | |
end | |
end | |
end | |
end | |
end | |
# app/models/user.rb | |
include CsvConversion | |
# Use modules for more complex and widely used logic | |
# Using modules/plugins | |
# in lib/plugins/sponsorable.rb | |
module Sponsorable | |
extend ActiveSupport::Concern | |
module InstanceMethods | |
def sponsors_count | |
end | |
end | |
module ClassMethods | |
def acts_as_sponsorable(configuration = {}) | |
# Do your logic and define the needed associations ex: product has many sponsors, | |
# A sponsor may sponsor many products , yes a polymorphic association | |
end | |
include InstanceMethods | |
end | |
end | |
ActiveRecord::Base.send(:include, Sponsorable) | |
# config/initializers/extension.rb | |
require 'sponsorable' | |
# app/models/product.rb | |
acts_as_sponsorable({ max_count: 5, sponsors_type: 'exclusive' }) | |
# Use modules/plugins for shared complex logic and use concerns for model related simple logic |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Decorators let you layer on functionality to existing operations, i.e serve a similar purpose to callbacks. | |
# For cases where including callback logic in the model would give the model too many responsibilities, a Decorator is useful. | |
# Imagine we have this logic on stackoverflow: | |
# A user can post a question on a stackoverflow, after posting an email notification is sent to | |
# all users have interest in the question topic, a post is shared on the user profile with a link to the question he posted | |
# BAD | |
# app/models/question.rb | |
after_create :handle_after_create_logic | |
private | |
def handle_after_create_logic | |
# queue email to be sent | |
# create post | |
end | |
# The question model should not be responsible for all this logic | |
# Good | |
class SOFQuestionNotifier | |
def initialize(question) | |
@question = question | |
end | |
def save | |
if @question.save | |
post_to_wall | |
queue_emails | |
end | |
end | |
private | |
def post_to_wall | |
end | |
def queue_emails | |
end | |
end | |
# app/controllers/questions_controller | |
def create | |
@question = SOFQuestionNotifier.new(Question.new(params[:question])) | |
if @question.save | |
# Success | |
else | |
# Error | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Law Of Demeter: Every unit should have limited knowledge about other units, simply don't talk to strangers! | |
# 2 dots are fair enough! | |
# BAD | |
class Invoice < ActiveRecord::Base | |
belongs_to :user | |
end | |
# In model/view | |
invoice.user.name | |
# Good | |
class Invoice < ActiveRecord::Base | |
belongs_to :user | |
delegate :name, to: :user, prefix: true | |
end | |
invoice.user_name |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# It is common in any rails application to have some meta tags in your HTML as title, description, keywords , fb meta tags,... | |
# BAD | |
# In app/views/layouts/application.html.erb | |
<meta name="description" content="<%= @description%>" /> | |
<meta property="og:image" content="<%= @image_url %>" /> | |
# In each controller | |
@description = | |
@image_url = | |
# Better | |
# in app/views/layouts/application.html.erb | |
<meta name="description" content="<%= yield(:description) %>" /> | |
<meta property="og:image" content="<%= yield(:image_url) %>" /> | |
# In each view | |
content_for(:title, @product.title) | |
content_for(:title, @product.description) | |
# Better | |
<%= | |
og_tags(standard_og_stuff(@product), { | |
:type => 'website', | |
:other_tag => 'something' | |
}) | |
%> | |
# app/helpers/application_helper.rb | |
def og_tags(*tags) | |
content = tags.reduce({}) do |result, set| | |
result.merge! set | |
end | |
raw(content.map do |key, value| | |
tag :meta, content: value, property: "og:#{key}" | |
end.join("\n")) | |
end | |
# Then a helper method that pulls standard attrs (name, desc, image, ...) from a piece of content: | |
def standard_og_stuff(content) | |
{ title: content.name, | |
description: content.description | |
} | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ContactForm | |
include ActiveModel::Validations | |
include ActiveModel::Conversions | |
attr_accessor :name, :email, :body | |
validates_presence_of :name, :email, :body | |
def initialize(attributes = {}) | |
attributes.each do |name, value| | |
send("#{name}=",value) | |
end | |
end | |
def persisted? | |
false | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Imagine that your application requires a layout that differs slightly from your regular application layout to support one particular controller | |
# The solution is nested layout! | |
#inside your different layout controller add: | |
layout 'custom' | |
# app/views/layouts/custom.html.erb | |
<% content_for :stylesheets do %> | |
<%= stylesheet_link_tag "custom" %> | |
<% end %> | |
<% content_for :content do %> | |
<div id="right_menu">Right menu items here</div> | |
<%= yield %> | |
<% end %> | |
<%= render "layouts/application" %> | |
# app/views/layouts/application.html.erb | |
<html> | |
<head> | |
<title>My Application</title> | |
<%= stylesheet_link_tag "layout" %> | |
<style><%= yield :stylesheets %></style> | |
</head> | |
<body> | |
<div id="top_menu">Top menu items here</div> | |
<div id="menu">Menu items here</div> | |
<div id="content"><%= content_for?(:content) ? yield(:content) : yield %></div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Avoid N+1 Queries | |
# Example Application: | |
# User has many followers | |
# BAD | |
user.followers.collect{ |f| f.user.name } | |
# Queries generated | |
# select followers where user_id = 1 | |
# select user where id=2 | |
# select user where id=3 | |
# .................. | |
# Good | |
user.followers.includes(:user){ |f| f.user.name } | |
# Queries generated | |
# select followers where user_id = 1 | |
# select user where id in(2,3,..) | |
# Counter Caching | |
# we want to get the number of retweets of a tweet | |
# BAD | |
tweet.retweets.length # Load the tweets into array and then call .length on that array | |
# Better | |
tweet.retweets.size OR tweet.retweets.count # make a count query to the db | |
# What if the above query is made in a loop? many queries right? | |
# Best using counter caching | |
# Add column retweets_count to the tweet model | |
# app/models/tweet.rb | |
belongs_to :original_tweet, class_name: 'Tweet', foreign_key: 'tweet_id', counter_cache: :retweets_count | |
has_many :retweets, class_name: 'Tweet', foreign_key: 'tweet_id' | |
tweet.retweets.size # No query , only look at the cache as the 'tweet' object is already fetched |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Sometimes complex read operations might deserve their own objects, Policy Object is the right solution. | |
# Policy Objects are similar to Service Objects, but it is conventional to use services for write operations | |
# and policies for read. | |
# They are also similar to Query Objects, but Query Objects focus on executing SQL to return a result set, | |
# whereas Policy Objects operate on domain models already loaded into memory. | |
# app/policies/twitter_policy.rb | |
class TwitterPolicy < Struct.new(:auth) | |
def first_name | |
auth['info']['name'].split(' ').first | |
end | |
def last_name | |
..... | |
end | |
.... | |
end | |
# app/policies/facebook_policy.rb | |
class FacebookPolicy < Struct.new(:auth) | |
def first_name | |
auth['info']['first_name'] | |
end | |
def last_name | |
..... | |
end | |
.... | |
end | |
# app/models/user.rb | |
def self.from_oauth(auth) | |
policy = "#{auth['provider']}_policy".classify.constantize.new(auth) | |
create_user_from_policy(policy) | |
end | |
# Check https://github.com/elabs/pundit for maximum usage of policies and OO design | |
# Pundit: Minimal authorization through OO design and pure Ruby classes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Presenters are created by Controllers/Views and know the current state of the view and the model | |
# Can be used to filter away tangled logic in the view | |
# Also can be used to skin the controller | |
# An example of a presenter used to remove logic from view(this is a small example , views can get it even more bloated with logic) | |
# app/views/categories/index.html | |
<div class="images"> | |
<%if @category.image_url%> | |
<%= link_to image_tag("/images/#{@category.id}.png") + "some extra text", category_path(@category), class: 'class-name', remote: true %> | |
<%else%> | |
<%= link_to image_tag("/images/default.png") + "some extra text", category_path(@category), class: 'class-name', remote: true %> | |
<%end%> | |
<div> | |
# app/presenters/category_presenter.rb | |
class CategoryPresenter < BasePresenter | |
presents :category | |
def featured_image | |
image_path = category.image_url ? category.id : "default" | |
link_to image_tag("/images/#{image_path}.png") + "some extra text", category_path(category), :class => 'class-name', :remote => true | |
end | |
def top_brands | |
@top_brands ||= category.top_brands | |
end | |
def top_keywords | |
@top_keywords ||= category.top_keywords | |
end | |
end | |
class BasePresenter | |
def initialize(object, template) | |
@object = object | |
@template = template | |
end | |
def self.presents(name) | |
define_method(name) do | |
@object | |
end | |
end | |
def method_missing?(*args, &block) | |
@template.send(*args, &block) | |
end | |
end | |
# app/views/category/index.html | |
<% present @category do |category_presenter|%> | |
<div class="images"> | |
<%= category_presenter.featured_image%> | |
<div> | |
<% end %> | |
# app/helpers/application_helper.rb | |
def present(object, klass = nil) | |
klass ||= "#{object.class}Presenter".constantize | |
presenter = object.new(object, self) | |
yield(presenter) | |
end | |
# To access presenters from controller | |
# app/controllers/application_controller.rb | |
private | |
def present(object, klass = nil) | |
klass ||= "#{object.class}Presenter".constantize | |
klass.new(object, view_context) | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Example Application: | |
# Product: name, price, brand, slug, description, delete_flag, image_url | |
# Category: name,slug | |
# Product belongs to Category | |
# Use scopes instead of chained queries scattered all over your application | |
# Get all available apple products with price > 1000 and not older than 1 week | |
# BAD | |
products = Product.where(brand: "apple").where("price > ?", 1000).where(delete_flag: false).where("created_at > ?", 1.week.ago) | |
# GOOD | |
scope :price_bigger_than, -> { |min_price| where("price > ?", min_price) } # Yes you can pass parameter to scope | |
scope :for_brand, -> { |brand| where(brand: brand) } | |
scope :un_deleted, where(delete_flag: false) | |
scope :newly_products, -> { |start_date = 1.week.ago| where("created_at > ?", start_date) } # Yes the scope parameter can have a default | |
scope :unavailable, where(delete_flag: true) | |
# Default Scope | |
# The default_scope is also applied while creating/building a record. It is not applied while updating a record. | |
# So, in the following example, new records will have delete_flag set to false upon creation | |
default_scope :available, where(delete_flag: false) | |
Product.create().delete_flag # Will be false | |
products = Product.for_brand("apple").price_bigger_than(1000).newly_products | |
# What if we want to override the default scope?! | |
# Get all apple unavailable products | |
products = Product.for_brand("apple") # Wont work, will get only the available | |
products = Product.for_brand("apple").unavailable # Wont work too, will get only the available | |
products = Product.unscoped.for_brand("apple").unavailable # works perfectly (Y) | |
# Now we want to get products updated today | |
# One can make | |
scope :most_updated, where("updated_at > ?", Date.today) # Watch Out!!! | |
# A scope is defined at initialization, so as you start up your server. Date.today gets executed once and a scope is created so a call to most_updated will return all the products that are updated on the day you start the server! | |
# This must be me done using Lambda OR without scopes | |
scope :most_updated, -> { where("updated_at > ?", Date.today) } | |
# Note: -> AND lambda are equivalent | |
# You can define scope dynamically | |
# Example for an api with api_keys | |
Status = { normal: 0, whitelisted: 1, blacklisted: 2} | |
Status.each_pair do |k,v| | |
scope "status_#{k}", where(status: v) # ApiKey.status_whitelisted,... | |
define_method "status_#{k}?" do # api_key.status_whitelisted?,... | |
self.status == v | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Use service object when: | |
# 1- The action is complex. | |
# 2- The action reaches across multiple models. | |
# 3- The action interacts with an external service (e.g. posting to social networks) | |
# 4- The action is not a core concern of the underlying model (e.g. sweeping up outdated data after a certain time period). | |
# 5- There are multiple ways of performing the action (e.g. authenticating with an access token or password). | |
class Authentication | |
def initialize(params, omniauth = nil) | |
@params = params | |
@omniauth = omniauth | |
end | |
def user | |
@user = @omniauth ? user_from_omniauth : user_with_password | |
end | |
def authenticated? | |
user.present? | |
end | |
private | |
def user_from_omniauth | |
# Authenticate with omniauth | |
end | |
def user_with_password | |
# Authenticate with password | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Not good | |
class User < ActiveRecord::Base | |
validates :appropriate_content | |
def appropriate_content | |
unless # some validation check on name | |
self.errors.add(:name, 'is inappropriate') | |
end | |
end | |
end | |
# Better | |
require 'appropriate_validator' | |
class User < ActiveRecord::Base | |
validate :name, appropriate: true | |
end | |
# /lib/appropriate_validator.rb | |
class AppropriateValidator < ActiveRecord::EachValidator | |
def validate_each(record, attr, value) | |
unless # some validation check on value | |
record.errors.add(:attr, "is inappropriate") | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment