Search code examples
swiftfunctionapi-designsingle-responsibility-principle

Using Swift enum to reduce number of parameters in a function


I'm facing the typical problem of too much parameters in a function.

protocol OfflineController {
    func cache(request: OfflineRequestConvertible, forId id: String?, data: Data, keepAliveUntil keepAlive: Date?, completion: @escaping OfflineControllerCompletionHandler)
    func get(request: OfflineRequestConvertible, forId id: String?, ifBefore before: Date?, completion: @escaping OfflineControllerCompletionHandler)
    func delete(request: OfflineRequestConvertible, forId id: String?, completion: @escaping OfflineControllerCompletionHandler)
}

As you can see it is a regular caching system with functions to cache, get the cached data and delete cached data.

Known solutions to this problem are:

  1. Take a look at SRP violations. I don't think it's the case here, at least I don't see any.
  2. 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 to foo(point: Point) but in this case I cannot identify nothing that can be encapsulated (maybe request and id), and it'll be an explosion of types as each method have a different signature. Also, it translates the complexity to the consumer of the API, having to instantiate a concrete object per method.
  3. Create a common parameter type and use a builder to populate it depending on the method invoked. IMHO it obscures the API, how I know I have to send keepAliveUntil when executing cache and not get or delete?

I was delaying the solution to this problem until I recalled Swift's enum. Now I'm thinking on something like this:

enum OfflineControllerAction {
    case cache(request: OfflineRequestConvertible, data: Data, id: String?, keepAliveUntil: Date?)
    case get(request: OfflineRequestConvertible, id: String?, ifBefore: Date?)
    case delete(request: OfflineRequestConvertible, id: String?)
}

protocol OfflineController {
    func execute(_ action: OfflineControllerAction, completion: @escaping OfflineControllerCompletionHandler)
}

Maybe it's more elegant but I think this is very similar to point 2, and I'm keeping the original methods to being dispatched in the enum switch.

The question is, what do you think about this solution? Is it another approach I don't know? Maybe there are no solution and this is simply a problem of design (SRP)?


Solution

  • The Step Builder Pattern may be a way to add parameters incrementally and depending on your needs. Also it lets you define parameters that are mandatory in some cases but not in others (as you mention in point 3).

    Simplifying you have to define a protocol for each parameter setter the builder is going to have. Next is to define a protocol chain, so each protocol defines the builder setter method and it returns the next protocol in the sequence. Finally the builder has to implement all the protocols.

    Using this pattern you define a particular sequence of invocations to builder methods, letting you create alternate paths in the invocations sequence.