这次,我想谈谈来自“四人帮”武器库的另一种生成设计模式- “ Builder” 。 事实证明,在获得我的经验(虽然不太广泛)的过程中,我经常看到该模式通常用于“ Java”代码中,尤其是用于“ Android”应用程序中。 在“ iOS”项目中,无论它们是用“ Swift”还是“ Objective-C”编写的,这种模式对我来说都是非常罕见的。 然而,尽管非常简单,但在合适的情况下,它还是很方便的,而且,如时髦的说法,它功能强大。

通过逐步构造所需的对象,并使用最后调用的终结方法,该模板可用来代替复杂的初始化过程。 这些步骤可以是可选的,并且不应具有严格的调用顺序。

基础实例
如果所需的“ URL”不是固定的,而是由组件(例如,主机地址和资源的相对路径)构成的,则可能使用了Foundation库中便捷的URLComponents
机制。
在大多数情况下, URLComponents
只是一个类,它结合了许多变量,这些变量存储了各种URL组件的值以及url
属性,该属性返回适用于当前组件集的适当URL。 例如:
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.user = "admin" urlComponents.password = "qwerty" urlComponents.host = "somehost.com" urlComponents.port = 80 urlComponents.path = "/some/path" urlComponents.queryItems = [URLQueryItem(name: "page", value: "0")] _ = urlComponents.url
实际上,以上用例是Builder模式的实现。 在这种情况下, URLComponents
本身就是构建器,将其值分配给它的各种属性( scheme
, host
等)是逐步对未来对象的初始化,而调用url
属性就像完成方法。
在评论中,围绕描述“ URL”和“ URI”的“ RFC”文档展开了激烈的战斗,因此,更确切地说,我建议例如假设我们仅在谈论远程资源的“ URL”,而不考虑此类问题。 “ URL”方案,例如“文件”。
如果您很少使用此代码并知道其所有细微之处,一切似乎都很好。 但是,如果您忘记了什么呢? 例如,主机地址等重要的东西吗? 您认为执行以下代码会如何?
var urlComponents = URLComponents() urlComponents.scheme = "https" urlComponents.path = "/some/path" _ = urlComponents.url
我们使用属性而不是方法,并且肯定不会“抛出”错误。 “ finalizing” url
属性返回一个可选值,所以也许我们得到nil
? 不,我们得到了一个URL
类型的完整对象,其值无意义-“ https:/ some / path”。 因此,我想到了练习根据上述“ API”编写自己的“构建器”。
(应该有一个“表情符号”,“自行车”,但“哈伯”不显示它)
尽管有上述规定,我还是认为URLComponents
很好且方便的“ API”,用于从组件中组装“ URL”,相反,是“解析”众所周知的“ URL”的组件。 因此,基于此,我们现在编写自己的类型,该类型从各个部分收集“ URL”并拥有(假设)当前需要的“ API”。
首先,我想通过为所有必要的属性分配新值来摆脱完全不同的初始化。 相反,我们实现了使用链接器调用的方法创建构建器实例并将值分配给所有属性的可能性。 链以终结方法结束,其调用结果将是URL
的相应实例。 也许您在一生中遇到过“ Java中的StringBuilder
”之类的东西-我们现在将努力争取这种“ API”。
为了能够调用方法步骤,它们中的每个步骤都必须返回当前构建器的一个实例,在该实例中将存储相应的更改。 由于这个原因,并且为了摆脱对象的多重复制以及摆脱mutating
方法的mutating
,尤其是在没有思想的情况下,我们将构造器声明为一个类 :
final class URLBuilder { }
考虑到以上要求,我们将声明用于指定将来“ URL”参数的方法:
final class URLBuilder { private var scheme = "https" private var user: String? private var password: String? private var host: String? private var port: Int? private var path = "" private var queryItems: [String : String]? func with(scheme: String) -> URLBuilder { self.scheme = scheme return self } func with(user: String) -> URLBuilder { self.user = user return self } func with(password: String) -> URLBuilder { self.password = password return self } func with(host: String) -> URLBuilder { self.host = host return self } func with(port: Int) -> URLBuilder { self.port = port return self } func with(path: String) -> URLBuilder { self.path = path return self } func with(queryItems: [String : String]) -> URLBuilder { self.queryItems = queryItems return self } }
我们将指定的参数保存在类的私有属性中,以供finalize方法将来使用。
对我们基于类的“ API”的另一个致敬是path
属性,与所有相邻属性不同,该属性不是可选的,并且如果没有相对路径,它将存储一个空字符串作为其值。
实际上,要编写此终结方法,您需要考虑一些其他事项。 首先,“ URL”有一些部分,如开头所述,没有URL部分就变得毫无意义了,这是scheme
和host
。 我们用默认值“奖励”了第一个,因此,忘记了默认值,我们很可能仍会收到预期的结果。
第二种方法则稍微复杂一些:无法为其分配一些默认值。 在这种情况下,我们有两种方法:在缺少此属性的值的情况下,返回nil
或引发错误,然后让客户端代码自行决定如何处理它。 第二个选项比较复杂,但是它将允许您显式指示特定的程序员错误。 例如,也许我们会走这条路。
另一个有趣的问题与user
和password
属性有关:只有同时使用它们才有意义。 但是,如果程序员忘记分配这两个值之一,该怎么办?
而且,可能要考虑的最后一件事是,作为finalize方法的结果,我们希望具有URLComponents
的url
属性的值,在这种情况下,这不是非常有用的可选。 虽然,对于nil
属性的设置值的任何组合,我们都不会得到它。 (只有一个空的,刚刚创建的URLComponents
实例将没有值。)要解决此问题,可以使用!
-运算符“强制展开”。 但是总的来说,我不想鼓励使用它,因此,在我们的示例中,我们简要地从“基础”的精妙知识中抽象出来,并将所讨论的情况视为系统错误,该错误的发生与我们的代码无关。
因此:
extension URLBuilder { func build() throws -> URL { guard let host = host else { throw URLBuilderError.emptyHost } if user != nil { guard password != nil else { throw URLBuilderError.inconsistentCredentials } } if password != nil { guard user != nil else { throw URLBuilderError.inconsistentCredentials } } var urlComponents = URLComponents() urlComponents.scheme = scheme urlComponents.user = user urlComponents.password = password urlComponents.host = host urlComponents.port = port urlComponents.path = path urlComponents.queryItems = queryItems?.map { URLQueryItem(name: $0, value: $1) } guard let url = urlComponents.url else { throw URLBuilderError.systemError
仅此而已! 现在,从示例开始的“ URL”的爆炸式创建可能看起来像这样:
_ = try URLBuilder() .with(user: "admin") .with(password: "Qwerty") .with(host: "somehost.com") .with(port: 80) .with(path: "/some/path") .with(queryItems: ["page": "0"]) .build()
当然,在do
- catch
之外使用try
还是没有操作员?
如果发生错误,将导致程序崩溃。 但是,我们为“客户”提供了处理他认为合适的错误的机会。
是的,使用此模板进行逐步构建的另一个有用功能是能够将步骤放置在代码的不同部分。 这不是最常见的情况,但是仍然如此。 感谢akryukov的提醒!
结论
该模板非常易于理解,并且所有简单的内容都非常巧妙。 反之亦然? 好吧,没关系。 最主要的是,我无需费心,可以说它(模板)已经发生了,它帮助我解决了创建大型而复杂的初始化过程的问题。 例如,准备与库中的服务器进行通信的会话的过程,大约两年前,我为一项服务编写了该过程。 顺便说一句,该代码是“开源的”,并且,如果需要的话,很有可能熟悉它。 (当然,尽管如此,此后大量的水涌入,其他程序员也对此代码进行了应用。)
我关于设计模式的其他文章:
这是我的“ Twitter” ,可以满足我对公共职业活动的假想兴趣。