任何软件体系结构中最重要的角色之一是使应用程序中的各种对象和值之间的关系尽可能清晰且定义明确。然而,保持这些关系 - 无论多么精心设计 - 可能真的具有挑战性,特别是随着时间的推移。
随着代码库的增长和新对象的引入,应用程序很容易意外地以未定义的状态结束- 例如当所需的值或对象最终丢失时。虽然Swift提供了许多语言功能来帮助我们避免这种情况 - 例如其严格的静态类型系统,以及可选项等概念 - 但充分利用这些功能可能说起来容易做起来难。
本周,我们来看看我们如何做到这一点 - 以及我们如何使用Swift强大的类型系统来设置锁和键,这些锁和键可以帮助我们避免未定义的状态,并获得更强大的编译时保证我们的应用程序将在运行时保持不变
同时小编这里有些书籍和面试资料哦(点击下载)
尴尬地丢失了对象
让我们首先看一下我们要解决的问题类型的示例。无论组织良好,大多数应用程序都需要处理某种形式的全局(或至少是半全局)状态 - 我们的大多数应用程序以某种方式依赖于某种价值或对象。
例如,我们的应用程序可能要求用户登录以访问某些屏幕,并使用某种形式的UserManager
单例来跟踪用户当前是否登录 - 看起来像这样:
class UserManager {
static let shared = UserManager()
private(set) var user: User?
func logIn(with credentials: LoginCredentials,
then handler: @escaping (Result<User>) -> Void) {
...
}
}
上面的代码可能看起来很简单,但是一旦我们开始在代码库的一部分中使用它,需要登录用户才能工作,问题就开始出现了。
例如,假设我们正在构建一个视图控制器来显示当前登录用户的配置文件。即使此视图控制器仅在用户登录后才会使用,我们只能UserManager.shared.user
作为可选项进行访问,并且需要打开该可选项以实际使用其值。因为如果解包失败就没有合理的回退,我们所能做的就是触发一个断言,如下所示:
class ProfileViewController: UIViewController {
private lazy var nameLabel = UILabel()
private lazy var imageView = UIImageView()
override func viewDidLoad() {
super.viewDidLoad()
// This guard statement is kind of awkward, since we're
// ideally never going to enter the else clause.
guard let user = UserManager.shared.user else {
assertionFailure("Uhm...no user exists? 🤔")
return
}
nameLabel.text = user.name
imageView.image = user.photo
}
}
我们在这里处理的是一个非可选的可选项 - 一个技术上可选的值,但实际上是我们的程序逻辑所需要的 - 这意味着如果它丢失了,我们就有可能以一个未定义的状态结束,并且编译器没办法帮助我们避免这种情况。
其中一个难题是我们通过单例访问当前登录的用户,而不是使用依赖注入。如果我们要将用户正确地注入视图控制器初始化器的一部分,我们至少可以在本地解决问题:
class ProfileViewController: UIViewController {
private let user: User
init(user: User) {
self.user = user
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
// We can now simply access our local user copy, rather
// than having to unwrap a singleton's optional.
nameLabel.text = user.name
imageView.image = user.photo
}
}
上述变化无疑是朝着正确方向迈出的一大步 - 特别是因为我们也减少了对单身人士和全球共享国家的使用。但是,在没有任何其他更改的情况下,我们只是在其他地方移动了那个尴尬的断言失败 - 最有可能是负责创建我们的代码片段ProfileViewController
:
func makeProfileViewController() -> UIViewController {
// Still no compile-time guarantee that a user exists whenever
// we're displaying the profile screen :(
guard let user = UserManager.shared.user else {
assertionFailure("Whoops, no user 🤷♂️")
return UIViewController()
}
return ProfileViewController(user: user)
}
我们当然可以继续在guard let
其他地方发表我们的声明,但这似乎毫无意义。相反,让我们看看我们是否可以解决核心问题 - 即我们依靠全局可选值来做出本地决策。
锁和钥匙
如果我们能够隐藏需要用户在某种形式的锁定后面登录的应用程序部分,那么用户模型最终会成为解锁该锁定的密钥怎么办?例如,为了能够首先创建一个ProfileViewController
,调用者需要提供一个非可选用户 - 使我们上面的方法看起来像这样:
func makeProfileViewController(for user: User) -> UIViewController {
return ProfileViewController(user: user)
}
如果我们能够为需要某些特定数据或状态的所有其他类做同样的工作,那么我们最终会得到更少的模糊代码 - 我们将能够摆脱大部分那些尴尬的guard let
陈述,我们只是为了满足编译器而真正放在那里。
这在理论上可能听起来很棒,但在实践中可能很难实现 - 所以让我们来看看这样做的一种方式。
多级工厂
该工厂模式可以隔离大对象的创建一个伟大的方式-比如视图控制器和一些我们在我们的应用程序中使用的核心对象。但更好的是,如果我们创建多个工厂,每个工厂都处理我们的应用程序的某个级别或范围。
例如,我们可以从创建一个RootFactory
能够创建所有不需要任何特定模型或状态的对象来开始工作。进入那个工厂,我们可以注入我们所依赖的各种单体 - 例如我们自己的UserManager
,以及系统提供的单体,如URLSession.shared
:
class RootFactory {
private let urlSession: URLSession
private let userManager: UserManager
init(urlSession: URLSession = .shared,
userManager: UserManager = .shared) {
self.urlSession = urlSession
self.userManager = userManager
}
}
有了上述内容,我们现在可以开始定义方法RootFactory
,每个方法都为我们提供了一种简单的方法来创建我们在整个应用程序中需要的一些对象,例如ImageLoader
用于登录我们应用程序的视图控制器。
由于我们的工厂已经拥有所有必需的系统依赖项,调用者只需要调用一个没有任何参数的方法,我们的工厂可以确保注入我们的对象所需的所有依赖项 - 可能包括工厂本身:
extension RootFactory {
func makeImageLoader() -> ImageLoader {
return ImageLoader(urlSession: urlSession)
}
func makeLoginViewController() -> UIViewController {
return LoginViewController(
userManager: userManager,
factory: self
)
}
}
这就是诀窍 - 我们将创建额外的工厂,而不是必须依赖非可选的选项和全局状态 - 每个工厂都绑定到特定的模型。例如,在电子邮件应用程序中,我们可能MessageBoundFactory
会创建一个依赖于电子邮件实例的所有对象 - 或者在我们的示例中,我们创建一个UserBoundFactory
绑定到当前登录用户的对象 - 如下所示:
class UserBoundFactory {
private let user: User
private let rootFactory: RootFactory
init(user: User, rootFactory: RootFactory) {
self.user = user
self.rootFactory = rootFactory
}
func makeProfileViewController() -> UIViewController {
let imageLoader = rootFactory.makeImageLoader()
return ProfileViewController(
user: user,
imageLoader: imageLoader
)
}
}
正如您在上面所看到的,我们UserBoundFactory
还使用其父RootFactory
级来创建不需要当前用户的对象 - 这是有效的,因为工厂永远不会保留他们创建的对象 - 因此所有“子工厂”都可以自由地保留其父级而不必担心导致任何保留周期。
上述方法的优点在于,一旦我们访问a UserBoundFactory
,我们就可以简单地创建我们需要的任何用户绑定对象,而无需经常传递用户,所有这些都不需要任何断言失败或冒着未定义状态的风险。在这种情况下ProfileViewController
,我们makeProfileViewController
只是像以前一样调用,但我们现在有一个编译时保证所需的数据实际可用。
最后,让我们创建一个锁,它采用一种方法的形状,RootFactory
这样就可以检索一个用户绑定的工厂,因为调用者可以使用所需的密钥 - 用户模型:
extension RootFactory {
func makeUserBoundFactory(for user: User) -> UserBoundFactory {
return UserBoundFactory(user: user, rootFactory: self)
}
}
通过删除所有非可选的选项和断言失败,我们不仅使代码更容易预测,我们还改进了代码库中关注点的分离 - 因为每个工厂只负责在自己的范围内创建对象👍。
把事情付诸实践
许多架构结构在纸面上看起来很好,但问题始终是它们在实践中的效果如何 - 所以让我们采用新的锁和键实现进行旋转!
正如我们之前看到的,我们LoginViewController
现在由my创建RootFactory
,它也将自身作为初始化程序的一部分传递给视图控制器。这样做的好处是,只要用户成功登录,我们就可以使用注入RootFactory
来打开我们的锁并访问a UserBoundFactory
,然后我们可以使用它来创建ProfileViewController
并将其推送到导航堆栈 - 如下所示:
private extension LoginViewController {
func handleLoginResult(_ result: Result<User>) {
switch result {
case .success(let user):
let userBoundFactory = factory.makeUserBoundFactory(for: user)
let profileVC = userBoundFactory.makeProfileViewController()
navigationController?.pushViewController(profileVC, animated: true)
case .failure(let error):
show(error)
}
}
}
由于每个工厂都能够将自己注入到它创建的对象中,我们不需要在任何特定的地方保留任何工厂 - 每个工厂的所有权只是在当前使用它的所有对象之间共享,并且只要它在不再需要(在UserBoundFactory
用户注销的情况下),它将自动释放。
工厂的主要好处之一,特别是与锁和钥匙一起使用时,我们可以轻松隐藏与创建给定对象的方式和原因相关的实现细节。我们甚至可以让我们RootFactory
决定应用程序的根视图控制器应该是什么 - 通过使用存储在钥匙串中的用户信息来解锁自己的锁,或以其他方式返回LoginViewController
:
extension RootFactory {
func makeRootViewController() -> UIViewController {
if let user = userManager.restoreFromKeychain() {
let factory = makeUserBoundFactory(for: user)
return factory.makeProfileViewController()
}
return makeLoginViewController()
}
}
这样,当我们在其中设置我们的应用程序时AppDelegate
,我们可以简单地创建我们的RootFactory
并让它为我们提供正确的根视图控制器,如下所示:
let factory = RootFactory()
let rootVC = factory.makeRootViewController()
window.rootViewController = UINavigationController(
rootViewController: rootVC
)
有了上述内容,我们的应用程序状态甚至会隐藏在我们的应用程序委托中,否则它往往会成为需要保持大量全局状态的对象 - 最有可能的形式是(偶尔是非可选的)选项。
结论
使用锁和键原则,只有在所需的依赖项可用时才能访问某些对象,这是使我们的代码库更可预测的好方法 - 并减少对非可选选项和笨拙guard
语句的需求。通过使用多个级别的工厂,每个工厂隐藏在一个明确定义的锁定之后,我们基本上可以在我们的应用程序中创建多个范围 - 每个范围变得越来越专用于给定的任务或数据。
但是,使用工厂并不是实现锁和密钥的唯一方法。使用其他技术也可以实现许多相同的原则 - 例如使用功能方法,其中每个功能解锁一组新的API,或设置多级依赖容器。与大多数事物一样,有多条路径可供选择,每条路径都可以让我们实现相同的目标。
你怎么看?您目前是否使用锁和密钥来管理应用中的对象,或者您将尝试使用它?请通过加我们的交流群 点击此处进交流群 ,来一起交流或者发布您的问题,意见或反馈。
网友评论