Skip to content

Commit

Permalink
add compisiter
Browse files Browse the repository at this point in the history
  • Loading branch information
hstoebel committed Aug 11, 2019
0 parents commit 4cac0df
Show file tree
Hide file tree
Showing 23 changed files with 738 additions and 0 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions adapter/README.md
Original file line number Diff line number Diff line change
@@ -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.
```

20 changes: 20 additions & 0 deletions adapter/color_cannon.rb
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions adapter/color_cannon_adaptor.rb
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions adapter/main.rb
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions builder/README.md
Original file line number Diff line number Diff line change
@@ -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.
32 changes: 32 additions & 0 deletions builder/car_builder.rb
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions builder/car_new.rb
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions builder/main.rb
Original file line number Diff line number Diff line change
@@ -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
23 changes: 23 additions & 0 deletions builder/parts.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions builder/tedious_car.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 4cac0df

Please sign in to comment.