-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4cac0df
Showing
23 changed files
with
738 additions
and
0 deletions.
There are no files selected for viewing
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
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. |
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
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. | ||
``` | ||
|
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
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 |
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
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 |
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
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 |
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
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. |
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
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 |
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
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 |
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
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 |
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
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 |
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
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 |
Oops, something went wrong.