In Progress
Unit 1, Lesson 1
In Progress

Safe Navigation Part 5: Adapter

Video transcript & code

In the previous episode we moved some core class patches into a refinement. We've made Ruby's Object and NilClass behave like optional types, so that we can safely chain operations together on singular values in the same way that we are used to on collections.

module OptionalEverything
  refine Object do
    def each
      yield self
      nil
    end

    def map
      yield self
    end
  end

  refine NilClass do
    def each
      nil
    end

    def map
      nil
    end
  end
end
require "./models2"
require "active_support"
require "active_support/core_ext"
require "./refinements"

using OptionalEverything

@product.departments[1].curator = nil
@product.map(&:departments).map(&:curator).compact.map(&:email_address).each do |email|
  puts "Emailing a complaint to #{email}..."
end

# >> Emailing a complaint to dan@example.com...

Is what we have here ideal? Not really. We're still papering over the underlying problem, of object attributes with unreliable return types.

If we control the object model ourselves, that's the issue that we really ought to be tackling. And in the first part of this miniseries, we did just that, using a simple version of the Null Object pattern.

But maybe we're just consumers of the object model, and we have no control over it. Still, we need to be careful of coupling our code to the internal structure in this object model we have no control over.

In that case, it behooves us to do what we can to push these fragile object navigation chains into the borders of our own code. One way to do that is with proxy or adapter objects.

For instance, in a separate file, we could load up our handy OptionalEverything refinements. Then we could define a SafeProduct class that wraps a product object. Inside this class, we'll put methods for accessing just the parts of the target objects that we care about. In this case, we'll encapsulate the mess of discovering the email addresses for a product's curators behind a single method.

require "./models2"
require "./refinements"
require "delegate"

using OptionalEverything

class SafeProduct
  def initialize(product)
    @product = product
  end

  def curator_emails
    @product.map(&:departments).map(&:curator).compact.map(&:email_address)
  end
end

Then in our application code, we can require the new file, and wrap up Product objects in our safe proxies. If we only interact with products via these adapters, we have a layer of insulation between our app code and the structure of the Product/Department/Curator relationships. If those relationships change, we only have to modify our own code in one place.

require "./models2"
require "./proxy"

SafeProduct.new(@product).curator_emails.each do |email|
  puts "Emailing a complaint to #{email}..."
end

# >> Emailing a complaint to dan@example.com...
# >> Emailing a complaint to jane@example.com...

Notice too that in our main application code, we're not making use of the refinements which made our lives so much easier over in the adapter class. They can stay as an implementation detail, only used where they are needed.

So in the end, what can we say about safe navigation? What are we trying to stay safe from, anyway? The dangers we are trying to avoid are the risks inherent in coupling our code to the unreliable interfaces and relationships of other objects. And #try methods or "safe navigation" operators are just a band-aid for one of many possible types of unreliability.

In the end, we are never truly "safe" from this kind of uncertainty. And these bandages are, or should be, short-term fixes. The longer-term approach is to wall-off uncertainty into small, well-delineated zones at the borders of our code.

And with that, we come to the end of our discussion of safe navigation. Happy hacking!

Responses