Created
March 23, 2011 08:58
-
-
Save urfolomeus/882813 to your computer and use it in GitHub Desktop.
Proving to myself Rory was right :P
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
# 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