Last active
August 29, 2015 14:20
-
-
Save missingno15/4bc7efabe7a45ff895fd to your computer and use it in GitHub Desktop.
Code you can run which mimics the "external framework API" that Sandi Metz talks about in her BathRuby 2015 - Nothing is Something talk https://youtu.be/9lv2lBq6x4A
This file contains hidden or 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
class Animal | |
DATABASE = [ | |
{ id: 1, name: "Mockingbird" }, | |
{ id: 2, name: "Pheasant" }, | |
{ id: 3, name: "Duck" }, | |
{ id: 4, name: "Platypus" }, | |
{ id: 5, name: "Penguin" }, | |
{ id: 6, name: "Peacock" }, | |
{ id: 7, name: "Hummingbird" }, | |
{ id: 8, name: "Owl" } | |
] | |
attr_accessor :id, :name | |
def self.find(id) | |
target_data = DATABASE.find { |data| data[:id] == id } | |
if target_data | |
self.new(target_data) | |
end | |
end | |
def initialize(attrs) | |
@id = attrs.fetch(:id) | |
@name = attrs.fetch(:name) | |
end | |
end | |
# Now we have a list of ids that we want to query | |
ids = [4, 8] | |
# Let's map this | |
birds = ids.map { |id| Animal.find(id) } | |
p birds # => [{:id=>4, :name=>"Platypus"}, {:id=>8, :name=>"Owl"}] | |
# Let's now add an id that doesn't exist in our database to our array of ids | |
# Now when we run map on the ids, we should get nil in the resulting array | |
ids << 48 | |
birds = ids.map { |id| Animal.find(id) } | |
p birds # => [{:id=>4, :name=>"Platypus"}, {:id=>8, :name=>"Owl"}, nil] | |
# However, let's say we don't want it to return 'nil', we want it to be | |
# descriptive and return 'no animal' instead when we call #name on each object | |
# With our current implementation, if you iterate through each element | |
# in the array, you'll get: | |
# birds.each { |bird| bird.name } | |
# => undefined method `name' for nil:NilClass (NoMethodError) | |
# Let's say we can't change Animal#find because it belonged to an | |
# external framework so we have no control whatsoever on it | |
# You could do something like this: | |
birds.each { |bird| bird ? (puts bird.name) : (puts "no animal") } | |
# However, we can do better | |
# Let's use the Null Object Pattern to substitute for our missing values | |
class MissingAnimal | |
def name | |
"no animal" | |
end | |
end | |
birds = ids.map { |id| Animal.find(id) || MissingAnimal.new } | |
# Now when when we call #name on each object | |
birds.each { |bird| puts bird.name } | |
# #=> Platypus | |
# Owl | |
# No animal | |
# However, Sandi Metz makes the point that everytime you want to do this, we will be calling on | |
# twice the amount of objects. Her solution is wrapping up the Null Object into another class | |
# which also handles the API of the thing that we have no control over. | |
class GuaranteedAnimal | |
def self.find(id) | |
Animal.find(id) || MissingAnimal.new | |
end | |
end | |
# Now in the rest of the areas of your app, you can now change this: | |
birds = ids.map { |id| Animal.find(id) || MissingAnimal.new } | |
# to this↓ | |
birds = ids.map { |id| GuaranteedAnimal.find(id) } | |
birds.each { |bird| puts bird.name } | |
# => Platypus | |
# Owl | |
# No animal |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment