Code and Cake

Delegation


When writing object oriented code, a pretty useful technique is delegation–letting one object figure things out by sending messages to another object. At my work, many teams prefer procedural code over OOP, and they often forgo delegation for constructing data upfront. Here’s an example!

# A very simplified domain for a movie purchasing website.
class Video
  attr_accessor(:title, :s3_key, :distributor_id, :runtime)
end

class User
  attr_accessor(:email, :is_internal_user)
end

class Invoice
  attr_accessor(:cost, :email, :download_url, :due_date)
end

class VideoService
  def self.buy_video(video, user)
    invoice = Invoice.new
    invoice.email = user.email

    # A very important partner who gets their own S3 bucket.
    if video.distributor_id == 123
      invoice.download_url = S3Client.get_url('special_bucket', video.s3_key)
    else
      invoice.download_url = S3Client.get_url('regular_bucket', video.s3_key)
    end

    invoice.cost = Distributor.find(video.distributor_id).price

    # Short video discount.
    if video.runtime < 30.minutes
      invoice.cost = invoice.cost / 2
    end

    # Internal users who are buying content for testing don't have to pay!
    if user.is_internal_user
      invoice.due_date = :never
    else
      invoice.due_date = Time.now + 30.days
    end

    EmailClient.send_invoice(invoice)
  end
end

Here we’re making a buy_video method that constructs and sends an invoice to a user that just bought a video. The “objects” here are just structs. All the behavior is in the buy_video method. This is pretty typical of a lot of procedural code I’ve seen at work.

I dunno about you, but I find this pretty hard to read. buy_video is a very complicated method! And it’s complicated in the worst way–it does different things in the same place.

I think delegation can offer us some solutions here. Check out this code:

class Video
  attr_accessor(:title, :s3_key, :distributor_id, :runtime)

  def short?
    runtime < 30.minutes
  end

  def price
    Distributor.find(distrubtor_id).price
  end

  def s3_bucket
    if distributor_id == 123
      'special_bucket'
    else
      'regular_bucket'
    end
  end
end

class User
  attr_accessor(:email, :is_internal_user)

  def internal?
    is_internal_user
  end
end

class Invoice
  def initialize(video, user)
    @video = video
    @user = user
  end

  def email
    @user.email
  end

  def download_url
    S3Client.get_url(@video.s3_bucket, @video.s3_key)
  end

  def cost
    if @video.short?
      @video.price / 2
    else
      @video.price
    end
  end

  def due_date
    if @user.internal?
      :never
    else
      Time.now + 30.days
    end
  end
end

class VideoService
  def self.buy_video(video, user)
    EmailClient.send_invoice(Invoice.new(video, user))
  end
end

Here instead of constructing Invoice upfront, we let Invoice delegate to Video and User. And now instead of having all of our logic inside one method, it’s spread around into places where it makes sense.

Delegation is a powerful tool!