How To Fight Premature Abstraction Early In The Code

7 min read

thumbnail

I sometimes have to clean up overly complex code.

Only to rewrite it myself, in an even worse way. (It’s harder to follow your own principles than to teach other. So I write this to remind myself, and keep this as a reference for the future.)

The inconvenient truth is this:

Your abstraction won’t matter in the future

My name is Till Carlos and I document how my team builds products. And today we are talking about what can kill progress in software projects, early on.

This is inspired by

And then one of my developers suggested of implemented a new layer, and my detector went off right away:

thumbnail

Let’s sort this out.

The Abstraction Trap

Eileen M. Uchitelle, eileencodes.com, on “What makes projects fail”, in my blog post

You’ve probably been there. You’re coding along, and suddenly you see a pattern. “Aha!” you think, “I can abstract this!” Before you know it, you’re knee-deep in creating a beautiful, reusable piece of code that will solve all your problems… or so you think.

But here’s the kicker: most of the time, that abstraction you’re so proud of? It won’t be used more than once or twice. And worse, it might make your codebase harder to understand for the next poor soul who has to maintain it (which, let’s face it, might be future you).

Do too much complexity out of the gate, then you end up with a failed project.

I often see this with Services. People start with the best intentions

module Service
  class GenerateUsersCsv < ::Service::Base
    EXPORT_LIMIT = 5

    def self.call(start_date, end_date)
      headers = [
        'Name', 'E-Mail', 'Registered Date'
      ]
      start_date = start_date.to_datetime.beginning_of_day
      end_date = end_date.to_datetime.end_of_day
      rows = User.where(created_at: start_date..end_date)
            .limit(EXPORT_LIMIT).map { |user| create_row(user) }
      AppServices.generate_csv(headers, rows)
    end
  ...
end

And then they want to keep consistent. Do you want to guess generate_csv ?

It’s this:

require 'csv'

module Service
  class GenerateCsv < ::Service::Base
    def self.call(headers, data_rows)
      CSV.generate(headers: true) do |csv|
        csv << headers
        data_rows.each { |row| csv << row }
      end
    end
  end
end

And on top of this, we now need to route the services:

class AppServices
  def self.generate_csv(*, **)
    ::Service::GenerateCsv.call(*, **)
  end

  def self.generate_users_csv(*, **)
    ::Service::GenerateUsersCsv.call(*, **)
  end
end

Someone just compacted 4 lines of code into one… at he expense of:

  • A new class that needs tests
  • 13 new lines of code
  • A new route in AppServices
  • Explaining to other devs they should now all use this new service.

It looks great when creating it. The world of “all services, everything” looks glossy on the outside.

But later on, it can convert to the dark side fast:

  • The Services router gets complex
  • Authorization gets more difficult (where to put the authorization calls?)
  • More tests are needed, that means more files.

Creating a service like this might be the best idea. But it should be done later, when the simplest approach does not work.

What’s the simplest approach?

This is what I learned from devs who are much better than I.

How to get out of it

Several things were said in a conversation, and I’ll document them here. The first one sounds controversial, especially to Rails devs. But it makes sense if you think about it.

Duplicate code

Yes, violate DRY. Build technical debt. Until you know better.

Then count how often your code gets duplicated. It’ll be probably stop after 2, maximum three times.

Once it goes over 4, then you can think ouf a beautiful solution.

Go the standard way

For us, it’s the Rails Way. We follow the MVC approach.

  • Views display the data from models
  • Controller initialize and modify data (also: Auth*)
  • Models persist and load data.

What does this mean for our service? We can copy the principle from 37signals, the company that built Rails. Not everything they touch is gold, but we get a good idea of how to keep things simple.

app/models/first_run.rb

class FirstRun
  ACCOUNT_NAME = "Writebook"

  def self.create!(user_params)
    account = Account.create!(name: ACCOUNT_NAME)

    User.create!(user_params.merge(role: :administrator)).tap do |user|
      DemoContent.create_manual(user)
    end
  end
end

This is a model. No, it doesn’t persist data, but it’s very easy to find in the app/models folder. The controller working on it is named similarly:

app/controllers/first_run_controller.rb

  def create
    user = FirstRun.create!(user_params)
    start_new_session_for user

    redirect_to root_url
  end

We should ask ourselves: do we really need a more complicated approach right now? Or can we go with the simplest possible??

Only go abstract, if

Just some pointers if we are still not sure:

  • You have seen something proven before (Yes, I’d count the services to that)
  • Are there real lines of code saved? Is real duplication avoided?
  • Does this new abstraction help the understanding of the code?
  • Will other developers be able to pick this up fast?

And lastly:

What would happen if nobody ever continues with your journey of abstraction? Would it still make sense there?

If you disagree with this: send me a nice message on twitter.


Till Carlos

I'm Till, a senior developer who started a software company. I explain software concepts for people in leading roles.