介绍
本文我将主要说明如何将大型和旧代码库 模块化 为小型、独立且可测试的组件。在此过程中,我们还将分享我们采用的一些解决方案,以解决 依赖关系管理 并在持续交付配置中优化 构建性能 。
总览
这是我们应用程序代码库在其初始形状时的样子。
Pods是CocoaPods项目,其中包含整个应用程序依赖项。
在我们开始的时候,这很简单,可以构建所需的大多数功能。但是,您可能会想像到,通过这种方法,任何新功能都会与数百个其他功能一起添加到混合中,并且 在关键边界 (例如UI,API,数据等) 之间没有明确的分隔 。
结果,错误、崩溃和令人头痛的事数量增加了,这主要是由于工程师团队的不断增长以及对构建新功能的需求不断增加所带来的不可预测的副作用。
初始方案
随着时间的推移,事情变得越来越糟,我们决定集中精力在代码库的主要宏大组件之间创建 清晰的边界 。因此,我们决定采用传统的 分层体系结构 。
每个框代表添加到工作区中的Xcode项目,其中每个框都包含一个dynamic framework。主APP的target仍将位于“旧功能(Old Features)”项目中,并将嵌入所有其他frameworks。
- 蓝色层:包含用于所有新功能(New Features主要是视图控制器)和样式逻辑的UI组件,并且所有功能(包括旧功能)都使用这些组件。
- 绿色层:包含非UI逻辑,例如DB和API客户端。所有上层都可以直接访问。
- 黄色层:包含utility方法和函数,它们完全独立于所有其他功能的逻辑,因此在整个代码库中共享。
我们开始看到直接的好处,尤其是对所有新功能的好处,实际上使之更加稳定和强大。
不幸的是,我们后来意识到这种方案仅节省了我们一些时间,实际上,随着我们不断增加地 慢的构建时间 展现,这些层开始像原始的一样逐渐发展成大型整体!
解决方案
- 大的UI层现在分为多个模块,根据其定义域分开。
- 核心服务和核心UI包含每个模块都需要共享的低级组件(例如API客户端,字体,颜色等)。
- 该 的CocoaPods 项目已经一去不复返了!现在,每个模块都仅链接所需的依赖,而这些依赖会预先构建成动态库(dynamic frameworks)。
每当我们添加新功能时,这都给我们带来了更多的稳定性,还使我们能够安全地尝试新的设计模式。
我们还大大降低了pull请求合并时产生冲突的风险,因为每个开发人员很可能将在单个定义域中工作。
那构建时间呢?我们采用的方案非常简单:我们只构建已更改的内容;我们仅对所需内容进行测试。这要归功于 Carthage ,它为第三方项目提供了轻松的缓存。
下面将展示了以上所有内容在实践中的方式:我们的第一个 模块 Chat。我们还将说明我们如何大规模地向这个新世界迁移。
什么是模块?
这是来自维基百科的 模块化编程 的定义:
模块化编程 是一种软件设计技术,它强调将程序的功能分成独立的,可互换的 模块 ,以使每个 模块 都包含仅执行所需功能的一个方面所必需的所有内容。
对我们来说,模块包含属于同一功能定义域的代码。看起来像这样:
在专用Xcode项目中定义为动态库(dynamic framework)的模块的视图。
此库(包括其单元测试)中定义了每种功能所需的代码和资源。
这些实现细节中的大多数都将隔离在此模块(或库)的范围内。通过库公共接口仅公开对外部使用者有用的接口。
当一个模块需要访问另一个模块时,这个起着重要的作用。让我们解释一下。
访问另一个模块
模块A无法直接访问模块B
我们希望每个模块都完全独立。这样,如果修改模块A,则不会对其任何使用者产生影响。这也意味着我们只需要重新编译修改后的模块,而不是整个项目。
何时需要访问另一个模块呢?
示例:我在模块A中,我想导航到模块B中定义的屏幕。代码如下所示:
func navigateToB() {
//从依赖项管理器获取ModuleB的实例
let module = Dependencies.shared.moduleB()
//从ModuleB获取screenB的视图控制器的实例
let vc = module.screenB(input)
// Push视图控制器
navigationController.pushViewController(vc, animated: true)
}
通过这种方式,模块可以解除耦合:我们不需要知道要导航到的视图控制器的具体类型,也不需要知道如何配置它。我们只需将任何输入传递给factory方法即可获得准备好呈现的泛型实例。
我们将解释Dependencies类是什么,但首先让我们定义模块公共接口。
模块公共接口
每个模块将其公共接口定义为Swift协议,描述将在外部公开的行为。所有这些接口都定义在公共的“Dependencies”层中。
public protocol ChatModuleProtocol {
//返回一个新的视图控制器,用于显示对话列表
func conversationsScreen() -> UIViewController
//返回一个新的视图控制器,用于与用户就产品进行聊天(可选)
func messagesScreen(user: User, product: Product?) -> UIViewController
//返回一个新对象,该对象可用于在后台发送消息。有关更多信息,请参见ChatMessageSender。
func messageSender(to receiver: User, about product: Product?) -> ChatMessageSender
}
//可用于发送有关特定对话的消息的对象。
public protocol ChatMessageSender {
//发送带有正文的新消息。完成处理程序将在完成时被调用。如果成功,则为创建的新消息提供有效的ID。
func sendNewMessage(with body: String, completion: @escaping (_ messageId: String?) -> Void)
}
该模块可以公开视图控制器和用于执行任何类型后台任务的普通对象。无论哪种情况,这些对象的真实类型都不会透露给使用者。
现在让我们看一个具体的实现。
实现模块
每个模块都必须符合其公共接口。这是通过在模块库内定义的此类来完成的:
import Dependencies
public class ChatModule: ChatModuleProtcol {
public init() {}
public func messageSender(to receiver: User, about product: Product?) -> ChatMessageSender {
//配置并返回一个对象,用于在后台发送msg
}
public func conversationsScreen() -> UIViewController {
//配置并返回视图控制器
}
public func messagesScreen(user: User, product: Product?) -> UIViewController {
//配置并返回视图控制器
}
}
现在,我们需要一种途径来请求该模块的实例并使用它。这是由依赖管理器完成的。
依赖管理器
public final class Dependencies: DependencyManager {
//我们通过单例将其公开给每个模块
public static let shared = Dependencies()
}
extension Dependencies {
//现在我们可以获得一个ChatModule
public var chatModule: ChatModuleProtocol {
return resolve(ChatModuleProtocol.self)!
}
}
注意:此类继承自DependencyManager,这是用于注册和解析依赖项的简单类。如果您想了解更多详细信息,请在博客文章末尾查看我们的示例项目。
现在,我们有一种途径来请求模块的实例。我们只需要最后一件事就可以使用它,告诉依赖管理器如何解决依赖。
解决依赖
主APP的target是为这些依赖关系提供具体实现的好地方,因为它“位于”任何模块之上。
import Dependencies
import Chat
//在应用程序启动完成后调用此函数。
func registerDependencies() {
let dependencies = Dependencies.shared
dependencies.register(ChatModuleProtocol.self) {
return ChatModule()
}
}
注意,这是整个项目中导入Chat模块的唯一位置。
每次任何模块发生更改时,都会重新编译主APP的target。因此,它应该非常轻巧且编译迅速。
而已!现在,我们可以从APP中的任何位置使用新模块。
结论
以下是我们所做工作的简要概述:
迁移之前(左)和迁移后(右)的项目。 Utils已被省略,因为没有实质性的变化。
这个过程可能会变得非常复杂,因此您需要一次 递增地处理 一个模块。
大致了解在开始迁移之前需要投入多少精力是很重要的。 评估工作量的 一种简单方法是:
- 确定属于您的定义域的核心文件。
- 创建一个全新的Xcode项目(在工作区外部),然后将那些文件复制到那里。
- 尝试编译。这将显示所有隐式依赖关系。
- 列出这些依赖项并相应地计划迁移。
- 从“较低”层(核心服务/核心UI)开始,然后向上移动,因为这将有助于达到具有有效绿色构建的中间阶段。
不幸的是,创建新的Xcode项目,将其添加到您的工作区并将其与正确的frameworks链接时涉及很多 样板文件 。记住重复使完美。专注于您的目标,稍后您将找到优化此目标的方法。
确保整个团队做出贡献,实现协作并促进反馈共享。在我们的案例中,我们发现 记录整个过程 非常有价值。
构建性能仍然是我们正在研究的问题。但是,我们刚刚描述的工作使我们能够引入 Carthage 来管理第三方依赖并缓存动态库。展望未来,我们也希望将这种方案也应用于我们的内部frameworks,以便我们仅构建和测试所需的东西。
我们期待与您尽快分享更多更新,并听听您的反馈!
示例项目
从我们的仓库中下载示例项目: