Skip to content

Instantly share code, notes, and snippets.

@kchens
Created April 7, 2015 04:58
Show Gist options
  • Save kchens/97c691ecee11176ce5e8 to your computer and use it in GitHub Desktop.
Save kchens/97c691ecee11176ce5e8 to your computer and use it in GitHub Desktop.
POODR: Chapter 8 - Combining Objects With Composition
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Updating the Bicycle Class
# 1 -------------------Okay
class Bicycle
attr_reader :size, :parts
def initialize(args={})
@size = args[:size]
@parts = args[:parts]
end
def spares
parts.spares
end
end
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Creating a Parts Hiearchy
# 2 -------------------Okay
class Parts
attr_reader :chain, :tire_size
def initialize(args={})
@chain = args[:chain] || default_chain
@tire_size = args[:tire_size] || default_tire_size
post_initialize(args)
end
def spares
{ tire_size: tire_size,
chain: chain }.merge(local_spares)
end
def default_tire_size
raise NotImplementedError
end
# subclasses may override
def post_initialize(args)
nil
end
def local_spares
{}
end
def default_chain
'10-speed'
end
end
class RoadBikeParts < Parts
attr_reader :tape_color
def post_initialize(args)
@tape_color = args[:tape_color]
end
def local_spares
{ tape_color: tape_color }
end
def default_tire_size
'23'
end
end
class MountainBikeParts < Parts
attr_reader :front_shock, :rear_shock
def post_initialize(args)
@front_shock = args[:front_shock]
@rear_shock = args[:rear_shock]
end
def local_spares
{ rear_shock: rear_shock }
end
def default_tire_size
'2.1'
end
end
# 3 -------------------Okay
road_bike = Bicycle.new(
size: 'L',
parts: RoadBikeParts.new(tape_color: 'red'))
road_bike.size
road_bike.spares
mountain_bike = Bicycle.new(
size: 'L',
parts: MountainBikeParts.new(rear_shock: 'Fox'))
mountain_bike.size # 'L'
mountain_bike.spares # {:tire_size => '2.1',
# :chain => '10-speed',
# :rear_shock => 'Fox'}
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Creating a Part
# 4 -------------------Okay
class Bicycle
attr_reader :size, :parts
def initialize(args={})
@size = args[:size]
@parts = args[:parts]
end
def spares
parts.spares
end
end
class Parts
attr_reader :parts
def initialize(parts)
@parts = parts
end
def spares
parts.select { |part| part.needs_spare}
end
end
class Part
attr_reader :name, :description, :needs_spare
def initialize(args)
@name = args[:name]
@description = args[:description]
@needs_spare = args.fetch(:needs_spare, true)
end
end
chain = Part.new(name: 'chain', description: '10-speed')
road_tire = Part.new(name: 'tire_size',  description: '23')
tape = Part.new(name: 'tape_color', description: 'red')
mountain_tire = Part.new(name: 'tire_size',  description: '2.1')
rear_shock = Part.new(name: 'rear_shock', description: 'Fox')
front_shock = Part.new(name: 'front_shock', description: 'Manitou', needs_spare: false)
# Create Parts from multiple Part
road_bike_parts = Parts.new([chain, road_tire, tape])
# 5 -------------------Okay
road_bike = Bicycle.new( size: 'L', parts: Parts.new([ chain, road_tire, tape]))
road_bike.size # 'L'
road_bike.spares
# -> [#<Part:0x00000101036770
#         @name="chain",
#         @description="10-speed",
#         @needs_spare=true>,
#  #<Part:0x0000010102dc60
#         @name="tire_size",
#         etc ...”
mountain_bike = Bicycle.new(
size: 'L',
parts: Parts.new([chain,
mountain_tire,
front_shock,
rear_shock]))
mountain_bike.size # 'L'
mountain_bike.spares
# -> [#<Part:0x00000101036770
#         @name="chain",
#         @description="10-speed",
#         @needs_spare=true>,
#     #<Part:0x0000010101b678
#         @name="tire_size",
#         etc ...”
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Making the Parts Object More Like an Array
# 5 -------------------Bad
mountain_bike.spares.size # 3
# mountain_bike.parts.size # NoMethodError
# This can be fixed by adding #size method to Parts
def size
parts.size
end
# If this happens, then we should just use inheritance
class Parts < Array
def spares
select { |part| part.needs_spare }
end
end
# Parts inherits '+' from Array, so you can
#    add two Parts together.
combo_parts =  (mountain_bike.parts + road_bike.parts)
# '+' definitely combines the Parts
combo_parts.size            # -> 7
# but the object that '+' returns
#   does not understand 'spares'
combo_parts.spares
# -> NoMethodError: undefined method 'spares'
#      for #<Array:...>
mountain_bike.parts.class   # -> Parts
road_bike.parts.class       # -> Parts
combo_parts.class           # -> Array !!!
# 6 -------------------Okay
require 'forwardable'
class Parts
extend Forwardable
def_delegators :@parts, :size, :each
include Enumerable
def initialize(parts)
@parts = parts
end
def spares
select { |part| part.needs_spare }
end
end
mountain_bike = Bicycle.new(
size: 'L',
parts: Parts.new([ chain,
mountain_tire,
front_shock,
rear_shock]))
mountain_bike.spares.size # 3
mountain_bike.parts.size # 4
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Manufacturing Parts
# 7 -------------------Bad
road_config = [['chain', '10-speed'], ['tire_size', '23'], ['tape_color', 'red']]
mountain_config = [['chain', '10-speed'],
['tire_size', '2.1'],
['front_shock', 'Manitou', false],
['rear_shock',   'Fox']]
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Creating the PartsFactory
# 7 -------------------Good
module PartsFactory
def self.build(config, part_class = Part, parts_class = Parts)
parts_class.new(
config.collect do |part_config|
part_class.new(
name: part_config[0],
description: part_config[1],
needs_spare: part_config.fetch(2, true))}
)
end
)
end
end
road_parts = PartsFactory.build(road_config)
mountain_parts = PartsFactory.build(mountain_config)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Leveraging the PartsFactory
# 8 -------------------Okay
class Part
attr_reader :name, :description, :needs_spare
def initialize(args)
@name = args[:name]
@description = args[:description]
@needs_spare = args.fetch(:needs_spare, true)
end
end
# 9 -------------------Good
require 'ostruct' #takes a hash as an argument
module PartsFactory
def self.build(config, parts_class = Parts)
parts_class.new(
config.collect do |part_config|
create_part(part_config)})
end
)
end
def self.create_part(part_config)
OpenStruct.new(
name: part_config[0],
description: part_config[1],
needs_spare: part_config.fetch(2, true)}
)
end
end
mountain_parts = PartsFactory.build(mountain_config)
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# The Composed Bicycle
# 10 -------------------Good
class Bicycle
attr_reader :size, :parts
def initialize(args={})
@size = args[:size]
@parts = args[:parts]
end
def spares
parts.spares
end
end
require 'forwardable'
class Parts
extend Forwardable
def_delegators :@parts, :size, :each
include Enumerable
def initialize(parts)
@parts = parts
end
def spares
select { |part| part.needs_spare }
end
end
require 'ostruct'
module PartsFactory
def self.build(config, parts_class = Parts)
parts_class.new( config.collect do |part_config|
create_part(part_config)
end
)
end
end
road_config = [[ 'chain', '10-speed'], ['tire_size', '23'], ['tape_color', 'red']]
mountain_config = [['chain', '10-speed'], ['tire_size', '2.1'],
['front_shock', 'Manitou', false], ['rear_shock', 'Fox']]
road_bike = Bicycle.new( size: 'L', parts: PartsFactory.build(road_config))
road_bike.spares
# -> [#<OpenStruct name="chain", etc ...
mountain_bike = Bicycle.new(size: 'L', parts: PartsFactory.build(mountain_config))
mountain_bike.spares
# -> [#<OpenStruct name="chain", etc ...”
recumbent_config = [['chain', '9-speed'],
['tire_size', '28'],
['flag', 'tall and orange']]
recumbent_bike = Bicycle.new(size: 'L', parts: PartsFactory.build(recumbent_config))
recumbent_bike.spares
# -> [#<OpenStruct
#       name="chain",
#       description="9-speed",
#       needs_spare=true>,
#     #<OpenStruct
#       name="tire_size",
#       description="28",
#       needs_spare=true>,
#     #<OpenStruct
#       name="flag",
#       description="tall and orange",
#       needs_spare=true>]”
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment