In Progress
Unit 1, Lesson 1
In Progress

The TOO HARD Code Smell

When I was a teenager I took flying lessons. I remember one day when I felt like I was really getting a hang of it. I was reacting to the motions of the aircraft, constantly making dozens of tiny control corrections. Just as I was starting to marvel a little at my own skill, my instructor—who could feel every movement I was making on the controls—said: “Hey, settle down! Relax, and stop making so many control inputs!”.

Just as with piloting airplanes, programming takes a lot of skill. But sometimes we can get so caught up in the complexity of what we do that we overcomplicate things for ourselves. In today’s episode, guest chef Chris Doyle is going to show you how sometimes, when what we’re working on seems especially hard, it’s really a sign that we need to take a step back and figure out what we’re missing. Enjoy!

Video transcript & code

The TOO HARD Code Smell

There's an underlying meme in programming that programming is hard, and that it's supposed to be hard. In many ways programming IS hard - there's a lot to learn, there are a lot of moving parts, things that can fail, and you might not even have access to those failing components. All that stuff is legitimately hard, and I don't want to minimize that essential complexity. But there's also incidental complexity. That's the pain you feel when you read code you wrote six months ago and can't quite remember how it works. It's the pain of watching your project grind to a standstill as you find edge case after edge case after edge case. It's the pain of slogging through a challenging problem and thinking there must be an easier way. Often you're right - these are things you CAN control, things that don't have to be hard.

Let me show you what I mean.

I work with regulations, and for a long time I had code matching the way the regulator organizes the rules.

An object for the rulebook itself,

which is broken down into chapters,

which themselves are broken into sections,

which are broken into rules.


    class Rulebook
        attr_accessor :name, :regulator_name
        attr_accessor :chapters, :rulebook
    end

    class Chapter
        attr_accessor :name
        attr_accessor :sections, :regulator
    end

    class Section
        attr_accessor :name
        attr_accessor :rules, :chapter
    end

    class Rule
        attr_accessor :name, :body
        attr_accessor :section
    end

I often want to know which regulator a rule came from, so I traverse the relationships from rule up to section, section to chapter, and finally to rulebook, which has the name of the regulator.


    irb> rule_regulator = rule.section.chapter.rulebook.regulator_name

A little verbose, but very explicit, and good enough for my purposes.

Until of course I find an edge case - a bigger rulebook that has subsections as well as sections. As my original assumptions break down, my code starts to get more complicated.

Now I need to adjust the attribute accessors for Sections.

...and where there used to be just a straight relationship from Sections to Rules, now I need some special logic to be able to list Rules that accounts for subsections if they're present.

...as well as different logic to traverse from Rules back to Sections.

It's difficult to write this code, and I can already feel like it's going to be difficult to maintain.


    class Section
      attr_accessor :name
      attr_accessor :subsections, :chapter
      attr_writer :rules

      def rules
        subsections.flat_map(&:rules).concat(@rules)
      end
    end

    class Subsection
      attr_accessor :name
      attr_accessor :rules, :section
    end

    class Rule
      attr_accessor :name, :body
      attr_accessor :subsection
      attr_writer :section

      def section
        subsection.section rescue @section
      end
    end

It's also more difficult to use. My original code still works, but anyplace I want to use subsections I have to remember they might not exist.


    irb> rule_regulator = rule.section.chapter.rulebook.regulator_name
    irb> rule_context = rule.subsection ? rule.subsection.name : rule.section.name

But who said I had to make objects for rules, sections, and subsections? No one! One day I finally realized I talk about this data as a tree all the time but don't actually have a tree concept in my code.

When modeled as a tree, everything falls into place much more naturally. With a single class to represent generic tree nodes

and the occasional subclass, like this one for Rules, I have a much more flexible way to represent this same data.


    class TreeNode
      attr_accessor :name
      attr_accessor :parent, :children

      def root
        parent.root rescue self
      end
    end

    class Rule < TreeNode
      attr_accessor :body
    end

My usage cleans up a lot too. I can get the name of the regulator and the section or subsection without creating brittle dependencies on my data structure.


    irb> rule_regulator = rule.root.name
    irb> rule_context = rule.parent.name

This a stripped down example, but experiences like this taught me about what I call the TOO HARD code smell.

When I'm really struggling with a piece of code, when I find myself getting adversarial and frustrated... when it just feels too dang hard! That's my cue to take a step back. Often what's happening is I'm missing an abstraction.

At its core, programming is about representing a tiny slice of the real world. And like any art form, the way you craft that slice creates a highly opinionated perspective.

You have to make so many choices every day about what to include and what to leave out.

High Fidelity Car With Silhouette

Are you working for an auto shop?

You're probably going to have very detailed models of cars, and maybe just the names and contact info of your customers. Are you working for a doctor?

High Fidelity Car With Person

You're going to have way more detailed information about those exact same people, and probably

Person

nothing about their cars.

Abstractions are so difficult because the words you use both inspire and limit your future ideas.

If I asked you to build a birdhouse and I gave you a hammer,

Wooden Birdhouse

I'm going to get a very different birdhouse than if I gave you a

Cloth Birdhouse

pair of scissors.

The tools don't dictate the solution, but they do encourage a certain type of solution. If I gave you a hammer, you could still have decided to find your own scissors and build that cloth birdhouse. But the fact that you're holding a hammer puts you in the mindset to use wood.

As developers, we pride ourselves on creatively using the tools at hand to solve challenging problems. But as developers, we also build our own tools.

This is a fundamental responsibility of development.

The tools we build for ourselves today are going to guide our thinking tomorrow. A year ago, I thought chapters and sections were the right tools for my problems - and when a problem came along and those tools weren't right anymore, it took me a LONG time to replace them because I was so habituated to that one perspective.

This is a lot of pressure, but luckily, there's no right and wrong when it comes to abstractions. There's no beginning and end, either. It's process of constant iteration. So definitely think carefully about your abstractions, but don't worry about it too much. You can always change them - IF, you can remember to take a step back every once in a while.

I want to leave you with a few strategies that I use when I encounter the TOO HARD code smell.

Strategies 1

Try writing a couple paragraphs in plain English about what you're doing, and pay attention to the words you use. Does your code have objects representing the nouns and methods representing the verbs? (For some more examples of paying attention to your domain terms, check out Episode #87)

Strategies 2

Try zooming out. Think about how this problem fits into the broader ecosystem. Or zoom in, and think about what are the most basic building blocks here.

Strategies 3

Do a quick refresh on your ruby design patterns - are you reinventing a wheel?

Strategies 4

Look for parallels in other domains. Well established domains like manufacturing for example have decades of research and good ideas incorporated into their solutions that you can adapt to fit your situation.

Strategies 5

Try anything you can think of to put down your existing words, existing solutions, and start fresh down a new road.

So the next time something just feels really hard, try to forget about your tools for a minute and start with a fresh perspective. Maybe your problem is intrinsically difficult - or maybe by revisiting your abstractions you can make it a little easier.

Responses