tdc 2012 - patterns e anti-patterns em ruby
DESCRIPTION
Palestra apresentada no The Developers Conference 2012 em São Paulo. Explicação sobre Patterns e Anti-Patterns em Ruby para quem está iniciando a aprender a linguagem.TRANSCRIPT
Donets BasinMina de carvão-Ucrânia
Genius is the gold in the mine; talent is the miner who works and brings it out.Lady Marguerite Blessington
“”
@akitaonrails
Ruby e Ruby on Rails - 2006
Rails Summit Latin America - 2008
RubyConf Brasil - 2010
Patterns e Anti-Patternsem Ruby
Fabio Akita (@akitaonrails)
Pattern PADRÃO
Pattern PADRÃO
Default
STANDARD
“Pattern”
“Standard”
Christopher Alexander
"...each pattern represents our current best guess ... to solve the problem presented. ..., the patterns are still hypotheses, all ... of them -and are therefore all tentative, all free to evolve under the impact of new experience and observation"
—Christopher Alexander, A Pattern Language, p. xv
ERB
require 'erubis'
def render_erb(filepath) content = File.read(filepath) template = Erubis::Eruby.new(content) context = { title: "Hello World", paragraph: "This is an ERB template." } output = template.evaluate(context)end
puts render_erb("sample.erb")
ERB
<html> <head><title><%= @title %></title></head> <body> <p><%= @paragraph %></p> </body></html>
<html> <head><title>Hello World</title></head> <body> <p>This is an ERB template.</p> </body></html>
Markdown
require 'rdiscount'
def render_markdown(filepath) content = File.read(filepath) template = RDiscount.new(content) output = template.to_htmlend
puts render_markdown("sample.md")
Markdown
# Hello World
This is a Markdown example
* [Markdown](http://daringfireball.net/projects/markdown/syntax/)
<h1>Hello World</h1>
<p>This is a Markdown example</p>
<ul><li><a href="http://daringfireball.net/projects/markdown/syntax/">Markdown</a></li></ul>
def render(filepath) case filepath when /erb$/ then render_erb(filepath) when /md$/ then render_markdown(filepath) when /sass$/ then render_sass(filepath) ... endend
puts render("sample.erb")
Strategy
Template Engines supported by Tilt
ENGINE FILE EXTENSIONS REQUIRED LIBRARIES-------------------------- ----------------------- ----------------------------ERB .erb, .rhtml none (included ruby stdlib)Interpolated String .str none (included ruby core)Erubis .erb, .rhtml, .erubis erubisHaml .haml hamlSass .sass haml (< 3.1) or sass (>= 3.1)Scss .scss haml (< 3.1) or sass (>= 3.1)Less CSS .less lessBuilder .builder builderLiquid .liquid liquidRDiscount .markdown, .mkd, .md rdiscountRedcarpet .markdown, .mkd, .md redcarpetBlueCloth .markdown, .mkd, .md blueclothKramdown .markdown, .mkd, .md kramdownMaruku .markdown, .mkd, .md marukuRedCloth .textile redclothRDoc .rdoc rdocRadius .radius radiusMarkaby .mab markabyNokogiri .nokogiri nokogiriCoffeeScript .coffee coffee-script (+ javascript)Creole (Wiki markup) .wiki, .creole creoleWikiCloth (Wiki markup) .wiki, .mediawiki, .mw wikiclothYajl .yajl yajl-ruby
ERB and Markdown through Tilt
require 'tilt'
template = Tilt.new("sample.erb")context = { title: "Hello World", paragraph: "This is an ERB template." }output = template.render(context)
puts output
template = Tilt.new("sample.md")output = template.render
puts output
Same example as previously
require 'tilt'
template = Tilt::ErubisTemplate.new("sample.erb")context = { title: "Hello World", paragraph: "This is an ERB template." }output = template.render(context)
puts output
template = Tilt::RDiscountTemplate.new("sample.md")output = template.render
puts output
tilt.rb
module Tilt ... @preferred_mappings = Hash.new @template_mappings = Hash.new { |h, k| h[k] = [] }
# Hash of template path pattern => template implementation class mappings. def self.mappings @template_mappings end ... # Register a template implementation by file extension. def self.register(template_class, *extensions) if template_class.respond_to?(:to_str) # Support register(ext, template_class) too extensions, template_class = [template_class], extensions[0] end
extensions.each do |ext| ext = normalize(ext) mappings[ext].unshift(template_class).uniq! end end ...
Tilt#register
require 'tilt/string'register StringTemplate, 'str'
require 'tilt/erb'register ERBTemplate, 'erb', 'rhtml'register ErubisTemplate, 'erb', 'rhtml', 'erubis'
require 'tilt/haml'register HamlTemplate, 'haml'
require 'tilt/css'register SassTemplate, 'sass'register ScssTemplate, 'scss'register LessTemplate, 'less'
...
Tilt#[]
> Tilt["erb"]=> Tilt::ErubisTemplate
> Tilt["md"]=> Tilt::RDiscountTemplate
> Tilt["haml"]=> Tilt::HamlTemplate
> Tilt['coffee']=> Tilt::CoffeeScriptTemplate
Tilt overrided #new
module Tilt ... # Create a new template for the given # file using the file's extension # to determine the the template mapping. def self.new(file, line=nil, options={}, &block) if template_class = self[file] template_class.new(file, line, options, &block) else fail "No template engine registered for #{File.basename(file)}" end end
Factory
Tilt::Template Lint
module Tilt # Base class for template implementations. # Subclasses must implement the #prepare method and one # of the #evaluate or #precompiled_template methods. class Template ... def render(scope=Object.new, locals={}, &block) evaluate scope, locals || {}, &block end
protected def prepare; ... end def evaluate(scope, locals, &block); ... end def precompiled(locals); ... end def precompiled_template(locals); ... end def precompiled_preamble(locals); ... end def precompiled_postamble(locals); ''; end def compiled_method(locals_keys); ... end endend
Tilt::RedClothTemplate Implementation
module Tilt # RedCloth implementation. See: # http://redcloth.org/ class RedClothTemplate < Template def self.engine_initialized? defined? ::RedCloth end
def initialize_engine require_template_library 'redcloth' end
def prepare @engine = RedCloth.new(data) @output = nil end
def evaluate(scope, locals, &block) @output ||= @engine.to_html end endend
Adapter
Simple Web
Mongrel
Thin
Webrick
Rails
Sinatra
Ramaze
CGI
Simple Web
Mongrel
Thin
Webrick
Rails
Sinatra
Ramaze
CGI
Simple Web
Mongrel
Thin
Webrick
Rails
Sinatra
Ramaze
CGIRack
Minimal Rack Compliant
class HelloWorld def call(env) [200, {"Content-Type" => "text/plain"}, ["Hello world!"]] endend
hello_world = ->(env) { [200, {"Content-Type" => "text/plain"}, ["Hello world!"]] }
rackup app.ru
hello_world = ->(env) { [200, {"Content-Type" => "text/html"}, ["<h1>Hello world!</h1>"]] }
use Rack::ContentType, "text/html"use Rack::ShowExceptionsuse Rack::Auth::Basic, "Rack Demo" do |username, password| 'secret' == passwordend
# Setup Rackrun Rack::URLMap.new( { "/hello" => hello_world, "/" => Rack::File.new( "index.html" )} )
rackup app.ru
module Rack class ContentType include Rack::Utils
def initialize(app, content_type = "text/html") @app, @content_type = app, content_type end
def call(env) status, headers, body = @app.call(env) headers = Utils::HeaderHash.new(headers)
unless STATUS_WITH_NO_ENTITY_BODY.include?(status) headers['Content-Type'] ||= @content_type end
[status, headers, body] end endend
rake middleware (Rails)
use ActionDispatch::Staticuse Rack::Lock...use Rack::Runtimeuse Rack::MethodOverrideuse ActionDispatch::RequestIduse Rails::Rack::Loggeruse ActionDispatch::ShowExceptionsuse ActionDispatch::DebugExceptionsuse ActionDispatch::RemoteIpuse ActionDispatch::Reloaderuse ActionDispatch::Callbacksuse ActiveRecord::ConnectionAdapters::ConnectionManagementuse ActiveRecord::QueryCacheuse ActionDispatch::Cookiesuse ActionDispatch::Session::CookieStoreuse ActionDispatch::Flashuse ActionDispatch::ParamsParseruse ActionDispatch::Headuse Rack::ConditionalGetuse Rack::ETaguse ActionDispatch::BestStandardsSupportuse Warden::Manageruse OmniAuth::Strategies::Twitteruse OmniAuth::Strategies::Facebookrun Rubyconf2012::Application.routes
Chain of Responsibility
class Relationship attr_accessor :state def initialize @state = :dating end def get_married make_vows @state = :married eat_wedding_cake end def get_divorced @state = :divorced end
def make_vows; "I do"; end def eat_wedding_cake; "Yummy"; endend
class Relationship attr_accessor :state def initialize @state = :dating end def get_married raise "Must date before marry" unless @state == :dating make_vows @state = :married eat_wedding_cake end def get_divorced raise "Must be married before divorce" unless @state == :married @state = :divorced end
def make_vows; "I do"; end def eat_wedding_cake; "Yummy"; endend
class Relationship include AASM
aasm do state :dating, initial: true state :married state :divorced
event :get_married, :before => :make_vows, :after => :eat_wedding_cake do transitions from: [:dating], to: :married end event :get_divorced do transitions from: [:married], to: :divorced end end def make_vows; "I do"; end def eat_wedding_cake; "Yummy"; endend
class Relationship attr_accessor :dating, :married, :divorced def initialize @dating, @married, @divorced = true, false, false end def get_married raise "Must date before marry" unless dating make_vows @dating, @married = false, true eat_wedding_cake end def get_divorced raise "Must be married before divorce" unless married @married, @divorced = false, true end
def make_vows; "I do"; end def eat_wedding_cake; "Yummy"; endend
State
class SaleOrder attr_accessor :items, :value, :checkout_date def initialize(*args) @items, @value, @checkout_date = args endend
sale = SaleOrder.new( ['Biscuit', 'Cheese'], 15.0, "2012-07-07")
require 'money'require 'time'class SaleOrder attr_reader :value, :checkout_date def initialize(options = {}) @items, @value, @checkout_date = [], Money.new(0.0, "USD"), nil self.items = options[:items] || [] self.value = options[:value] || 0.0 self.checkout_date = options[:checkout_date] end def items=(items); @items += items.dup; end def items(index); @items[index]; end def value=(value); @value = Money.new(value.to_f, "USD"); end def checkout_date=(date) @checkout_date = Date.parse(date) if date endend
sale = SaleOrder.new(items: ['Biscuit', 'Cheese'], value: 15.0, checkout_date: "2012-07-07")
Primitive Obsession
http://u.akita.ws/avdi-null
def slug(title) # 1 if title.nil? title.strip.downcase.tr_s('^[a-z0-9]', '-') end
# 2 title.strip.downcase.tr_s('^[a-z0-9]', '-') if title
# 3 (title || "").strip.downcase.tr_s('^[a-z0-9]', '-')
# 4 title.strip.downcase.tr_s('^[a-z0-9]', '-') rescue nilend
ActiveSupport - Try
class Object def try(*a, &b) if a.empty? && block_given? yield self else public_send(*a, &b) end endend
class NilClass def try(*args) nil endend
http://u.akita.ws/avdi-null
require 'active_support/core_ext/object/try'
def slug(title) title.try(:strip).try(:downcase). try(:tr_s, '^[a-z0-9]', '-')end
<%= title ? title.downcase : "" %>
<%= title.try(:downcase) %>
http://u.akita.ws/avdi-null
class NullObject def method_missing(*args, &block) self end def to_a; []; end def to_s; ""; end def to_f; 0.0; end def to_i; 0; end def tap; self; end def to_value; nil; endend
def Maybe(value) value.nil? ? NullObject.new : valueend
class Object def to_value self endend
http://u.akita.ws/avdi-null
def slug(title) Maybe(title).strip.downcase.tr_s('^[a-z0-9]', '-')end
def slug(title) title = Maybe(title) if title.to_value # do something useful end title.strip.downcase.tr_s('^[a-z0-9]', '-')end
http://u.akita.ws/avdi-null
git clone git://github.com/akitaonrails/null.gitcd nullgem build null.gemspecgem install null-0.1.gem
> require 'null'> Maybe(someobj).foo.bar.something#=> null
> (100.0 / (NULL * 15.5) - 150)#=> null
object = Maybe(someobj)if object.truthy? # something usefulend
Null Object
class SalesOrder < Struct.new(:products, :total, :buyer_name) def html_receipt html_items = products.inject("") do |html, item| html += "<li>#{item}</li>" end
html = %{<h1>Thanks for the Purchase #{buyer_name}!</h1> <p>You purchased:</p> <ul> #{html_items} </ul> <p>Total: $#{total}</p>} end end
order = SalesOrder.new(["Bacon", "Cheese"], 10.0, "John Doe" )
> order.html_receipt
=> "<h1>Thanks for the Purchase John Doe!</h1> <p>You purchased:</p> <ul> <li>Bacon</li><li>Cheese</li> </ul> <p>Total: $10.0</p>"
class SalesOrder < Struct.new(:products, :total, :buyer_name) def html_receipt html_items = products.inject("") do |html, item| html += "<li>#{item}</li>" end
html = %{<h1>Thanks for the Purchase #{buyer_name}!</h1> <p>You purchased:</p> <ul> #{html_items} </ul> <p>Total: $#{total}</p>} end end
order = SalesOrder.new(["Bacon", "Cheese"], 10.0, "John Doe" )
class SalesOrder < Struct.new(:products, :total, :buyer_name)end
class SalesOrderDecorator < SimpleDelegator def html_receipt html_items = products.inject("") do |html, item| html += "<li>#{item}</li>" end
html = %{<h1>Thanks for the Purchase #{buyer_name}!</h1> <p>You purchased:</p> <ul> #{html_items} </ul> <p>Total: $#{total}</p>} end end
order = SalesOrder.new(["Bacon", "Cheese"], 10.0, "John Doe" )decorated_order = SalesOrderDecorator.new(order)
> decorated_order.html_receipt
=> "<h1>Thanks for the Purchase John Doe!</h1> <p>You purchased:</p> <ul> <li>Bacon</li><li>Cheese</li> </ul> <p>Total: $10.0</p>" > decorated_order.total
=> 10.0
> decorated_order.products
=> ["Bacon", "Cheese"]
# originalorder = SalesOrder.new(["Bacon", "Cheese"], 10.0, "John Doe" )decorated_order = SalesOrderDecorator.new(order)
# newclass SalesOrderDecorator < SimpleDelegator def initialize(*args) if args.first.is_a?(SalesOrder) super(args.first) else order = SalesOrder.new(*args) super(order) end end ... end
decorated_order = SalesOrderDecorator.new(["Bacon", "Cheese"], 10.0, "John Doe" )decorated_order.html_receipt
Decorator/Presenter
Strategy
Factory
Adapter (Proxy, Bridge)
Chain of Responsibility
State
Primitive Obsession
Null Object
Decorator (Presenter)
www.codeminer42.com.br+55 11 3729 14 22
slideshare.net/akitaonrails
MUITO OBRIGADO