• No results found

Data helper layer

In document Swift 4 Protocol-Oriented Programming (Page 196-199)

In a design such as this, there is a need to have good error checking, which will let external code know when something bad happens; therefore, we will start the data helper layer by defining the errors that can be thrown. Swift's error-handling framework will be used; therefore, the errors are defined in an enumeration, as follows:

enum DataAccessError: Error { case datastoreConnectionError case insertError case deleteError case searchError case nilInData }

We will see how these errors are thrown as we go through the code. Depending on the type of persistence that is used, the error types may change to give more details about the actual errors that occurred.

The data helper layer will be used to persist the data. This is the layer that will change as our storage mechanism changes. In this example, the data will be stored in an array; however, the types in this layer should have the ability to change as different storage mechanisms are needed in the future. This layer will contain one type for each data type in our data model layer. These types will be used to read and write the data.

We will begin by creating a DataHelper protocol that will define the minimum set of methods that each data helper type must implement. The DataHelper protocol looks as follows:

protocol DataHelper { associatedtype T

static func insert(_ item: T) throws -> Int64 static func delete(_ item: T) throws -> Void static func findAll() throws -> [T]?

}

Within this protocol, we define three methods. These are as follows: insert: This inserts a row into the table

delete: This deletes a row from the table findAll: This returns all the rows in the table

There is only one method defined to query the data. This is done because the methods to query each individual data type may vary depending on the data; therefore, the method(s) needed to query these types could be different. We need to evaluate the query method(s) needed for each data type on an individual basis.

Now let's build the TeamDataHelper type that will conform to the DataHelper protocol. This type will be used to persist the team data:

struct TeamDataHelper: DataHelper { // Code goes here

}

This type starts by defining an associatedtype and then creates an array to store the data in the following:

typealias T = TeamData

static var teamData: [T] = []

The teamData array is defined as static, so there will be one and only one instance of this array in the code. The typealias T variable is set to the TeamData type. Now, let's look at how we can implement each of the three methods defined in the DataHelper protocol plus one extra method that will search the data by its unique identifier of the team. We will not discuss the implementation details here because we are more concerned with the design rather than how we store/search the information in an array.

The first method that we will implement is the insert() method, which will insert an item into the array. This method will return an Int64 value representing the unique ID of the item if everything was stored properly. This method will also throw an error if there is an issue with the data. If another storage mechanism was being used, this method may need to throw additional errors:

static func insert(_ item: T) throws -> Int64 {

guard item.teamId != nil && item.city != nil && item.nickName != nil && item.abbreviation != nil else {

throw DataAccessError.nilInData }

teamData.append(item) return item.teamId! }

Now, let's create the delete() function to remove an item from the array. This method will throw an error if the item does not exist or if teamId is nil:

static func delete (_ item: T) throws -> Void { guard let id = item.teamId else {

throw DataAccessError.nilInData }

let teamArray = teamData

for (index, team) in teamArray.enumerated() where team.teamId == id { teamData.remove(at: index)

return }

throw DataAccessError.deleteError }

Now, let's implement the findAll() method, which will return all of the teams in the array. This method can throw an error, but that is more for future needs:

static func findAll() throws -> [T]? { return teamData

}

Finally, the find() method is implemented to search and return a single item from the team array. We may need additional find() methods depending on needs, but this method will return the team with the particular teamId. This method is also marked to throw an error, but it is also for future needs. If teamId is not found in the array, it will return a nil value:

return team }

return nil }

The PlayerDataHelper type is implemented just like the TeamDataHelper type. To see the code for the PlayerDataHelper class, please download the code from the Packt website.

Ideally, for the data access layer, the data (PlayerData and TeamData) and data helper (PlayerDataHelper and TeamDataHelper) types would be decoupled from the main business logic. If we look through the design patterns that we discussed earlier in this book, we will see that the bridge pattern can be used here. Let's see how we will use the bridge pattern to maintain a good separation layer between our data access layer and our application code.

We will want to start off by defining how to model the data within the application itself. This data can be modeled exactly like the data model within the data access layer, or it can be designed significantly differently.

I usually find that if I properly normalize my data, there are usually significant differences between how I store the data and how I use it within my application. By separating our data access layer from our application code, we are also able to model our data differently between these two layers.

Let's now look at how we will design our bridge layer.

In document Swift 4 Protocol-Oriented Programming (Page 196-199)