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!

written by
Craig
published
2011-04-21
Disagree? Found a typo? Got a question?
If you'd like to have a conversation about this post, email craig@barkingiguana.com. I don't bite.
You can verify that I've written this post by following the verification instructions:
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,}