From 4cac0dff19ecb8938ddc2a43270d3032afa08930 Mon Sep 17 00:00:00 2001 From: Jacob Stoebel Date: Sun, 11 Aug 2019 13:23:07 -0400 Subject: [PATCH] add compisiter --- README.md | 3 + adapter/README.md | 47 ++++++++++++ adapter/color_cannon.rb | 20 +++++ adapter/color_cannon_adaptor.rb | 18 +++++ adapter/main.rb | 14 ++++ builder/README.md | 89 +++++++++++++++++++++++ builder/car_builder.rb | 32 ++++++++ builder/car_new.rb | 14 ++++ builder/main.rb | 35 +++++++++ builder/parts.rb | 23 ++++++ builder/tedious_car.rb | 10 +++ command/README.md | 125 ++++++++++++++++++++++++++++++++ command/commands.rb | 27 +++++++ command/hvac.rb | 15 ++++ command/main.rb | 33 +++++++++ command/new_wall_panel.rb | 19 +++++ command/wall_panel.rb | 29 ++++++++ composite/README.md | 39 ++++++++++ composite/box.rb | 26 +++++++ composite/main.rb | 59 +++++++++++++++ composite/product.rb | 11 +++ composite/shipment.rb | 20 +++++ composite/tty_tree.md | 30 ++++++++ 23 files changed, 738 insertions(+) create mode 100644 README.md create mode 100644 adapter/README.md create mode 100644 adapter/color_cannon.rb create mode 100644 adapter/color_cannon_adaptor.rb create mode 100644 adapter/main.rb create mode 100644 builder/README.md create mode 100644 builder/car_builder.rb create mode 100644 builder/car_new.rb create mode 100644 builder/main.rb create mode 100644 builder/parts.rb create mode 100644 builder/tedious_car.rb create mode 100644 command/README.md create mode 100644 command/commands.rb create mode 100644 command/hvac.rb create mode 100644 command/main.rb create mode 100644 command/new_wall_panel.rb create mode 100644 command/wall_panel.rb create mode 100644 composite/README.md create mode 100644 composite/box.rb create mode 100644 composite/main.rb create mode 100644 composite/product.rb create mode 100644 composite/shipment.rb create mode 100644 composite/tty_tree.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f2e818 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Notes on Design Patterns + +This repo represents my learning on design patterns using Ruby. The tough thing about studying design patterns is that reading about design patterns feels too abstract to mean a whole lot. Wanting to cement my learning, I challenged myself to make a small trivial example modeling each pattern, and then (just as important) writing a little something about them. Yes, there are plenty of other examples out there, but these examples were thought up by me, and, as a result I now understand the patterns and when I might want to use them, better than if I had simply read about them. \ No newline at end of file diff --git a/adapter/README.md b/adapter/README.md new file mode 100644 index 0000000..5f63a4e --- /dev/null +++ b/adapter/README.md @@ -0,0 +1,47 @@ +The Adaptor pattern is a way to make two objects work together even if they don't have compatable interfaces. An obivous analogy can be found with most people who use a Macintosh computer. I have a Macbook with usb-c ports and a monitor that uses HDMI. My mac can't talk to my monitor as is. It needs an adaptor to bridge the gap! + +# Color Cannon + +The `ColorCannon` class is a simple object that when given an array of colors can fire them off one by one or in rapid fire. + +```ruby +require './color_cannon' +require './color_cannon_adaptor' + +# using the color cannon the way it was first imagined. With an array of colors +canon = ColorCannon.new(['#DD4686', '#A1E237', '#01FEC5']) +canon.fire_all + +# Boom! Here's #DD4686 +# Boom! Here's #A1E237 +# Boom! Here's #01FEC5 +``` + + +Cool! I'd like to load it up with some random colors from the [GitHub hex bot](https://github.com/noops-challenge/hexbot/). But there's one problem: the API returns an complex object with colors and color cannon needs a simple array. To make it work we have some options. + + 1. We could convert the data to the format needed by `ColorCannon` + +This is certinaly valid, but imagine that we have colors coming from lots of different sources. Does `ColorCannon` need to be able to handle all of them? This would violate the Single Responsability Pinciple. The `ColorCannon` should be concerned with firing colors, not with a sprawling problem of converting data formats. + + 2. We could extend the `ColorCannon` class to accept the data we're getting from the API. + +There's nothing wrong with this either, but that could get messy. Again, imagine colors coming from lots of different sources. + +3. We could write an adaptor + +The adaptor is an entierly seperate class who responsability is to help two classes that are other wise compatable be able to talk to each other. In this case we need to make a string representation of json work with the `ColorCannon` + +```ruby +# here we are fetching colors that come to us in a non compatable format. An adaptor encapsulates the conversion to make them compatable. +require 'net/http' +colors_json_str = Net::HTTP.get(URI.parse('https://api.noopschallenge.com/hexbot?count=3')) +colors = ColorCannonAdaptor.new(colors_json_str) +canon = ColorCannon.new(colors) +canon.fire_all + +# Boom! Here's #1B6497. +# Boom! Here's #AC1344. +# Boom! Here's #F234D0. +``` + diff --git a/adapter/color_cannon.rb b/adapter/color_cannon.rb new file mode 100644 index 0000000..50400d4 --- /dev/null +++ b/adapter/color_cannon.rb @@ -0,0 +1,20 @@ +require 'json' +# I am a color cannon! Load me up with colors and fire them off one by one +class ColorCannon + # colors: A string representation of json + # of An array-like of strings representing hexadecimal colors + def initialize(colors) + @colors = colors + end + + # fire a color from the cannon + def fire + @colors.shift + end + + def fire_all + @colors.length.times do + puts "Boom! Here's #{fire}." + end + end +end diff --git a/adapter/color_cannon_adaptor.rb b/adapter/color_cannon_adaptor.rb new file mode 100644 index 0000000..606ec40 --- /dev/null +++ b/adapter/color_cannon_adaptor.rb @@ -0,0 +1,18 @@ +# Adaptor class between hexbot api (https://github.com/noops-challenge/hexbot/) +# and the ColorCannon +class ColorCannonAdaptor + # json: a json response from hebot + # structure: + # colors: [{value: '#000000'}] + def initialize(json) + @colors = JSON.parse(json)['colors'] + end + + def shift + @colors.shift['value'] + end + + def length + @colors.length + end +end diff --git a/adapter/main.rb b/adapter/main.rb new file mode 100644 index 0000000..e08bb9a --- /dev/null +++ b/adapter/main.rb @@ -0,0 +1,14 @@ +require './color_cannon' +require './color_cannon_adaptor' + +# using the color cannon the way it was first imagined. With an array of colors +canon = ColorCannon.new(['#DD4686', '#A1E237', '#01FEC5']) +canon.fire_all + +# here we are fetching colors that come to us in a non compatable format. An adaptor encapsulates the conversion to make them compatable. +require 'net/http' +puts 'fetching colors...' +colors_json_str = Net::HTTP.get(URI.parse('https://api.noopschallenge.com/hexbot?count=3')) +colors = ColorCannonAdaptor.new(colors_json_str) +canon = ColorCannon.new(colors) +canon.fire_all diff --git a/builder/README.md b/builder/README.md new file mode 100644 index 0000000..6bc040e --- /dev/null +++ b/builder/README.md @@ -0,0 +1,89 @@ +In this example we are writing software for a company that builds custom vintage cars. The company needs a way to describe the details of each order. To construct a car object we could do it by providing all of the parameters to `new`... + +```ruby +require './tedious_car' +require './parts' + +color = '#FF0000' +engine = Parts::Engine.new(8) +transmission = Parts::Engine.new(:automatic) +sound_system = Parts::SoundSystem.new('48D3T8') +warranty_numbers = ['G42WV7', 'E4CDWQ'] +car1 = TediousCar.new(color, engine, transmission, sound_system, warranty_numbers) +``` + +As my class name implies, constructing this object is a bit tedious. We have to pass in a bunch of arguments, including instantiating three other objects first. A constructor with so many required parameters is a code smell. A new team member reading this code months or years later could have a really tough time teasing apart how this code actually works. And s the codebase grows things could only get worse! + +The builder pattern proposes a different way. When constructing an object becomes less simple, let's separate the responsibility of constructing an object away from an object itself. Under this pattern a `Car` object will encapsulate the car itself but the `CarBuilder` object just represents how the `Car` is created. This is aligned with the Single Responsibility Principal. When constructing an object becomes complex enough to be considered itself a concern, we should extract that responsibility to a new class. + +Here's our new car. Notice that we are providing a way to construct a car without a builder class, if one really wanted to, but the class is designed with the intention of using a builder class. +```ruby +require './car_builder' + +class Car + attr_accessor :color, :engine, :transmission, :sound_system + attr_reader :warranty_numbers + # Car may be inited with starting values but it doesn't have to be. + # The intended way to construct this class is by using the builder class. + def initialize(**attrs) + @warranty_numbers = [] + attrs.each do |attr, val| + instance_variable_set "@#{attr}", val + end + end +end +``` + +and the builder class: + +```ruby +require './parts' +require './car_new' + +class CarBuilder + attr_reader :car + def initialize(&block) + # raise 'Car needs a block to be built' unless block_given? + @car = Car.new + self.instance_eval(&block) if block_given? + end + + def add_color(c) + @car.color = c + end + + def add_engine(cylinders) + @car.engine = Parts::Engine.new cylinders + end + + def add_transmission(type) + @car.transmission = Parts::Transmission.new type + end + + def add_sound_system(serial_number) + @car.sound_system = Parts::SoundSystem.new serial_number + end + + def add_warranty(number) + @car.warranty_numbers << number + end +end +``` + +The `CarBuilder` creates a simple ruby DSL that let's us build up a car class by sending a `CarBuilder` class messages about what the car should look like. + +```ruby +car_builder = CarBuilder.new do + add_color '#FF0000' + add_engine 8 + add_transmission :manual + add_sound_system :G5T6U8 + add_warranty :G5T3E5 + add_warranty :HY6D45 +end + +car3 = car_builder.car +p car3 +``` + +Much cleaner, right? Someone coming back to this code later on can tell what I am up to more easily. Also, in creating the car, I am only concerned with the actual data I need to pass in. When I'm creating a car, I don't really car about what a `Parts::Transmission` class is. I only care if its a `manual` or `automatic`. Let the builder handle the rest of the details! The result is cleaner, more readable, more testable code. diff --git a/builder/car_builder.rb b/builder/car_builder.rb new file mode 100644 index 0000000..a5755dd --- /dev/null +++ b/builder/car_builder.rb @@ -0,0 +1,32 @@ +require './parts' +require './car_new' + +class CarBuilder + attr_reader :car + + def initialize(&block) + # raise 'Car needs a block to be built' unless block_given? + @car = Car.new + self.instance_eval(&block) if block_given? + end + + def add_color(c) + @car.color = c + end + + def add_engine(cylinders) + @car.engine = Parts::Engine.new cylinders + end + + def add_transmission(type) + @car.transmission = Parts::Transmission.new type + end + + def add_sound_system(serial_number) + @car.sound_system = Parts::SoundSystem.new serial_number + end + + def add_warrenty(number) + @car.warrenty_numbers << number + end +end diff --git a/builder/car_new.rb b/builder/car_new.rb new file mode 100644 index 0000000..efe7c37 --- /dev/null +++ b/builder/car_new.rb @@ -0,0 +1,14 @@ +require './car_builder' + +class Car + attr_accessor :color, :engine, :transmission, :sound_system + attr_reader :warrenty_numbers + # Car may be inited with starting values but it doesn't have to be. + # The intended way to construct this class is by using the builder class. + def initialize(**attrs) + @warrenty_numbers = [] + attrs.each do |attr, val| + instance_variable_set "@#{attr}", val + end + end +end diff --git a/builder/main.rb b/builder/main.rb new file mode 100644 index 0000000..ceed778 --- /dev/null +++ b/builder/main.rb @@ -0,0 +1,35 @@ +require './tedious_car' +require './parts' + +# first lets create a car by cramming a bunch of data into the constructor +engine = Parts::Engine.new(8) +transmission = Parts::Engine.new(:automatic) +sound_system = Parts::SoundSystem.new('48D3T8') + +car1 = TediousCar.new('#FF0000', engine, transmission, sound_system, ['G42WV7', 'E4CDWQ']) + +require './car_builder' +require './car_new' +# CarNew _can_ be constructed by passing in a hash, but it doesn't have to be. + +car2 = Car.new({ + color: 'red', + engine: engine, + transmission: transmission, + sound_system: sound_system, + warrenty_numbers: ['g5t1d5', 'h7yt5d'] +}) + +p car1 + +car_builder = CarBuilder.new do + add_color '#FF0000' + add_engine 8 + add_transmission :manual + add_sound_system :G5T6U8 + add_warrenty :G5T3E5 + add_warrenty :HY6D45 +end + +car3 = car_builder.car +p car3 \ No newline at end of file diff --git a/builder/parts.rb b/builder/parts.rb new file mode 100644 index 0000000..932ca44 --- /dev/null +++ b/builder/parts.rb @@ -0,0 +1,23 @@ +module Parts + class Engine + attr_reader :cylinders + def initialize(cylinders) + @cylinders = cylinders + end + end + + class Transmission + attr_reader :cylinders + def initialize(type) + raise "must be either automatic or manual" unless %i[automatic manual].include? type.to_sym + @type = type + end + end + + class SoundSystem + attr_reader :serial_number + def initialize(serial_number) + @serial_number = serial_number.to_sym + end + end +end diff --git a/builder/tedious_car.rb b/builder/tedious_car.rb new file mode 100644 index 0000000..2ed421b --- /dev/null +++ b/builder/tedious_car.rb @@ -0,0 +1,10 @@ +class TediousCar + attr_accessor :color, :engine, :transmission, :sound_system, :add_ons + def initialize(color, engine, transmission, sound_system, warrenty_numbers=[]) + @color = color + @engine = engine + @transmission = transmission + @sound_system = sound_system + @warrenty_numbers = warrenty_numbers + end +end diff --git a/command/README.md b/command/README.md new file mode 100644 index 0000000..25467be --- /dev/null +++ b/command/README.md @@ -0,0 +1,125 @@ +The command pattern seeks to separate an object that performs an action from the commands that it will receive. + +Imagine that we have a simple HVAC system. We can toggle between modes (heat and cool) and we can change the desired temperature. + +``` +class HVAC + attr_accessor :desired_temp, :mode + def initialize + @desired_temp = 70 + @mode = :heat + end + + def heat + @mode = :heat + end + + def cool + @mode = :cool + end +end +``` + +We also have a wall panel class that is aware of the HVAC it controls. Users wanting to interact with the hvac do so via the wall panel. (This is an example of delegation, btw). +``` +class WallPanel + def initialize(hvac) + @hvac = hvac + end + + def mode + @hvac.mode + end + + def toggle_mode + if @hvac.mode == :heat + @hvac.mode = :cool + return + end + @hvac.mode = :heat + end + + def temp + @hvac.desired_temp + end + + def temp_up + @hvac.desired_temp += 1 + end + + def temp_down + @hvac.desired_temp -= 1 + end +end +``` + +This seems to work just fine. But what happens when there is not a simple one to one relationship between wall pannels? What if, for example, a wall panel needs to control more than one HVAC system? Or if wall panels can vary in the types of commands they can perform? The command pattern proposes that a command sent to the HVAC system should be its own object, and that wall panels should only know what commands they have access to, but not what those commands ultimatly do. In a more complex system, this more fine grained seperation of concerns might be a good idea. + +In our revised version, a wall panel can have an arbitrary number of buttons each of which is wired to a command. The wall panel and button are not concerend with who will recieve the command or what that command will do. In a complex system this decoupling may be useful (but like all patterns it might also be over engineered -- knowing when to use a pattern is an art in itself!) + +``` +class Command + def initialize(hvac) + @hvac = hvac + end +end + +class ToggleMode < Command + def execute + if @hvac.mode == :heat + @hvac.mode = :cool + return + end + @hvac.mode = :heat + end +end + +class TempUp + def exectue + @hvac.desired_temp += 1 + end +end + +class TempDown + def execute + @hvac.desired_temp -= 1 + end +end + +class NewWallPanel + def initialize + @buttons = [] + end + + def add_button(button) + @buttons << button + end +end + +class PanelButton + def initialize(cmd) + @cmd = cmd + end + + def on_press + @cmd.execute + end +end + +``` + + +If for example we want to create a minimal wall panel that only lets the user toggle between heat and cool, but not change the temp: + +``` +hvac2 = HVAC.new + +puts "mode is #{hvac2.mode}" # heat + +cmd_toggle_mode = ToggleMode.new hvac2 +btn_toggle_mode = PanelButton.new cmd_toggle_mode + +btn_toggle_mode.execute + +puts "mode is #{panel.mode}" # heat +``` \ No newline at end of file diff --git a/command/commands.rb b/command/commands.rb new file mode 100644 index 0000000..2895d44 --- /dev/null +++ b/command/commands.rb @@ -0,0 +1,27 @@ +class Command + def initialize(hvac) + @hvac = hvac + end +end + +class ToggleMode < Command + def execute + if @hvac.mode == :heat + @hvac.mode = :cool + return + end + @hvac.mode = :heat + end +end + +class TempUp + def exectue + @hvac.desired_temp += 1 + end +end + +class TempDown + def execute + @hvac.desired_temp -= 1 + end +end diff --git a/command/hvac.rb b/command/hvac.rb new file mode 100644 index 0000000..6aacdee --- /dev/null +++ b/command/hvac.rb @@ -0,0 +1,15 @@ +class HVAC + attr_accessor :desired_temp, :mode + def initialize + @desired_temp = 70 + @mode = :heat + end + + def heat + @mode = :heat + end + + def cool + @mode = :cool + end +end diff --git a/command/main.rb b/command/main.rb new file mode 100644 index 0000000..9fa38ec --- /dev/null +++ b/command/main.rb @@ -0,0 +1,33 @@ +require './hvac' +require './wall_panel' + +hvac = HVAC.new +panel = WallPanel.new hvac + +puts "mode is #{panel.mode}" # heat +panel.toggle_mode +puts "mode is #{panel.mode}" # cool +panel.toggle_mode +puts "mode is #{panel.mode}" # heat + +puts "temp is set to #{panel.temp}" # 70 +3.times {panel.temp_up} +puts "temp is set to #{panel.temp}" # 73 +6.times {panel.temp_down} +puts "temp is set to #{panel.temp}" # 67 + + +# using command pattern +require './commands' +require './new_wall_panel' + +hvac2 = HVAC.new + +puts "mode is #{hvac2.mode}" # heat + +cmd_toggle_mode = ToggleMode.new hvac2 +btn_toggle_mode = PanelButton.new cmd_toggle_mode + +btn_toggle_mode.execute + +puts "mode is #{panel.mode}" # heat diff --git a/command/new_wall_panel.rb b/command/new_wall_panel.rb new file mode 100644 index 0000000..01fe53c --- /dev/null +++ b/command/new_wall_panel.rb @@ -0,0 +1,19 @@ +class NewWallPanel + def initialize + @buttons = [] + end + + def add_button(button) + @buttons << button + end +end + +class PanelButton + def initialize(cmd) + @cmd = cmd + end + + def on_press + @cmd.execute + end +end diff --git a/command/wall_panel.rb b/command/wall_panel.rb new file mode 100644 index 0000000..972affa --- /dev/null +++ b/command/wall_panel.rb @@ -0,0 +1,29 @@ +class WallPanel + def initialize(hvac) + @hvac = hvac + end + + def mode + @hvac.mode + end + + def toggle_mode + if @hvac.mode == :heat + @hvac.mode = :cool + return + end + @hvac.mode = :heat + end + + def temp + @hvac.desired_temp + end + + def temp_up + @hvac.desired_temp += 1 + end + + def temp_down + @hvac.desired_temp -= 1 + end +end diff --git a/composite/README.md b/composite/README.md new file mode 100644 index 0000000..5170c38 --- /dev/null +++ b/composite/README.md @@ -0,0 +1,39 @@ +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. + +``` +require './box' +require './product' + +b1 = Box.new +hammer_box = Box.new +hammer = Product.new 'hammer', 10.0 + +hammer_box.add_item hammer +b1.add_item hammer_box + +recipt = Product.new 'recipt', 0.1 +b1.add_item recipt + +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 + +charger_box = Box.new +charger = Product.new 'charger', 0.1 +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 +``` + +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 diff --git a/composite/box.rb b/composite/box.rb new file mode 100644 index 0000000..4e532a5 --- /dev/null +++ b/composite/box.rb @@ -0,0 +1,26 @@ +class Box + attr_reader :contents + + ROOT = Box.new() + def initialize(parent) + @parent.parent + end + + def item(item) + puts "adding item #{item}" + @contents << item + end + + def weight + @contents.inject(0.0) { |total, item| total + item.weight } + end + + # recursivly prints a box's complete contents + def full_inventory + + end + + def to_s + "" + end +end diff --git a/composite/main.rb b/composite/main.rb new file mode 100644 index 0000000..f2d15d5 --- /dev/null +++ b/composite/main.rb @@ -0,0 +1,59 @@ +require './box' +require './product' + +# b1 = Box.new +# hammer_box = Box.new +# hammer = Product.new 'hammer', 10.0 + +# hammer_box.add_item hammer +# b1.add_item hammer_box + +# recipt = Product.new 'recipt', 0.1 +# b1.add_item recipt + +# 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 + +# charger_box = Box.new +# charger = Product.new 'charger', 0.1 +# 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 + +require './shipment' + +outer_box = Box.new do + item Box.new do + item Product.new 'hammer', 10.0 + end + + # item Product.new 'recipt', 0.1 + + # 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 + + # charger_box = Box.new do |cb| + # cb.add_item Product.new 'charger', 0.1 + # end + + # mb.add_item phone_box + # mb.add_item charger_box + # end + + # b.add_item medium_box +end + +puts outer_box.weight diff --git a/composite/product.rb b/composite/product.rb new file mode 100644 index 0000000..3bc4632 --- /dev/null +++ b/composite/product.rb @@ -0,0 +1,11 @@ +class Product + attr_reader :name, :weight + def initialize(name, weight) + @name = name + @weight = weight + end + + def to_s + "" + end +end diff --git a/composite/shipment.rb b/composite/shipment.rb new file mode 100644 index 0000000..78993cc --- /dev/null +++ b/composite/shipment.rb @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000..6ecd14c --- /dev/null +++ b/composite/tty_tree.md @@ -0,0 +1,30 @@ +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. +