Difficulty: Beginner | Easy | Normal | Challenging
This article has been developed using Xcode 12.2, and Swift 5.3
There is an accompanying YouTube video @link)
- You'll need to either be able to write an iOS application or write some Swift code in Playgrounds
Initialization: The process of preparing an instance of a class, structure, or enumeration for use Parameter: A special kind of variable referring to a piece of data passed to a function
You might be a most excellent coder and separate out your UI code from your business logic. You might then notice that some of your business logic classes have rather long parameter lists, and you might have heard that having more than three or four (this is one of those rules is a code smell.
Oh dear and oh my or oh my goodness. We must be able to fix that in order to pass our code review.
You might be told that a long parameter list is contradictory (as in the network manager example below) or confusing. Depending on the techniques you use, it may be difficult for you to determine whether an object has all of the data it needs in order to be instantiated. You might be told that it is hard to provide the parameters in the correct order as it is not clear what each one does.
This problem is often caused by a function trying to do too much (that is violate the Single Responsibility Principle
You may be able to derive one or more of your parameters from the other parameters that are passed. For example if you are dealing with width, height and volume as three parameters volume can be calculated by multiplying width and height, meaning that just two parameters are needed.
More likely, you may be working out some UI element sizes according to an enum
.
This can be called with
let viewWidth = self.view.frame.width
let sizeRatio = ratio(width: viewWidth, size: .small)
where the enum and the function is:
enum UserSize: Double {
case small = 0.8
case medium = 1.0
case large = 1.2
}
func ratio(width: CGFloat, size: UserSize) -> CGFloat {
return width * CGFloat(size.rawValue)
}
rather than passing the width, an alternative here is to use the view's width from within the function, removing one of the parameters from the function.
func ratio(size: UserSize) -> CGFloat {
return self.view.frame.width * CGFloat(size.rawValue)
}
If we derive the data from existing parameters, we are simplifying the function. However, in the case of using an available parameter like the view we are restricting where the function will be usable - so care should be taken in that case.
Rather than using several values from an object, you can pass the whole object instead.
So you might have the following averageHeight
function
_ = averageHeight(lowValue: 1, highValue: 3, n: 3)
func averageHeight(lowValue: Double, highValue: Double, n: Int) -> Double {
return highValue - lowValue / Double(n)
}
A refactored version of this is with Swift's Variadic parameters, and excuse the force-unwrapping.
and we just pass all the values to the function, and reduce the complexity of the parameters that we need to use:
_ = averageHeight(values: 1,2,3)
func averageHeight(values: Double...) -> Double {
return values.max()! - values.min()! / Double(values.count)
}
this does mean that our method has a potentially more limited interface, however. This is, as ever, an "it depends" answer to solve the problem of long parameter lists.
It is possible to use a data object to pass information to a class or function. However, there is a question about whether you should be doing this if you only use that data class one time - creating a one time class (or, perhaps a tuple although that is a value type.
func studentScore(name: String, age: Int, testScore: Int, testSubject: String){
print ("\(name): \(age) got \(test) out of 10")
}
which can be refactored into the following:
struct Test {
let testScore: Int
let testSubject: String
init (testSubject: String, testScore: Int) {
self.testSubject = testSubject
self.testScore = testScore
}
}
struct Student {
let name: String
let age: Int
init (name: String, age: Int) {
self.name = name
self.age = age
}
}
func studentScore(test: Test, student: Student){
print ("\(student.name): \(student.age) got \(test.testScore) out of 10")
}
Which does use Swift interpolation in the example.
Now this looks like we haven't achieved anything, after all we have the same number of parameters just "moved" to the data object. The point is that this is understandable, and when we call studentScore
we just have one parameter to deal with. In this example, you can claim that we haven't solved a problem here, that there should be two objects here (one for Test and one for Student) and that is getting to the heart of the problem - this should be much easier to read and understand.
Ok, this is something that you really should have by now. A nice little network manager module that you can use and import and export to your projects at will.
In the deep and distant past I developed a basic network manager, that I then updated with an updated network layer.
I did notice that one of the most essential things that I changed was the initialization process.
This came as part of adding to the functionality of the network library, that is adding more than just get and post up to making all of these REST methods:
- GET
- POST
- PATCH
- PUT
- DELETE
all work.
However, we don't need to pass data in the same way for all of these methods for example only a post has body data.
To solve this, here is a naive function signature:
fetch(url: URL, method: HTTPMethod, headers: [String : String] = [:], token: String? = nil, body: [String: Any], completionBlock: @escaping (Result<Data, Error>)
Note that default arguments are used (and the article goes on to overcome issues in using default parameters in a protocol).
This is a classic case of a function that is doing too much!
Essentially I'm making a better interface, one for each of the REST methods. I'd usually do that with a protocol, but here I actually opted for an enum with associated values (get me!):
public enum HTTPMethod {
case get(headers: [String : String] = [:], token: String? = nil)
case post(headers: [String : String] = [:], token: String? = nil, body: [String: Any])
case put(headers: [String : String] = [:], token: String? = nil)
case delete(headers: [String : String] = [:], token: String? = nil)
case patch(headers: [String : String] = [:], token: String? = nil)
}
this meant that only the required data would be used for each of the HTTP Methods, which is then passed into my fetch function:
func fetch(url: URL, method: HTTPMethod, completionBlock: @escaping (Result<Data, Error>) -> Void)
which is rather wonderful! Job done! Fewer Parameters.
Also note: There are multiple functions (this is fetch, but I'm sure you can think of others).
Limiting the parameters is something you should really think about when coding in Swift. This article has gone through some of the ways in which you can limit the number of parameters you might use, and even come up with an example you might use.
I hope this article has helped you out a little bit, and helped you on your coding journey.
There are always different methods, with varying outcomes to overcome coding challenges. You might have other methods of doing the same - let me know!
If you've any questions, comments or suggestions please hit me up on Twitter