Contents

Getting Hurt With Swift Protocol Extensions & Default Parameter Values


Of course, Swift protocol extensions and default parameter values are great features. And they are always safe, aren’t they? Well, not really.

Overview

In this article, I’ll show you how to get hurt using protocol extensions and default parameter values together. If you don’t know well these two features, no worries, I’ll explain them, briefly, in the first two paragraphs—feel free to skip them if you know well the subjects.

After the explanation of these two features, I’ll introduce the threat step by step with some examples. In the end, I’ll provide some suggestions to remove it. Happy Reading.

Protocol Extensions

Protocols can be extended to provide method and property implementations to conforming types. This allows you to define behavior on protocols themselves, rather than in each type’s individual conformance or in a global function.

We have a protocol APIRequestProtocol, which contains a method request and the members baseUrl and query. Then, we create two classes, UsersAPIRequest and GroupsAPIRequest, to get the users and groups data from an API request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
protocol APIRequestProtocol {
    var baseUrl: String { get }
    var query: String { get }

    func request() -> Any
}

class UsersAPIRequest: APIRequestProtocol {

    var baseUrl: String {
        return "my_baseUrl"
    }

    var query: String {
        return "?get=users"
    }

    func request() -> Any {
        let url = baseUrl + query
        // send api request to url
    }
}

class GroupsAPIRequest: APIRequestProtocol {

    var baseUrl: String {
        return "my_baseUrl"
    }

    var query: String {
        return "?get=groups"
    }

    func request() -> Any {
        let url = baseUrl + query
        // send api request to url
    }
}

You can notice that both classes have the same value for the member baseUrl and the same implementation for the method request. To get rid of this duplication of code, we can use Protocol Extensions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
protocol APIRequestProtocol {
    var baseUrl: String { get }
    var query: String { get }

    func request() -> Any
}

// Protocol Extensions
extension APIRequestProtocol {
    var baseUrl: String {
        return "my_baseUrl"
    }

    func request() -> Any {
        let url = baseUrl + query
        // send api request to url
    }
}

class UsersAPIRequest: APIRequestProtocol {

    var query: String {
        return "?get=users"
    }
}

class GroupsAPIRequest: APIRequestProtocol {

    var query: String {
        return "?get=groups"
    }

}

After the refactor, both classes use the default implementation inside extension APIRequestProtocol.

In this way, if the compiler doesn’t find an APIRequestProtocol implementation inside UsersAPIRequest/GroupsAPIRequest, it will be able to use the implementation inside the protocol extension.

Default Parameter Values

You can define a default value for any parameter in a function by assigning a value to the parameter after that parameter’s type. If a default value is defined, you can omit that parameter when calling the function.

We have a class View, which has a constructor, init, to set its background color. Then, we initialize 4 View objects using its constructor:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class View {

    private let backgroundColor: UIColor

    init(backgroundColor: UIColor) {
        self.backgroundColor = backgroundColor
    }
}

let view1 = View(backgroundColor: .clear)
let view2 = View(backgroundColor: .clear)
let view3 = View(backgroundColor: .yellow)
let view4 = View(backgroundColor: .clear)

You can notice that, most of the time, we set a background color .clear. Instead of using every time .clear as argument, we can assign a default parameter to backgroundColor. In this way, we can omit it and leave its value implicit:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class View {

    private let backgroundColor: UIColor

    init(backgroundColor: UIColor = .clear) {
        self.backgroundColor = backgroundColor
    }
}

let view1 = View()
let view2 = View()
let view3 = View(backgroundColor: .yellow)
let view4 = View()

Notes:

We can use the default parameter also in methods with several parameters:

1
2
3
func setupLabel(background: UIColor = .clear, fontColor: UIColor = .black, fontSize: Int = 12, isHidden: Bool = false, placeholder: String = "Enter text", text: String) {

}

The Threat

For the sake of explanation, I changed APIRequestProtocol, now it has just a method request:

1
2
3
4
5
6
7
8
9
protocol APIRequestProtocol {
    func request(baseUrl: String, query: String, entriesLimit: Int?) -> Any
}

extension APIRequestProtocol {
    func request(baseUrl: String = "my_baseUrl", query: String, entriesLimit: Int? = nil) -> Any {
        // fetch the data
    }
}

