In Progress
Unit 1, Lesson 21
In Progress

Exploratory Refactoring

Improving the design of existing software is a creative and imaginative process. In order to discover creative refactoring solutions, it’s important to be able to quickly try out new design ideas. But it can be hard to do this kind of playful prototyping when we’re limited by dependencies, broken configurations, or slow builds.

In today’s episode, guest chef Nick Sutterer shows us how he approaches refactoring and redesign in Rails applications. By using some clever tricks to temporarily exclude database dependencies, he’s able stay focused on the domain model as he plays with new approaches. It’s a novel and productive technique, and I hope it inspires you. Enjoy!

Video transcript & code

Unreadable class diagram on recycled paper

When refactoring a highly complex system there's a point at the beginning where we want to play around with different potential designs. It's very important in this phase to not lose focus on your actual goal: Which is to simplify the architecture without changing the behavior.

When refactoring a Rails application, I usually start by taking a quick look at the database structure.


  create_table "shipping_codes", force: :cascade do |t|
    t.string "code", default: ""
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.bigint "brand_id"
    t.bigint "address_id"
    t.bigint "retailer_id"
    t.index ["address_id"], name: "index_shipping_codes_on_address_id"
    t.index ["brand_id"], name: "index_shipping_codes_on_brand_id"
    t.index ["retailer_id"], name: "index_shipping_codes_on_retailer_id"
  end

  create_table "size_breaks", force: :cascade do |t|
    t.string "size", default: ""
    t.string "attribute_size"
    t.string "source", default: ""
    t.string "integration_id"
    t.string "md5"
    t.bigint "product_id"
    t.bigint "brand_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "attribute_type"
    t.string "barcode"
    t.string "pack_barcodes"
    t.string "state", default: "Active"
    t.integer "pack_unit", default: 1
    t.index ["brand_id", "integration_id"], name: "index_size_breaks_on_brand_id_and_integration_id"
    t.index ["brand_id", "md5"], name: "index_size_breaks_on_brand_id_and_md5"
    t.index ["brand_id"], name: "index_size_breaks_on_brand_id"
    t.index ["md5"], name: "index_size_breaks_on_md5"
    t.index ["product_id"], name: "index_size_breaks_on_product_id"
  end

Unfortunately in most Rails apps this dictates the application's architecture. Once I understand the database schema, I take a step back and introduce domain objects that model the application's data and behavior through a very, very limited interface.

In the first step, those domain objects are simple decorator objects that represent the application's domain, and not only the database structure. They help me understanding what the application does and how data is flowing.

The approach

I refactor through tests. And instead of running the entire test suite and trying to understand all the nitty-gritties of the system, I always start a pristine test where I isolate a problem that interests me.

I normally use Minitest since I don't need all the crazy stubbing, but here we go with Rspec.

In the test, I don't even bother to formulate all the describe and it and context and whatever since all that is absolutely not relevant at this point.


# spec/variant_api_prototype_spec.rb

describe "A problem I want to understand" do
  it do

  end
end

Yes you can do that!

Ideally, the application already has some factories so we can play with the problem.

Here is some sample code we've encountered duplicated in multiple places throughout the application.

Having redundant code like this makes for very very fragile code. It's super hard to refactor, because we're spreading knowledge about database internals throughout the codebase. And these details... "product", and "size breaks"? What even is that?

Problem 1


# spec/variant_api_prototype_spec.rb

describe "A problem I want to understand" do
  it do
    product = create(:product)

    price = product.size_breaks[2].price / 100
  end
end

We want to hide these detailed concerns behind a more beautiful interface.


# spec/variant_api_prototype_spec.rb

describe "A problem I want to understand" do
  it do
    product = create(:product)

    price = product.size_breaks[2].price / 100

    expect(price).to eq(9.99)
  end
end

I run only this test file, nothing else. That's a part about Rspec that I really like.


$ rspec spec/variant_api_prototype_spec.rb

PORO Database

