Using Step Builder Pattern in Swift
A Java-like approach to reduce the number of parameters passed to a method.
Although the Builder pattern is one of the less spread patterns on iOS (in fact, I cannot recall a single builder in Cocoa Touch or Core Foundation/Foundation), it is a well known pattern on Android, more specifically in Java.
In this article I want to explain the process I took to refactor a piece of code using the Step Builder pattern.
These days I’ve received the specification of a caching system to allow offline consult on iOS applications. It is very simple in terms of API: cache data, get the cached data, delete certain cached data and invalidate the whole cache; the usual methods for the simplest cache system. Additionally there are four more features:
- Every single cached data can be related to an optional given
id
, to store differentdata
for the samerequest
. - Every single cached data can be encrypted with an optional
encryptionKey
. - When caching data, an optional
keepAliveUntil
date can be passed to mark the expiration date of the stored data. - When getting data, an optional
ifBefore
date can be passed to retrieve the cached data only if it is not expired.
So, taking into account these requisites, as a first approach I came up with this design:
It is obvious that the number of parameters passed to the methods is huge. This leads to problems of testability and API consuming; indeed, as it is a Swift protocol, there is no possibility to set default parameters, consuming the method nil
ing parameters as needed (default parameters in protocols can be mimicked with a protocol extension but I think it is not very elegant).
I decided to refactor it so I begun to think and search for a solution, coming up to the following:
- Maybe it is a problem of design. Is it desirable to allow different
id
orencryptionKey
in the sameOfflineController
or is it better to instantiate a controller perid
andencryptionKey
and pass them to the initializer? That could be the solution but it creates another layer of complexity in order to manage several controllers. - Take a look at SRP violations. I don’t think this is the case, at least, I don’t see any.
- Try to encapsulate related data in new types and pass an instance. This is clear in the classic example
foo(x: Double, y: Double)
being converted tofoo(point: Point)
but in this case I cannot identify anything that can be encapsulated, and it will be an explosion of types as each method has a different signature. It also translates the complexity to the consumer of the API, having to instantiate a concrete object per method. - Create a parameter type and use a builder to populate it depending on the method to invoke. I think it obscures the API: how do I know I can send
keepAliveUntil
when executingcache
but it does not makes sense when invokingget
ordelete
?
I was a bit stuck, and I was beginning to think of using another approach like Swift enum
s to try to model this differently. But then, an Android developer friend of mine told me about a pattern in Java, the Step Builder. As mentioned here:
One of the main Step Builder pattern benefits is providing the client with the guidelines on how your API should be used. […] this pattern is often referred to as a wizard for building objects.
As the Builder approach was the one more likely to fit my requisites, I think this is what I was looking for.
The main difference between the Step Builder and the regular Builder is that the Step Builder guides the API consumer in the process of constructing the object, that is, as you set a field, the returned builder meant to continue the construction process depends on the previous value set. As the process of building is guided through a finite path, there is no possibility of wrong constructions.
Take a look at how a regular Builder called OfflineActionBuilder
can be used for my purposes (obviating Swift closure builder this time):
That is a perfect building chain that returns a correct object, but this is possible too:
It does not makes sense to cache no data
or to pass ifBefore
date. When build()
is called, we will get a nil
object due to missing mandatory fields, but in terms of readibility this is not the best approach, and it could be complicated in terms of validation.
So let’s design the Step Builder for this offline system. First of all, we will list the features that our Step Builder has to have:
- It has to allow the building of
cache
,get
, anddelete
actions. - It has to allow to
build()
only when the object returned is going to be correct in terms of consistency and API consuming. - Optional properties cannot be set until mandatory ones are populated.
Next, we are going to identify the path for each of the three actions:
cache
has to be built mandatorily withrequest
anddata
, and optionally withid
,encryptionKey
andkeepAliveUntil
.get
needsrequest
but doesn’t needdata
, and optional fields areid
,encryptionKey
andifBefore
.delete
only needsrequest
as mandatory, withid
andencryptionKey
as optional fields.
It is important to notice again that the optional fields can only be set after setting the mandatory ones.
Less words, show me the code! First, we write the object to be built:
A pretty simple DTO.
Now let’s define the protocols for the steps of the builder.
Let’s describe each one:
BuildStep
speaks by itself. It is the step responsible of building the final object.RequestStep
is the entry point to the building process. It exposes three methods, one per type of action we want to create, and depending on each method, it returns the builder casted to the next step.DataStep
has to populatedata
and returns the next stepCacheCommonsStep
.CommonsStep
is responsible to populateid
andencryptionKey
.CacheCommonsStep
is responsible of populatingkeepAliveUntil
if constructing acache
action.GetCommonsStep
is responsible of populatingifBefore
if constructing aget
action.
Both CacheCommonsStep
and GetCommonsStep
inherits from CommonsStep
as the three steps populate optional fields. In addition to that, CommonsStep
also inherits from BuildStep
as it is the only step that allows the building of the object. So, given this protocols hierarchy, all three CacheCommonsStep
, GetCommonsStep
and CommonsStep
can build()
a correct object.
The implementation is pretty straightforward, each method sets its fields and returns the next step.
Wrapping up, this is how the final solution looks like:
Let’s see some examples of how it can be used for each action type:
It looks like a regular Builder but you will see it in action while typing in Xcode. For example, if you begin the building of the action .toCache(_:)
, the next step will be .data(_:)
, and after that you could populate the rest of the properties allowed in a cache
action.
On the other hand, right after you begin to build a .toDelete(_:)
action, you could call build()
because there are no more mandatory fields to populate.
Let’s see it:
Now, the OfflineController
protocol looks like this, and the implementer is the responsible of handling the passed OfflineAction
to dispatch it properly:
You can play with this example in this playground.
I think this is a smart approach to this kind of problem but, as everything, it has pros and cons.
Pros
- The API consumer is guided in the construction of a correct object with no possibility to build wrong ones.
build()
can only be called when the resultant object is going to be in a consistent state.- You cannot set optional properties until mandatory ones have been set.
- There is no need of validation when calling
build()
.
Cons
- The implementation of the Step Builder is not trivial. If the object to be constructed is complicated in terms of paths, it could be difficult to identify each
Step
to make a proper hierarchy balancing readibility and not repeated code. - It needs fine adjusting to get it perfect in terms of encapsulation, and it could be a bit difficult to read.
In summary, the Step Builder comes to fill the lacks of the common Builder pattern in terms of valid construction chain and readability. It could be a good new resource in your toolbox.
If you are interested in reading more, here there are some resources:
Thanks for reading and comment!