Once created our APIRequestProtocol, and extended with the default implementation, we create a new class UsersAPIRequest which conforms to APIRequestProtocol:

1
2
3
class UsersAPIRequest: APIRequestProtocol {

}

This class doesn’t implement request, but uses the default implementation.

Now, we can call the method request:

1
2
let usersRequest = UsersAPIRequest()
usersRequest.request(query: "?get=users")

request uses the default parameter values for baseUrl and entriesLimit.

So far so good. Let’s introduce the threat:

Your boss introduces a new business logic. To achieve it, UsersAPIRequest cannot use the default implementation of the protocol extensions anymore, therefore we add a custom request implementation inside the class:

1
2
3
4
5
class UsersAPIRequest: APIRequestProtocol {
    func request(baseUrl: String, query: String, entriesLimit: Int?) -> Any {
        // custom fetch the data
    }
}

This code works and is fine.

But, if we call this new method, we’ll have an unexpected behaviour:

1
2
let usersRequest = UsersAPIRequest()
usersRequest.request(query: "?get=users")

⚠️ We expect the compiler to call the method request inside UsersAPIRequest, instead, it calls the method inside the protocol extension.⚠️

This is the reason:

We have two methods request in our hierarchy: Rpe (Request of protocol extension) and Ruar (Request of user api request).

When we write usersRequest.request(query: "?get=users"), we ask the compiler to call Ruar with just a parameter query. It goes inside UsersAPIRequest to read the implementation, but, unfortunately, it doesn’t find a method request with just an explicit parameter query, since Ruar has 3 explicit parameter baseUrl, query, entriesLimit:

func request(baseUrl: String, query: String, entriesLimit: Int?)

Usually, when Swift doesn’t find the right parameters of a method, it shows a compile error error: missing argument. In this case, it doesn’t throw an error because we still have Rpe, which has just an explicit query parameter, and this is exactly what the compiler is looking for.

When we declare Ruar, we don’t override the protocol extension implementation. To do it, Ruar and Rpe should have a default value in the same parameters—the values can be different, doesn’t matter.

Notes:

You may have noticed that I added the default parameter values in the extension instead of in the protocol. Swift doesn’t allow default values in the protocol declaration, but just in its extension—like in our example–or in the classes which conform to the protocol:

1
2
3
4
5
6
7
8
9
protocol APIRequestProtocol {
    func request(baseUrl: String, query: String, entriesLimit: Int?) -> Any
}

class MyClass: APIRequestProtocol {
    func request(baseUrl: String = "my_baseUrl", query: String, entriesLimit: Int? = nil) -> Any {
        // fetch the data
    }
}

The Suggestions

We can remove this threat in different ways:

Adding the missing default parameters values also in UsersAPIRequest implementation

1
2
3
4
5
class UsersAPIRequest: APIRequestProtocol {
    func request(baseUrl: String = "my_baseUrl", query: String, entriesLimit: Int? = nil) -> Any {
        // custom fetch the data
    }
}

Adding the parameters explicitly when we call the method

1
2
let usersRequest = UsersAPIRequest()
usersRequest.request(baseUrl: "my_baseUrl", query: "?get=users", entriesLimit: nil)

Refactoring the method

We can create a new struct, which contains the parameters of the method, and move the default values inside it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct ApiRequestConfig {
    let baseUrl = "my_baseUrl"
    let query: String
    let entriesLimit: Int? = nil
}

protocol APIRequestProtocol {
    func request(config: ApiRequestConfig) -> Any
}

extension APIRequestProtocol {
    func request(config: ApiRequestConfig) -> Any {
        // fetch the data
    }
}

class UsersAPIRequest: APIRequestProtocol {
    func request(config: ApiRequestConfig) -> Any {
        // custom fetch the data
    }
}

let config = ApiRequestConfig(query: "?get=users")
let usersRequest = UsersAPIRequest()
usersRequest.request(config: config)

This kind of refactoring was introduced by Martin Fowler in his book Refactoring.

Conclusion

I agree, it may be a silly threat, and it occurs because of the developer’s distraction. Nevertheless, it can happen, and you would waste a lot of time understanding what’s going on. Sometimes, the issues because of distraction are the most difficult to solve.