在现代 Swift 项目中,很流行一种形式叫做库房形式,英文是 Repository Pattern。这个形式主要用于构建数据层代码。依照一般的 App 层级区分,一般从上到下区分为 UI 层,事务层,数据层,那么库房形式的运用方位能够参阅下图:

仓库模式及其在 Swift 项目中的应用

能够看到库房运用在数据层,事务层经过接口来拜访库房。

不运用库房形式时的代码

为了研讨为什么要运用库房形式,咱们先看看不运用库房形式时咱们是怎样写代码的。一般打开一个界面,会发送网络恳求来获取这个界面所需的数据,这时会在 ViewController 写相似下面的代码:

func viewDidLoad() {
    super.viewDidLoad()
    requestData()
}
func requestData() {
    API.request(xxxId: 12345) { result in
        switch result {
        case .success(let model):
            handle(model)
            DispatchQueue.main.async {
                render(model)
            }
        case .failure(let error):
            print(error.localizedString)
        }
    }
}

这种写法在小项目中是没有问题的,可是在稍具规划的项目中,就会对项目的扩展性,保护性,团队合作,开发效率有比较高的要求,这时分就更应该依据科学的软件规划准则来规划更好的架构

上面的代码在稍具规划的项目中会有以下的缺陷:

  • 数据拜访代码写在了 ViewController 中,无法测验
  • ViewController 会过于臃肿,难以保护
  • 假如数据拜访方法修正了,需求修正 ViewController 中的代码

稍具规划的项目一般会选用 MVVM 等架构,所以以上的代码会写在 ViewModel 中,来防止 ViewController 过分臃肿,可是仍是会有无法测验和修正数据拜访方法时改动比较大的问题。

运用库房有什么优点?

总的来说,便是能供给高层次的笼统,然后获得由于笼统带来的一系列优点。

库房形式供给了数据层的笼统,能够让你的事务代码只依靠一个简略的笼统接口就能够工作。这使得代码松耦合,事务代码不需求知道数据的详细获取和存储细节。

库房形式让代码能够测验,关于网络恳求和数据库读写的部分,能够完成一个供给测验数据的库房实例,这样就能够编写相应的测验代码。

下面就来看一看怎样运用库房形式。

规划库房接口

依照依靠倒转准则,数据拜访层作为架构中的一个整体,上层对象在调用数据拜访层时应该依靠接口,而不依靠于完成,这样数据拜访层的逻辑就能够灵敏的改变、替换,比方在网络恳求和本地数据之间切换,在不同的网络恳求协议之间切换等。

所以规划库房形式应该先界说接口,界说接口有两种方法,一种是依据详细的数据界说特定的库房接口,例如关于一个新闻列表的接口:

protocol NewsListRepository {
    func readNewsList() async throws -> [News] 
}

或者是依据不同的事务数据的拜访逻辑其实大同小异,能够规划统一的泛型接口:

protocol Repository {
    associatedtype T
    func query(with predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?) async throws -> [T]
    func save(entity: T) async throws
    func delete(entity: T) async throws
}

完成库房接口

咱们一般会依据数据的存放方法来界说不同的库房完成,比方关于网络恳求的数据,界说一种库房的完成,关于本地数据库中存放的数据界说一种库房的完成,也能够界说一种假数据库房来编写测验代码。

比方关于网络恳求的数据,能够界说一个如下的库房完成:

class DefaultNewsListRepository: NewsListRepository {
    let remoteDataSource: NewsListRemoteAPI
    init(remoteDataSource: NewsListRemoteDataSource) {
        self.remoteDataSource = remoteDataSource
    }
    func readNewsList() async throws -> [News] {
        return remoteDataSource.requestNewsList();
    }
}

关于本地数据,能够规划一个如下的库房完成:

class DatabaseNewsListRepository: NewsListRepository {
    let newsListDataStore: NewsListDataStore
    init(newsListRemoteAPI: NewsListRemoteAPI) {
        self.newsListRemoteAPI = newsListRemoteAPI
    }
    func readNewsList() async throws -> [News] {
        return newsListRemoteAPI.requestNewsList();
    }
}

