Code to an interface (aka stop using instance variables)
We're all used to the idea that we should use only methods that classes declare public, avoiding use of protected and private methods since these may change at any time. In other words, we usually code to the public interface, and try not to depend on how the class is implemented. It keeps our code clean and means that when the internals of the class change we don't need to change the clients of that class.
Unfortunately we don't normally use the same thought process when we're managing state in our class which can make refactoring a problem.
Here's an example Book class from a hypothetical book store application. Books have titles and authors. They are published at a certain date, but their publication date can change if for example the author fails to deliver their manuscript on time or editing takes longer than expected. Titles can change, authors won't.
class Book
attr_reader :author
attr_accessor :title, :published_at
def initialize author, title, published_at
@author = author
@title = title
@published_at = published_at
end
def to_s
"\"#{@title}\" by #{@author}. Publication date: #{@published_at}"
end
end
A few weeks pass, and we start to make more deals with publishers. Suddenly they want the book store to exclusively sell the upcoming book for A.N. Big Author. Great!... except we can't deal with books that don't have a publication date yet. We need to change the class so we can model books that aren't published:
class Book
attr_reader :author
attr_accessor :title, :published_at
# published_at = nil if the book doesn't have a publication date
def initialize author, title, published_at
@author = author
@title = title
@published_at = published_at
end
def to_s
"\"#{@title}\" by #{@author}. Publication date: #{@published_at ? @published_at : 'not yet published'}"
end
end
That's not too bad for this trivial class, but it's not particularly readable and it's very easy to imagine that there would be several methods dealing with @published_at
. Making this change in all of those areas would take time and it doesn't read very nicely. It's a good candidate for the Introduce Null Object refactoring, but because we're accessing @published_at
directly everywhere we still have a lot of changes to make. We could introduce a Null Object during instantiation except the publication date could be changed at any time - it's possible that the publisher calls us and tells us that they can't make a publication date they've told us and that they're not sure when they'll be able to publish the book.
Here's an alternative class that I wish I had started with. It exposes the same public API but also uses the accessors internally.
class Book
attr_accessor :author, :title, :published_at
private :author=
def initialize author, title, published_at
self.author = author
self.title = title
self.published_at = published_at
end
def to_s
"\"#{title}\" by #{author}. Publication date: #{published_at}"
end
end
When I get the call from the published about the as yet unpublished book this time I can now insert the Null Object easily by hijacking the attr_reader
for published_at
, here's the change I make:
class MissingPublicationDate
include Singleton
def to_s
'not yet published'
end
end
class Book
attr_accessor :author, :title, :published_at
private :author=
def initialize author, title, published_at
self.author = author
self.title = title
self.published_at = published_at
end
def published_at_with_null_object
published_at_without_null_object || MissingPublicationDate.instance
end
alias_method :published_at_without_null_object, :published_at
alias_method :published_at, :published_at_with_null_object
def to_s
"\"#{title}\" by #{author}. Publication date: #{published_at}"
end
end
It's a little more code in this example, but very few of the methods that use published_at
need to change and it's massively more readable. Ace!
curl -LO http://barkingiguana.com/2011/04/21/code-to-an-interface-aka-stop-using-instance-variables.html.orig
curl -LO http://barkingiguana.com/2011/04/21/code-to-an-interface-aka-stop-using-instance-variables.html.orig.asc
gpg --verify code-to-an-interface-aka-stop-using-instance-variables.html.orig{.asc,}
If you'd like to have a conversation about this post, email craig@barkingiguana.com. I don't bite.