原文:Creating a Service Layer in Swift
什么是服务层?
服务层答应你将与结构和 API 相关的逻辑转移到它们自己的类或结构体中。一个好的做法是创立一个 protocol 并增加所需的办法和计算属性。你的完结将是一个遵守该协议的类或结构体。
为本文创立的示例项目运用了 UIKit、MVVM 设计模式和苹果的 Combine 结构的一部分。假如你对 Combine 不熟悉,那也没关系。你不需求成为 Combine 的专家,也能从本文中受益。
优点
创立服务层答应你从视图模型(MVVM)或视图控制器(MVC)中抽取出特定的结构逻辑。下面是这种办法的几个优点:
可重用
比方说,你需求从几个不同的视图模型中找到一个特定的端点。你不期望重复这个网络逻辑。经过将网络逻辑放在一个服务中,你能够从视图模型的服务实例中拜访这些端点办法。
protocol JsonPlaceholderServiceProtocol {
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)
}
final class JsonPlaceholderService: JsonPlaceholderServiceProtocol {
<...>
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
guard let url = URL(string: baseUrlString + Endpoint.users.rawValue) else { return }
urlSession.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
do {
let users = try JSONDecoder.userDecoder().decode([User].self, from: data!)
completion(.success(users))
} catch let err {
completion(.failure(err))
}
}.resume()
}
}
更简单编写单元测验
将你的服务创立为一个协议,然后用一个类或结构来完结,这是一个好的做法。经过创立一个协议,为单元测验目的创立一个服务的模仿将愈加简单。你不期望在你的测验中碰到实践的 REST APIs。
@testable import ServiceLayerExample
final class MockJsonPlaceholderService: JsonPlaceholderServiceProtocol {
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
let pathString = Bundle(for: type(of: self)).path(forResource: "users", ofType: "json")!
let url = URL(fileURLWithPath: pathString)
let jsonData = try! Data(contentsOf: url)
let users = try! JSONDecoder.userDecoder().decode([User].self, from: jsonData)
completion(.success(users))
}
}
可读性
将你的依靠关系别离到他们自己的类型中,关于新的和现有的开发者来说,生活会更简单。经过将所有的结构/API 逻辑保存在一个文件中,开发人员能够快速了解项目中运用的内容。
可替换性
假定你现在的应用运用 Firebase,而你想切换到 Realm。你所有的存储提供者的逻辑都将集中在一个当地,使这个大的改变能够更顺畅一些。例如,Firebase 和 MongoDB Realm 都有用于验证其服务的办法。把这些功能集中在一个当地会使转化变得更简单。
示例项目概述
下面的概述部分的代码已经缩短,以减少文章的长度。你能够在 GitHub 上找到完好的文件。
View
UserViewController
将包括一个 UITableView
来显示检索到的用户。我没有运用 Storyboard,所以视图控制器是以编程方式构建的。
import Combine
import UIKit
fileprivate let cellId = "userCell"
final class UserViewController: UIViewController {
private var cancellables: Set<AnyCancellable> = []
private let viewModel = UserViewControllerViewModel()
private let tableView: UITableView = {
let tv = UITableView(frame: .zero, style: .insetGrouped)
tv.register(UITableViewCell.self, forCellReuseIdentifier: cellId)
tv.translatesAutoresizingMaskIntoConstraints = false
return tv
}()
override func viewDidLoad() {
super.viewDidLoad()
//<view setup>
// 1 - Subscribes to the viewModel to be notified when there are changes
viewModel.objectWillChange
.receive(on: RunLoop.main) // 切换到主线程
.sink { [weak self] in
self?.tableView.reloadData()
}.store(in: &cancellables)
}
}
extension UserViewController: UITableViewDelegate, UITableViewDataSource {
//<tableView set up methods>
}
- 视图控制器经过 Combine 的
ObservableObject
协议对视图模型进行订阅。当用户目标从/users
端点被检索届时,视图控制器将被告诉。
由于 fetchUsers
办法是在后台线程上调用 URLSession
的 dataTask
办法,咱们需求经过调用.receive(on:)
操作符保证咱们在主线程上收到这些更新。
ViewModel
正如在介绍中提到的,该示例项目运用的是 MVVM 架构。UserViewController
将持有咱们的 UserViewControllerViewModel
的一个实例。咱们将运用 Combine 的 ObservableObject
协议来订阅视图模型的变化以更新视图。这个订阅是在视图控制器的 viewDidLoad
办法中创立的。
import Combine
import Foundation
final class UserViewControllerViewModel: ObservableObject {
@Published var users: [User] = []
// 1 - used to access the `fetchUsers` method
private let service: JsonPlaceholderServiceProtocol
// 2 - pass in an instance of `JsonPlaceholderServiceProtocol`.
// This will be used to pass in a mock during testing.
init(service: JsonPlaceholderServiceProtocol = JsonPlaceholderService()) {
self.service = service
retrieveUsers()
}
// 3 - fetches users from the service.
private func retrieveUsers() {
service.fetchUsers { [weak self] result in
switch result {
case .success(let users):
self?.users = users.sorted(by: { $0.name < $1.name })
case .failure(let error):
print("Error retrieving users: (error.localizedDescription)")
}
}
}
}
- 用于拜访
fetchUsers
办法的服务属性。 - 服务属性是经过传入一个契合服务协议的
JsonPlaceholderService
实例来设置的。 - 视图模型的
retrieveUsers
办法经过服务属性拜访服务的fetchUsers
办法。
Service
本文的示例项目将从 Jsonplaceholder 上打到 /user
端点。这个端点将返回一个由十个不同的用户 JSON 目标组成的数组。假如你想测验扩展这个项目,这个网站也有一些其他的端点,你能够点击。JsonPlaceholderServiceProtocol 只要求一个办法的一致性。fetchUsers
办法运用 URLSession
的 dataTask
办法从 /user
端点检索 json 数据:
// 1 - This will be the type that is passed into the `UserViewControllerViewModel`.
// This will also be used to "mock" the service.
protocol JsonPlaceholderServiceProtocol {
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void)
}
// 2 - A concrete implementation of the JsonPlaceholder service.
final class JsonPlaceholderService: JsonPlaceholderServiceProtocol {
// MARK: Types
enum Endpoint: String {
case users = "/users"
}
// MARK: Properties
private let baseUrlString = "https://jsonplaceholder.typicode.com"
private let urlSession: URLSession
// MARK: Initialization
init(urlSession: URLSession = URLSession.shared) {
self.urlSession = urlSession
}
// MARK: Methods
// 3 - this method will retrieve the user objects from the /users endpoint.
// This method will be mocked.
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
guard let url = URL(string: baseUrlString + Endpoint.users.rawValue) else { return }
urlSession.dataTask(with: url) { (data, response, error) in
if let error = error {
completion(.failure(error))
}
do {
let users = try JSONDecoder.userDecoder().decode([User].self, from: data!)
completion(.success(users))
} catch let err {
completion(.failure(err))
}
}.resume()
}
}
- 这将是该服务的类型。这个类型是一个协议,所以它能够被模仿(在下一节解说)。
-
JsonPlaceholderService
是该协议的具体完结。这将被用来点击服务的端点并检索用户目标。 - 用来经过
URLSession
dataTask
办法检索用户目标的办法。
Mock Service
为了对服务进行适当的单元测验,你需求创立一个模仿的服务。这能够经过多种方式完结,但我更喜爱协议方式。假如你觉得这样更简单了解,你也能够经过子类创立模仿。
@testable import ServiceLayerExample
final class MockJsonPlaceholderService: JsonPlaceholderServiceProtocol {
func fetchUsers(completion: @escaping (Result<[User], Error>) -> Void) {
// 1 - retrieves a path to the users.json file in the Fixtures folder.
let pathString = Bundle(for: type(of: self)).path(forResource: "users", ofType: "json")!
let url = URL(fileURLWithPath: pathString)
let jsonData = try! Data(contentsOf: url)
// 2 - decodes the fixtures into `User` objects.
let users = try! JSONDecoder.userDecoder().decode([User].self, from: jsonData)
completion(.success(users))
}
}
- 这一行是一个小技巧,能够从你的测验包中取得
.json
文件。.json
文件应该包括你测验的端点的有用 JSON 目标。 - 将
.json
文件解码成一个模仿成功网络呼应的用户目标数组。
单元测验
UserViewControllerViewModelTests
类有一个测验办法,以保证 fetchUsers
办法能正确解码并返回 JSON。
import XCTest
@testable import ServiceLayerExample
class UserViewControllerViewModelTests: XCTestCase {
var mockJsonPlaceholderServiceProtocol: JsonPlaceholderServiceProtocol!
var subject: UserViewControllerViewModel!
override func setUp() {
super.setUp()
mockJsonPlaceholderServiceProtocol = MockJsonPlaceholderService()
subject = UserViewControllerViewModel(service: mockJsonPlaceholderServiceProtocol)
}
// 1 - ensures the `fetchUsers` method of the JsonPlaceholderServiceProtocol properly decodes the JSON into `User` objects.
func testFetchUsers() {
//method `retrieveUsers` call in the UserViewControllerViewModel's init. This occurs in the setUp method above.
XCTAssertEqual(subject.users.count, 10)
//`users` array is sorted A-Z
let firstUser = subject.users.first!
XCTAssertEqual(firstUser.id, 5)
XCTAssertEqual(firstUser.name, "Chelsey Dietrich")
}
}
- 一个测验办法,检查用户数组是否包括与
.json
文件相同数量的用户目标。当主体在setUp
办法中被初始化时,这个办法被调用。
总结
在你的代码库中增加一个服务层有许多优点。它不仅能使你的代码库坚持模块化,你还会从可重用性、单元测验覆盖率、可读性和可替换性中受益。
我总是觉得在解说新的概念时,最好是坚持比如极端纤细和简单。坚持你的代码库模块化的全部含义在于,你能够轻松地扩展其功能。
这篇文章的样本项目能够在我的 GitHub 上找到。
Josh 是 Livefront 的一名 iOS 开发者,他是明尼苏达州东南地区的大使。