Skip to content

More on declarative code. And a little glance at a pseudo-DSL

Note: This post could be considered as the third in a series about declarative vs imperative code. The first post was Declarative vs imperative code, and the second one was More on declarative code. And simplifying APIs.

Let’s continue where we left off in the previous post. Our API was neat:

But there was still something ugly; the way the ProgramConstraints struct is declared:

There you have it: an initialiser (implicit in this case, and inferred by the compiler) with four parameters. Not cool. The reasons why it is not cool are basically the same that were enumerated in the previous post, but just to summarise:

  • Cognitive load. Too many parameters make the initialiser signature difficult to understand with just one glance.
  • It is still possible, even though Swift supports named parameters, to make mistakes and mess the parameter order.
  • Also, it seems likely that, along the way, we find the need to add more properties to that object, which would require even more parameters in the constructor.
  • If four parameters are too many for an API method, four parameters are also too many for an object constructor.

The simplest approach

One might be tempted to say: forget about the constructor, just write ProgramConstraints with an empty initialiser, and a bunch of read/write properties. That would make creating an instance of it way easier, and you should not need to care about the amount of parameters you pass to the initialiser.

The simplest approach is not always the optimal one.

There are a few issues with that approach. But the main issue, to me, would be…

Immutability is good. And temporal coupling is bad.

If you don’t need to mutate something, you would probably be better off if you made it immutable. Why?

  • It makes everything simpler. To begin with, you don’t need those properties to be read/write, so there is no need to declare them as such. Also, if you make those properties readonly, you don’t need to care about thread-safety.
  • It avoids temporal coupling. Leaving the fancy name aside, that means that it avoids having an object in your system, alive, but in an unsafe or incomplete state. For example, that’s what would happen if you create an instance of ProgramsContraints but do not set all of its properties? You could either provide default values or declare those properties as optionals, but none of those are an ideal solution.

Also, there are other, more subtle concerns. Imagine that you need to gather the values of the parameters you need to pass though ProgramContraints initialiser from different parts of the system. Even better (or worse, in this case) if at least one of those parameters must be fetched from an async operation. Again, you would need to store some of those values somewhere, wait until you know you have them all, and then build you object.

Construction Builder to the rescue. This is the bit about declarative code and DSLs

Construction Builder, as defined in Domain Specific Languages, seems like the perfect solution to these issues.

The difference between Construction Builder and the Builder Pattern is very subtle. Their intent is basically the same, or at least it is very similar, but at least when it comes to the canonic definition, the Builder Pattern is more oriented to deal with optional parameters in the initialiser, which is not exactly our case.

Anyway, the point here is not discussing the Builder Pattern, but discussing how a little bit of extra abstraction can help make our code more declarative, and therefore easier to understand and consume.

And, as a bonus point, in this case, we get to take advantage of some neat Swift features: guard and throws.

The Builder.

Basically a Construction Builder is an object that can construct other another object. To do so, it will store all the values of all properties of the object to be constructed and then, when asked for it, build said object and return it.

Now, with a little tweak, we can turn the Builder’s API into a tiny DSL. Or at least, let’s agree that we can make it quite declarative.

Notice how each setter returns the builder itself, so using this builder would look like this:

Neat! A fluid API, declarative, easy to understand at a glance. Big win.

Oh, no! Did you just force-unwrap all the things!

Yes, I did. And, yes, that’s a huge no-no.

Seriously, if you are force-undraping your optionals, you are probably doing something wrong (except outlets: force-unwrap IBOutlets!).

Guard and throws to the rescue!

But fear not, there is a very elegant way to avoid force unwrapping and also guaranteeing that the builder has al the information needed to build the object under construction.

We are going to add a little validation to the build method, that will allow us to ensure that all the properties of the ProgramConstraints object are set. And if that does not happen, then we will throw an error:

And this would be how we create and use an instance of ProgramConstraints:

Notice how, as an added benefit, we can be sure that whatever we pass to fetchPrograms, is valid.

Now, this example might seem a little trivial. I give you that. But now imagine that you need to validate the values of the properties in the ProgramConstraints object, before sending it to the fetchPrograms method, according to a particular set of business rules that might change at runtime. Your new builder can take care of that, and only create and actual instance of ProgramContraints if that validation is satisfactory. And none of your client code would need to care a bit about that.

Final words

I guess this is time to remind, again, that the best way to deal with complexity is rely on abstraction. And that, a little bit of abstraction goes a long way in terms of what can be achieved.

In this case, an apparently unnecessary object (a builder) written with a little bit of extra care (providing a fluid API) makes code more declarative, easier to understand, and adds more flexibility and robustness to the overall design.

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *