WWDC 2013 Session笔记 - iOS7中的ViewController切换
几句代码快速集成自定义转场效果+ 全手势驱动
TransitionAnimation 学习笔记
iOS --- 一张图看懂转场动画
iOS自定义转场动画实战讲解
iOS自定义转场动画实战讲解
iOS 视图控制器转场详解
iOS 视图控制器转场详解
iOS 转场动画探究(一)
iOS 转场动画探究(二)
iOS 转场动画详解
iOS自定义转场动画
密码规则 / UITextInputPasswordRules
也难怪 hipster 们着迷于工艺品和手工制品。不管是一片厚切鳄梨吐司、一瓶限量(非乳制)姜黄奶或一杯完美的手冲咖啡——其中的人情味是无法替代的。
相反,好密码和工艺品截然不同。密码应该完全没有任何意义,除非它是一个 90 年代骇客电影的标题或者一个密室逃脱游戏的答案。
有了 iOS 12 和 macOS Mojave 中的 Safari,生成可以想象到的最强、最没有意义、最难猜到的密码从未如此简单——这都要感谢一些新功能。
理想的密码策略非常简单:强制要求最少字符数(至少 8 位)并且允许长密码(64 位或者更多)。
其他更复杂的策略,像预置的安全问题、周期性的失效密码或者强制要求一些奇怪的符号,只不过让这些策略想要保护人感到厌烦。
但是不要太相信我说的话——我不是安全专家
相对的,请查看美国国家标准技术研究所最新发布(2017 年 6 月)的 Digital Identity Guidelines
好消息是越来越多的公司和组织开始注意安全性最佳实践了。坏消息则是改变这些事情需要进行一系列影响数百万人的大范围数据改动。事实上前面说到的安全性反面模式并不会很快消失,因为公司和政府做任何事情都需要花很久的时间。
自动式强密码
Safari 的自动填充从 iOS 8 起就可以生成密码了,但是它有一个缺点就是不能保证生成的密码符合某些服务的要求。
Apple 通过 iOS 12 和 macOS Mojave 里 Safari 中的自动式强密码功能来解决这个问题。
WebKit 工程师 Daniel Bates 在 3 月 1 日给 <acronym title="Web Hypertext Application Technology Working Group" style="max-width: 100%;">WHATWG</acronym> 提交了这个提案。6 月 6 日,WebKit 团队发布了 Safari Technology Preview 58,使用新属性 passwordrules 来支持强密码生成。同时,WWDC 发布了 iOS 12 beta SDK,包括新的 UIText<wbr style="max-width: 100%;">Input<wbr style="max-width: 100%;">Password<wbr style="max-width: 100%;">Rules API,还有验证码自动输入和联合身份验证等其他一些密码管理功能。
密码规则就像是密码生成器的配方。根据一些简单的规则,密码生成器就可以随机生成满足服务提供方需求的新密码。
密码规则由一个或多个键值对组成:
required: lower; required: upper; required: digit; allowed: ascii-printable; max-consecutive: 3;
每个规则可以指定下列键:
required: 需要的字符类型
allowed: 允许使用的字符类型
max-consecutive: 允许字符连续出现次数的最大值
minlength: 密码最小长度
maxlength: 密码最大长度
required 和 allowed 键使用下面列出的字符类别作为值。max-consecutive、minlength 和 maxlength 使用非负整数作为值。
required 和 allowed 键可以使用下面的字符类别作为值:
upper (A-Z)
lower (a-z)
digits (0-9)
special (-~!@#$%^&\*\_+=|(){}[:;"'<>,.? ]` 和空格)
ascii-printable (U+0020 — 007f)
unicode (U+0 — 10FFFF)
除了这些预置字符类别,还可以用方括号包住 ASCII 字符来指定自定义字符类别(比如 [abc])。
Apple 的 Password Rules Validation Tool 让你可以对不同的规则进行实验,并得到实时的结果反馈。甚至可以生成并下载上千个密码用来开发和测试!
指定密码规则
在 iOS 上,给 UIText<wbr style="max-width: 100%;">Field 的 password<wbr style="max-width: 100%;">Rules 属性设置一个 UIText<wbr style="max-width: 100%;">Input<wbr style="max-width: 100%;">Password<wbr style="max-width: 100%;">Rules 对象(同时也应该将 text<wbr style="max-width: 100%;">Content<wbr style="max-width: 100%;">Type 属性设置为 .new<wbr style="max-width: 100%;">Password):
Swift
let newPasswordTextField = UITextField()
newPasswordTextField.textContentType = .newPassword
newPasswordTextField.passwordRules = UITextInputPasswordRules(descriptor: "required: upper; required: lower; required: digit; max-consecutive: 2; minlength: 8;")
在网页上,设置 <input> 元素(且 type="password")的 passwordrules 属性:
<input type="password" passwordrules="required: upper; required: lower; required: special; max-consecutive: 3;"/>
如果没有指定,默认的密码规则是 allowed: ascii-printable。如果表单中有密码验证区域,它的密码规则会从上一个区域继承下来。
在 Swift 中生成密码规则
不光是只有你会觉得直接使用没有良好抽象的字符串格式令人感到不安。
下面是一种将密码规则封装成 Swift API 的方式(可以作为 Swift package 获取):
enum PasswordRule {
enum CharacterClass {
case upper, lower, digits, special, asciiPrintable, unicode
case custom(Set<Character>)
case required(CharacterClass)
case allowed(CharacterClass)
case maxConsecutive(UInt)
case minLength(UInt)
case maxLength(UInt)
extension PasswordRule: CustomStringConvertible {
var description: String {
switch self {
case .required(let characterClass):
return "required: \(characterClass)"
case .allowed(let characterClass):
return "allowed: \(characterClass)"
case .maxConsecutive(let length):
return "max-consecutive: \(length)"
case .minLength(let length):
return "minlength: \(length)"
case .maxLength(let length):
return "maxlength: \(length)"
extension PasswordRule.CharacterClass: CustomStringConvertible {
var description: String {
switch self {
case .upper: return "upper"
case .lower: return "lower"
case .digits: return "digits"
case .special: return "special"
case .asciiPrintable: return "ascii-printable"
case .unicode: return "unicode"
case .custom(let characters):
return "[" + String(characters) + "]"
有了这个,我们就可以在代码里指定一些规则,然后用它们生成有效的密码规则语法字符串:
let rules: [PasswordRule] = [ .required(.upper),
.required(.lower),
.required(.special),
.minLength(20) ]
let descriptor = rules.map{ "\($0.description);" }
.joined(separator: " ")
// "required: upper; required: lower; required: special; max-consecutive: 3;"
只要你愿意,你甚至可以扩展 UIText<wbr style="max-width: 100%;">Input<wbr style="max-width: 100%;">Password<wbr style="max-width: 100%;">Rules 给它添加一个接收 Password<wbr style="max-width: 100%;">Rule 数组的 convenience initializer。
extension UITextInputPasswordRules {
convenience init(rules: [PasswordRule]) {
let descriptor = rules.map{ $0.description }
.joined(separator: "; ")
self.init(descriptor: descriptor)
如果你是一个在个人认证信息上非常有感情的人,喜欢在密码输入区域中的小圆点后面输入你的大学、小狗或者最喜欢的运动团队,请考虑不要再这样做了。
就我个人来说,我无法想象没有密码管理器的日子。当你知道任何时候你都能访问到你需要的信息,并且只有你能访问到时,你的心灵将会获得极大的平静。
从现在开始改变,你就能完全利用上在之后今年发布的 iOS 12 和 macOS Mojave 的 Safari 中的这些改进。
作者 Mattt
翻译者 Bei Li — 2018年7月23日
Effective Objective-C 2.0
本书是iOS开发进阶的必读书籍之一。文中部分名词的中文翻译略坑,比如对block和GCD的翻译。其他整体还好,原作者写的比较用心。代码规范讲了不少,底层原理讲了一点点,且主要集中在第二章。另第六章对GCD的讲解还算不错。作者原文写了52条编码建议,不过本人在整理读书笔记时并未按照原来的条数来做区分,只是把自己认为比较重要的做了标记,并记录了下来。
1.对于NSString *someString = @“the string”; someString是一个分配在栈上的指针,@“the string”是分配在堆上的内存块。Objective-C的对象所占内存总是分配在堆上。 一个指针在32位架构的手机上占4个字节,在64位架构的手机上占8个字节。
2.在类的头文件中尽量少引入其他类的头文件,可以使用@class进行前向声明。协议如果在多个类里使用,则最好单独放在一个文件。目的都是为了解耦。
3.用字面量语法创建数组时,若数组元素中有对象为nil则会抛出异常,反而有助于排查bug,程序更安全。字典中的键和值都必须是Objective-C对象。使用字面量创建出来的字符串、数组、字典都是不可变的。
4.多用类型常量,少用#define预处理指令。且变量一定要同时用static和const声明,避免同名冲突的同时还可避免被无意修改。
5.凡是需要以按位或操作来组合的枚举都应使用NS_OPTIONS定义,若是枚举不需要互相结合,则应使用NS_ENUM来定义。如果用枚举来定义状态机,比如switch-case,则最好不要有default分支,避免加一个枚举值时漏掉状态机,若没有default则编译器会给出警告。
1.属性的getter和setter方法是由编译器在编译期合成的。@dynamic关键字告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。
2.atomic并不能保证线程安全,若要实现线程安全的操作,还需要采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读到不同的属性值。——用dispatch_queue可以解决
3.在初始化方法和dealloc方法中,总是应该直接通过实例变量来读写数据。
——具体原因见这里
4.关于NSObject的isEqual:方法和hash方法,NSObject类对这两个方法的默认实现是:当且仅当其指针值完全相等时,这两个对象才相等。如果“isEqual:”方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是,如果两个对象的hash方法返回同一个值,那么“isEqual:”方法未必会认为两者相等。
5.当对象接收到无法解读的消息后,就会启动消息转发机制。整个的流程为:动态方法解析-》备援接收者-》完整的消息转发。 resolveInstanceMethod-》forwardingTargetForSelector-》forwardInvocation-》消息未能处理。
6.“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例。
——两个方法的具体实现细节见这里
1.用前缀避免命名空间冲突,尤其是要打包成库给第三方使用的代码。——可参考SDWebImage这个库,所有方法均添加sd_前缀。
2.description和debugDescription方法在NSLog打印数据时会调用类的这两个方法,如果想打印时输出更多细节,可自己实现这两个方法。
3.应该尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。当然即使变量声明为只读,也可以通过KVC或计算指针偏移量后更改指针值的方式来改变变量。
4.方法名里不要使用缩略后的类型名称,给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
5.在ARC下使用try-catch捕获异常要注意内存泄露的问题,打开-fobjc-arc-exceptions编译器标志可以确保编译器添加异常安全的代码,以避免内存泄露。
6.无论当前实例是否可变,若需获取其可变版本的拷贝,均应该调用mutableCopy方法。同理,若需要不可变的拷贝,则总应通过copy方法来获取。
7.Foundation框架中的所有collection类在默认情况下都执行浅拷贝,也就是说,只拷贝容器对象本身,而不复制其中的数据。
1.将类的实现代码分散到便于管理的多个类别中去。——这一点在一个类功能较为复杂时常用,把某个功能点抽出来作为一个类别方便管理,也方便阅读。
2.为第三方类的类别名称加前缀。将类别方法加入类中这一操作是在运行期系统加载类别时完成的。如果类中本来就有该方法的实现,而类别又实现了一次,那么类别中的方法会覆盖原来类中的那一份代码实现。如果多个类别都实现了同一个方法,多次覆盖的结果以最后一个加载的类别为准。
3.一般说来,类别无法把实现属性所需的实例变量合成出来,但是使用关联对象可以解决这个问题,不过除非不得已,尽量避免这样设计。
4.使用类扩展是隐藏类的实现细节,包括一些方法和属性,实例变量,能够不暴露给外界的尽量隐藏在类扩展里。
5.Objective-C动态消息系统的工作方式决定了其不可能实现真正的私有方法或私有实例变量。因为在运行期总可以调用某些方法绕过此限制(比如KVC)。不过从一般意义上来说,它们还是私有的。
6.我们通常不直接访问实例变量,而是通过设置访问方法来做,因为这样能够触发KVO通知。
7.若观察者正在读取属性值而内部又在写入该属性时,则有可能引发竞争条件,合理使用同步机制能缓解此问题(dispatch_queue)。
1.ARC几乎把所有内存管理事宜都交给编译器来决定,内存管理的代码也是编译的时候由编译器在合适的地方插入retain和release等操作。
2.使用ARC时一定要记住,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加。ARC在调用release、retain这些方法时,并不通过普通的Objective-C消息派发机制,而是直接调用其底层C语言版本。
3.__autoreleasing符号表示把对象“按引用传递”给方法时,使用这个特殊的修饰符。此值在方法返回时自动释放。
4.使用ARC则不必要再编写dealloc方法了(仅指Objective-C对象,CoreFoundation对象还是需要自己管理内存的),ARC会借助C++对象的析构函数在.cxx_destruct方法中生成清理内存所需的代码。
5.ARC只负责管理Objective-C对象的内存,CoreFoundation等非Objective-C对象(比如CoreText,CoreGraphics)的内存需要自己管理。
6.在dealloc方法中移除通知以及KVO观察。还需注意,不要在dealloc里随意调用其他方法,包括getter和setter。
7.自动释放池可以嵌套使用,嵌套的好处在于可以控制程序的内存峰值。
8.僵尸对象(Zombie Object)是调试内存管理问题的最佳方式。将NSZombieEnabled环境变量设置为YES即可开启此功能。——通过选择edit scheme->run->Diagnostics,然后勾选Enable Zombie Objects即可。
9.单例对象的引用计数值不会改变,这种对象的retain和release操作都是空操作。尽量避免依赖对象的引用计数值来编码,因为其不可靠(autorelease)。
1.在block的内存布局中,最重要的就是invoke变量,这是个函数指针,指向block的实现代码。GCD是纯C语言的API。
2.block会把它所捕获的所有变量都拷贝一份。不过拷贝的并不是对象本身,而是指向这些对象的指针变量。
3.block一旦复制到堆上,就成了带引用计数的对象了。后续的复制操作都不会真的执行复制,而只是递增block对象的引用计数。
4.全局的block(NSConcreteGlobalBlock)的拷贝操作是个空操作,因为全局block不可能为系统所回收。这种block实际上相当于单例。
5.对于通知来说,若没有指定队列,则按默认方式执行,也就是说,将由发出通知的那个线程来执行。
6.尽量多的使用dispatch_queue,少用同步锁,包括@synchronized和NSLock。
7.dispatch_barrier_async称之为栅栏方法,栅栏block必须单独执行,不能与其他block并行执行。这支队并发队列有意义,因为串行队列的块总是按顺序逐个来执行的。并发队列如果发现接下来要处理的块是个栅栏块,那么就一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块。
8.dispatch_group机制可以把任务分组,等组内任务全部完成后再做其他操作。如果是网络请求,比如需要等待好几个网络请求回调之后才做刷新UI的操作,可以使用dispatch_group_notify函数,需要注意的是,dispatch_group_notify的block里的代码的执行时机是等group里的所有任务都完成之后才执行,但是网络请求又是异步的,GCD默认网络请求发出后该任务即执行完成,所以如果是需要等网络回调之后才算任务完成的话,可以使用dispatch_group_enter和dispatch_group_leave来对分组里要执行的任务数进行递增和递减操作(在网络发出之前递增,在网络回调后递减)。
9.不要使用dispatch_get_current_queue,因为容易出现死锁。
1.Cocoa本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。
2.iOS8以后应用程序可以使用动态库,也就是说,如果你的app仅支持iOS8及以上系统,可以添加自己的动态库。——注:书里说iOS程序不能包含动态库,这一规则已经在2014年WWDC过时。
3.关于桥接技术。__bridge本身的意思是:ARC仍然具备这个Objective-C对象的所有权。而__bridge_retained则与之相反,意味着ARC将交出对象的所有权。与此相似,反向转换可以通过__bridge_transfer来实现。
4.构建缓存时选用NSCache而不是NSDictionary。NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。另外NSCache是线程安全的,多个线程可以同时访问NSCache。此外,它与字典不同,并不会拷贝键。
5.对于类的+load方法,必定会调用而且只调用一次。如果类别和类里都定义了load方法,则先调用类里的,再调用类别里的。
6.因为无法判断出各个类的加载顺序,所以在load方法里使用其他类是不安全的,因为很可能此时其他类还未加载。
7.如果某个类本身没有实现load方法,那么不管其各级父类是否实现了此方法,系统都不会调用。此外,类别和其所属的类里,都可能出现load方法。此时两种实现代码都会调用,类的实现要比类别的实现先执行。
8.load方法务必实现的精简一些,因为整个应用程序在执行load方法时都会阻塞。
9.+initialize方法:对每个类来说,该方法会在程序首次使用该类之前调用,且只调用一次。
10.+initialize方法只应该用来设置内部数据。不应该在其中调用其他方法,即便是本类自己的方法,也最好别调用。
11.只有把NSTimer放在NSRunLoop里,它才能正常出发任务。
12.NSTimer会保留其目标对象,直到自身失效时再释放此对象。调用invalidate方法可令计时器失效。
1.简历格式
程序员请用PDF格式的简历,可以先用Word进行编辑排版,完了之后再转格式。 具体原因:
①部分看简历的人使用Mac,不一定安装了office系列工具,而Mac自带的预览工具对Word的支持效果很一般;
②Word本身也分很多版本,不同版本之间兼容性不好,打开之后有时会出现排版混乱;
③部分技术人员(也许是果粉)对windows没什么好感,连带着对office系列软件也持保守态度。
PDF的文件没有上述问题。 推荐一个把Word转换成PDF文件的网站,亲测效果不错。
关于简历的命名,请不要使用 个人简历.pdf这种了,请使用应聘岗位+姓名+电话的格式来命名。 这么写提供了更多的信息,有几个好处:
①岗位——不管是hr还是负责招聘的技术人员,招人的时候都会收到不止一份的简历,这样写方便对方把不同的岗位应聘者分别进行分类,加快处理速度;
②姓名和电话——如果查看简历后决定邀约面试,你的简历名字上有你的姓名和电话,会方便的多;
③其他信息(比如邮箱)不要写到简历的名字上,以上三个信息足够了,再多会让名字变得太长。
尽量保持以上的顺序,当然,姓名和电话的位置调换一下也无妨,但是我相信HR会比较关注一头一尾的信息。
3.重要的个人信息要突出,方便HR浏览
除了简历命名上不要出现”个人简历.pdf“这种情况,简历的排版也应该避免在最上方出现”个人简历“四个字,因为这四个字提供不了任何有效信息。
可以把姓名、电话、邮箱、应聘岗位这几个东西放在最上面以突出重点。一般说来,重要信息的字体可以加大加粗,甚至设置成红色以吸引查阅者。 我比较习惯把名字放在简历的最上面,加大字体且加粗,然后设置成居中,整体效果感觉还可以。
第一栏一般会写个人概况,个人概况中比较重要的是应聘岗位的工作年限,比如2年iOS开发、3年安卓开发等等,还有学历、性别,如果是计算机相关专业毕业还可以加上专业,一些半路转行过来编程的同学写大学专业就不大合适了,至少不是加分项。当然你要是为了凑字数那另说了。
身高体重就算了吧,照片尽量别贴,除非你有吴彦祖的脸。对多数程序员来说这不是加分项好伐。
4.简历页数和内容
简历尽量保持一页,如果是工作年限比较长、经历的项目比较多,或者一些技术大拿、leader,可以写两页,但绝对不要超过两页。 原因:
①绝大多数人没有耐心看完多页的简历;
②既然是简历,就是要简单化,写的太长,反而会让人觉得你不会抓重点;
③在简历翻页的时候,容易忘掉上一页的内容;
在简历内容上,没必要按照时间来排序。最重要的工作经历或项目经验写在最前面,把最能体现你能力的内容写在最前面。
一般来说,面试初级工程师时会以功能为主,考察的是面试者是否能够承担一部分简单的功能,释放其他工程师的时间。 中级工程师以上会更多的考察解决问题的能力,不止问项目过程中做了什么,而且会重点问项目过程中遇到了哪些问题,怎么解决的,做了哪些优化等等。 而且从说服力的角度来说,有数据支持的最好。比如app启动速度从XX秒优化到XX秒,崩溃率从XX百分比降低到XX百分比。
保持真实,不要浮夸,可以包装,但不要虚假。简历上写的东西一定要是你的个人真实情况,这样子面试官问到了你也能答出来。
如果换工作过多就不要以时间为轴来写工作经历,甚至避免写工作经历,可以用项目经历来代替。当然如果是面试的时候问起来,如实回答就好,这样子只是尽量避免hr筛选掉你的简历。
写自我评价的时候,有数据支撑的效果比较好。比如写学习能力较强,如果只这样写,会比较虚,可以在后面补上:经过XX天,学习并实现XX功能/技术点。
遵守代码规范比精通XX语言重要,清晰的表达能力比熟练使用某个IDE工具重要。
技术博客和github是加分项,但是如果贴了那最好博客和github上要有内容,不能是空白页。写博客其实就很不错,面试的时候可以装逼,拿出手机打开博客网址给对方看O(∩_∩)O哈哈~
工作经历太少没东西写,一页简历写不满怎么破?那就试试写项目技术实现的细节吧,千万不要堆砌技术名词哦。
如果工作超过1年了,大学期间的那些获奖、开发经历啥的就别写了,会让人感觉稚气未脱。
其他写简历的细节请看如何写面向互联网公司的求职简历。
部分面试官会问你对上一家公司或上一任老大、同事等的印象,请不要吐槽或给负面评价,尽量说优点。不要问为什么。
如果去大公司面试,比较合适的离职原因是公司的发展赶不上自身的发展速度,上升空间有限。如果去创业公司,则可以说对公司产品有兴趣,或喜欢小团队的氛围。
一般有个1年以上的工作经验后,多半会被问到职业规划。通常说来,在选择的技术方向上继续深入2-3年是不错的选择,也是不错的回答。当然如果是5年以上工作经验了,大概面临纯技术方向或技术管理方向的选择吧,我也不清楚哒~
从企业的角度看,招人无非从这几个点来衡量:
①职业能力;——企业现在可以获得的价值,一般是对等交易,程序员则表现为技术实力
②学习能力;——企业未来可以获得的价值,也就是企业投资的增值
③职业态度;——工作中是否积极主动,是否维护企业的利益等
④沟通能力和性格。——团队协作
⑤是否可以长期留在公司。——降低企业招人的损耗
职场中一个人的核心竞争力也是如此,所以不管是写简历还是面试,都是围绕以上几个点展开的。
去model化这个说法其实有点儿难听,model化就是使用数据对象,去model化就是不使用数据对象。所以这篇文章主要讨论的问题就是:数据传递时,是否要采用数据对象?这里的数据传递并不是说类似RPC的场景,而是在单个工程内部,各对象之间、各组件之间、各层之间的数据传递。
所谓数据对象,就是把不同类型的数据映射到不同类型的对象上,这个对象仅用于表达数据,数据通过对象的property来体现。瘦Model、贫血模型就属于这一类。
去Model化,就是不使用特定对象迎合特定数据的映射的方式,来表达数据。比如我们可以使用NSDictionary,或者其他手段例如reformer、virtual record,来避免这种数据映射对象。
关于这个问题的讨论涉及以下内容:
如何理解面向对象思想
为什么不使用数据对象
去Model化都有哪些手段
通过以上三点,我希望能够帮助大家建立对面向对象的正确理解,让大家明白如何权衡是否要采用对象化的设计。以及最终当你决定不采用对象化思想而采用非对象化思想时,应该如何进行架构设计。
如何理解面向对象思想
面向对象的思想简单总结一下就是:将一个或多个复杂功能封装成为一个聚合体,这个聚合体经过抽象后,仅暴露少部分方法,这些方法向外部获取实现功能所需要的条件后,就能完成对应功能。传统的面向过程只针对功能的实现做了封装,也就是函数。经过这层封装后,仅暴露参数列表和函数名,用于外部调用者调用并完成功能。
我们可以推导出:函数封装了实现功能所需要的代码,因此对象实质上就是再次对函数进行了封装。将函数集合在一起,形成一个函数集合,面向对象思想的提出者把这个函数集合称之为对象,把对象的概念从理论映射到实际的工程领域,我们也可以叫它类。
然而我们很快就能发觉,只是单纯地把函数集合在一起是不够的,这些函数集有可能互相之间需要共用参数或共享状态。因此面向对象的理论设计者让对象自己也能够提供属性(property),来满足函数集间共用参数和共享状态的需求。这个函数集现在有了更贴切的说法:领域。因此当这个领域中的个别函数不需要共用参数或共享状态,仅仅是提供功能时,这些相关函数就可以体现为类方法。当领域里的函数需要共用参数或共享状态时,这些函数的体现就是实例方法。
这里补充一下,领域的概念我们更多会把它理解得比较大,比如多个相关对象形成一个领域。但一个对象自身所包含的所有函数也是一个领域,是大领域里的一个子领域。
以上就是一个对面向对象思想的朴素理解。在这个理解的基础上,还衍生出了非常多的概念,不过这并不在本文的讨论范围中。
总之,一个对象事实上是对一个较小领域的一种封装。对应到本文要讨论的问题来看,如果拿一个对象去表达一套数据而非一个领域,这在一定程度上是违背面向对象的设计初衷的。你看着好像是把数据对象化了,就是面向对象编程了,然而事实上并非如此。Martin Fowler早年也在他的《Anemic Domain Model》中提出了一样的看法:
The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design, exactly the kind of thing that object bigots like me (and Eric) have been fighting since our early days in Smalltalk. What's worse, many people think that anemic objects are real objects, and thus completely miss the point of what object-oriented design is all about.
为什么不使用数据对象
根据上一小节的内容,我们可以把对象抽象地理解为一个函数集,一个领域。在这一节里,我们再进一步推导:如果这些函数集里的所有函数,并不都是处在同一个问题领域内,那么面向对象的这种实践是否依旧成立?
答案是成立的,但显然我们并不建议这么做。不同领域的函数集如果被封装在了一起,在实际工程中,这种做法至少会带来以下问题:
当需要维护某个问题领域内的函数时,如果修改到某个需要被共用的参数或者需要被共享的对象,那么其他问题领域也存在被影响的可能。牵一发而动全身,这是我们不希望看到的。
当需要解决某个问题时,如果引入了某个充满了各种不同问题领域的函数集,这实质就是引入了对不同问题领域解决方案的依赖。当需要做代码迁移或复用时,就也要把不相关的解决方案一并引入。拔出萝卜带出泥,这也是我们不希望看到的。
当需要维护某个问题领域内的函数时,如果修改到某个需要被共用的参数或者需要被共享的对象,那么其他问题领域也存在被影响的可能。牵一发而动全身,这是我们不希望看到的。
我们在进行对象化设计时,必须要分割好问题域,才能保证设计出良好的架构。
业界所谓的各种XX建模、XX驱动设计、XXP,大部分其实都是在强调合理分割这一点,他们提供不同的方法论去告诉你应该如何去做分割的事情,以及如何把分割出来的部分再进一步做封装。然而这些XX概念要成立的话,就都一定需要具备这样一个前提条件:一个被封装出来的对象的活动领域,必须要小于等于当前被分割出来的子问题领域。如果不符合这个前提条件的话,一个大的问题领域即使被强行分割成各种小的问题领域,这些小的问题领域还是依旧难以被封装成为对象,因为对象的跨领域活动势必就要引入其它领域的问题解决方案,这就使得分割名不副实。
然而,一个被封装出来的对象的活动领域,必须要小于等于当前被分割出来的子问题领域这个前提在实际业务场景实践中,是否一定成立呢?如果一定成立的话,那么这种做法和这些方法论就是没问题的。如果在某些场景中不成立,对象化设计在这些场景就有问题了。
事实上,这个前提在实际业务场景中,是不一定成立的。在实际业务场景中,一个数据对象被多个业务领域使用是非常常见的。一个数据对象在不同层、不同模块中被使用也是非常常见的。所以,如果两个业务对象之间需要传递的仅是数据,在这个场景下就不适合传递对象化的数据。
当需要解决某个问题时,如果引入了某个充满了各种不同问题领域的函数集,这实质就是引入了对不同问题领域解决方案的依赖。当需要做代码迁移或复用时,就也要把不相关的解决方案一并引入。拔出萝卜带出泥,这也是我们不希望看到的。
这种场景其实就很好理解了。实际工程中,对象化数据往往不是一个独立存在的对象,而是依附于某一个领域。例如持久层提供的对象化数据,往往依附于持久层。网络层提供的对象化数据往往依附于网络层。当你的业务层某个模块使用来自这些层的对象化数据时,将来要迁移这个模块,就必须不得不把持久层或者网络层也跟着迁移过去。迁移发生的场景之一就是大型工程的组件化拆分,实施组件化时遇到这种问题是一件非常伤脑筋的事情。
所以,在数据传递时,我不建议采用对象化设计,尤其是数据传递的两个实体是跨层实体或者跨模块实体时,对象化设计对架构的伤害非常大。
从实际而非理论的角度上讲,数据对象的使用主要存在这些问题:
数据对象并不符合面向对象的设计初衷
数据对象有变为支持多领域对象的可能
数据对象使得领域间依赖性变强
去Model化都有哪些手段
这种做法是最原始最简单的做法,我就不多说了。
reformer
reformer是这样的工作原理:
------------------ ------------------
| | | |
.| Reformer_A | .... | View_A |
. | | | |
. ------------------ ------------------
------------------ . ------------------ ------------------
| |. | | | |
| APIManager |......| Reformer_B | .... | View_B |
| |. | | | |
------------------ . ------------------ ------------------
. ------------------ ------------------
. | | | |
.| Reformer_C | .... | View_C |
| | | |
------------------ ------------------
APIManager提供了来自网络层的数据。Reformer_A,Reformer_B,Reformer_C,分别代表不同的领域。View_A,View_B,View_C,分别就是各领域对不同的数据应用之后产生的结果。在讲网络层的文章中,我设计了reformer的方式来实现非对象化。更详细的讲述和实际的Demo文章里都有,我在这里就不多说了。
Virtual Record
Virtual Record事实上把reformer和某个领域相关对象集合在了一起。Virtual Record和reformer的区别在于:reformer更加有利于单数据对应多对象的场景,Virtual Record更加有利于多数据对单对象的场景
------------------ ------------------
| | | |
| DataCenter_A | .....| VirtualRecordA |.
| | | | .
------------------ ------------------ .
------------------ ------------------ . ------------------
| | | | . | |
| DataCenter_B |......| VirtualRecordB |.......| View_B |
| | | | . | |
------------------ ------------------ . ------------------
------------------ ------------------ .
| | | | .
| DataCenter_C | .....| VirtualRecordC |.
| | | |
------------------ ------------------
事实上这幅图有个地方画的不太贴切,Virtual Record其实只是View_B的一个protocol,它并不是一个实例,所以才Virtual。关于Virtual Record的详细解释和案例,在讲持久层的文章里有。
将数据对象化事实上是一个不符合面向对象思想的做法。
这种说法看起来很反直觉,但事实上如果你对面向对象有深入的理解,就能够明白其中的原因。这种不符合面向对象思想的做法,也导致了工程实践上代码的高耦合和组件难以复用的情况,这都是我们不希望看到的。我在这篇文章里提供了几种去Model化的做法,但看起来这应该不是所有的手段,很有可能还有其它方法。未来如果我遇到了其他场景想到了其它方法的话,会对它进行补充。如果各位读者还有不同的方法或其它的问题,也欢迎在评论区一起交流。
其实应该叫惰性求值(Lazy Evaluation)比较标准。
就在大约一两个小时之前,有一位我博客的读者在评论区里留言,提到最近臧成威写了一篇《聊一聊iOS开发中的惰性计算》,里面提到了一个观点是除了创建非常大的属性、或者创建对象的时候有一些必要的副作用不能提前创建之外,几乎不应该使用惰性求值来处理类似逻辑。。并且主要给出了六点理由。他给出的结论和我提倡的做法相悖,问我是什么看法。
在这里我先离题一下:我完整地看完了这篇文章,很喜欢这种立场鲜明,而且有清晰理由的文章。我先不说理由是否合理,立场是否正确,至少我看到国内技术圈子里的大多数文章其实没有任何观点和立场,都只是教你XXX怎么用,而且写得又不比官方文档好,含金量很低。即便在少数有观点的文章中,大部分又只有观点,没有任何理由。臧成威这篇文章是逻辑清晰,有观点而且有理由的,这篇文章在这一点上其实是做的很好的。
那么,接下来我就要在这篇文章中辨析一下他的文章里提供的六个理由了。
如果真的是很大的属性,一般它比较重要,几乎一定会被访问,所以加上这个不如直接在 init 的时候创建。
这个理由其实是有逻辑问题的。
虽然这句话并没有很绝对地说很大的属性就一定比较重要,给你造成一种看起来说得很客观,很有道理的假象。更何况,事实上一个属性的重要程度其实是和属性本身的大小也是无关的。
但另外一点是,惰性求值并不影响属性的可访问性,即使前面属性很大、属性很重要、属性一定会被访问都满足,我实在看不出这三个条件能够给出在init的时候创建的倾向。惰性求值和及早求值的差别完全不在那三点前提,这里的理由和给出的结论其实是完全无关的。
这句话其实就是类似这样的句子:因为西瓜很大,所以西瓜一般比较重要,而且夏天基本上一定都会吃西瓜,所以西瓜还不如直接用小卡车运进城,就不要用拖拉机了。 我再离题一下:这种话术其实很有欺骗性,我们国家也经常采用这种话术来欺骗百姓,不过这里我们不谈国事,你懂的。
总结来说就是,臧成威的这条理由根本无法去支撑他的论点,这本质上并不是技术问题,是思维逻辑问题,我不去揣测臧成威的动机是故意还是无意的,我只指出这逻辑是错的。
@property 的 atomic、nonatomic、copy、strong 等描述在有 getter 方法的属性上会失效,后人修改代码的时候可能只改了 @property 声明,并不会记得改 getter,于是隐患就这样埋下了。
我当时看到这个理由的时候我非常吃惊,除了atomic和nonatomic以外,其它的其实都是修饰setter的啊,为什么用了getter就失效了?这不合常识啊。于是我做了求证:
首先,Strong/Weak 在getter中编译器是会warning的,从编译器的warning上看,谈不上失效。看下面的例子:
@interface ViewController ()
@property (nonatomic, weak) NSArray *testArray;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@", self.testArray);
#pragma mark - getters and setters
- (NSArray *)testArray
if (_testArray == nil) {
_testArray = [[NSArray alloc] init]; // 此处会报warning: Assigning retained object to weak variable; object will be released after assignment.
return _testArray;
然后,copy在有getter方法的属性上也不会失效,因为copy完全修饰的是setter方法,与getter无关。看下面的例子:
@interface ViewController ()
@property (nonatomic, copy) NSArray *testArray;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray *aArray = [[NSMutableArray alloc] initWithObjects:@123, nil];
self.testArray = aArray;
[aArray addObject:@456];
NSLog(@"%@", aArray); // 输出123,456
NSLog(@"%@", self.testArray); // 输出123,而不是123,456。证明copy并没有失效,如果copy失效,那应该也输出123,456。
#pragma mark - getters and setters
- (NSArray *)testArray
if (_testArray == nil) {
_testArray = [[NSArray alloc] init];
return _testArray;
最后是nonatomic和atomic,这个我现在并不知道有什么比较好的手段去求证这个问题,我现在比较忙并没有时间去查资料。但针对这个情况我要说两点:
实际开发工作中基本上都是用的nonatomic去修饰一个property,如果真的要进行原子操作,往往是自己用锁来建立临界区,很少情况是用atomic。原因见第二条:
因为atomic并不能保证线程安全,线程安全应当由工程师自己通过锁来建立临界区。我记得这个描述苹果官方文档有说过,并且举了一个Person对象的例子。实际出处的链接我一时半会儿找不到了,大家如果有空的话可以帮我找一下。
针对第二条有网友补充:在多个atomic property的情况下,atomic并不能保证他们取值赋值的时序,因此不能保证线程安全。但对于单个property而言,atomic是安全的。在实际工作中,往往临界区涉及的属性和数据并不惟一,因此实际开发场景都是推荐工程师自建临界区,另一个角度上,这也方便将来增加或删除临界区相关的变量。
所以如果要自建临界区的话,其实用getter只会比不用getter更好,因为临界区里面涉及的逻辑和变量有可能很复杂,而我们并不希望这部分复杂的代码泄漏到与之无关的主要逻辑中去,这样会使得主要逻辑不清晰,难以维护。
代码含有了隐私操作,尤其 getter 中再混杂了各种逻辑,使得程序出现问题非常不好排查。后人哪会想到someObj.someProperty这样一个简简单单的取属性发生了很多奇妙的事。
是否要在getter中写逻辑,这其实是一个主观问题。
如果你决定要在getter中写逻辑,那么就应当只写跟初始化过程有关的逻辑,跟初始化过程无关的逻辑就不要在getter里面写。因为getter本质上其实是工厂方法,工厂方法是不应当跟业务掺杂过多的。
实际开发过程中,确实有人把不必要的逻辑写进getter中,这些都是我在code review的过程会打回让他重写的。一般新人进我的team都会有一个月的code review过程来进行教育,所以乱写的情况很少。
最后,这本质上是一个主观问题并不是一个客观技术问题,更不属于getter的技术缺陷,真要说技术缺陷的话,上面一条更加类似。而且,对于一个傻逼来说,不管使不使用getter,他都一样会给你写出难以排查问题的代码。所以真正要做的事情是把傻逼教好,而不是不使用getter。
这叫因噎废食,傻逼不会吃饭噎到自己了,不去考虑怎么学习吃饭的正确方法,反而决定不吃饭了。
很多人的 getter 写得并不是完全标准,例如上述代码会导致多线程访问的时候,出现很多神奇的问题。一旦形成习惯,后续的很多稀奇古怪的 crash 就接踵而至了。
这个结论其实并没有详细的理由去支撑。神奇的问题具体是什么?跟getter有关吗?在我做过的项目中,由于使用了getter,排查问题时就能够非常有目的性,只要先搞清楚是变量初始化的问题,还是逻辑操作的流程问题,就基本上能够很快定位到问题点了。
这种模糊表达的话术,其实就是典型的当年秦桧的莫须有。嗯,有crash出来了,而且莫名其妙,所以,可能就是getter导致的吧?这个还真难反驳,但如果臧成威你遇到了这个神奇的问题,且与getter有关,那就列举出来,然后我们再来就这个理由继续讨论。在此之前,这个锅getter表示不背。
至于getter写得不标准,其实我在上一条里面已经说清楚了:即使你不写getter,傻逼们一样会给你搞出各种莫名其妙的crash,相信你即使不去blame getter,也会去blame 其它。
代码多,本来代码只需要在init方法中创建用上一两行,结果用了至少 7 行的一个 getter 方法才能写出来,想想一个程序轻则数百个属性,都这么搞,得多出多少行代码?另外代码格式几乎完全一样,不符合 DRY 原则。好的程序员不应该总是写重复的代码,不是么?
其实这个问题其实是这样的,使用getter和不使用getter,在代码行数上的差别仅多出5行,剩下的其实都一样。
然后一个程序轻则数百个属性,这个我是不认可的,一个程序里面,20-30个属性已经算是非常大了,我真从来没见过有哪个对象有数百个属性的,如果真的存在,那说明这个工程的模块划分、对象划分存在问题,这是一个比使用和不使用getter都更加严重的问题。即使你不使用getter,如果遇到了数百属性的对象,首先要做的事情也必须是重新考虑模块划分和对象划分。而且再退一步说,一个对象中也不是每个属性都要有getter的。
臧成威的这种说法其实是潜移默化地在扩大范围,如果所有XXX都这么搞,那得XXX?这个话术其实就是在夸大问题范围,以前别人写过一篇文章讨论过这种话术,不过我现在也找不到出处了。
而且,就这个问题来看,使用getter的好处十分明显,一个程序的初始化区域和逻辑执行区域被分隔开了,这样就能使得即使很多行代码的文件,其代码分配结构就会变得非常清晰。所以即使在非常大的对象里,使用getter来划分代码在文件中的组织结构,是非常有利于大对象维护的。
最后,关于DRY,臧成威你是不是不知道XCode有自定义的code snippt功能?我觉得至少你应该在写GCD相关代码的时候也用过吧?谈何重复?
性能损耗,对于属性取值可能会非常的频繁,如果所有的属性取值之前都经过一个if判断,这不是平白浪费的性能?
这里的性能损耗其实是一个权衡问题,也是使用惰性求值和及早求值的主要差别之一。在不使用惰性求值的时候,程序的内存foot print会因为一个对象的初始化而形成一个陡峭的曲线。使用惰性求值的好处在于能够避免不必要的内存占用。在整个程序的生命周期上,能够提高内存的使用效率,在生命周期中的某个时间维度上,可以保证后续逻辑的高效完成。
举个例子,一条逻辑分别有A,B,C三项任务构成,分别需要使用a,b,c三个属性。假设内存一次只能装得下三个属性中的任意两个,如果不使用惰性计算,这个程序的内存使用效率就非常低,不得不走swap。但如果使用了惰性计算,就完全不必去走swap来解决内存不够的问题。
相比于内存的使用效率,以及由于过大内存导致的swap所消耗的时间,这两者的性能损耗跟单纯一个if判断相比,实在是微不足道。
脱离剂量谈毒性是不对的,再退一步说,单纯多的一步if判断消耗的时间是纳秒级别,而且差不多只是两位数的纳秒,微乎其微。但是因此带来内存使用效率的提高,却是非常显著的,因此从性能角度来说,这个权衡应该更加偏向使用惰性求值才对。
所以结论已经很明显了,六个理由对于getter来说其实根本站不住脚,而且使用getter的好处一方面带来了文件中更清晰的代码分布,另一方面提高了内存的使用效率。这也是为什么我推荐使用getter的原因。更加具体的原因我在这篇文章里也已经说清楚了,没看过的同学可以过去看一下。
我认为"封装"的概念在面向对象思想中是最基础的概念,它实质上是通过将相关的一堆函数和一堆对象放在一起,对外有函数作为操作通道,对内则以变量作为操作原料。只留给外部程序员操作方式,而不暴露具体执行细节。大部分书举的典型例子就是汽车和灯泡的例子:你不需要知道不同车子的发动机原理,只要踩油门就可以跑;你不需要知道你的灯泡是那种灯泡,打开开关就会亮。我们都会很直觉地认为这种做法非常棒,是吧?
但是有的时候还是会觉得有哪些地方不对劲,使用面向对象语言的时候,我隐约觉得封装也许并没有我们直觉中认为的那么好,也就是说,面向对象其实并没有我们直觉中的那么好,虽然它已经流行了很多很多年。
1. 将数据结构和函数放在一起是否真的合理?
函数就是做事情的,它们有输入,有执行逻辑,有输出。 数据结构就是用来表达数据的,要么作为输入,要么作为输出。
两者本质上是属于完全不同的东西,面向对象思想将他们放到一起,使得函数的作用被限制在某一个区域里,这样做虽然能够很好地将操作归类,但是这种归类方法是根据"作用领域"来归类的,在现实世界中可以,但在程序的世界中,有些不妥。
不妥的理由有如下几个:
在并行计算时,由于执行部分和数据部分被绑定在一起,这就使得这种方案制约了并行程度。在为了更好地实现并行的时候,业界的工程师们发现了一个新的思路:函数式编程。将函数作为数据来使用,这样就能保证执行的功能在时序上的正确性了。但你不觉得,只要把数据表达和执行部分分开,形成流水线,这不就能够非常方便地将并行数提高了么?
我来举个例子: 在数据和函数没有分开时,程序的执行流程是这样:
A.function2() -> A.function3() 最后得到经过处理的A
当处于并发环境时,假设有这么多任务同时到达
A.f1() -> A.f2() -> A.f3() 最后得到经过处理的A
B.f1() -> B.f2() -> B.f3() 最后得到经过处理的B
C.f1() -> C.f2() -> C.f3() 最后得到经过处理的C
D.f1() -> D.f2() -> D.f3() 最后得到经过处理的D
E.f1() -> E.f2() -> E.f3() 最后得到经过处理的E
F.f1() -> F.f2() -> F.f3() 最后得到经过处理的F
假设并发数是3,那么完成上面类似的很多个任务,时序就是这样
| time | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
|------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|
| A | A.1 | A.2 | A.3 | | | | | | | | | |
| B | B.1 | B.2 | B.3 | | | | | | | | | |
| C | C.1 | C.2 | C.3 | | | | | | | | | |
| D | | | | D.1 | D.2 | D.3 | | | | | | |
| E | | | | E.1 | E.2 | E.3 | | | | | | |
| F | | | | F.1 | F.2 | F.3 | | | | | | |
| G | | | | | | | G.1 | G.2 | G.3 | | | |
| H | | | | | | | H.1 | H.2 | H.3 | | | |
| I | | | | | | | I.2 | I.2 | I.3 | | | |
| J | | | | | | | | | | J.1 | J.2 | J.3 |
| K | | | | | | | | | | K.1 | K.2 | K.3 |
| L | | | | | | | | | | L.1 | L.2 | L.3 |
当数据和函数分开时,并发数同样是3,就能形成流水线了,有没有发现吞吐量一下子上来了?
| time | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12|
|------|---|---|---|---|---|---|---|---|---|---|---|---|
| f1() | A | B | C | D | E | F | G | H | I | J | K | L |
| f2() | Z | A | B | C | D | E | F | G | H | I | J | K |
| f3() | Y | Z | A | B | C | D | E | F | G | H | I | J |
你要是粗看一下,诶?怎么到了第13个周期K才刚刚结束?上面一种方案在第12个周期的时候就结束了?不能这么看的哦,其实在12个周期里面,Y、Z也已经交付了。因为流水线吞吐量的提升是有过程的,我截取的片段应该是机器在持续运算过程中的一个片段。
我们不能单纯地去看ABCD,要看交付的任务数量。在12个周期里面,大家都能够完成12个任务,在11个周期里面,流水线完成了11个任务,前面一种只完成了9个任务,流水线的优势在这里就体现出来了:每个时间段都能稳定地交付任务,吞吐量很大。而且并发数越多,跟第一种方案比起来的优势就越大,具体的大家也可以通过画图来验证。
数据部分就是数据部分,执行部分就是执行部分,不同类的东西放在一起是不合适的
函数就是一个执行黑盒,只要满足函数调用的充要条件(给够参数),就是能够确定输出结果的。面向对象思想将函数和数据绑在一起,这样的封装扩大了代码重用时的粒度。如果将函数和数据拆开,代码重用的基本元素就由对象变为了函数,这样才能更灵活更方便地进行代码重用。
嗯,谁都经历过重用对象时,要把这个对象所依赖的所有东西都要移过来,哪怕你想用的只是这个对象里的一个方法,然而很有可能你的这些依赖是跟你所需要的方法无关的。
但如果是函数的话,由于函数自身已经是天然完美封装的了,所以如果你要用到这个函数,那么这个函数所有的依赖你都需要,这才是合理的。
2. 是否所有的东西都需要对象化?
面向对象语言一直以自己做到"一切皆对象"为荣,但事实是:是否所有的东西都需要对象化?
在iOS开发中,有一个类叫做NSNumber,它封装了所有数值:double,float,unsigned int, int...等等类型,在使用的时候它弱化了数值的类型,使得非常方便。但问题也来了,计算的时候是不能直接对这个对象做运算的,你得把它们拆成数值,然后进行运算,然后再把结果变成NSNumber对象,然后返回。这是第一点不合理。第二点不合理的地方在于,运算的时候你不知道原始数据的类型是什么,拆箱装箱过程中难免会导致内存的浪费(比如原来uint8_t的数据变成unsigned int),这也十分没有必要。
还有就是我们的file descriptor,它本身是一个资源的标识号,如果将资源抽象成对象,那么不可避免的就会使得这个对象变得非常庞大,资源有非常多的用法,你需要将这些函数都放到对象里去。在真正传递资源的时候,其实我们也只是关心资源标识而已,其它的真的无需关心。
我们已经有函数作为黑盒了,拿着数据塞到黑盒里就够了。
3. 类型爆炸
由于数据和函数绑定到了一起,在逻辑上有派生关系的两种对象往往可以当作一种,以派生链最上端的那个对象为准。单纯地看这个现象直觉上会觉得非常棒,父亲有的儿子都有。但在实际工程中,派生是非常不好控制的,它导致同一类类型在工程中泛滥:ViewController、AViewController、BViewController、ThisViewController、ThatViewController...
你有没有发现,一旦把执行和数据拆解开,就不需要这么多ViewController了,派生只是给对象添加属性和方法。但事实上是这样:
struct A { Class A extends B
struct B b; {
int number; int number;
} {
前者和后者的相同点是:在内存中,它们的数值部分的布局是一模一样的。不同点是:前者更强烈地表达了组合,后者更强烈地表达的是继承。然而我们都知道一个常识:组合要比继承更加合适,这在我这一系列的第一篇文章中有提到。
上两者的表达在内存中没有任何不同,但在实际开发阶段中,后者会更容易把项目引入一个坏方向。
为什么面向对象会如此流行?我想了一下业界关于这个谈论的最多的是以下几点:
它能够非常好地进行代码复用
它能够非常方便地应对复杂代码
在进行程序设计时,面向对象更加符合程序员的直觉
第一点在理论上确实成立,但实际上大家都懂,在面向对象的大背景下,写一段便于复用的代码比面向过程背景下难多了。关于第二点,你不觉得正是面向对象,才把工程变复杂的么?如果层次清晰,调用规范,无论面向对象还是面向过程,处理复杂业务都是一样好,等真的到了非常复杂的时候,对象间错综复杂的关系只会让你处理起来更加头疼,不如面向过程来得简洁。关于第三点,这其实是一个障眼法,因为无论面向什么的设计,最终落实下来,还是要面向过程的,面向对象只是在处理调用关系时符合直觉,在架构设计时,理清需求是第一步,理清调用关系是第二步,理清实现过程是第三步。面向对象让你在第二步时就产生了设计完成的错觉,只有再往下落地到实现过程的时候,你才会发现第二步中都有哪些错误。
所以综上所述,我的观点是:面向对象是在架构设计时非常好的思想,但如果只是简单映射到程序实现上来,引入的缺点会让我们得不偿失。
距离上一次博文更新已经快要一个月了,不是我偷懒,实在是太忙,现在终于有时间可以把"跳出面向对象"系列完成了。针对面向对象的3个支柱概念我写了三篇文章来挑它的刺,看上去有一种全盘否定的感觉,而我倒不至于希望大家回去下一个项目就开始面向过程的开发,我希望大家能够针对这一系列文章提出的面向对象的弊端,严格规范代码的行为,知道哪些可行哪些不可行。过去的工作中我深受其苦,往往没有时间去详细解释为什么这么直觉的东西实际上不可行,要想解释这些东西就得需要各种长篇大论。最痛苦的是,即便长篇大论说完了,最后对方还无法理解,照样写出垃圾代码出来害人。
现在好了,长篇大论落在纸上了,说的时候听不懂,回去总可以翻文章慢慢理解了吧。
多态一般都要跟继承结合起来说,其本质是子类通过覆盖或重载(在下文里我会多次用到覆盖或重载,我打算把它简化成覆重,意思到就好,不要太纠结这种名词。)父类的方法,来使得对同一类对象同一方法的调用产生不同的结果。这里需要辨析的地方在:同一类对象指的是继承层级再上一层的对象,更加泛化。
举个例子:
Animal -> Cat
Animal -> Dog
Animal.speak() // I'm an Animal
Cat.speak() // I'm a Cat
Dog.speak() // I'm a Dog
此处Cat和Dog虽然不是同一种对象,但它们算是同一类对象,因为他们的父类都是Animal。种和类的表达可能不是很对,其实我也不知道谁更大一点,在文章中我打算用这样的符号来表示两者区别:^和^^
^ 表示他们是同一类
^^ 表示他们同种同类
Animal -> Cat
Animal -> Dog
Cat kitty, kate
Dog lucky, lucy
我们可以这么说:
kitty ^^ kate 同种同类,他们都是猫
kitty ^ lucy 同类不同种,他们都是Animal
kitty !^^ lucy 因为kitty是猫,lucy是狗
kitty ^ kate 他们当然同种啦,都是Animal
应该算是能够描述清楚了吧?嗯,我们开始了。
一般来说我们采用多态的场景还是很多的,有些在设计的时候就是用于继承的父类,希望子类覆盖自己的某些方法,然后才能够使程序正常运行下去。比如:
BaseController需要它的子类去覆盖loadView等方法来执行view的显示逻辑
BaseApiManager需要它的子类去覆盖methodName等方法来执行具体的API请求
以上是我列举的应用多态的几个场景,在基于上面提到的需求,以及站在代码观感的立场,我们在实际采用多态的时候会有下面四种情况:
父类有部分public的方法是不需要,也不允许子类覆重
父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法
父类有一些方法是可选覆重的,一旦覆重,则以子类为准
父类有一些方法即便被覆重,父类原方法还是要执行的
这四种情况在大多数支持多态的语言里面都没有做很好的原生限制,在程序规模逐渐变大的时候,会给维护代码的程序员带来各种各样的坑。
父类有部分public的方法是不需要,也不允许子类覆重
对于客户程序员来说,他们是有动机去覆重那些不需要覆重的方法的,比如需要在某个方法调用的时候做UserTrack,或者希望在方法调用之前做一些额外的事情,但是又找不到外面应该在哪儿做,于是就索性覆重一个了。这样做的缺点在于使得一个对象引入了原本不属于它的业务逻辑。如果在引入的这些额外逻辑中又对其他模块产生依赖,那么这个对象在将来的代码复用中就会面临一个艰难的选择:
是把这些不必要的逻辑删干净然后移过去?
还是所以把依赖连带着这个对象一起copy过去?
前者太累,后者太蠢。
如果是要针对原来的对象进行功能拓展,但拓展的时候发现是需要针对原本不允许覆重的函数进行操作,那么这时候就有理由怀疑父类当初是不是没有设计好了。
父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法
这非常常见,由于逻辑的主要代码在父类中,若要跑完整个逻辑,则需要调用一些特定的方法来基于不同的子类获得不同的数据,这个特定的方法最终交由子类通过覆重来实现。如果不在父类里面写好这个方法吧,父类中的代码在执行逻辑的时候就调用不到。如果写了吧,一个空函数放在那儿十分难看。
也有的时候客户程序员会不知道在派生之后需要覆重某个方法才能完成完整逻辑,因为空函数在那儿不会导致warning或error,只有在发现程序运行结果不对的时候,才会感觉哪儿有错。如果这时候程序员发现原来是有个方法没覆重,一定会拍桌子骂娘。
总结一下,其实就是代码不好看,以及有可能忘记覆重。
父类有一些方法是可选覆重的,一旦覆重,则以子类为准
这是大多数面向对象语言默认的行为。设计可选覆重的动机其中有一个就是可能要做拦截器,在每个父类方法调用时,先调一个willDoSomething(),然后调用完了再调一个didFinishedSomething(),由子类根据具体情况进行覆重。
一般来说这类情况如果正常应用的话,不会有什么问题,就算有问题,也是前面提到的容易使得一个对象引入原本不属于它的业务逻辑。
父类有一些方法即便被覆重,父类原方法还是要执行的
这个是经典的坑,尤其是交付给客户程序员的时候是以链接库的模式交付的。父类的方法是放在覆重函数的第一句调用呢还是放在最后一句调用?这是个值得深思的问题。更有甚者索性就直接忘记调用了,各种傻傻分不清楚。
面向接口编程(Interface Oriented Programming, IOP)是解决这类问题比较好的一种思路。下面我给大家看看应该如何使用IOP来解决上面四种情况的困境:
(示例里面有些表达的约定,可以在这里看完整的上下文规范。)
<ManagerInterface> : APIName() 我们先定义一个ManagerInterface接口,这个接口里面含有原本需要被覆重的方法。
<Interceptor> : willRun(), didRun() 我们再定义一个Interceptor的接口,它用来做拦截器。
BaseManager.child<ManagerInterface> 在BaseController里面添加一个property,叫做child,这就要求这个child必须要满足<ManagerInterface></ManagerInterface>这个接口,但是BaseManager不需要满足<ManagerInterface>这个接口。
BaseManager.init() {
self.child = self 在init的时候把child设置成自己
# 如果语言支持反射,那么我们可以这么写:
if self.child implemented <ManagerInterface> {
self.child = self
# 如上的写法就能够保证我们的子类能够基于这些接口有对应的实现
self.interceptor = self # interceptor可以是自己,也可以在初始化的时候设为别的对象,这个都可以根据需求不同而决定。
BaseManager.run() {
self.interceptor.willRun()
apiName = self.child.APIName() # 原本是self.APIName(),然后这个方法是需要子类覆重的,现在可以改为self.child.APIName()了,就不需要覆重了。
request with apiName
self.interceptor.didRun()
通过引入这样面向接口编程的做法,就能相对好地解决上面提到的困境,下面我来解释一下是如何解决困境的:
父类有部分public的方法是不需要,也不允许子类覆重
由于子类必须要遵从<ManagerInterface>,架构师可以跟客户程序员约定所有的public方法在一般情况下都是不需要覆重的。除非特殊需要,则可以覆重,其他情况都通过实现接口中定义的方法解决。由于这是接口方法,所以即便引入了原本不需要的逻辑,也能很容易将其剥离。
父类有一些特别的方法是必须要子类去覆重的,在父类的方法其实是个空方法
因为引入了child,父类不再需要摆一个空方法在那儿了,直接从child调用即可,因为child是实现了对应接口的,所以可以放心调用。空方法就消灭了。
父类有一些方法是可选覆重的,一旦覆重,则以子类为准
我们可以通过在接口中设置哪些方法是必须要实现,哪些方法是可选实现的来处理对应的问题。这本身倒不是缺陷,正是多态希望的样子。
父类有一些方法即便被覆重,父类原方法还是要执行的
由于我们通过接口规避了多态,那么这些其实是可以通过在接口中定义可选方法来实现的,由父类方法调用child的可选方法,调用时机就可以由父类决定。这两个方法不必重名,因此也不存在多态时,不能分辨调用时机或是否需要调用父类方法的情况。
总结一下,通过IOP,我们做好了两件事:
将子类与可能被子类引入的不相关逻辑剥离开来,提高了子类的可重用性,降低了迁移时可能的耦合。
接口实际上是子类头上的金箍,规范了子类哪些必须实现,哪些可选实现。那些不在接口定义的方法列表里的父类方法,事实上就是不建议覆重的方法。
什么时候用多态
由于多态和继承紧密地结合在了一起,我们假设父类是架构师去设计,子类由客户程序员去实现,那么这个问题实际上是这样的两个问题:
作为架构师,我何时要为多态提供接入点?
作为客户程序员,我何时要去覆重父类方法?
这本质上需要程序员针对对象建立一个角色的概念。
举个例子:当一个对象的主要业务功能是搜索,那么它在整个程序里面扮演的角色是搜索者的角色。在基于搜索派生出的业务中,会做一些跟搜索无关的事情,比如搜索后进行人工加权重排列表,搜索前进行关键词分词(假设分词方案根据不同的派生类而不同)。那么这时候如果采用多态的方案,就是由子类覆重父类关于重排列表的方法,覆重分词方法。如果在编写子类的程序员忘记这些必要的覆重或者覆重了不应该覆重的方法,就会进入上面提到的四个困境。所以这时候需要提供一套接口,规范子类去做覆重,从而避免之前提到的四种困境:
Search : { search(), split(), resort()}
采用多态的方案:
Search -> ClothSearch : { [ Search ], @split(), @resort() }
function search() {
self.split() # 如果子类没有覆重这个方法而父类提供的只是空方法,这里就很容易出问题。如果子类在覆重的时候引入了其他不相关逻辑,那么这个对象就显得不够单纯,角色复杂了。
self.resort()
采用IOP的方案:
<SearchManager> : {split(), resort()}
Search<SearchManager> : { search(), assistant<SearchManager> } # 也可以是这样:Search : { search(), assistant<SearchManager> },这么做的话,则要求子类必须实现<SearchManager>
function search() {
self.assistant.split() # self.assistant可以就是self,也可以由初始化时候指定为其他对象,将来进行业务剥离的时候,只要将assistant里面的方法剥离或者讲assistant在初始化时指定为其他对象也好。
self.assistant.resort()
Search -> ClothSearch<SearchManager> : { [ Search ], split(), resort() } # 由于子类被接口要求必须实现split()和resort()方法,因而规避了前文提到的风险,在剥离业务的时候也能非常方便。
外面使用对象时:ClothSearch.search()
如果示例中不同的子类对于search()方法有不同的实现,那么这个时候就适用多态。
Search : { search() }
ClothSearch : { [Search], @search() }
此时适用多态,外面使用对象时:ClothSearch.search()
总结是否决定应当使用多态的两个要素:
如果引入多态之后导致对象角色不够单纯,那就不应当引入多态,如果引入多态之后依旧是单纯角色,那就可以引入多态
如果要覆重的方法是角色业务的其中一个组成部分,例如split()和resort(),那么就最好不要用多态的方案,用IOP,因为在外界调用的时候其实并不需要通过多态来满足定制化的需求。
其实这是一个角色问题,越单纯的角色就越容易维护。还有一个就是区分被覆重的方法是否需要被外界调用的问题。好了,现在我们回到这一节前面提出的两个问题:何时引入接入点和何时采用覆重。针对第一个问题架构师一定要分清楚角色,在保证角色单纯的情况下可以引入多态。另外一点要考虑被覆重的方法是否需要被外界使用,还是只是父类运行时需要子类通过覆重提供中间数据的。如果是只要子类通过覆重提供中间数据的,一律应当采用IOP而不是多态。
针对第二个问题,在必须要覆重的场合下就采取覆重的方案好了,主要是可覆重可不覆重的情况下,客户程序员主要还是要遵守:
覆重的方法本身是跟逻辑密切相关的,不要在覆重方法里做跟这个方法本意不相关的事情
如果要覆重一系列的方法,那么就要考虑角色问题和外界是否需要调用的问题,这些方法是不是这个对象的角色应当承担的任务
比如说不要在一个原本要跑步的函数里面去做吃饭的事情,如果真的要吃饭,父类又没有,实在不行的时候,就需要在覆重的方法里面启用IOP,在子类里面弥补架构师的设计缺陷。把这个不属于跑步的事情IOP出去,负责实现对应接口的可以是self,也可以是别人。只要不是强耦合地去覆重,这样在代码迁移的时候,由于IOP的存在,使得代码接收方也可以接受并实现对应的interface,从而不影响整体功能,又能提供迁移的灵活性。
多态在面向对象程序中的应用相当广泛,只要有继承的地方,或多或少都会用到多态。然而多态比起继承来,更容易被不明不白地使用,一切看起来都那么顺其自然。在客户程序员这边,一般是只要多态是可行方案的一种,到最后大部分都会采用多态的方案来解决问题。
然而多态正如它名字中所暗示的,它有非常大的潜在可能引入不属于对象初衷的逻辑,巨大的灵活性也导致客户程序员在面对问题的时候不太愿意采用其他相对更优的方案,比如IOP。在决定是否采用多态时,我们要有一个清晰的角色概念,做好角色细分,不要角色混乱。该是拦截器的,就给他制定一个拦截器接口,由另一个对象(逻辑上的另一个对象,当然也可以是自己)去实现接口里的方法集。不要让一个对象在逻辑上既是拦截器又是业务模块。这样才方便未来的维护。另外也要注意被覆重方法的作用,如果只是单纯为了提供父类所需要的中间数据的,一律都用IOP,这是比直接采用多态更优的方案。
IOP能够带来的好处当然不止文中写到的这些,它在其他场合也有非常好的应用,它最主要的好处就在于分离了定义和实现,并且能够带来更高的灵活性,灵活到既可以对语言过高的自由度有一个限制,也可以灵活到允许同一接口的不同实现能够合理地组合。在架构设计方面是个非常重要的思想。
我会在这篇这一系列文章中谈谈面向对象思想的几个部分,并且给出对应的解决方案,这些解决方案有些是用面向过程的思路解决的,有些也还是停留在面向对象中。到最后我会给大家一个比较,然后给出结论。
上下文规范
在进一步地讨论这些概念之前,我需要跟大家达成一个表达上的共识,我会采用下面的语法来表达对象相关的信息:
所有的大写字母都是类或对象,小写字母表示属性或方法。
FOO:{ isLoading, _data, render(), _switch() } 这表示一个FOO对象,isLoading、_data是它的属性,render()、_switch()是它的方法,加下划线表示私有。
A -> B 这表示从A派生出了B,A是父类。
A -> B:{ [a, b, c(), d()], e, f() } []里面是父类的东西,e、f()是派生类的东西
B:{ [ A ], e, f() } 省略了对父类的描述,用类名A代替,其他同上
B:{ [ A ], e, f(), @c() } 省略了对父类的描述,函数前加@表示重载了父类的方法。
B:{ [ A,D ], e, f() } 多继承,B继承了A和D
B<protocol> 符合某个protocol接口的对象。
<protocol>:{foo(), bar} protocol这个接口中包含foo()这个方法,bar这个属性。
foo(A, int) foo这个函数,接收A类和int类型作为参数。
来,我们谈谈对象
面向对象思想三大支柱:继承、封装、多态。这篇文章说的是继承。当然面向对象和面向过程都会有好有坏,但是做决定的时候,更多地还是去权衡值得不值得放弃。关于这样的立场问题,我都会给出非常明确的倾向,不会跟你们打太极。
如果说这个也好那个也好,那还发表毛个观点,那叫没有观点。
继承从代码复用的角度来说,特别好用,也特别容易被滥用和被错用。不恰当地使用继承导致的最大的一个缺陷特征就是高耦合。
在这里我要补充一点,耦合是一个特征,虽然大部分情况是缺陷的特征,但是当耦合成为需求的时候,耦合就不是缺陷了。耦合成为需求的例子在后面会提到。
我们来看下面这个场景:
有一天,产品经理Yuki说:
我们不光首页要有一个搜索框,在进去的这个页面,也要有一个搜索框,只不过这个搜索框要多一些功能,它是可以即时给用户搜索提示的。
Casa接到这个任务,他研究了一下代码,说:OK,没问题~
Casa知道代码里已经有了一个现成的搜索框,Casa立刻从HOME_SEARCH_BAR派生出PAGE_SEARCH_BAR
嗯,目前事情进展到这里还不错:
HOME_SEARCH_BAR:{textField, search(), init()}
PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() }
过了几天,产品经理Yuki要求:
用户收藏的东西太多了,我们的app需要有一个本地搜索的功能。
Casa轻松通过方法覆盖摆平了这事儿:
HOME_SEARCH_BAR:{textField, search()}
PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() }
LOCAL_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], @search() }
app上线一段时间之后,UED不知哪根筋搭错了,决定要修改搜索框的UI,UED跟Casa说:
把HOME_SEARCH_BAR的样式改成这样吧,里面PAGE_SEARCH_BAR还是老样子就OK。
Casa表示这个看似简单的修改其实很蛋碎,HOME_SEARCH_BAR的样式一改,PAGE_SEARCH_BAR和LOCAL_SEARCH_BAR都会改变,怎么办呢? 与其每个手工修一遍,Casa不得已只能给HOME_SEARCH_BAR添加了一个函数:initWithStyle()
HOME_SEARCH_BAR:{ textField, search(), init(), initWithStyle() }
PAGE_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], overlay, prompt() }
LOCAL_SEARCH_BAR:{ [ HOME_SEARCH_BAR ], @search() }
于是代码里面就出现了各种init()和initWithStyle()混用的情况。
无所谓了,先把需求应付过去再说。
Casa这么想。
有一天,另外一个team的leader来对Casa抱怨:
搞什么玩意儿?为毛我要把LOCAL_SEARCH_BAR独立出来还特么连带着把那么多文件都弄出来?我就只是想要个本地搜索的功能而已!!
这是因为LOCAL_SEARCH_BAR依赖于它的父类HOME_SEARCH_BAR,然而HOME_SEARCH_BAR本身也带着API相关的对象,同时还有数据解析的对象。 也就是说,要想把LOCAL_SEARCH_BAR移植给另外一个TEAM,拔出萝卜带出泥,差不多整个Networking框架都要移植过去。 嗯,Casa又要为了解耦开始一个不眠之夜了~
以上是典型的错误使用继承的案例,虽然继承是代码复用的一种方案,但是使用继承仍然是需要好好甄别代码复用的方式的,不是所有场景的代码复用都适用于继承。
继承是紧耦合的一种模式,主要的体现就在于牵一发动全身。
第一种类型的问题是改了一处,到处都要改,但解决方案还算方便,多添加一个特定的函数(initWithStyle())就好了。只是代码里面难看一点。
第二种类型的问题是代码复用的时候,要跟着把父类以及父类所有的相关依赖也复制过去,高耦合在复用的时候造成了冗余。
对于这样的问题,业界其实早就给出了解决方案:用组合替代继承。将Textfield和search模块拆开,然后通过定义好的接口进行交互,一般来说可以选择Delegate模式来交互。
解决方案:
<search_protocol>:{search()}
SEARCH_LOGIC<search_protocol>
SEARCH_BAR:{textField, SEARCH_LOGIC<search_protocol>}
HOME_SEARCH_BAR:{SearchBar1, SearchLogic1}
PAGE_SEARCH_BAR:{SearchBar2, SearchLogic1}
LOCAL_SEARCH_BAR:{SearchBar2, SearchLogic2}
这样一来,搜索框和搜索逻辑分别形成了两个不同的组件,分别在HOME_SEARCH_BAR, PAGE_SEARCH_BAR, LOCAL_SEARCH_BAR中以不同的形态组合而成。 textField和SEARCH_LOGIC<search_protocol>之间通过delegate的模式进行数据交互。 这样就解决了上面提到的两种类型的问题。 大部分我们通过代码复用来选择继承的情况,其实都是变成组合比较好。 因此我在团队中一直在推动使用组合来代替继承的方案。 那么什么时候继承才有用呢?
纠结了一下,貌似实在是没什么地方非要用继承不可的。但事实上使用继承,我们得要分清楚层次,使用继承其实是如何给一类对象划分层次的问题。在正确的继承方式中,父类应当扮演的是底层的角色,子类是上层的业务。举两个例子:
Object -> Model
Object -> View
Object -> Controller
ApiManager -> DetailManager
ApiManager -> ListManager
ApiManager -> CityManager
这里是有非常明确的层次关系的,我在这里也顺便提一下使用继承的3大要点:
父类只是给子类提供服务,并不涉及子类的业务逻辑
Object并不影响Model, View, Controller的执行逻辑和业务
Object为子类提供基础服务,例如内存计数等
ApiManager并不影响其他的Manager
ApiManager只是给派生的Manager提供服务而已,ApiManager做的只会是份内的是,对于子类做的事情不参与。
层级关系明显,功能划分清晰,父类和子类各做各的。
Object并不参与MVC的管理中,那些都只是各自派生类自己要处理的事情
DetailManager, ListManager, CityManager都只是处理各自业务的对象
ApiManager并不应该涉足对应的业务。
父类的所有变化,都需要在子类中体现,也就是说此时耦合已经成为需求
Object对类的描述,对内存引用的计数方式等,都是普遍影响派生类的。
ApiManager中对于网络请求的发起,网络状态的判断,是所有派生类都需要的。
此时,牵一发动全身就已经成为了需求,是适用继承的
此时我们回过头来看为什么HOME_SEARCH_BAR,PAGE_SEARCH_BAR,LOCAL_SEARCH_BAR采用继承的方案是不恰当的:
他们的父类是HOME_SEARCH_BAR,父类不只提供了服务,也在一定程度上影响了子类的业务逻辑。派生出的子类也是为了要做搜索,虽然搜索的逻辑不同,但是互相涉及到搜索这一块业务了。
子类做搜索,父类也做搜索,虽然处理逻辑不同,但是这是同一个业务,与父类在业务上的联系密切。在层级关系上,HOME_SEARCH_BAR和其派生出的LOCAL_SEARCH_BAR, PAGE_SEARCH_BAR其实是并列关系,并不是上下层级关系。
由于这里所谓的父类和子类其实是并列关系而不是父子关系,且并没有需要耦合的需求,相反,每个派生子类其实都不希望跟父类有耦合,此时耦合不是需求,是缺陷。
可见,代码复用也是分类别的,如果当初只是出于代码复用的目的而不区分类别和场景,就采用继承是不恰当的。我们应当考虑以上3点要素看是否符合,才能决定是否使用继承。就目前大多数的开发任务来看,继承出现的场景不多,主要还是代码复用的场景比较多,然而通过组合去进行代码复用显得要比继承麻烦一些,因为组合要求你有更强的抽象能力,继承则比较符合直觉。然而从未来可能产生的需求变化和维护成本来看,使用组合其实是很值得的。另外,当你发现你的继承超过2层的时候,你就要好好考虑是否这个继承的方案了,第三层继承正是滥用的开端。确定有必要之后,再进行更多层次的继承。
所以我的态度是:万不得已不要用继承,优先考虑组合
NSString 简单细说
NSString 简单细说(一)—— NSString整体架构NSString 简单细说(二)—— NSString的初始化NSString 简单细说(三)—— NSString初始化NSString 简单细说(四)—— 从URL初始化NSString 简单细说(五)—— 向文件或者URL写入NSString 简单细说(六)—— 字符串的长度NSString 简单细说(七)—— 与C字符串的转化NSString 简单细说(九)—— 字符串的合并NSString 简单细说(十)—— 字符串的分解NSString 简单细说(十一)—— 字符串的查找NSString 简单细说(十二)—— 字符串的替换NSString 简单细说(十三)—— 字符串的分行和分段NSString 简单细说(十四)—— 字符串位置的计算NSString 简单细说(十五)—— 字符串转化为propertyListNSString 简单细说(十六)—— 画字符串NSString 简单细说(十七)—— 字符串的折叠和前缀NSString 简单细说(十八)—— 字符串中大小写子母的变换NSString 简单细说(十九)—— 根据映射获取字符串NSString 简单细说(二十)—— 获取字符串的数值NSString 简单细说(二十一)—— 字符串与编码NSString 简单细说(二十二)—— 与路径相关(一)NSString 简单细说(二十三)—— 与路径相关(二)NSString 简单细说(二十四)—— 与URL相关NSString 简单细说(二十五)—— 语言标签与分析
Swift 简单总结
1. swift简单总结(一)—— 数据简单值和类型转换2. swift简单总结(二)—— 简单值和控制流3. swift简单总结(三)—— 循环控制和函数4. swift简单总结(四)—— 函数和类5. swift简单总结(五)—— 枚举和结构体6. swift简单总结(六)—— 协议扩展与泛型7. swift简单总结(七)—— 数据类型8. swift简单总结(八)—— 别名、布尔值与元组9. swift简单总结(九)—— 可选值和断言10. swift简单总结(十)—— 运算符11. swift简单总结(十一)—— 字符串和字符12. swift简单总结(十二)—— 集合类型之数组13. swift简单总结(十三)—— 集合类型之字典14. swift简单总结(十四)—— 控制流15. swift简单总结(十五)—— 控制转移语句16. swift简单总结(十六)—— 函数17. swift简单总结(十七)—— 闭包(Closures)18. swift简单总结(十八)—— 枚举19. swift简单总结(十九)—— 类和结构体20. swift简单总结(二十)—— 属性21. swift简单总结(二十一)—— 方法22. swift简单总结(二十二)—— 下标脚本23. swift简单总结(二十三)—— 继承24. swift简单总结(二十四)—— 构造过程25. swift简单总结(二十五)—— 构造过程26. swift简单总结(二十六)—— 析构过程27. swift简单总结(二十七)—— 自动引用计数28. swift简单总结(二十八)—— 可选链29. swift简单总结(二十九)—— 类型转换30.swift简单总结(三十)—— 嵌套类型31.swift简单总结(三十一)—— 扩展32.swift简单总结(三十二)—— 协议33.swift简单总结(三十三)—— 泛型34.swift简单总结(三十四)—— 访问控制35.swift简单总结(三十五)—— 高级运算符第一个简单的swift程序
AFNetworking 源码探究
1. AFNetworking源码探究(一) —— 基本介绍2. AFNetworking源码探究(二) —— GET请求实现之NSURLSessionDataTask实例化(一)3. AFNetworking源码探究(三) —— GET请求实现之任务进度设置和通知监听(一)4. AFNetworking源码探究(四) —— GET请求实现之代理转发思想(一)5. AFNetworking源码探究(五) —— AFURLSessionManager中NSURLSessionDelegate详细解析(一)6. AFNetworking源码探究(六) —— AFURLSessionManager中NSURLSessionTaskDelegate详细解析(一)7. AFNetworking源码探究(七) —— AFURLSessionManager中NSURLSessionDataDelegate详细解析(一)8. AFNetworking源码探究(八) —— AFURLSessionManager中NSURLSessionDownloadDelegate详细解析(一)9. AFNetworking源码探究(九) —— AFURLSessionManagerTaskDelegate中三个转发代理方法详细解析(一)10. AFNetworking源码探究(十) —— 数据解析之数据解析架构的分析(一)11. AFNetworking源码探究(十一) —— 数据解析之子类中协议方法的实现(二)12. AFNetworking源码探究(十二) —— 数据解析之子类中协议方法的实现(三)13. AFNetworking源码探究(十三) —— AFSecurityPolicy与安全认证 (一)14. AFNetworking源码探究(十四) —— AFSecurityPolicy与安全认证 (二)15. AFNetworking源码探究(十五) —— 请求序列化之架构分析(一)16. AFNetworking源码探究(十六) —— 请求序列化之协议方法的实现(二)17. AFNetworking源码探究(十七) —— _AFURLSessionTaskSwizzling实现方法交换(转载)(一)18. AFNetworking源码探究(十八) —— UIKit相关之AFNetworkActivityIndicatorManager(一)19. AFNetworking源码探究(十九) —— UIKit相关之几个分类(二)20. AFNetworking源码探究(二十) —— UIKit相关之AFImageDownloader图像下载(三)21. AFNetworking源码探究(二十一) —— UIKit相关之UIImageView+AFNetworking分类(四)22. AFNetworking源码探究(二十二) —— UIKit相关之UIButton+AFNetworking分类(五)23. AFNetworking源码探究(二十三) —— UIKit相关之UIWebView+AFNetworking分类(六)24. AFNetworking源码探究(二十四) —— UIKit相关之UIProgressView+AFNetworking分类(七)25. AFNetworking源码探究(二十五) —— UIKit相关之UIRefreshControl+AFNetworking分类(八)26. AFNetworking源码探究(二十六) —— UIKit相关之AFAutoPurgingImageCache缓存(九)
1. 友盟集成(一) —— UShare模块之快速集成(一)2. 友盟集成(二) —— UShare模块之快速集成(二)3. 友盟集成(三) —— UShare模块之第三方登录(一)4. 友盟集成(四) —— UShare模块之进阶说明之第三方平台SDK说明(一)5. 友盟集成(五) —— UShare模块之进阶说明之分享到第三方平台(二)6. 友盟集成(六) —— UShare模块之进阶说明之分享面板UI(三)7. 友盟集成(七) —— UShare模块之进阶说明之自定义平台(四)8. 友盟集成(八) —— UShare模块之进阶说明之U-Share API说明(五)9. 友盟集成(九) —— UShare模块之UShare常见问题 (六)10. 友盟集成(十) —— UShare模块之UShare日志说明 (七)11. 友盟集成(十一) —— 几个遇到的坑之QQ授权名称的设置(一)12. 友盟集成(十二) —— 几个遇到的坑之HTTP分享到微博失败(二)13. 友盟集成(十三) —— 几个遇到的坑之分享到QQ不显示缩略图(三)14. 友盟集成(十四) —— 分享的工程实践(一)
OpenGL ES 框架详细解析
OpenGL ES 框架详细解析(一) —— 基本概览OpenGL ES 框架详细解析(二) —— 关于OpenGL ESOpenGL ES 框架详细解析(三) —— 构建用于iOS的OpenGL ES应用程序的清单OpenGL ES 框架详细解析(四) —— 配置OpenGL ES的上下文OpenGL ES 框架详细解析(五) —— 使用OpenGL ES和GLKit进行绘制OpenGL ES 框架详细解析(六) —— 绘制到其他渲染目的地OpenGL ES 框架详细解析(七) —— 多任务,高分辨率和其他iOS功能OpenGL ES 框架详细解析(八) —— OpenGL ES 设计指南OpenGL ES 框架详细解析(九) —— 调整您的OpenGL ES应用程序OpenGL ES 框架详细解析(十) —— 使用顶点数据的最佳做法OpenGL ES 框架详细解析(十一) —— 并发和OpenGL ESOpenGL ES 框架详细解析(十二) —— 采用OpenGL ES 3.0OpenGL ES 框架详细解析(十三) —— Xcode OpenGL ES工具概述OpenGL ES 框架详细解析(十四) —— 使用纹理工具压缩纹理OpenGL ES 框架详细解析(十五) —— 适用于Apple A7 GPU及更高版本的OpenGL ES 3.0 及相关词汇
YYKit 源码探究
1. YYKit源码探究(一) —— 基本概览2. YYKit源码探究(二) —— NSString分类之Hash(一)3. YYKit源码探究(三) —— NSString分类之Encode and decode(二)4. YYKit源码探究(四) —— NSString分类之Drawing(三)5. YYKit源码探究(五) —— NSString分类之Regular Expression(四)6. YYKit源码探究(六) —— NSString分类之NSNumber Compatible(五)7. YYKit源码探究(七) —— NSString分类之Utilities(六)8. YYKit源码探究(八) —— NSNumber分类(一)9. YYKit源码探究(九) —— UIFont分类之架构分析和Font Traits(一)10. YYKit源码探究(十) —— UIFont分类之Create font(二)11. YYKit源码探究(十一) —— UIFont分类之Load and unload font(三)12. YYKit源码探究(十二) —— UIFont分类之Dump font data(四)13. YYKit源码探究(十三) —— UIImage分类之框架结构和Create image部分(一)14. YYKit源码探究(十四) —— UIImage分类之Image Info(二)15. YYKit源码探究(十五) —— UIImage分类之Modify Image(三)16. YYKit源码探究(十六) —— UIImage分类之Image Effect(四)17. YYKit源码探究(十七) —— UIImageView分类之架构和image部分(一)18. YYKit源码探究(十八) —— UIImageView分类之highlight image部分(二)19. YYKit源码探究(十九) —— UIScreen分类(一)20. YYKit源码探究(二十) —— UIScrollView分类(一)21. YYKit源码探究(二十一) —— UITableView分类(一)22. YYKit源码探究(二十二) —— UITextField分类(一)23. YYKit源码探究(二十三) —— UIView分类(一)24. YYKit源码探究(二十四) —— UIPasteboard分类(一)25. YYKit源码探究(二十五) —— UIGestureRecognizer分类(一)26. YYKit源码探究(二十六) —— UIDevice分类框架及Device Information(一)27. YYKit源码探究(二十七) —— UIDevice分类之Network Information(二)28. YYKit源码探究(二十八) —— UIDevice分类之Disk Space(三)29. YYKit源码探究(二十九) —— UIDevice分类之Memory Information(四)30. YYKit源码探究(三十) —— UIDevice分类之CPU Information(五)31. YYKit源码探究(三十一) —— UIControl分类(一)32. YYKit源码探究(三十二) —— UIColor分类之Create a UIColor Object(一)33. YYKit源码探究(三十三) —— UIColor分类之Get color's description(二)34. YYKit源码探究(三十四) —— UIColor分类之Retrieving Color Information(三)35. YYKit源码探究(三十五) —— UIButton分类之image(一)36. YYKit源码探究(三十六) —— UIButton分类之background image(二)37. YYKit源码探究(三十七) —— UIBezierPath分类(一)38. YYKit源码探究(三十八) —— UIBarButtonItem分类(一)39. YYKit源码探究(三十九) —— UIApplication分类(一)40. YYKit源码探究(四十) —— NSTimer分类(一)41. YYKit源码探究(四十一) —— NSParagraphStyle分类(一)42. YYKit源码探究(四十二) —— NSObject分类之YYModel(一)43. YYKit源码探究(四十三) —— NSObject分类之KVO(二)44. YYKit源码探究(四十四) —— NSObject分类之Sending messages with variable parameters(三)45. YYKit源码探究(四十五) —— NSObject分类之Swap method (Swizzling)(四)46. YYKit源码探究(四十六) —— NSObject分类之Associate value(五)47. YYKit源码探究(四十七) —— NSObject分类之Other(六)48. YYKit源码探究(四十八) —— NSNotificationCenter分类(一)49. YYKit源码探究(四十九) —— NSKeyedUnarchiver分类(一)50. YYKit源码探究(五十) —— NSDictionary分类之Dictionary Convertor(一)51. YYKit源码探究(五十一) —— NSDictionary分类之Dictionary Value Getter(二)52. YYKit源码探究(五十二) —— NSDictionary分类之NSMutableDictionary(三)53. YYKit源码探究(五十三) —— NSDate分类之Component Properties(一)54. YYKit源码探究(五十四) —— NSDate分类之Date modify(二)55. YYKit源码探究(五十五) —— NSDate分类之Date Format(三)56. YYKit源码探究(五十六) —— NSData分类之Hash(一)57. YYKit源码探究(五十七) —— NSData分类之Encrypt and Decrypt(二)58. YYKit源码探究(五十八) —— NSData分类之Encode and decode(三)59. YYKit源码探究(五十九) —— NSData分类之Inflate and deflate(四)60. YYKit源码探究(六十) —— NSData分类之Others(五)61. YYKit源码探究(六十一) —— NSBundle分类(一)62. YYKit源码探究(六十二) —— NSAttributedString分类之基本(一)63. YYKit源码探究(六十三) —— NSAttributedString分类之Retrieving character attribute information(二)64. YYKit源码探究(六十四) —— NSAttributedString分类之Get character attribute as property(三)65. YYKit源码探究(六十五) —— NSAttributedString分类之Get paragraph attribute as property(四)66. YYKit源码探究(六十六) —— NSAttributedString分类之Get YYText attribute as property(五)67. YYKit源码探究(六十七) —— NSAttributedString分类之Query for YYText(六)68. YYKit源码探究(六十八) —— NSAttributedString分类之Create attachment string for YYText(七)69. YYKit源码探究(六十九) —— NSAttributedString分类之Utility(八)70. YYKit源码探究(七十) —— NSMutableAttributedString分类之Set character attribute(九)71. YYKit源码探究(七十一) —— NSMutableAttributedString分类之Set character attribute as property(十)72. YYKit源码探究(七十二) —— NSMutableAttributedString分类之Set paragraph attribute as property(十一)73. YYKit源码探究(七十三) —— NSMutableAttributedString分类之Set YYText attribute as property(十二)74. YYKit源码探究(七十四) —— NSMutableAttributedString分类之Set discontinuous attribute for range(十三)75. YYKit源码探究(七十五) —— NSMutableAttributedString分类之Convenience methods for text highlight(十四)76. YYKit源码探究(七十六) —— NSMutableAttributedString分类之Utilities(十五)77. YYKit源码探究(七十七) —— NSArray分类(一)78. YYKit源码探究(七十八) —— NSMutableArray分类(一)79. YYKit源码探究(七十九) —— MKAnnotationView分类(一)80. YYKit源码探究(八十) —— CALayer分类之YYWebImage(一)81. YYKit源码探究(八十一) —— CALayer分类之YYAdd(二)82. YYKit源码探究(八十二) —— 网络相关YYReachability(一)83. YYKit源码探究(八十三) —— 线程安全计数器YYSentinel(一)84. YYKit源码探究(八十四) —— 线程安全的可变数组子类YYThreadSafeArray(一)85. YYKit源码探究(八十五) —— 线程安全的可变字典的子类YYThreadSafeDictionary(一)86. YYKit源码探究(八十六) —— 定时器YYTimer(一)87. YYKit源码探究(八十七) —— 事物YYTransaction(一)88. YYKit源码探究(八十八) —— YYWeakProxy防止强引用(一)
系统推送的集成
1. 系统推送的集成(一) —— 基本集成流程(一)2. 系统推送的集成(二) —— 推送遇到的几个坑之BadDeviceToken问题(一)3. 系统推送的集成(三) —— 本地和远程通知编程指南之你的App的通知 - 本地和远程通知概览(一)4. 系统推送的集成(四) —— 本地和远程通知编程指南之你的App的通知 - 管理您的应用程序的通知支持(二)5. 系统推送的集成(五) —— 本地和远程通知编程指南之你的App的通知 - 调度和处理本地通知(三)6. 系统推送的集成(六) —— 本地和远程通知编程指南之你的App的通知 - 配置远程通知支持(四)7. 系统推送的集成(七) —— 本地和远程通知编程指南之你的App的通知 - 修改和显示通知(五)8. 系统推送的集成(八) —— 本地和远程通知编程指南之苹果推送通知服务APNs - APNs概览(一)9. 系统推送的集成(九) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 创建远程通知Payload(二)10. 系统推送的集成(十) —— 本地和远程通知编程指南之苹果推送通知服务APNs - 与APNs通信(三)11. 系统推送的集成(十一) —— 本地和远程通知编程指南之苹果推送通知服务APNs - Payload Key参考(四)12. 系统推送的集成(十二) —— 本地和远程通知编程指南之Legacy信息 - 二进制Provider API(一)13. 系统推送的集成(十三) —— 本地和远程通知编程指南之Legacy信息 - Legacy通知格式(二)