不要用子类!Swift的核心是面向协议

作者:ray1689718授权本站转载。

Swift的核心

我们可以通过等式的传递性来理解swift:

  • Swift的核心是面向协议的编程。

  • 面向协议的编程的核心是抽象(abstraction)和简化(simplicity)。

  • 所以swift的核心就是抽象和简化。

你可能对我的标题感到诧异。我并不是说子类没有价值,尤其在使用单一继承(single inheritance)的情况下,类和子类当然是强有力的工具。然而我想说的是,iOS日常开发的问题是对类和继承的过度使用。作为面向对象的编程者(object-oriented programmer,后面统一替换为OOP编程者;object-oriented programming后面统一简写为OOP)我们总是会自然的倾向于使用引用类型和类去解决问题,但是我个人还是认为应该反过来,倾向于用值类型代替引用类型。我们还是要去写模块化的,可伸缩的并且可重用的代码,这一点不会变。swift中强大的值类型就可以帮我们实现此目的,且不需要对引用类型有过强的依赖。我认为不仅面向协议的编程(protocol oriented programming,后统一替换为POP)可以帮我们实现这点,另外2种编程类型也可以,且都具有抽象和简化的核心思想,这两种分别是:面向值的编程(value-oriented programming,后面统一替换成VOP)和函数式编程(functional programming)。

先说清楚,我绝不是这些种编程类型(POP,VOP和函数式编程)的专家。和你一样,从MMM时代(manual memory management - 手动内存管理)开始我就是一个OOP编程者。通过自学,从开始我就很重视值抽象(value abstraction)和简化的思想。我都没有意识到自己是一个倾向于函数式编程(functional programming)的OOP编程者,而且很多时候用的都是VOP和POP的思路。这可能是我为什么在第一天就兴高采烈的加入了swift的浪潮之中的原因。在WWDC的一整周里,swift的核心理念与我认为的该怎样去编程是如此之契合,这个感受一直充斥在我脑海中。通过这篇文章,我希望能帮助你(OOP的编程者)打开思路,去考虑该如何用更加Non-OOP(非OOP)的方式去解决问题。

OOP的问题(和我不得不去学它的原因)

我会是第一个跳出来说的:不用OOP的话做出iOS应用很难。Cocoa的核心就是OOP。没有OOP的话你根本写不出来一个iOS应用。有时候我会幻想这不是真的。如果你有不同观点,赶快证明我是错的吧。我真的需要这样,求你了,证明我是错的吧!

不管怎么样,你总会遇到必须用对象、用引用类型解决问题的时候,然后由于Cocoa的规定而被迫使用类(classes)。这种情况下你碰到的问题都是我们大家熟知并热爱的:

  • 传递class的实例这个做法好像总是有种不可思议的能力:你想用一个实例的时候,让这个实例的状态(state)和你所期望的不一样。(这是由于可变状态(mutable state)导致,你这个对象的另一个享有者在它觉得合理的时候能够改变此对象的属性。)

  • 如果不用多继承的话,从一个很棒的class派生出子类从而获得它的扩展功能妨碍了你使用另外一些很棒的class的更多更能,而且还增加了复杂性。(举例来说,试着去把2个UITextField的子类结合起来,生成一个拥有这2者特性的超级UITextField吧。)

  • 上面一条的另外一个问题是会引出意外行为(unexpected behavior)。如果你遇见了类似上面一条所描述的情况,你就陷入到了一个依赖问题中:你连接了2个superclass各自的特性,对于其中一个superclass的一处改动可能会给另外一个superclass带来不良影响。这就是被周知的class之间紧耦合(tight coupling)所带来的问题。

  • 单元测试中的mocking。有些classes在系统中的环境状态下耦合过于紧密,想完全测试这些classes就需要你创建每个class的假表象。我都不用告诉你本质上你并没有真正的测试了这个class,你不过是在假装测试它。这里就不提很多Mocking的库是用运行时的小把戏来造一个假的class了。

  • 并发(Concurrency)问题。这和上面提到的可变状态是伴随出现的。你从多个线程中同时改变一个引用就会引起这个问题,在运行时使对象之间的同步发生异常,这点也真的不用和你说了。

  • 很容易导致出现像上帝类(God classes - 承担着很多subclasses需要的重要高层级代码的所有责任),Blobs(有过多职权的classes),Lava Flow(因为含有太多的非法代码导致任何人都不敢碰的classes)等等这些种反面模式(anti patterns)。

