Skip to content

Instantly share code, notes, and snippets.

@modsaid
Forked from AhmedElSharkasy/best_practices
Last active January 2, 2016 07:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save modsaid/8273218 to your computer and use it in GitHub Desktop.
Save modsaid/8273218 to your computer and use it in GitHub Desktop.
Rails as it has never been before :)
# 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- Lambda in a method will return the value to the method and the method continues normally, while Proc stops method execution
def proc_return
Proc.new{ |n| puts n }
puts 'see me if you can'
end
def lambda_return
lambda{ |n| puts n }
puts 'hi, i am here'
end
proc_return
=> Proc
lambda_return
=> hi, i am here
# Note proc can't have a return in it , while lambda can
# The same can be done using method objects
def do_math_operation(amount, method)
block.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
# 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 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
# If you forgot this line all instances of ActiveRecord::Base will have these methods !
include InstanceMethods
end
module InstanceMethods
def sponsors_count
end
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
# 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
# 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
# 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
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
# 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>
# 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
# 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
# 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
# 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
# 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
# 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