The forgotten art of construction

Danny Preussler
ProAndroidDev
Published in
6 min readJun 22, 2020

--

How tools made us forget how to write sane constructors

https://unsplash.com/photos/qvBYnMuNJ9A

In an ideal world, developers get smarter every day. The code we write this year should be better than the code we wrote 10 years ago, which in turn should be better than the code 20 years ago. Today we have better tools, more modern languages, and better practices.

But as often in life, we realize we are not living in that ideal world. In nearly every codebase I see today, there are things that would shock a developer two decades ago. I am speaking of what Mark Seemann called “Constructor Over Injection”.

We’ve all learned to inject our dependencies into constructors. And ideally use a tool for that like Spring or Dagger.

If you look at some random classes from your codebase, how many fields are you injecting? Three? Five? More? I’m pretty sure you easily can find classes with even more, like this one:

class ProfilePresenter
@Inject
constructor(
@MainThreadScheduler private val mainScheduler: Scheduler,
@IOScheduler private val ioScheduler: Scheduler,
private val profileApi: ProfileApi,
private val userRepository: UserRepository,
private val analytics: Analytics,
private val errorReporter: ErrorReporter
private val referrerTracker: ReferrerTracker,
private val shareTracker: ShareTracker,
private val tracksRepository: TracksRepository,
private val playlistRepository: PlaylistRepository
)

If you would show this constructor to a developer from 20 years ago they would probably look at you as if you would be crazy. No one would want to call this constructor and provide all these parameters.

But these days we don’t care. We don’t have to. We have a tool that will provide us with all those parameters, right?

This does not make it right though!

The proof

If you would use some manual injection code or a service locator like Koin, you would notice more what’s going on because you would need to write code like this:

ProfilePresenter(get(), get(), get(), get(), get(), get(), get(), get(), get(), get())

This makes the insanity of these constructors obvious!

get(), get(), get(), get(), get()… © giphy.com

Testing this class is also no fun anymore as you need to construct or mock a lot of instances.

So what is the underlying problem here?

Single Responsibility

Large constructors are a code smell, not a problem per se. But as with most code smells, they give you a hint that something is wrong. It’s the Single Responsibility Principle that is being violated.

If your class needs ten parameters, there is a high risk that your class is doing more than one thing. Why would it need all these objects otherwise?

So the obvious question now is: how many constructor arguments are acceptable?

Uncle Bob argues in Clean Code:

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic) followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.

That sounds tough, doesn’t it?

There is some valid theory behind this principle. In 100 Things Every Designer Needs to Know about People the author writes, that according to studies:

People can hold three or four things in working memory as long as they aren’t distracted and their processing of the information is not interfered with.

So if you need a max number as a general rule of thumb: make it less than four!

How do we decrease the number of our constructors then?

If you ask the question or look at StackOverflow, often the first answer you will get is: use the Builder pattern!

This does not really help us here though. The complexity of our class would not decrease. And creating an instance of our class is not our problem, our dependency injection library handles this for us anyway.

Extract common dependencies

I would start to look for things that belong together. In our example above we were injecting 2 schedulers:

@MainThreadScheduler private val mainScheduler: Scheduler,
@IOScheduler private val ioScheduler: Scheduler,

It is a good practice grouping those into sth like:

interface RxSchedulers {
val io: Scheduler
val computation: Scheduler
val main: Scheduler
}

Facades

In many other cases simply extracting some fields into some wrapper objects will not help much.

Mark Seemann would call this solution Deodorant:

Reducing thirteen constructor arguments to a single Parameter Object doesn’t address the underlying problem. It only applies deodorant to the smell.

What we can do is hiding some of the details. Again, look for classes that do similar things:

private val analytics: Analytics,
private val referrerTracker: ReferrerTracker,
private val shareTracker: ShareTracker

These seem all related to tracking. Our class does not need to do know all the details! Hide them behind a Facade and expose those only those methods you need here.

class ProfileTracker(
private val analytics: Analytics,
private val referrerTracker: ReferrerTracker,
private val shareTracker: ShareTracker
fun trackProfileOpened(referrer: String) =
referrerTracker.profileOpened(referrer)
fun trackProfileShared() =
shareTracker.profileShared()
}

A good thing to keep in mind is that our presenter should have no idea that it's tracking to Firebase for example. That is an implementation detail! You don't want to add another field just because you add another analytics library. If you see something like that, extract and encapsulate:

interface Analytics {...}class DefaultAnalytics
@Inject
constructor(
private val firebaseAnalytics: FirebaseAnalytics,
private val mixPanel: MixpanelAPI
): Analytics

Via Interfaces

Remember that a class can implement multiple interfaces! You could build facades simply by splitting into interfaces.

class DefaultAnalytics : ScreenTracker, ShareTracker, ProfileTracker

Your favorite dependency injection library handles that for you without exposing details. Example for Dagger:

@Binds abstract fun bindScreenTracker(analytics: DefaultAnalytics): ScreenTracker@Binds abstract fun bindShareTracker(analytics: DefaultAnalytics): ShareTracker@Binds abstract fun bindProfileTracker(analytics: DefaultAnalytics): ProfileTracker

Use cases

In my experience classes like Presenters or ViewModels, which interact with views, tend to become large. You will often find that those dealing with multiple repositories:

    private val userRepository: UserRepository,
private val tracksRepository: TracksRepository,
private val playlistRepository: PlaylistRepository,

Like shown earlier, those can be combined. But users, tracks and playlists don’t really have something in common, so maybe a simple facade repository is not the best way. As we probably need them in a very specific way for profiles, Clean Architecture suggests the UseCases design pattern here.

class ProfileUseCase(
private val userRepository: UserRepository,
private val tracksRepository: TracksRepository,
private val playlistRepository: PlaylistRepository,
)

This way you can simply expose what is needed for the profile “use case”.

Split it up

At some point, you will end up with arguments that don’t have much in common. This is the point when you should revisit whats the purpose of your class. What is it’s responsibility? Then start splitting it up!

Uncle Bob gives the tip for the Single Responsibility Principle:

Gather together the things that change for the same reasons. Separate those things that change for different reasons.

Some developers don’t like the idea of having multiple view models or presenters. This is a matter of taste and your architecture, you can combine them into one that is the source of truth for your view.

Sum up

As with other things all these rules are guidelines. You probably do not want to start with Facades and Use Cases on the green field.

© Walt Disney Company

Those large constructors normally grow over time and end up being the monster we saw. To be sure to not let the creep in, here are some things you might want to consider checking regularly:

  • Try to stay below four parameters
  • Group things together that belong together
  • Check if your class has only one responsibility, otherwise, start splitting

Important is that you keep some sanity!

--

--

Android @ Soundcloud, Google Developer Expert, Goth, Geek, writing about the daily crazy things in developer life with #Android and #Kotlin