POP 面向协议的编程

陷入OOP的反面模式特别容易。多半时间我们(包括我)就是太懒而不愿意去点File>New File。结果是在现有class的基础上添加一个函数是如此轻松,我们就不愿意从零开始建一个新的class了。如果你一直这么干,而且一直非常懒的从一个"很重要"的class派生subclass的话,你就把上帝类/死星类给弄出来了。实际上我之前就这么干过:我给一个app里的每个view Controller都加了能呈现一个指向navigationController的navigationBar的error view的功能。唉,我可真蠢。直到要改动那个Error上帝类行为的时候,我不得不把整个app都改一遍。这不是聪明的做法,你真应该看看那些bug。

一个掌管一切的class = bug满天飞

如果使用了POP,这个Error上帝类很大程度上就能很容易的抽象出来,以后改进它也方便。(顺便说下如果你想学POP,我极力推荐你去看这个视频)。想想就会觉得好笑,因为在这个视频中Apple自己都说:

"从一个protocol开始,别从class开始。"——Dave Abrahams: 毁你三观教授

这是一个能展示(之前的方式)有多残暴的例子:

1
2
3
4
5
6
7
8
classPresentErrorViewController:UIViewController{
var errorViewIsShowing:Bool= false
funcpresentError(message:String=“Error!",withArrowshouldShowArrow:Bool= false ,backgroundColor:UIColor=ColorSalmon,withSizesize:CGSize=CGSizeZero,canDismissByTappingAnywherecanDismiss:Bool= true ){
//写下了复杂的,脆弱的代码
}
}
//说一下,有100个class继承了这个class
EveryViewControllerInApp:PresentErrorViewController{}

随着项目的进行事情马上变的明了:并不是每一个UIViewController需要这个error逻辑,或是真的需要这个class所提供的每一个功能。我团队里任何一个人都可以轻易的在这个superclass里改点儿什么,从而影响整个app。这就让代码变得脆弱。还使得代码呈现出了多态。当本应该是由子类决定它自己的行为,这里的superclass却给帮着决定了。下面是在swift 2.0中我们如何用POP来更好的构建这段代码:

8
9
10
11
12
13
14
15
protocolErrorPopoverRenderer{
funcpresentError(message:String,withArrowshouldShowArrow:Bool,backgroundColor:UIColor,withSizesize:CGSize,canDismissByTappingAnywherecanDismiss:Bool)
}
extensionUIViewController:ErrorPopoverRenderer{ //使所有遵从于ErrorPopoverRenderer协议的UIViewController具有一个presentError的默认实现
//加上呈现error视图的默认实现
}
}
classKrakenViewController:UIViewController,ErrorPopoverRenderer{ //DroptheGodclassandmakeKrakenViewControllerconformtothenewErrorPopoverRendererProtocol.
funcmethodThatHasAnError(){
//…
//抛出error,原因是Kraken海妖今天吃人会感到不适。
presentError( /*blahblahblah好多参数*/ )
}
}

看,这里发生了很炫酷的事情。我们不仅消除了上帝类的存在,还让代码更加的模块化并增强了它的扩展性。通过创建一个 ErrorPopoverRenderer协议,就会让任何遵循了该协议的class具有呈现出一个ErrorView的能力。还不止这些,我们的KrakenViewController class不用必须实现presentError这个函数,因为我们扩展了UIViewController,让它提供了一个默认实现。

唉不过等下!这有个问题!我们每次想要呈现一个ErrorView的时候都必须要去实现每一个参数。这就有点儿让人不爽了,因为我们不能在protocol协议函数声明中为参数提供默认值。

