diff --git a/composite/README.md b/composite/README.md index 5170c38..97a9eff 100644 --- a/composite/README.md +++ b/composite/README.md @@ -1,10 +1,10 @@ -problem borrowed from [here](https://refactoring.guru/design-patterns/composite) +Problem borrowed from [here](https://refactoring.guru/design-patterns/composite) -Let's say that I am building inventory tracking software for a company that has to package and ship complex orders of products. An order is a box. A box can contain other boxes or products. I need a way to get the total weight of any given box. The composite pattern let's me strucutre all of this as a tree. Both the `Box` and `Product` type respond to the `weight` method. Products know their own weight. Boxes ask all of their direct children their weight and return the sum. +Let's say that I am building inventory tracking software for a company that has to package and ship complex orders of products. An order is a box. A box can contain other boxes or products. I need a way to get the total weight and inventory count of any given box. The composite pattern let's me strucutre all of this as a tree. Both the `Box` and `Product` type respond to the `weight` and `item_count` method. Products know their own weight and that they have an item count of 1 (duh). Boxes ask all of their direct children their weight and item_count and return the sum. Recursion! ``` -require './box' -require './product' +require './v1/box' +require './v1/product' b1 = Box.new hammer_box = Box.new @@ -13,7 +13,7 @@ hammer = Product.new 'hammer', 10.0 hammer_box.add_item hammer b1.add_item hammer_box -recipt = Product.new 'recipt', 0.1 +recipt = Product.new 'recipt', 0 b1.add_item recipt other_box = Box.new @@ -25,15 +25,83 @@ phone_box.add_item phone phone_box.add_item headphones charger_box = Box.new -charger = Product.new 'charger', 0.1 +charger = Product.new 'charger', 0.2 charger_box.add_item charger other_box.add_item phone_box other_box.add_item charger_box b1.add_item other_box -puts b1.instance_variable_get '@contents' -puts b1.weight + +puts "shipment: #{b1.item_count} item(s), #{b1.weight} lbs" +``` + +This is convenient because we have a common interface for getting at item's (either product or collection of products) weight and count. Think of it another way, if I want to know the weight of an item, I shouldn't have to be concerned with dumping out its entire contents and computing it myself. Instead just pass the `weight` message and let my objects do their thing. + +The above code gets the job done, but its awfully verbose. Defining a product on one line and then telling it what box it goes in is tedious. Instead lets use the builder pattern and blocks to specify how we want things structured. + +``` +require './v2/shipment' + +s = Shipment.new do + box do + product 'hammer', 10.0 + end + + box do + box do + product 'phone', 1.0 + product 'headphones', 0.1 + end + + box do + product 'charger', 0.2 + end + end + + product 'recipt', 0 +end + +puts "shipment: #{s.item_count} item(s), #{s.weight} lbs" +``` + +Ah, much better! Now we have a `Shipment` instance which represents all of the items in a shipment. `Shipment` has two public methods `box` and `product` to let us define those items + +``` + def box(&block) + box = Box.new + parent.pack box + add_node(box, &block) + end + + def product(name, weight) + product = Product.new(name, weight) + parent.pack product + add_node(product) + end +``` + +Both of those methods use a method called `add_node` to add keep track of the item + +``` + # ads a node to the shipment tree. + # node: a node like object (Box or Product) + def add_node(node, &block) + # apppend node to list of nodes + # if no block given return + # push node to the stack + # eval node's block + # -> this will lead to some recursion + # when we resurface (all children have been delt with), pop node from the stack + + @nodes << node + + return unless block_given? + + @nodes_stack << node + instance_eval(&block) + @nodes_stack.pop + end ``` -This is convenient because we have a common interface for getting at item (either product or collection of products) weight. Think of it another way, if I want to know the weight of an item, I shouldn't have to be concerned with dumping out its entire contents and computing it myself. Instead just pass the `weight` message and let my objects do their thing. \ No newline at end of file +Basically we are recursively evaling blocks until we get to a leaf node (product). It uses a stack to keep track of previous items added. At any given time when we are adding an item, its parent is the last item on the stack. If the stack is empty the item's parent is the top level box (stored as `@root_box`). Shipment handles creating an item, adding it to the stack and loading it into its parent box. The win here is that we've now created an interface that lets us simply express the structure of a shipment, and not worry about the messy details of loading products and boxes in to their parent. \ No newline at end of file diff --git a/composite/main.rb b/composite/main.rb index f2d15d5..4fd1ae9 100644 --- a/composite/main.rb +++ b/composite/main.rb @@ -1,59 +1,54 @@ -require './box' -require './product' +require './v1/box' +require './v1/product' -# b1 = Box.new -# hammer_box = Box.new -# hammer = Product.new 'hammer', 10.0 +b1 = Box.new +hammer_box = Box.new +hammer = Product.new 'hammer', 10.0 -# hammer_box.add_item hammer -# b1.add_item hammer_box +hammer_box.add_item hammer +b1.add_item hammer_box -# recipt = Product.new 'recipt', 0.1 -# b1.add_item recipt +recipt = Product.new 'recipt', 0 +b1.add_item recipt -# other_box = Box.new +other_box = Box.new -# phone_box = Box.new -# phone = Product.new 'phone', 1.0 -# headphones = Product.new 'headpones', 0.1 -# phone_box.add_item phone -# phone_box.add_item headphones +phone_box = Box.new +phone = Product.new 'phone', 1.0 +headphones = Product.new 'headpones', 0.1 +phone_box.add_item phone +phone_box.add_item headphones -# charger_box = Box.new -# charger = Product.new 'charger', 0.1 -# charger_box.add_item charger +charger_box = Box.new +charger = Product.new 'charger', 0.2 +charger_box.add_item charger -# other_box.add_item phone_box -# other_box.add_item charger_box +other_box.add_item phone_box +other_box.add_item charger_box -# b1.add_item other_box -# puts b1.instance_variable_get '@contents' -# puts b1.weight +b1.add_item other_box -require './shipment' +puts "shipment: #{b1.item_count} item(s), #{b1.weight} lbs" -outer_box = Box.new do - item Box.new do - item Product.new 'hammer', 10.0 - end - - # item Product.new 'recipt', 0.1 +require './v2/shipment' - # medium_box = Box.new do |mb| - # phone_box = Box.new do |pb| - # pb.add_item Product.new 'phone', 1 - # pb.add_item Product.new 'headphones', 0.1 - # end +s = Shipment.new do + box do + product 'hammer', 10.0 + end - # charger_box = Box.new do |cb| - # cb.add_item Product.new 'charger', 0.1 - # end + box do + box do + product 'phone', 1.0 + product 'headphones', 0.1 + end - # mb.add_item phone_box - # mb.add_item charger_box - # end + box do + product 'charger', 0.2 + end + end - # b.add_item medium_box + product 'recipt', 0 end -puts outer_box.weight +puts "shipment: #{s.item_count} item(s), #{s.weight} lbs" \ No newline at end of file diff --git a/composite/shipment.rb b/composite/shipment.rb deleted file mode 100644 index 78993cc..0000000 --- a/composite/shipment.rb +++ /dev/null @@ -1,20 +0,0 @@ -require './box' -require './product' - -class Shipment - attr_reader :nodes - - def initialize(&block) - @nodes = [] - - @nodes_stack = [] - - instance_eval(&block) - - freeze - end - - def box - parent = @nodes_stack.empty? ? Box::ROOT : @nodes_stack.alst - end -end diff --git a/composite/tty_tree.md b/composite/tty_tree.md deleted file mode 100644 index 6ecd14c..0000000 --- a/composite/tty_tree.md +++ /dev/null @@ -1,30 +0,0 @@ -I'm trying to work with nested blocks. Something like - -``` -outer_box = Box.new do - item Box.new do - item Product.new 'hammer', 10.0 - end -end -``` - -would create a box, containing a box, containing a hammer. That code however does not work as expected. I found [this library](https://github.com/piotrmurach/tty-tree) which seems to basically do the same thing - -``` -tree = TTY::Tree.new do - node 'dir1' do - node 'config.dat' - node 'dir2' do - node 'dir3' do - leaf 'file3-1.txt' - end - leaf 'file2-1.txt' - end - node 'file1-1.txt' - leaf 'file1-2.txt' - end -end -``` - -Let's see if I can dig into that code to see how they did it. - diff --git a/composite/box.rb b/composite/v1/box.rb similarity index 57% rename from composite/box.rb rename to composite/v1/box.rb index 4e532a5..9663108 100644 --- a/composite/box.rb +++ b/composite/v1/box.rb @@ -1,13 +1,11 @@ class Box attr_reader :contents - ROOT = Box.new() - def initialize(parent) - @parent.parent + def initialize + @contents = [] end - def item(item) - puts "adding item #{item}" + def add_item(item) @contents << item end @@ -15,11 +13,9 @@ def weight @contents.inject(0.0) { |total, item| total + item.weight } end - # recursivly prints a box's complete contents - def full_inventory - + def item_count + @contents.inject(0) { |total, item| total + item.item_count } end - def to_s "" end diff --git a/composite/v1/product.rb b/composite/v1/product.rb new file mode 100644 index 0000000..0c446d2 --- /dev/null +++ b/composite/v1/product.rb @@ -0,0 +1,12 @@ +class Product + attr_reader :name, :weight, :item_count + def initialize(name, weight) + @name = name + @weight = weight + @item_count = 1 + end + + def to_s + "" + end +end \ No newline at end of file diff --git a/composite/v2/box.rb b/composite/v2/box.rb new file mode 100644 index 0000000..8a1064f --- /dev/null +++ b/composite/v2/box.rb @@ -0,0 +1,26 @@ +class Box + attr_reader :contents + + def initialize + @contents = [] + end + + # load an item into this box + # item: either a Box or a Product + def pack(item) + @contents << item + end + + def weight + @contents.inject(0.0) { |total, item| total + item.weight } + end + + def item_count + @contents.inject(0) { |total, item| total + item.item_count } + end + + def to_s + "" + end +end + diff --git a/composite/product.rb b/composite/v2/product.rb similarity index 71% rename from composite/product.rb rename to composite/v2/product.rb index 3bc4632..8514110 100644 --- a/composite/product.rb +++ b/composite/v2/product.rb @@ -1,8 +1,9 @@ class Product - attr_reader :name, :weight + attr_reader :name, :weight, :item_count def initialize(name, weight) @name = name @weight = weight + @item_count = 1 end def to_s diff --git a/composite/v2/shipment.rb b/composite/v2/shipment.rb new file mode 100644 index 0000000..119a02f --- /dev/null +++ b/composite/v2/shipment.rb @@ -0,0 +1,59 @@ +require './v2/box' +require './v2/product' + +class Shipment + def initialize(&block) + @nodes_stack = [] + @root_box = Box.new + instance_eval(&block) + freeze + end + + def box(&block) + box = Box.new + parent.pack box + add_node(box, &block) + end + + def product(name, weight) + product = Product.new(name, weight) + parent.pack product + add_node(product) + end + + # how many products in this shipment? + def item_count + @root_box.item_count + end + + def weight + @root_box.weight + end + + private + + # ads a node to the shipment tree. + # node: a node like object (Box or Product) + def add_node(node, &block) + # apppend node to list of nodes + # if no block given return + # push node to the stack + # eval node's block + # -> this will lead to some recursion + # when we resurface (all children have been delt with), pop node from the stack + + return unless block_given? + + @nodes_stack << node + instance_eval(&block) + @nodes_stack.pop + end + + # returns the parent of the node being created. + # this is defined as the top node on the stack. + # If stack is empty, it means its a top level item of the shipment. + # push it to the @root_box + def parent + @nodes_stack.empty? ? @root_box : @nodes_stack.last + end +end