为了编写测验代码,能够供给一个假数据的库房完成:

class FakeNewsListRepository: NewsListRepository {
    func readNewsList() async throws -> [News] {
        return [
            News(),
            News(),
            News()
        ]
    }
}

假如需求从接口恳求到数据后放入本地数据库缓存,然后从本地数据库中读取数据渲染在界面上,也能够用一个库房搞定。

class DefaultNewsListRepository: NewsListRepository {
    let newsListRemoteAPI: NewsListRemoteAPI
    let newsListDataStore: NewsListDataStore
    init(newsListRemoteAPI: NewsListRemoteAPI, newsListDataStore: NewsListDataStore) {
        self.newsListRemoteAPI = newsListRemoteAPI
        self.newsListDataStore = newsListDataStore
    }
    func readNewsList() async throws -> [News] {
        var newsList = newsListDataStore.readNewsList()
        if newsList.count == 0 {
            let news = newsListRemoteAPI.requestNewsList()
            newsListDataStore.save(newsList)
            newsList = news
        }   
        return newsList
    }
}

选择用哪个库房完成

在现代 App 项目中,一般会用 MVVM 等架构来安排代码。这里以 MVVM 为例,ViewModel 会依靠库房接口来存取数据。

class NewsListViewModel: ViewModel {
    let newsListRepository: NewsListRepository
    init(newsListRepository: NewsListRepositoy) {
        self.newsListRepository = newsListRepository
    }
}

为了进步代码的保护性和扩展性,最好运用依靠注入的方法来给 ViewModel 注入 Repository 的依靠,这样能够便利得替换库房的完成而不用修正 ViewModel 的代码。

能够在创立 ViewModel 时创立对应的库房对象,也能够运用依靠注入容器。

init(newsListRepository: NewsListRepository = DIContainer.shared.resolve(NewsListRepository.self)) {
    self.newsListRepository = newsListRepository
}

处理数据源的改变

当遇到需求改变数据源的时分,例如本地数据库从 CoreData 切换到 SQLite 或 Realm。或者更换了网络库,从 NSURLSession 换成 Alamofire,这些状况库房形式就能发挥它的优势。无需修正事务方代码,只需求替换成一种新的库房完成即可。

还有另一种状况,便是关于同一个事务,后端协议改变了。假如运用了库房形式,也能够很便利的进行代码调整。

一般,事务层会有一个事务模型,比方关于用户的信息,在事务层界说了一套模型:

struct DomainUser {
    let name: String
    let age: Int
    let nickname: String
}

本来经过接口回来的 json 字符串:

{
    "name": "zhangsan",
    "age": 20,
    "nickname": "xiaozhang"
}

能够直接经过解析 JSON 然后构造出 DomainUser 对象,可是突然某一天后端说要技能调整,迁移到新的接口,新接口回来的结构和曾经不一样了。

假如没有用库房形式,事务方直接依靠详细的数据模型,假如接口结构调整了,那么一切的事务调用方的代码都要调整。

运用了库房形式,事务方依靠于库房,库房能够在获取到数据结构后将它转为事务方需求的数据模型,这样不管后端协议怎样改变,都能够仅在数据层增加一种新的库房完成,不需求改动事务方代码。这遵守了开放-关闭程序规划准则。

总结

在稍具规划的项目中运用库房形式,能够让代码笼统度更高,耦合度更低,便利扩展和保护,能够编写测验代码,在大型项目中,能够便利的完成数据源和数据结构的切换。库房形式也将数据存储的细节和程序的其它部分分脱离,使得责任更清晰。

库房形式也有一些缺陷:为了完成这一形式需求编写更多的代码,增加了代码复杂性。需求编写映射代码来讲数据映射为事务模型。假如是小型的项目就不需求运用库房形式了。

参阅资料

  • iOS: Repository pattern in Swift
  • Repository Pattern in Swift
  • github.com/kudoleh/iOS…
  • Repository pattern in Swift