我还挺喜欢这些参数的!更糟的是在让代码更具模块化特征的过程中我们引入了复杂度。还是继续吧,用swift 2.0中新加的一个小妙招来多少的补偿一下:

funcpresentError()
extensionErrorPopoverRendererwhereSelf:UIViewController{
funcpresentError(){
//在这里加默认实现,并提供ErrorView的默认参数。
presentError() //Woohoo!没有参数了!我们现在有默认实现了!
好了,现在看起来已经很不错了。我们不仅消除了这些烦人的参数,还用swift 2.0的新特性在protocol的层级上用Self给了presentError一个默认实现。用Self意味着当且仅当协议的遵循者是继承自UIViewController的情况下,这个扩展才会有效。这就让我们能够把ErrorPopoverRenderer真的当做是一个UIViewController,而甚至不需要对后者做扩展!更棒的是,从现在开始,Swift的运行时是以静态调度而非动态调度去调用presentError()方法。大致的意思就是我们在函数调用点给presentError()方法增强了一点性能。

哎,不过还是有个问题。到这里我们POP的旅途暂时告一段落,但对它的完善依旧不会停止。我们的问题就是如果只想对一部分参数使用默认值,对剩下的不用默认值该怎么做?在这方面用POP的话基本帮不上什么忙,但是我们可以寻求另外一种方法。现在,我们使用VOP吧。

VALUE-ORIENTED PROGRAMMING

看到了吧,POP和VOP总是伴随出现。在上面的WWDC视频链接中,Crusty提出了一些大胆的论断:我们用struct和enum类型就可以做到一切class能做到的事。我很大程度上同意这点,但没这么极端。依我看,protocol本质上是把VOP粘合在一起的胶水,这点我和Crusty持相同态度。实际上既然我们说到了Swift的核心理念以及VOP,我想给你们看看从Andy Matuschak的精彩访谈中关于Swift中的VOP

的话题里面摘出来的一张极好的图:

能看出来Swift的标准库中,仅有的4个class,和余下的95个struct和enum的实例共同构建了Swift功能的核心。

Andy如此阐述道:用Swift编程的时候我们要去考虑用一层很薄的对象层,和一层很厚的值类型层。Class是有它们的地方,但是我想尽最大程度的去认为它们的位置只应该处于对象层中的一个很高的级别上,在这里通过操纵值类型层中的逻辑来管理各种行为。

"把逻辑和行为分开"——Andy Matuschak

和你所了解的一样,值类型被赋给一个变量或者常量,抑或是传给函数做参数时是它的值被拷贝的。这就让值类型在任何时候只有一个享有者,从而降低复杂度。和引用类型相反,在赋值过程中引用类型会有很多享有者,其中一部分你甚至都没意识到。在任何时间点使用引用的话会带来一些副作用:引用的享有者会捣蛋,在背后偷偷改变这个引用。Class = 高复杂度,值 = 低复杂度。

通过利用值类型的简约特性,咱们实现一下之前提过的默认参数的设计吧。我们用的是Brian Gesiakvalue options paradigm方法:

15
16
17
18
19
20
21
22
23
24
structColor{
letred:Double
letgreen:Double
letblue:Double
init(red:Double=0.0,green:Double=0.0,blue:Double=0.0){
self.red=red
self.green=green
self.blue=blue
}
}
structErrorOptions{
letmessage:String
letshowArrow:Bool
letbackgroundColor:UIColor
letsize:CGSize
letcanDismissByTap:Bool
init(message:String= "Error!" ){
self.message=message
self.showArrow=shouldShowArrow
self.backgroundColor=backgroundColor
self.size=size
self.canDismissByTap=canDismiss
}
使用上面的选项型struct(是值类型!)就使我们的POP带上了一些VOP的色彩,如下:

funcpresentError(errorOptions:ErrorOptions)
funcpresentError(errorOptions=ErrorOptions()){
funcfailedToEatHuman(){
presentError(ErrorOptions(message: "Ohnoes!Ididn'tgettoeattheHuman!" 如你所见,对于用view controller做error处理,我们给与它了一种完全抽象的,可伸缩的和模块化的方式,还不用强迫所有的view controller去继承一个上帝类。当你有一个具有不同功能的上帝类的时候,上面的例子尤其能帮到你。除此之外,用这种方式去实现类似上面error功能的其他功能时,你把实现该功能的代码放哪儿都行,不必做太多的重构或者改变代码框架。

函数式编程

咱们来解决这个。我也刚开始接触函数式编程,不过我知道一点:这种范式(paradigm)要求一种鼓励编程者去避免可变数据(mutable data)和改变状态(changing state)的编程方式。和数学函数类似,函数式编程是由一些输出结果仅取决于输入参数的函数组成,而且函数的输出结果不会被本体之外的相依性(dependency)所影响。这就是众所周知的"data in,data out",意思是每次传进来一个值,这个值传出去的时候和传进来时候总要是一样的。想想单元测试就明白了!

如果我们用函数式的思想去写代码,就可以把VOP与函数式编程结合,利用其中的诸多优点,这些优点包括但不仅限于:

  • 完全线程安全的代码(值类型变量在并发代码中被分配时是被拷贝的,意思是另一个线程更改不了与它平行线程中的变量)。

  • 更详尽的单元测试

  • 不再需要在单元测试中用mock(用了值类型的变量就不用再重建一个必须使用mock对象的环境,只为了去测试仅仅少部分的功能。本质上通过初始化一个从任意依赖关系中抽象出来的特性,你可以重建任何你想要的东西。)

  • 代码更简洁(说实话,能和瓷器一样精致)。

  • 让你身边的小伙伴惊呆

  • 很炫酷

  • 让Kraken疯狂的崇拜你

什么时候用子类

什么时候应该用子类呢?答案是当你没选择的时候。比如:

  • 当系统要求的时候。许多Cocoa的API要求你使用class,你不应该非要用值类型来跟系统对着干。UIViewController是要派生子类的,要不然你的app就啥都没有了。别跟系统对着干!

  • 当你需要有东西来帮你管理在其他class实例之间的值类型变量,而且还需要与这些值类型变量通信的时候。对于这种情况Andy Matuschak给了一个很好的例子:用一个class把一个值类型的绘图系统计算好的值取过来,传递给一个Cocoa的class来把这个绘图系统绘制到屏幕上。

  • 当你需要或者想在许多享有者之间做隐式共享的时候。此种情况的例子是Core Data。数据持久化变幻无常,用Core Data的时候,使用子类给诸多需要同步的享有者做同步就很有效。但是要小心并发问题!这是你处理此类问题的时候必须要做的取舍。

  • 当你不知道对于引用类型来说它的拷贝意味着什么的时候。你会拷贝一个单例么(singleton)?不会。你会拷贝一个UIViewController么?不会。一个window?绝对不会。(你其实可以,这是你的特权。)

  • 当一个实例的声明周期与外部效应(external effect)绑定的时候,或者就只是需要一个稳定个体(stable identity)的时候。单例就是特别典型的例子。

结论

作为OOP的编程者我们已经习惯了用class来解决问题。长期以来我们开发了很多模式来弥补引用类型所带来的弊端。我的观点是在编程中换一种思路可以有效的减轻对这类折衷方案的使用。如果我们真的重视可伸缩性和可重用性,就得接受模块化的编程才是正道。使用值类型并结合Swift 2.0中新增并改进了的protocol特性就会轻松的达到这个目的。虽然之前OOP的思维方式会使我们比较难用VOP和POP的方式来思考,但是在swift中写的多了,VOP和POP的模式就会开始成为我们的第二天性。我们的大脑可能得需要我们多写一些代码才能适应这种思维方式,但我相信iOS社区作为一个整体能接纳这些做法,从而极大的降低我们日常解决问题的难度。Swift的核心是一个极为强大的值类型系统,坦白说,我们应该一开始就用VOP的思想磨练自己来发扬这个值系统的优势。但愿这篇文章能多多少少的帮助到你,让你每天写出来更加详尽的,天生安全的代码。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。