In Progress
Unit 1, Lesson 21
In Progress

Protocol Complication

Video transcript & code

Let's imagine we're building an online bookstore, which enables readers to page through books right on the website.

As part of the infrastructure for this program, we have a concept of a "book iterator" object.

A book iterator has methods for paging backwards and forwards through a book.

require "./book_iterators"

itr = BookIterator.new(WAR_AND_PEACE)
itr.next_page                   # => 2
itr.next_page                   # => 3
itr.previous_page               # => 2

It also has methods for finding out the #first_page_number and #last_page_number.

require "./book_iterators"

itr = BookIterator.new(WAR_AND_PEACE)
itr.first_page_number           # => 1
itr.last_page_number            # => 1456

On some parts of the site we want to be able to show a range from first to last page. Here's a very rudimentary version of that code. This method just takes a book iterator, and formats the start-to-end range along with the current page and a remaining page count.

Here's how it looks when used with our War and Peace iterator:

This formatting function implies that there is a protocol it can expect its inputs to comply with. Let's think about what that protocol consists of.

  • there is a #current_page_number message, which returns an integer.
  • There is a #first_page_number message, which returns an integer.
  • There is a #last_page_number message, which returns an integer.

The iterator objects that are passed to this method may support other functionality, but it doesn't care. All it is concerned with is this very simple three-method page numbering protocol.

def format_page_range(book_iterator)
  remaining = book_iterator.last_page_number - book_iterator.current_page_number
  "#{book_iterator.first_page_number}"\
  "<--[#{book_iterator.current_page_number}]-->"\
  "#{book_iterator.last_page_number}"\
  " (#{remaining} to go)"
end
require "./book"
require "./book_iterators"
require "./formatter"

itr = BookIterator.new(WAR_AND_PEACE)

format_page_range(itr)
# => "1<--[1]-->1456 (1455 to go)"

As our application is developed, we add more kinds of iterators. For instance, we add an "sample iterator" which just picks pages at random to show to prospective book readers.

require "./book_iterators"

itr = SampleIterator.new(THE_HOBBIT)
itr.next_page                   # => 290
itr.next_page                   # => 194
itr.next_page                   # => 269

We also introduce a "bookshelf iterator", that can step through the pages of a whole list of books.

require "./book_iterators"

itr = BookshelfIterator.new([THE_HOBBIT, WAR_AND_PEACE, GOODNIGHT_MOON])

All of these iterators support the concept of a #first_page_number and a #last_page_number. Let's see how they work with our page range formatter.

When we feed a SampleIterator into the formatting function, we sometimes see output where the starting page number is higher than the ending page number. That's disconcerting, from a user's point of view. What's worse, the "pages to go count" is now negative.

require "./book_iterators"
require "./formatter"

itr = SampleIterator.new(THE_HOBBIT)
itr.current_page_number         # => 52
format_page_range(itr)
# => "52<--[52]-->29 (-23 to go)"

Now let's try a BookshelfIterator. Except that we're going to make it an iterator for someone who has cleared out their bookshelf and has no books left on their queue.

require "./book_iterators"
require "./formatter"

itr = BookshelfIterator.new([])
format_page_range(itr)
# =>

# ~> NoMethodError
# ~> undefined method `-' for nil:NilClass
# ~>
# ~> /home/avdi/Dropbox/rubytapas/370-protocol-complication/formatter.rb:3:in...
# ~> xmptmp-in101592nv.rb:5:in `<main>'

Whoopsie. Looks like some of those methods return nil when there are no books on the self.

Now that we've tried out a few iterators, let's think about the updated page-numbering protocol that our formatter method needs to be aware of.

  • there is a #current_page_number message, which returns:
    • an integer
    • or nil
  • There is a #first_page_number message, which returns:
    • an integer
    • which may be equal, greater, or lesser than the #current_page_number
    • …or nil
  • There is a #last_page_number message, which returns:
    • an integer
    • which may be equal, greater, or lesser than the #current_page_number and first_page_number
    • …or nil

Now, obviously these examples are contrived, and the correctness of these return values is questionable. But the point I hope to make is this:

The protocol, or interface, your clients have to deal with is the union of the behavior of all of the methods in all of the classes which may play a given role.

When we add a new implementation of a protocol, we may broaden the return values or other behavior of the methods in a way that seems sensible from the point of view of that particular object's behavior. But what we have to keep in mind is that we are adding that behavior to the total set of possibilities that users of the protocol have to be aware of.

The lesson to take away is, first, that it's important to think of the protocols or interfaces our objects define in a way that's more abstract than the behavior of a single object. Ruby doesn't have type signatures or interface definitions. But it pays to at least think about the interfaces we are defining, and what rules those interfaces ought to follow irrespective of specific implementations.

And second, we need to remember that every time we add a new implementation of an existing interface, we are potentially adding to the total behavior set that clients of that interface have to be aware of. This situation calls for an attitude of conservatism: if possible, we should find a way to make the new implementation behave consistently with the existing ones. Including with unstated rules such as "the last page number is greater than or equal to the current page number".

Perhaps another time we can talk about how these disparate iterators might have been more conservative in their implementation of the page-numbering protocol. For now, I hope this episode provides some food for thought about the implied protocols you're defining in your own object families. Happy hacking!

Responses