发展到现在, 各种关于 iOS 开发的架构就层出不穷, 那么哪个才是好的?
剧透下…其实没有最好的, 看看自己项目的规模换个常用都爱的逻辑, 看看哪个最合适.
MVVM
改进, 正确使用MVC
的滥用
好的架构总是希望有以下几种特性:
- 每个对象都有一个特定, 明确的角色
- 有一个简单的数据流, 单向数据流允许放置一个断点, 查看数据的变化,
- 架构不依赖某个特定的框架 or 服务.
- 足够灵活, 修改起来容易; 又不能太灵活, 加点东西要绕很多圈. 也就是简单而不过度设计. 技术性表达的话就是: 架构应该是关注点分离, 具备单一的清晰权责
测试
可测试性是一个架构很重要且很有帮助的能力, 这不是说依赖测试本身, 而是当我们需要进行测试, 接入测试时, 能够很快的进行. 这要求我们避免各种依赖关系, 特别是框架依赖(你换了个框架, 就不能进行测试了? 这将是毁灭性的…)
关注点分离: 一个对象通常应该只有一种角色, 但是有时候类太多了, 我们往往会违反合格角色, 做一些其他事情, 比如让类充当协调器(coordinate)的角色. 但换个方面看, 我们仍然是单一原则的, 这些类承担的是在不同的子系统中进行沟通交流的责任.
要写出一个可测试性的应用, 也许会使用 TDD 的方式来编写, 这是就需要首先把测试写了, 才去实现功能.
在编写 API 的时候, 要朝着易于测试去写, 不然的话测试就会是一件非常痛苦的事.(但这很难, 可能是框架使用出了问题, 让这个事情变得棘手, 不好去推进测试)
在定制框架时, 我们应该炒河让测试变的简单, 变的易于搭建. 这也以为这让接口变得简单, 以为这我们可以轻松的在系统中交换元素.
借助红-绿-重构 (red-green-refactor),这也是 TDD 所采用的一种方法,您首先就需要编写失败测试,然后以最简单的方式进行编译,然后使其以最简单的方式实现,之后再对其进行重构。实际上,您最终变得到了一个更为简洁的实现方式,不仅仅测试如此,对于代码实现而言也是如此。
这绝对是我在每个项目中都会考虑的事情。如果您必须要创建大量的伪对象 (fake object),以便测试某个特定功能的话,那么通常是不可行的。这样会导致对象的角色过多,或者系统的依赖关系过于复杂。
如果应用中有很多依赖与其他的库, 那么我们最好做一层封装, 这样在迁移到第三方提供的服务时会简单的多.
糟糕的架构
怎么算糟糕?
e.g. Swift中某个 controller 有超过3000行的代码, 或者对应的测试代码超过4000行, 那这就很明显了.
作者做了一个脚本判断文件是否过长, 这个好像 swiftlint 也有, 可以插插你看, 作者已经不更新这个脚本了. 估计还能用.
还有, 可以看看项目中是否使用了全局状态或者 Up Delegate(这是什么? 大概是..上层的代理?) 严格来说, 并没有什么理由使用这些, 状态可能会产生一些副作用. 但是使用了 Up Delegate 来管理全局属性, 而不是使用依赖注入, 也部署使用容器. 也没哟使用单例来写的话, 那么这就是个去全局状态. 可以使用一个依赖可视化的工具, 然后看看, 可以看看自己的结构.
(todo: 工具应该在视频里有提到的, 可以看看, 我看了会更新到这里)
设计模式
某个设计模式只在某个特定场景下很适用, 但并不是你了解了一个设计模式就到处都适用这个设计模式了.
e.g. 单例模式的滥用是的这个模式让应用架构变得很糟糕
你需要有足够的经验, 花费足够的时间到应用中, 才会理解什么时候用什么设计模式.
我们可以实际一点, 当你发现某个特定的模式不适用于这里, 但是能让代码变得更好更易于测试. 那么我们就可以考虑更换到这个模式来
比如单例是为了组员征用而创立的, 用于只允许它拥有一个接口. 但是大多数人不会, 特别 iOS, 大家都是他他当成全局状态同步器. 什么都扔进去.
比如 Logger, 通常会是一个单例, 但是我们不应该暴露出他使用的是单例.
我们需要记录某一个消息, 但是不想给项目中的每个类都传一个 logger, 我们只是在我们需要时, 能记录消息.
即使我们使用了单例, 我们也不必将其暴露, 特别是 Swift, 我们可以穿件一个 Loggable 协议, 它有一个内部使用了单例的默认实现. 我们有个类实现 loggable 协议, 我们能立即访问 log 函数, 函数内部用了单例, 这时候应用中其他部分并不知道, 我就可以在测试中改变它, 也可以重写这个实现. 设置可以测试日志是否正确记录我感兴趣的东西.
单例的滥用, 通常是缺乏依赖注入, 因此我们要让依赖注入变得容易.
组合
作者十分推荐大家使用组合模式(Composition), 它能够自然的引导出很好的选择, 遵循组合模式-> 代码会拥有单一原则 -> 代码会遵循 DRY -> 你也会遵循 DRY 来使用 -> 拥有完全可重用的代码, 而且将易于测试. 当用来组合的对象足够小, 其可变性越小, 测试代码也会越小.
如很多游戏系统是通过组合模式构建的.
依赖注入
一旦有了这些小的对象, 为什么不进行依赖注入, 为什么要将其硬编码到一个地方去使用, 而不是将其注入到一个地方使用.
如果硬编码, 我们没改动一个小对象的使用, 都将创建一个新的类. 这会死的…
注入的话, 你也可以注入测试的对象替代这个对象, 达到测试的目的.
MVVM 的使用
在保证V View 独立, Model 独立的情况下, 使用一个 ViewModal 组件来结合两个部分.
通过一种机制, 将 Modal 的数据, 绑定到 View 的对应属性上.
e.g.
1 | struct M { |
ViewModal.swift:
1 | protocol ModalProtocol { |
更进一步, 可以使用 Sourcery
来自动生成一些模板代码
1 | extension View: AutoView { |