Oh, but there's a problem! For some reason, the factories don't work! Maybe they are abandoned. Maybe there's some sneaky database setup trick that we don't know about. Whatever the problem is, fixing it could totally derail our attempt to refactor!

We're here to experiment with new designs, not to fix database failures. So let's stay focused.

Instead of getting sidetracked, let's forget about the rest and simply use a fake database.

Let's introduce some very simple Structs here. Structs are just data structures that are shipped with the Ruby stdlib. They allow you to define accessors for a new object and that's it.

Here's our fake database that still resembles the real situation of the application.

And again, we put all our code into one it block for now. This how I like to do it. You might call me lazy, but I don't bother to structure properly with let and it and describe, because hey, we're prototyping! I like Avdi's name for this: a "narrative test".


# spec/variant_api_prototype_spec.rb

describe "A problem I want to understand" do
  it do
    # setup
    Product   = Struct.new(:size_breaks)
    SizeBreak = Struct.new(:price)

    product = Product.new(
      [
        SizeBreak.new(999)
      ]
    )

    # run
    price = product.size_breaks[2].price / 100

    # test
    expect(price).to eq(9.99)
  end
end

Running the isolated test passes.


$ rspec spec/variant_api_prototype_spec.rb

Let's move on to the refactoring!

Domain object

So that little test case, it's not even really a test, helps us understand the data structures and the domain.

In the next step what we do is to prototype the API we want. As already mentioned, we want some kind of abstraction that hides the fact there are "size breaks" and where they come from.

Let's experiment with introducing a new abstraction. Let's call it "variant". It will have attributes for the distinguishing characteristics of a physical product, such as "size" and "color".

We simply write what we need in this very moment, which is the price for a specific variant.

A super simple assertion checks if the price is still correct.


it do
  # ...

  variant = Variant.new(product, size: "MEDIUM", color: "RED")

  expect(variant.price).to eq 9.99

Variant

Now let's actually create the Variant class.

We do not think about where this class will go, what will be the ultimate name, or if we need namespacing or whatever. For now we just put it in the same test file.

The constructor finds the objects that we want to hide with our new API, in this case it's a specific size break.


class Variant
  def initialize(product, size:, color:)
    @size_break = product.size_breaks.find do |sb|
      sb.size == size && sb.color == color
    end
  end
end

And since we want to know the price, we add a price method.


class Variant
  def initialize(product, size:, color:)
    @size_break = product.size_breaks.find do |sb|
      sb.size == size && color == color
    end
  end

  def price
    @size_break.price / 100
  end
end

Let's run the test again. Works.


$ rspec spec/variant_api_prototype_spec.rb

When we're happy with our new API, it's time to use the real database---which in the meantime has been magically fixed by one of our coworkers! That implies a simple change in our Variant constructor, only.


class Variant
  def initialize(product, size:, color:)
    @size_break = product.size_breaks.where(size: size, color: color)
  end

  def price
    @size_break.price / 100
  end
end

Obviously, the test code needs to use the database as well.


# spec/variant_api_prototype_spec.rb

describe "Variant" do
  it "exposes #price" do
    product = create(:product_with_size_breaks)

    variant = Variant.new(product, size: "MEDIUM", color: "RED")

    expect(variant.price).to eq(9.99)
  end
end

Test runs smoothly. Success.


$ rspec spec/variant_api_prototype_spec.rb


# spec/variant_api_prototype_spec.rb

describe "Variant" do
  it "exposes #price" do
    product = create(:product_with_size_breaks)

    variant = Variant.new(product, size: "MEDIUM", color: "RED")

    expect(variant.price).to eq(9.99)
  end
end

So what we did here was nothing crazy.

We introduced a more domain-focused API via a, let's call it a decorator, without getting lost in setup hell and by ignoring database implications in the first phase.

I love this technique and use it all the time in refactorings, - it helps me focusing, it helps me playing with higher-level APIs and interfaces and that without all the framework noise.

Now go and introduce some nice domain objects! Good luck and cheers!

Responses