I recently had code review feedback on a piece of code that I didn’t expect. The feedback is based on a perspective with a couple of good points, but among a couple of different perspectives with their own good points.
(DRY = Don’t Repeat Yourself)
I am here seeking further opinions, perspectives, and/or logical reasons for choosing any implementation style over another, among the following implementations.
Setup:
open class A {
fun doThing() {}
}
class B : A()
class C: A()
val foo = A()
val bar = B()
val baz = C()
Now, I want to invoke doThing() on foo, bar, and baz. There are plenty of ways to do this. Here are three approaches:
- Straightforward approach:
foo.doThing()
bar.doThing()
baz.doThing()
- DRY approach
listOf(foo, bar, baz).forEach { it.doThing() }
- Declarative DRY approach
inline fun <T> forEach(vararg items: T, action: (T) -> Unit) {
for (element in items) action(element)
}
forEach(foo, bar, baz) { it.doThing() }
Here is what I see so far:
- Straightforward
- Pros
** No excess constructs. Easy to read when your operation is a single function invocation. - Cons
** Repetitive (violates DRY). Code evolution could get hairy if the repeated routine becomes more complex than a single function invocation.
- DRY
- Pros
** No hand written code repetition (no copy/pasting code). Altering the repeated routine for one item will give the same altered routine to the other items without fail. Enclosing the repeated routine in a lambda is good if the routine ever grows more complex. - Cons
** Less efficient execution than straightforward approach
** declares a list, when having a list is not what we are actually interested in.
- Declarative DRY
- Pros
** Same pros as DRY approach
** Declares the exact intent, without excess declaration of a random list just so we can find our way to a forEach. - Cons
** Less efficient execution than straightforward approach
** Not in the standard library, and so unfamiliar. Initial readability may suffer
Now I request the thoughts of the public:
- Is there a reason you would reject any of the above approaches almost universally?
- Or perhaps would you decide on an approach strictly on a case by case basis, measuring all pros and cons?
- Or would you generally lean towards one or two approaches over the others (maybe based on more than just immediate needs), only falling back to your lesser-preferred when the cons significantly stand out?
Circumstantial variables to keep in mind:
- What happens when the list of items we want to affect grows? foo, bar, baz, buz, beep, boop, so on, so forth
- What happens when the routine we want to perform grows more complex? e.g. instead of a single function invocation, perhaps a very complex routine written in place
- What happens when neither of the above are true, and the list stays short, or the routine stays simple?