Skip to content

Instantly share code, notes, and snippets.

@missingno15
Last active August 29, 2015 14:20
Show Gist options
  • Save missingno15/4bc7efabe7a45ff895fd to your computer and use it in GitHub Desktop.
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
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