Type Gymnastics with Builders - Part 7 - Why bother?

Views
Article hero image

So far in this series we’ve been focused on the What and How of more complex builders, and we’re aware that this does leave the question of “Why” hanging in the background.

Guest post by Morgen Peschke

This is a very fair question, because putting together the full feature set does require quite a bit of work. In this post we’ll go over a set of rough criteria for when to add each level of complexity. It’s worth pointing out that, as with most things of this nature, this is an exercise in tradeoffs with no absolute rules and as such there will be a lot more of my personal opinions here than the previous, technically-focused, entries.

Disclaimer!

As a general rule, don't use a builder at all, if you can avoid it - for most situations, building manually isn't particularly difficult, and the time spent putting together a builder will generally be better spent doing other things.

Basic Builder

Basic builders as described in part 1 are usually good to start considering when both of these conditions hold:

  1. You have to create something frequently
  2. The object is complex enough that this becomes noticeably annoying

Put another way: start looking into builders only when building unassisted starts to become painful.

While a case could be made for starting with a Java-Style builder, personally I find their tendency to fail at runtime with only a single error frustrating enough that I’ll generally go straight to one of the flavors of Basic Builder that leverage more of Scala’s type system.

Better UX

The added cost of implementing the better error info introduced in part 3 is fairly marginal, but for smaller builders the added benefit is also pretty small. It really starts to shine once any of these conditions hold:

  1. There are more than about 5 parameters
  2. There are more than about 3 parameters with the same type
  3. You start getting annoyed by the error messages

Personally, I find the cost of creating a typeclass to be fairly minimal, so I’d be tempted to just add this one in, but that’s very much a case-by-case, developer-by-developer decision.

Flexibile Vargs

Honestly, I nearly default to the style introduced in part 4 whenever there’s something that needs configured with a collection of any type. It’s just so much easier than having to wrap everything in a list and submit it at once.

That being said, if that isn’t a pain point for your usecase, it’s about the most optional thing we’ve covered in this series.

Added Constraints and Internal values

Things really start getting complicated when moving on to adding sequencing and internal intermediate values, as introduced in part 5 and expanded on in part 6.

This really shines in situations like these, which make unassisted building awkward:

  1. Enough steps require common dependencies or configuration that it gets annoying to manage
  2. Some steps return an effect, especially if that effect changes (e.g. switches from F[_] to Resource[F, _] )
  3. Steps need to be done in a particular order, but the types can’t really help.

Case 3 usually shows up when I’m dealing with types that are provided in ways that make it difficult to subclass them (e.x: many abstract methods which have a private default implementation), and delegation needs to happen in a very specific order.

Special mention goes out to using typeclasses to control the intermediate steps and their order. This is almost never worth doing and was explained for didactic, rather than practical, goals.

Conclusion

All of the previous recommendations can be summed up in this rule of thumb:

Builders are a practical solution to practical problems. If you don’t have a problem, you probably don’t need a builder.

Thanks for sticking with us through this series, and I hope it’s been as helpful for you to read as it has been fun for us to write.

If you want the finished code, there is one last stop in this series.

scala builder types series