TL;DR
Firstly, your originally-posted code didn't define the proper relationship or collaboration between your objects, which is part of the problem. Secondly, the way one needs to capture and forward blocks is often rather non-intuitive, so the reasoning behind doing this with blocks rather than simple dependency injection (e.g. initializing a new Person with a specific Animal to collaborate with) or message-passing makes this a bit harder than it needs to be. Simple is usually better, and often easier to debug and extend later on.
That said, the following redesign is semantically clearer, but also shows how to forward blocks as Proc objects between collaborators. Person now takes an optional block, and when present passes that block as a Proc object to a method on Animal where it can be called.
Your Code Redesigned
Consider the following redesign, which strives to attach the right behavior to the right objects:
class Person
def speaks_to_animal species, phrase, &reaction
animal = Animal.new species
animal.reacts_to phrase, &reaction
end
end
class Animal
attr_reader :species
def initialize species
@species = species.to_s
end
def reacts_to phrase
case species
when 'lion'; pp "The #{species} eats you."
when 'dog'
block_given? ? yield : pp("The #{species} barks at you.")
else pp "The #{species} doesn't know what to do."
end
end
end
In particular, the goal here is to redesign the code such that Person#speaks_to_animal while Animal#reacts_to_phrase spoken by a Person. This keeps the behavior where is belongs, insofar as a Person doesn't #fetch and an Animal shouldn't have to know anything about the internals of the Person object to collaborate.
As a byproduct, this redesign provides greater flexibility. Your blocks are now optional, and while they are passed to a Person they are then forwarded and called by the Animal, which seemed to be the intent of your original code.
You now interact with an Animal through a Person, and that Person can talk to any type of Animal species you choose to specify without the need to subclass Animal or hard-code reactions. For example:
person = Person.new
person.speaks_to_animal :dog, "Fetch, boy!"
person.speaks_to_animal(:dog, "Fetch, boy!") do
pp "The dog brings the stick back to you."
end
person.speaks_to_animal :lion, "Fetch, boy!"
If you don't pass a block to person, then the dog doesn't know what to do, and just barks at you. If you pass a behavioral expectation as a block, that block gets forwarded to animal#reacts_to where it's called via yield
. Of course, if you ask a lion to play fetch, bad things are going to happen.
Repackaging the behavior and relationship between the objects allows you to do all sorts of things, like keying off elements of the phrase spoken by person to enable more complex responses, or allowing animal to key off elements of the phrase to respond differently based on its species. Mostly, though, this new code solves the problem of how to pass an optional block representing the Animal's reaction without coupling the objects too tightly.
Semantically, a Person should know what Animal they're talking to, and what they hope the Animal will do in response. Whether passing a block around is really the best way to represent the Person's expectations or the Animal's reactions is more arguable, and I would personally opt to focus more on defining reactions based on species and phrase rather than on passing Proc objects. Your mileage may vary.