Extract Default to Method
Video transcript & code
In Episode #482 we ended up with this code here.
class HitPresenter def initialize(hand, new_card, card_formatter: nil) @hand = hand @new_card = new_card @card_formatter = card_formatter || Object.new.extend(CardHelper) end end
As you can see, one of the initialization parameters is an optional collaborator object called
card_formatter. This parameter defaults to
nil. Later in the initializer, a
nil value is treated as a special flag meaning "use the default card formatter".
Let's take a closer look at that default argument.
This idiom, of having a nil value act as a flag for "use the default", is a fairly common one in Ruby libraries. It can even be found in some of Ruby's standard libraries.
But even widely practiced idioms can have their flaws. And I find this one to be less than ideal.
As we talked about back in Episode #108, the trouble with
nil is that it can mean so many things that it winds up meaning nothing at all. in this case, the meaning of the
nil default is completely opaque to someone scanning through the source code.
Imagine we're reading this code for the first time, looking to understand how to instantiate and customize a
nil here mean that ordinarily, no
card_formatter is used? Or does it mean that there is a secret default, hidden somewhere in the code? If we are reading generated documentation it's even less useful, since that documentation is likely to show default argument values, but with the method body either missing or hidden.
Of course, a documentation comment could help clear things up. But it's always better to look for ways to make code self-documenting before we turn to comments.
Making this particular initializer self-documenting turns out to be quite easy. All we have to do is move the default into the parameter list.
class HitPresenter def initialize(hand, new_card, card_formatter: Object.new.extend(CardHelper)) @hand = hand @new_card = new_card @card_formatter = card_formatter end end
Since in Ruby any expression that is valid inside a method body is also valid as a parameter default, we can safely move this code without worrying about breakage.
This works well when the creation code for the default collaborator object is a single short expression. But sometimes the code to build the default is more complex than this.
require "net/http" require "uri" class Client def initialize(login, password, connection: nil) @login = login @password = password @connection = connection || begin uri = URI("http://example.com") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http end end end
But because the construction code for the default HTTP connection object is complex, we've just left an opaque
nil in its place.
Later, down in the initialization code we use a begin...end block, which we learned about in Episode #227, to treat a multiline block of code as a single expression. This block builds up an SSL-enabled Net::HTTP connection object before returning it.
Even if it's technically legal to move this entire
begin...end block into the default parameter position, we're not going to do that, because it would turn the method signature into an unreadable mess.
But leaving the method as is, the signature doesn't tell readers anything about what kind of
connection collaborator it might expect.
This is a case where I think it's worthwhile to extract a method even though it is only going to be used in one place. If we extract this connection builder code into a
default_connection method …
…we can then replace our
nil flag with an invocation of the extracted method.
require "net/http" require "uri" class Client def initialize(login, password, connection: default_connection) @login = login @password = password @connection = connection end def default_connection uri = URI("http://example.com") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http end end
Coming at it as a reader of this method, this version has two advantages: first, it makes it clear that there is a default value for this attribute. It's not some optional object, as the
nil flag hinted.
And second,it gives the reader a clear signpost for where to find out more about what sort of object is expected for this parameter. all they need to do is look up the
There is one more variation on this which I think can be worthwhile. Imagine a user of our code is learning about it by playing with it in a REPL. In order to interactively discover exactly what sort of object normally plays the role of
connection, they first have to instantiate a client object. Then they have to send the default connection message to it.
And if it turns out to be a private method, they will have even more work to do to play with it.
c = Client.new("bob", "xyzzy") c.default_connection # => #<Net::HTTP example.com:80 open=false>
If, on the other hand, we convert the
default_connection method to a class method - updating the parameter default to match …
require "net/http" require "uri" class Client def initialize(login, password, connection: self.class.default_connection) @login = login @password = password @connection = connection end def self.default_connection uri = URI("http://example.com") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http end end
…It now becomes possible to interactively inspect the default HTTP connection value without any extra steps.
Client.default_connection # => #<Net::HTTP example.com:80 open=false>
This new version is both self-documenting and very explorable, two properties I find highly desirable in a Ruby library. Happy hacking!