Combatting Overengineering: Don't Be Afraid To Throw Away Your Code

Written by nfrankel | Published 2021/04/30
Tech Story Tags: over-engineering | dry | wet | good-practice | engineering | engineering-management | coding | code-quality

TLDR This week’s post is pretty short, but this adds a personal touch. I had to rewrite my Jet Train demo to use another data provider. I refactored the code in order to comply with the DRY principle. When looking at the code, it didn’t look more readable and the code added to every function was minimal anyway. I threw away my refactoring work in favor of the WET principle. There are two lessons here: think before you code - this one I regularly forget.via the TL;DR App

This week’s post is pretty short. I’ve already written about overengineering, but this adds a personal touch.
I had to rewrite my Jet Train demo to use another data provider, switching from a Swiss one to a Bay Area one. One of the main components of the demo is a streaming pipeline. The pipeline:
  1. Reads data from a web endpoint
  2. Transforms data through several steps
  3. Writes the final data into an in-memory data grid
Most of the transform steps in #2 enrich the data. Each of them requires implementation of a
BiFunction<T,U,T>
.
These implementations all follow the same pattern:
  • We evaluate the second parameter of the
    BiFunction
    .
  • If it is null, we return the first parameter;
  • if not, we use the second parameter to enrich the first parameter with and return the result.
It looks like this snippet:
fun enrich(json: JsonObject, data: String?): JsonObject =
  if (data == null) json
  else JsonObject(json).add("data", data)
In the parlance of Object-Oriented Programming, this looks like the poster child for the Template Method pattern. In Functional Programming, this is plain function composition. We can move the null-check inside a shared function outside of the bi-function.
fun unsafeEnrich(json: JsonObject, data: String?): JsonObject =
    JsonObject(json).add("data", data)                                          // 1

fun <T, U> nullSafe(f: BiFunction<T, U?, T>): BiFunction<T, U?, T> =            // 2
    BiFunction<T, U?, T> { t: T, u: U? ->
        if (u == null) t
        else f.apply(t, u)
    }

val unsafeEnrich = BiFunction<JsonObject, String?, JsonObject> { json, data ->  // 3
  unsafeEnrich(json, data)
}

val safeEnrich = nullSafe(unsafeEnrich)                                         // 4
  1. Move the null-check out of the function.
  2. Factor the null-check into a
    BiFunction
    .
  3. Create a
    BiFunction
    variable from the function
  4. Wrap the non-null-safe
    BiFunction
    into the safe one
We can now test:
println(safeEnrich.apply(orig, null))
println(safeEnrich.apply(orig, "x"))
It works:
{"foo":"bar"}
{"foo":"bar","data":"x"}
When I finished the code, I looked at the code and thought about the quote from Jurassic Park:
Your scientists were so preoccupied with whether or not they could, they didn’t stop to think if they should.
I’m no scientist, but I felt it applied to the work I just did. I realized that I refactored in order to comply with the DRY principle. When looking at the code, it didn’t look more readable and the code added to every function was minimal anyway. I threw away my refactoring work in favor of the WET principle.
There are two lessons here:
  1. Think before you code - this one I regularly forget.
  2. Don’t be afraid to throw away your code.

Written by nfrankel | Developer Advocate for Apache APISIX *** Learner *** Author of http://leanpub.com/integrationtest
Published by HackerNoon on 2021/04/30