Swift中的可重用性和组成

尼采,梭罗和黑森最有可能试图逃避他们一生的一个概念:依赖性。 即使一个人不同意或拥护他们的哲学,程序员应该还是必须? -在编程时运用他们的思维方式。

让我们先定义问题:

耦合

想象一下,没有一部可以拆卸的汽车。 从座椅到车轮,从底盘到天窗都是一体的。 如果由于在高速公路上钉了一些讨厌的钉子而使轮胎漏气,我不能只买新轮胎,就需要买一辆新车。 因此,我们将此设计称为耦合设计,或者以一种更真诚,更礼貌的方式进行; 愚蠢的设计。 编程没有什么不同。 如果某个类或功能的更改需要我重构程序的许多其他部分,那么我已经设计了这款不可拆卸的汽车。 不酷 一点都不酷。

那么,当我们意识到我们的代码紧密耦合时,我们该怎么办? 我们花了片刻的沉默。

我们意识到我们编写了一些糟糕的代码,并宣誓不再这样做。 靠这些话活着: 封装单一责任继承构成 。 我将以一些简单的示例开始,这些示例说明了我们如何在程序中尊重这些概念,然后将其构建为相当(但不是真的)复杂的示例。

钢琴家或小提琴家

您的功能签名应严格执行其声称的功能; 不多一点。 让它成为钢琴家或小提琴家,而不是两者。

所以我有一个函数应该打印字符串中某些字符的数量:

  func printNumber(of char:Character,in string:String?)} 
如果让string = string {
  var count = 0 
对于字符串{中的s
如果s == char {count + = 1}
}
 打印(数) 
}
}

此功能做不到预期的。 如果为空字符串,则不打印任何内容,而应打印“ 0”。 我可以通过在将字符串提供给函数之前解开字符串来解决此问题,尽管由于我通过了解(或假设)函数的实现细节来采取行动,因此这将反对封装

这是一个功能比应做的事:

  func removeNils(来自字符串:[String?])-> [String] { 
返回strings.flatMap {$ 0} .filter {!$ 0.isEmpty}
}

尽管它也会从字符串数组中删除空字符串,但它的名称为removeNils 。 即使此函数的调用者对此感到满意,也可能导致错误或意外行为。

最强人功能

功能就像我们的好朋友Frodo一样,它们已经承担了沉重的负担,它们不应该承担更多的负担。 假设您需要验证文本字段中的输入才能激活继续 按钮validateInputs函数使用正则表达式验证用户电子邮件和用户密码。 实现此目标的显而易见的正确方法是具有其他两个功能来帮助validateInputs减轻负担: validateEmailvalidatePassword可以使那些可分离逻辑的实现脱离validateInputs 。 这样, validateInputs将更短且易于阅读,并且我们可以在程序的其余部分中重用电子邮件和密码验证功能。 尽管我们喜欢编码,但我们尽量不要编写太多代码:]就像循环:尽可能地重用。 尽管尝试重用使您编写更多的代码,但是您要么将其推得太远(尝试用锤子钉住钉子),要么您一开始就没有编写可重用的代码。

具特异性的诱惑

从UI组件到逻辑层,尝试通过尽可能少地具体化来设计一切。 就像左边这个丑陋的小家伙。 作为程序员,我们喜欢那种丑陋的无色家伙。 通过添加功能,我可以将他转变为几乎所有东西。 相比之下,右侧的花哨的混蛋只有在我需要一只脖子紫色,腹部呈浅蓝色等的鸟时才有用。 为了让鸟有红头,我必须重写他的半蓝半灰头。 我们可以在这一点上涉及装饰器模式。 即使我们现在需要一只花哨的鸟,为了将来能够减少花哨的鸟或其他种类的花哨的鸟,我们也应该首先制造丑陋的鸟,然后为其添加功能。

我认为是时候停止阅读文学作品并展示一些Swift了

为什么不继承?

上述问题也可以通过继承来解决,如下所示:

 鸟类{ 
func fly(){
//实现
}
}
 类芬奇:鸟{ 
func showOff(){
//实现
}
}

