Getting Hurt With Swift Protocol Extensions and Default Parameter Values
Swift protocol extensions and default parameter values features are not always safe to use together. Learn to avoid the dangers.
Join the DZone community and get the full member experience.
Join For FreeOf course, Swift protocol extensions and default parameter values are great features. And they are always safe, aren’t they? Well, not really.
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 the subjects well.
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:
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:
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 initialise 4 View
objects using its constructor:
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:
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()
Note: We can use the default parameter also in methods with several parameters:
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
:
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 we've created our APIRequestProtocol
, and extended with the default implementation, we create a new class UsersAPIRequest
which conforms to APIRequestProtocol
:
class UsersAPIRequest: APIRequestProtocol {
}
This class doesn’t implement request
, but uses the default implementation.
Now, we can call the method request
:
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:
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 behavior:
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 the protocol extension) and Ruar
(request of the 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 parameters: 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:
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:
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:
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:
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.
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.
Opinions expressed by DZone contributors are their own.
Comments