In this chapter, we will refactor an existing code that doesn't use an object-oriented programming approach and make it easier to understand, expand, and maintain. We will discuss functional programming and how Swift implements many functional programming concepts. We will work with many examples of how to mix functional programming with object-oriented programming.
You're reading from Object???Oriented Programming with Swift 2
Sometimes, we are extremely lucky and have the possibility to follow best practices as we kick off a project. If we start writing object-oriented code from scratch, we can take advantage of all the features that we used in our examples throughout the book. As the requirements evolve, we might need to further generalize or specialize the blueprints. However, as we started our project with an object-oriented approach and by organizing our code, it is easier to make adjustments to the code.
Most of the time, we aren't extremely lucky and have to work on projects that don't follow best practices, and we, in the name of agility, generate pieces of code that perform similar tasks but without decent organization. Instead of following the same bad practices that generate error-prone, repetitive, and difficult-to-maintain code, we can use the features provided by Xcode and additional helper tools to refactor existing code and generate...
Swift is a multiparadigm programming language, and one of its supported programming paradigms is functional programming. Functional programming favors immutable data and, therefore, avoids state changes. The code written with a functional programming style is as declarative as possible, and it is focused on what it does instead of how it must do it.
As it happens in many modern programming languages, functions are first-class citizens in Swift. You can use functions as arguments for other functions or methods. We can easily understand this concept with a simple example: array filtering. However, take into account that we will start by writing imperative code with functions as first-class citizens, and then, we will create a new version for this code that uses a functional approach in Swift through a filter operation.
The following lines declare the applyFunctionToNumbers
function that receives an array of Int
and numbers
and a function type,...
The following lines declare a myFunction
variable with a function type—specifically, a function that receives an Int
argument and returns a Bool
value. The variable works in the same way as an argument that specifies a function type for a function:
var myFunction: (Int -> Bool) myFunction = divisibleBy5 let myNumber = 20 print("Is \(myNumber) divisible by 5: \(myFunction(myNumber))")
Then, the code assigns the divisibleBy5
function to myFunction
. It is very important to understand that the line doesn't call the divisibleBy5
function and save the result of this call in the myFunction
variable. Instead, it just assigns the function to the variable that has a function type. The lack of a parenthesis after the function name makes the difference.
Then, the code prints whether the Int
value specified in the myNumber
constant is divisible by 5
or not using the myFunction
variable to call the referenced function with myNumber
as an argument.
The following...
The collections included in Swift allow us the use of higher order functions—that is, functions that take other functions and use them to perform transformations on datasets. For example, an array provides us with the filter
, map
, and reduce
methods.
As previously explained, the preceding code represents an imperative version of array filtering. We can achieve the same goal with a functional approach using the filter
method included in all the types that conform to the SequenceType
protocol. The Array<Element>
struct conforms to the SequenceType
protocol and many other protocols.
Tip
As it happens in most modern languages, Swift supports closures, which are also known as anonymous functions. Closures are self-contained blocks of functionality that we can pass around and use within our code as functions without names. Closures automatically capture everything we reference, such as variables and functions that aren't defined within the closure...
It is possible to omit the type for the closure's parameter and return type. The following lines show a simplified version of the previously shown code that generates the same result. Note that the closure code is really simplified and doesn't even include the return statement because it uses an implicit return. Swift evaluates the code we write after the in
keyword and returns its evaluation as if we included the return statement before the expression. Swift infers the return type:
public func filterNumbersByCondition(condition: Int -> Bool) -> [Int] { return numbersList.filter({ (number) in condition(number) }) }
We can go a step further and use the argument shorthand notation. This way, the closure omits the type for the parameters and its return type, takes advantage of implicit returns, and also uses the argument shorthand notation. The dollar sign followed by the argument number identifies each of the arguments for...
Now, we want to create a repository that provides us with entities so that we can apply the functional programming features included in Swift to retrieve and process data from these entities. First, we will create an EntityProtocol
protocol that defines the requirements for an entity. We want any class that conforms to this protocol to have a read-only id
property of the Int
type to provide a unique identifier for the entity:
public protocol EntityProtocol { var id: Int { get } }
The next lines create a Repository<T>
generic class that specifies that T
must conform to the recently created EntityProtocol
protocol in the generic type constraint. The class declares a getAll
method that we will override in the subclasses:
public class Repository<T: EntityProtocol> {
public func getAll() -> [T] {
return [T]()
}
}
The next lines create the Entity
class, which is the base class for all the entities. The class...
We can use our new repository to restrict the results retrieved from more complex data. In this case, the getAll
method returns an array of Game
instances that we can use with the filter
method to retrieve only the games that match certain conditions. The following lines declare a new getGamesWithHighestScoreGreaterThan
method for our previously coded GameRepository
class:
public func getGamesWithHighestScoreGreaterThan(score: Int) -> [Game] { return getAll().filter({ (game) in game.highestScore > score }) }
The getGamesWithHighestScoreGreaterThan
method receives a score: Int
argument and returns Array<Game>
. The code calls the getAll
and filter
methods for the result with a closure that specifies the required condition for the games in the array to be returned in the new array. In this case, only the games whose highestScore
value is greater than the score
value received as an argument will appear in the resulting Array<Game>
...
The map
method takes a closure as an argument, calls it for each item in the array, and returns a mapped value for the item. The returned mapped value can be of a different type from the item's type.
The following lines declare a new getGamesNames
method for our previously coded GameRepository
class that performs the simplest map operation:
public func getGamesNames() -> [String] { return getAll().map({ game in game.name.uppercaseString }) }
The getGamesNames
parameterless method returns Array<String>
. The code calls the getAll
method and calls the map
method for the result with a closure that returns the name
value for each game converted to uppercase. This way, the map
method transforms each Game
instance into a String
with its name converted to uppercase. The result is an Array<String>
generated by the call to the map
method.
The following line uses the GameRepository
instance called gameRepository
to call the previously added getGamesNames...
The following lines show an imperative code version of a
for
in
loop that calculates the sum of all the highestScore
values for the games:
var sum = 0 for game in gameRepository.getAll() { sum += game.highestScore } print(sum)
The code is very easy to understand. The sum
variable has a starting value of 0
, and each iteration of the for in
loop retrieves a Game
instance from the Array<Game>
returned by the gameRepository.getAll
method and increases the value of the sum
variable with the value of the highestScore
property.
We can combine the map and reduce operations to create a functional version of the previous imperative code to calculate the sum of all the highestScore
values for the games. The next lines chain a call to map
to a call to reduce
to achieve this goal. Take a look at the following code:
let highestScoreSum = gameRepository.getAll().map({ $0.highestScore }).reduce(0, combine: { sum, highestScore in return sum + highestScore }) print...
We can chain filter
, map
, and reduce
. The following lines declare a new
calculateGamesHighestScoresSum
method for our previously coded GameRepository
class that chains filter
, map
, and reduce
calls:
public func calculateGamesHighestScoresSum(minPlayedCount: Int) -> Int { return getAll().filter({ $0.playedCount >= minPlayedCount }).map({ $0.highestScore }).reduce(0) { sum, highestScore in return sum + highestScore } }
The calculateGamesHighestScoresSum
method receives a minPlayedCount
argument of the Int
type and returns an Int
value. The code calls the getAll
and filter
methods to generate a new Array<Game>
with only the Game
instances, whose playedCount
value is greater than or equal to the value specified in the minPlayedCount
argument. The code calls the map
method to transform an Array<Game>
into an Array<Int>
with the values specified in the highestScore
stored property. Then, the code calls the reduce
method...
We can chain solve algorithms with reduce
by following a functional approach. The following lines declare a new getSeparatedGamesNames
method for our previously coded GameRepository
class that solves an algorithm by calling the reduce
method:
public func getSeparatedGamesNames(separator: String) -> String { let gamesNames = getGamesNames() return gamesNames.reduce("") { concatenatedGameNames, gameName in print(concatenatedGameNames) let separatorOrEmpty = (gameName == gamesNames.last) ? "" : separator return "\(concatenatedGameNames)\(gameName)\(separatorOrEmpty)" } }
The getSeparatedGamesNames
method receives a separator
argument of the String
type and returns a String
value. The code calls the getGamesNames
method and saves the result in the gamesNames
reference constant. Then, the code calls the reduce
method with an empty string as the initial
value for an accumulated value. The code uses a trailing closure to...
Add new methods to the GameRepository
class we created in this chapter. Make sure you create a new method to solve each algorithm and that you use a functional programming approach:
Retrieve all the games whose average score is lower than a maximum average score received as an argument.
Generate a string with the first letter of each game name followed by the highest score value. Use a hyphen as a separator for each game name and highest score value pair. That last value pair shouldn't include a hyphen after it.
Calculate the minimum
playedCount
value.Calculate the maximum
playedCount
value.
The
{ (game: Game) -> Bool in game.highestScore == highestScore && game.playedCount == playedCount }
closure is equivalent to:{ $0.highestScore == highestScore && $1.playedCount == playedCount }
{ $0.highestScore == highestScore && $0.playedCount == playedCount }
{ 0 -> 0.highestScore == highestScore && 0.playedCount == playedCount }
The
closure { return condition($0) }
is equivalent to:{ (number: Int) -> Bool in return condition(number) }
{ (number -> Bool) -> Int in condition <- (number) }
{ 0 -> condition(number) }
A function type specifies:
The parameter and return types for the function.
Only the parameter names required for the function.
The required function name and the return value without any details about the parameters.
Which of the following lines declare a variable with a function type:
var condition: { 0 -> Int -> Bool }
var condition: Int $0 returns Bool
var condition: (Int -> Bool)
After...
In this chapter, you learned how to refactor existing code to take full advantage of object-oriented code. We prepared the code for future requirements, reduced maintenance cost, and maximized code reuse.
We worked with many functional programming features included in Swift and combined them with everything we discussed so far about object-oriented programming. We analyzed the differences between imperative and functional programming approaches for many algorithms.
Now that you have learned about refactoring code to take advantage of object-oriented programming and include functional programming pieces in our object-oriented code, we are ready to extend and build object-oriented code, which is the topic of the next chapter.