这里似乎没有问题。 雀是鸟,它们会飞。 除此之外,他们还可以炫耀自己花哨的羽毛。 如果我需要Gouldian Finch ,则可以从Finch类继承。 问题就出在这里:如果我需要一门Canary课程怎么办。 尽管他们有一个共同的祖先,但他们只是表亲(我不是鸟类学家,如果一个人是另一个祖先,请原谅我,就好像他们不是,对我来说就是❤)。 所以我只能从Bird类继承。 当我遇到很多这样的情况时,我最终会得到一棵比塔加延家族的家谱树更宽,更复杂的树 。 解决此问题的方法是在可能的情况下(当然)在合理的情况下偏向于继承而不是继承

组成

解决与组成相同的问题:

 协议禽类{ 
func fly()
}
 扩展禽类{ 
func fly(){
//默认实现(如果有用)
}
}
 协议FlameBreather { 
func flameBreath()
}
 扩展程序FlameBreather { 
func flameBreath(){
//默认实现(如果有用)
}
}

现在创建一个Dragon类,我将使用上述协议。

 龙类:禽类,烈焰生物{ 
//实现
}

我们没有将Dragon变成一只鸟,而是通过协议为其提供了必要的功能。 而且,更好的是,如果将来需求发生变化,我们可以像拆卸乐高零件一样删除功能。 我们可能仍需要进行一些重构,但是相对于继承,这就像在桶中射鱼。 随着我们为工具箱增加更多协议而扩展,我们将能够组成ParrotsPterosaursNazgul Mounts而无需任何其他实现。

视图控制器的依赖注入

如果将来有很小的机会需要重用,则任何可分离的逻辑,特征或功能都应使用其自己的协议。 无论是视图模型还是交互器,视图控制器的数据提供程序都应定义为协议,而不是具体类型。 并且只要数据提供者符合视图控制器的期望,我们就不必关心具体的类型。

这是Spotify每月收听者屏幕 为简单起见,请忽略“ 每月听众”文本下方的所有内容。 在这里,我们有一堆可以滑动的图像,一个大标签和一种字幕标签。 在这种情况下,我们的视图控制器将要求图像和文本显示在屏幕上。 与其将其称为ListenersViewControllerSwipableImagesViewController将其SwipableImagesViewController 。 我的控制器应要求提供所需的数据,并且不在乎任何其他内容。

 协议DataProvider { 
var imageUrls:[URL] {get}
var标题:字符串? {得到}
var说明:字符串? {得到}
}

和视图控制器:

  SwipableImagesViewController类:UIViewController { 
var dataProvider:DataProvider!
//将图像添加到集合视图并分配文本
// ...
}

初始化视图控制器时,必须立即用所需的具体类型实例化数据提供程序。

 让viewController = SwipableImagesViewController() 
viewController.dataProvider = ArtistDataProvider()

现在,我可以使用其他数据提供程序,它可能会向不同的端点进行服务调用,依此类推。 如果我想禁用某些屏幕的刷卡行为,可能是针对未付费用户或某些用户的刷卡行为,可以将该标志添加到协议中。

 协议DataProvider { 
// ...
var isSwipable:布尔
}

在控制器中:

  SwipableImagesViewController类:UIViewController { 
// ...
覆盖func viewDidLoad(){
super.viewDidLoad()
collectionView.isUserInteractionEnabled = dataProvider.isSwipable
}
}

以相同的方式,您可以使视图控制器越来越可重用。 只是提防极端。 您不想将其推得太远,因为它可能会过大。 我个人喜欢覆盖高度相似的UI,可能只有几个额外的按钮或一些额外的功能,而几乎没有差异。 但是,在命名和设计控制器时,我发现在每种情况下都有用。 思考“我的控制器真的需要这个属性吗?”促使我设计更简单的接口,因此编写了更简洁的代码。

单一责任,组成和依赖性注入

  • 事物应该只有一个目的,只有一个目的。
  • 首选has a关系而不是关系(遵循协议并不完全是has a关系,因为我们不保留或拥有任何对象,但是视角相同。) 继承很容易失控,增加复杂性而不是降低复杂度。
  • 使用接口(协议)而不是具体类型。 它在提高可重用性的同时为您提供了灵活性。

一个简单而独立的思想不会在任何王子的竞标中辛劳。 —亨利·大卫·梭罗