Skip to content

Instantly share code, notes, and snippets.

@urfolomeus
Created March 23, 2011 08:58
Show Gist options
  • Save urfolomeus/882813 to your computer and use it in GitHub Desktop.
Save urfolomeus/882813 to your computer and use it in GitHub Desktop.
Proving to myself Rory was right :P
# This is a basic implementation and is essentially what Rory had.
# I just had to prove to myself that it worked. ;)
# Decorator pattern is precisely the way to go here.
# This is the base class that we want to decorate.
# It needs to have a default method for each of the lifecycle events.
# i.e. what does it do in the event that there are no products.
class Post
attr_accessor :products, :featured, :hidden
# In the Rails app has_many :products, :as => decoratable would go here
def initialize
@products = Array.new
@hidden = true
@featured = false
end
def payment_successful
@hidden = false
end
# Dictates what happens when a SellableItem is purchased.
# In real life there would be a generic purchased method
# in Sellable which would prompt the author to provide
# a purchased method in the SellableItem class.
def purchased(product)
@products << product
end
# this method will be generic to all classes that can have products
# so should probably be in a module too
def decorators
@products.map{|p| p.name}
end
end
# This mimics what will be a record in the database in the real world.
# Having a separate record for each owner and product gives the most flexibility,
# although you could just have a string per owner with the full list of products.
# ID DECORATABLE_TYPE DECORATABLE_ID PRODUCT_NAME
# == ================ ============== =================
# 1 Post 1 'FeaturedProduct'
# 2 Post 1 'SaleProduct'
# 3 Badger 2 '3rdPartyProduct'
# 4 Mushroom 3 'FeaturedProduct'
#
# Or: -
#
# ID DECORATABLE_TYPE DECORATABLE_ID PRODUCT_NAME
# == ================ ============== =============================
# 1 Post 1 'FeaturedProduct,SaleProduct'
# 2 Badger 2 '3rdPartyProduct'
# 3 Mushroom 3 'FeaturedProduct'
#
# Obviously the latter option adds some extra string monging to the process.
# I've gone with the first option here for simplicity.
class Product
attr_accessor :name
# In the Rails app belongs_to :decoratable, :polymorphic => true goes here
def initialize(name)
@name = name
end
end
# This little nugget of joy from Luke Redpath (http://lukeredpath.co.uk/blog/decorator-pattern-with-ruby-in-8-lines.html)
# basically allows any PORO to become a decorator
module Decorator
def initialize(decorated)
@decorated = decorated
end
# If the decorator doesn't implement the method, chuck it up the call stack
# until you either find a class that does or you run out of classes
# and then throw a NoMethodMissing for real, which is handled by the
# ProductLifecycleEventHandler (see below)
def method_missing(method, *args)
args.empty? ? @decorated.send(method) : @decorated.send(method, args)
end
end
# An example of a decorator
# When this is called it will call the event handler on it's caller
# and then implement it's own behaviour (i.e. classic decorator)
class FeaturedProduct
include Decorator
def payment_successful
@decorated.payment_successful
@decorated.featured = true
end
end
# Handles all calls to lifecycle events
class ProductLifecycleEventHandler
# Builds the method chain required to execute all necessary behaviour
def self.get_decorators_for(decorated)
begin
decorated.decorators.inject(decorated) { |object_chain, product| Kernel.const_get(product).new(object_chain) }
rescue NameError => error_message
puts "No such decorator: #{error_message}"
end
end
# Event handlers
def self.payment_successful(item)
do_eet(item, 'payment_successful')
end
def self.admin_review(item)
do_eet(item, 'admin_review')
end
# TODO: needs better name ;)
def self.do_eet(item, method)
begin
decorated_item = get_decorators_for(item)
decorated_item.send(method)
rescue NoMethodError # in the base class doesn't implement the method, warn the author
puts "#{item.class.name} does not implement #{method}"
end
end
end
# This code gives an executable example
post = Post.new
# This would normally be called from Order
# and only sets up the products attached to Post
# when it is purchased.
product = Product.new('FeaturedProduct')
post.purchased(product)
# To prove nothing has been modified yet
puts "Post is featured = #{post.featured}"
puts "Post is hidden = #{post.hidden}"
# when Order receives a successful payment it would call
# the ProductLifecycleEventHandler, passing in the SellableItem
ProductLifecycleEventHandler.payment_successful(post)
# Now we see the values have changed according to the given decorators
puts "Post is featured = #{post.featured}"
puts "Post is hidden = #{post.hidden}"
# This demonstrates what happens if an unhandled event is called
ProductLifecycleEventHandler.admin_review(post)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment