面向协议的编程简介

在过去的几年中,面向协议的程序设计已经获得了很大的关注,并在Swift社区中成为流行语。 有些人喜欢它,有些人讨厌它,但实际上是什么呢? 应该解决什么问题? 它与我们钟爱的面向对象编程有什么关系?

面向协议编程不是竞争对手,也不是面向对象编程的替代品,尽管它的名称可能暗示其他含义。 这是思考非常具体的问题集的一种方法,可以帮助您创建灵活,可维护和易于阅读的代码。 应该将其更多地视为对面向对象方法的补充(实际上,面向对象编程已经在面向协议编程中纳入了许多中心思想)。

我们将首先研究协议(或接口,在许多其他语言中称为接口)如何帮助我们进行封装和信息隐藏。 看一下CarCarrier类的以下示例:

 车类{ 
变量位置:CGPoint
var isLocked = true

初始化(位置:CGPoint){
self.position =位置
}

公共职能转移(x:CGFloat,y:CGFloat){
self.position.x + = x
self.position.y + = y
}

公共功能锁(){
self.isLocked = true
}

公共功能unlock(){
self.isLocked =否
}
}

运营商类{
变量位置:CGPoint
var loadingCars = [Car]()

初始化(位置:CGPoint){
self.position =位置
}

公共职能转移(x:CGFloat,y:CGFloat){
self.position.x + = x
self.position.y + = y
self.loadedCars.forEach {(汽车)在
car.move(x:x,y:y)
}
}

公共函数itsWeirdThatACarrierCanDoThis(){
self.loadedCars.forEach {(汽车)在
car.unlock()
}
}
}

特别注意我们的Carrier类中的最后一个方法。 太奇怪了吧? 承运人不应该能够解开正在运输的汽车。 这是Carrier类的副作用,它确切地知道其要传输的内容,因此可以访问那些对象中的所有公共方法。 我们可以对Car实现一个Vehicle超级类,并在其中放置移动逻辑,但是如果Vehicle类还包含其他方法,我们可能会遇到同样的问题。

现在让我们看一下针对此问题的面向协议的解决方案。 我们首先将类划分为最小的,可管理的构建块,然后将它们放入协议中,以收集适合在一起的属性和方法。 对于这种特殊情况,我们可以将功能分为三个不同的协议MoveableLockableLoadable

 协议可移动:AnyObject { 
var位置:CGPoint {获取}
func move(x:CGFloat,y:CGFloat)
}

协议可锁定:AnyObject {
var isLocked:布尔{get}
func lock()
func unlock()
}

协议可加载:AnyObject {
func load(_ something:可移动)
func unload(_ something:可移动)
}

这似乎可以解决问题。 我们已经排除了所有应该公开可用的属性和方法,并将它们转变为处理功能良好分离的协议。 请注意, Moveable.positionLockable.isLocked均为只读属性。 我们之所以选择这种设计,是因为这两个属性对于其他实例可能都是有意义的,但是只有对象本身才应该能够操纵它们(或者通过方法实现选择其他人如何操纵它们)。 我们将通过将它们都设置为“计算属性”并为其声明一个私有支持存储来解决此问题的实现。

现在,我们已经完成了协议中功能的指定,我们重构了我们的类以使其符合适当的协议,并且隐藏了类外部不应该提供的所有内容:

 轿车:可移动,可锁定{ 
私人变量_position:CGPoint
私人var _isLocked = true

public var isLocked:布尔{
返回self._isLocked
}

公共变量职位:CGPoint {
返回self._position
}

初始化(位置:CGPoint){
self._position =位置
}

公共职能转移(x:CGFloat,y:CGFloat){
self._position.x + = x
self._position.y + = y
}

公共功能锁(){
self._isLocked = true
}

公共功能unlock(){
self._isLocked =否
}
}

运营商类别:可移动,可装载{
私人变量_position:CGPoint
私人var loadingStuff = [Moveable]()

公共变量职位:CGPoint {
返回self._position
}

初始化(位置:CGPoint){
self._position =位置
}

公共职能转移(x:CGFloat,y:CGFloat){
self._position.x + = x
self._position.y + = y
self.loadedStuff.forEach {(事物)在
something.move(x:x,y:y)
}
}

公共功能负载(_事物:可移动){
self.loadedStuff.append(事物)
}

公共功能卸载(_事物:可移动){
self.loadedStuff = self.loadedStuff.filter({$ 0!== something})
}

//此方法不再有效,
//因为Moveable对象没有unlock()方法。
//
//公共函数itsWeirdThatACarrierCanDoThis(){
// self.loadedStuff.forEach {(事物)在
// Thing.unlock()
//}
//}

}

请注意,当我们让Carrier处理实现Moveable协议的对象时,它将失去对与移动无关的所有方法和属性的访问。 使这种方法更好的是,它为我们的代码带来了可扩展性。 我们的运输商不再确切知道它在四处移动,因此只要我们让新的类实现Moveable协议,我们就可以拖拉汽车,轮船,家具……几乎所有有意义的东西都可以移动,而无需更改单个Carrier类中的代码行。

您可能还记得以前的文章中,结构和布尔型是值类型。 我们的计算属性能够直接返回后备存储变量,这是因为值类型每次传递到某个地方时都将被复制,这意味着我们的头寸永远不会出现任何别名问题。

我很高兴你问。
协议并不全都是关于信息隐藏的。 我们的第二个示例展示了协议(以及泛型)如何使您的程序更灵活,并减少所需编写的代码量。 看看这个:

 类算术{ 
静态函数add(a:Int,b:Int)-> Int {
返回a + b
}
}

这是一个非常人为的例子,但我认为这将说明问题。
您所看到的是一种执行简单加法并返回结果的方法,它本身并没有做错任何事情。

但是,当我们仔细观察时,就会意识到我们将需要编写很多这样的文章来涵盖您可能希望加在一起的所有不同类型的数字。 浮点数将需要一个方法,双精度数将需要一个方法,依此类推。我们想要实现一个可以接受几种不同类型并提供正确答案的函数,但是我们还想对这些类型可以是什么指定约束(它不会,例如,在这种情况下尝试添加两个String是有意义的)。 让我们尝试在下一个示例中优化此方法定义:

 类算术{ 
静态函数add (a:Type,b:Type)-> Type {
返回a + b
}
}

美丽! 现在,我们有了一个使用协议作为通用约束的函数。 这使我们可以使用相同的方法处理任何数量的类型,条件是该类型必须实现Numeric协议,从而允许我们对其执行算术运算。

希望本文能带来一些新的见解,并可能引发一些关于如何在自己的项目中使用面向协议的编程的想法。 如果您有任何疑问,请随时发表评论,然后继续获取有关未来文章的通知。


要了解有关iOS开发的更多信息,请查看我以前的文章:

使用Swift扩展清理代码

Swift扩展的创建是为了向无法使用源代码的实体添加功能,但是我们可以使用…

medium.com

了解ARC对应用程序性能的影响

什么是ARC?它如何影响代码的性能?

medium.com

这个故事发表在中等规模最大的企业家精神出版物The Startup中,紧随其后的是+401,714人。

订阅以在此处接收我们的热门新闻。