Dakku Hanto: How POODR helped me solve the case and find the duck who I named a crime fighting duck

Do you remember???

So I’m sitting there crouched over Sandi Metz’ Practical Object-Oriented Design in Ruby and I’m deep in the chapter on duck typing and suddenly I’m having one of those Keanu-whoas. Be like: I’m literally working on code for a project that has THIS anti-pattern. And now I know how to fix it.

This is a story about when duck typing really hit home for me sometime in 2016.

Let me run it back a little: I worked at Science Exchange until earlier this year on their web app(s) which powered a two-sided marketplace. The marketplace is an exchange of experimental services between scientific researchers seeking services and labs providing the services.

For our purposes here, you should understand that when a provider first receives a request through the site an “order” is created. The researcher or provider can visit this order at /{username}/orders/{id}. Once an order is created, the provider can visit the order page to generate a “quote” for the requested service. The quote and order are both database objects where Order has_many Quotes.

One day new business requirements came down the pipeline to allow more flexibility in how we displayed service fees to requesters on the quote. The engineering task would be to “unwrap” the single-line fee totals: instead of only displaying an aggregate line item total as part of the subtotal, we wanted to display the unique service fee separately with each line item. This would give us the ability to flexibly hide fee totals for requesters during the order flow if we were running a promotion or applying a special type of fee. One possible UX impact would be bundling the fee into the line item total.

Here’s an example of the changes to the quote builder interface where the provider can still see the fee, but as a separate column:

And the requester side, where the fee is “hidden” by being bundled with the line item subtotal:

As I scoped the project, I knew tweaking the frontend components would be easy once our API could send down a new attribute on each serialized representation of LineItem model, something like object.service_fee, but that didn’t exist yet.

In my (optimistic) imagination this was going to be a fairly simple change to our Rails code where I’d port whatever fee calculation method already existed on other domain objects like Quote over to LineItem. My assumption was that there was a common interface for checkout-related models like Quote or Order to receive a message calculate_fee and return a fee value. But lo and behold not so. Instead I discovered the inverse: a service object (Class) which received of domain objects and returned a fee value. It looked something like:

Ok, sure: the class had an obvious interface — just pass an instance of anything that needed fees calculated like:

FeeCalculator.calculate_fee(quote)

BUT SANDI METZ SAYS NAHHHHHH
(I was crouching there reading all about why this could be bad.)

A gotcha: this service class, likely abstracted to provide encapsulation and clarity, actually has to know a lot about the objects passed in. 🚨 : tight coupling. Metz warns that while encapsulation can obscure some imperfection, it can’t hide it for long in a growing application.

Case above, in point: the case statement has multiple conditions that track into unique handlers for each domain object and the control flow is already a little hairy and there are signs of possible duplication. It’s possible the design came from someone’s idea of skinny models. Or perhaps the fee calc math was more universal at first. Though more likely it was that timeless yo homie, these are core objects of our data layer they won’t change that much. Didn’t matter too much now. Even though the cyclomatic complexity wasn’t thaaaat high I didn’t want to be responsible for increasing the mental overhead for the next dev by 25% or take out another loan against our tech debt.

So, Sandi Metz. The hanto had begun.

What was amazeface was that this case statement is literally Sandi Metz’ first example of code smell that helps you recognize a hidden duck type. (A pattern that once exposed can help recast the shape of your code.)

The most common, obvious pattern that indicates an undiscovered duck is…a case statement that switches on the class names of domain objects of your application. (p96)

I was face to face with a textbook definition of a code smell where we had traded off extensibility for concretion (Metz’ word) and avoided the dakku hanto. Sure, it’s easy to peer into that one place in the code — FeeCalculator.calculate_fee — and inspect the when condition arguments to know what objects it’s designed to handle. But, as Metz warns, adding that kind of tight coupling and dependency to class means you’re putting a lot of faith that additional objects won’t be added and that the current objects won’t change. What’s more, you’ve missed the chance to identify and utilize an abstract class or “across-class interface” that will make your application more flexible and trusthworthy. I love this idea of building trust into your application code. Trust is the foundation of collaboration for people, and therefore code objects too (human made things)!

Flexible applications are built on objects that operate on trust; it is your job to make your objects trustworthy. (p98)

Nonetheless, even as my eyes danced back and forth between the book and that monokai blackness, the dakku looming larger and larger in my sight…seeing that code puts you at a crossroads. Deciding what to do next is one of those classic moments of software engineering where you pull out your tech debt ledger and do a tradeoff standoff. Is now the right time to refactor? What size of refactor are we talking about here? How much will it cost the company to add more fibonnaci points to the sprint????

I dug my heels in and decided this wasn’t gonna be a huge one and wouldn’t delay writing the new APIs and interfaces for the frontend in an acceptable margin (turned out to be true!). All I needed to do was three things to tidy up:

  1. Rip generic math code from the specific domain object behavior in FeeCalculator
  2. Implement a polymorphic interface for the new dakku type on each domain object as needed: Quote, Order, LineItem, etc…
  3. Move domain object behavior to the models

I was excited that all said and done we’d end up with a codebase that was more resilient to future changes and ambiguity, objects defined by behavior and not by their class, and most importantly more polymorphism — which makes you seem smart. I was willing to take the risk and thusly trained my barrel on that dakku and steeled myself to ignore any hairy yaks coming round.

The first task was relatively easy since the previous dev had defined methods for the maths like calculate_fee_percentage ,convert_to_usd, round_price , etc… I just needed to make sure no code lingered in those method blocks that cared about domain object types. The happy side effect of separating domain behavior from maths during this step is that I was able to eliminate an entire layer of supportive logic that did stuff like type check Quote and Order — which are similarly shaped — and determined Order fees based on the state of their Quote objects (since orders has_many :quotes).

The second task was also relatively simple, though conceptually more complex. I needed to create an interface for our new virtual dakku type. Let’s call him Darkwing (one of my fave cartoon heroes of ALL TIME):

Disney animation that first ran from 1991 to 1992, when I was 10 year old

Any object of Darkwing type would implement what becomes a polymorphic method signature calculate_fee. So rather than having to send calculate_fee on a service object, our domain models would be able to receive the message:

Lastly, I moved domain specific behavior into these methods, which would end up looking something like this:

class Quote < ActiveRecord::Base
def calculate_fee()
fee = price.to_f * fee_percentage
FeeCalculator.round_price(fee)
end
end

You can now clearly see the boundary between the domain specific business logic and the fee calculation maths. Having identified and implemented an interface for Darkwing, there is much more flexibility to modify, extend, or swap the behavioral logic for calculating fees on any of these domain models.

Beware dakku. I will find you, and type you.

Never released new Duck Hunt for NES Wii U, I think?
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.