Part1: 设计与声明
1、编写代码要遵守Cocoa API约定
- 返回对象的方法(init)通常通过返回 nil 来表示“创建失败”或“没有对象可以返回”;(一般在运行时错误、其他非例外的条件下会出现)
- 某些可能返回 nil 的方法可以通过最后一个参数引用传值的方式来返回错误信息;(常见的 NSError **err 参数)
- 执行某些系统操作的方法(例如文件的读写),通常会返回一个 BOOL 值来表示成功还是失败;
- Cocoa 会用空的容器对象来表示默认值或没有值,例如:
@""、@[]、@{}
,而nil
通常表示有问题的对象参数; -
NSString
和NSCharacter
两种表示字符的类型,Cocoa 中一般使用NSString
; - Cocoa 框架的类型一致性,在设计类、方法、函数、常量和其他符号的时候,要注意以下一些约定的习惯:
1. 在对类名和类相关联的符号(如函数和 typedef 定义的函数上)命名时,要使用前缀,避免命名冲突。前缀的命名一般使用2-3个大写字母,例如:CGImageRef中的“CG”;
2. 在 API 的命名上,清楚比简洁更重要。例如:`removeObjectAtIndex:` 比 `remove:` 方法更加清楚;
3. 避免模棱两可的命名。例如:`displayName`、`handlexxx` 等不直观的命名;
4. 类一般由行为和特征组成,而行为表示一个动作,也是我们常说的方法,所以在命名一个方法的时候,常用动词。例如:`removeObjectAtIndex:`、`addxxx` 等一目了然的动作;
5. 如果是要获取某些信息,我们习惯添加 `get` 前缀,如果是判断条件,我们习惯添加 `is` `has` 等表示判断前缀;
6. 如果是设置属性值的方法,我们习惯添加 `set` 前缀,对于属性的设置方法 Cocoa 帮我们做了,但是其他的 `setter` 方法,仍然需要添加前缀;
7. API 不要使用缩写;
2、洞悉实例变量
这个是比较早期的一个用法,现在不推荐使用实例变量,而是使用属性,下一节会讲到
- 变量的命名一般是小写,如果有多个词,通常用驼峰命名方式;
- 实例变量一般是由实例持有,存有该实例的一些私有属性,如果需要在类的多个实例中共享,则可以使用全局变量;
- 只加入一些绝对必要的实例变量,否则容易造成大的开销;
- 永远不要将实例变量设置为 @public,这违反了封装的原则;
- 确保类基本属性对应的实例变量有存取方法;
3、透彻了解属性的里里外外
实例变量:存在对象的 bitdata -> ro -> ivarList 中,是只读的;
属性:存在对象的 bitdata -> rw -> propertyList 中,是可以动态添加的;
除了动态添加到属性列表中,还可以添加到对象的关联对象表中,所以尽量使用属性;
- 尽量使用属性,而不是实例变量,也不要直接使用属性自动生成的下划线开头的实例变量,通过 getter 方法去取值,通过 setter 方法去设值;
- 在声明属性时,要明确属性的特性,例如:atomic/nonatomic/strong/copy/assgin/readonly/readwrite/getter=isxxx/class 等关键字;
- public 属性尽量定义成 readonly,如果必须要修改它,提供新的 setter 方法用来设置它;
- 如果可以,尽量使用从父类继承来的属性,不用定义比较类似的属性,例如:UITableViewCell 中的属性,
@synthesize imageView=_imageView, detailTextLabel=_detailTextLabel;
; - 对于协议中定义的属性,我们要用
synthesize
关键字合成 getter 和 setter 方法; - 属性可以动态添加,而实例变量不能动态添加;
- 对于 GCD 系列的类型,也要当做对象处理,它们都是包装过的类型。例如:
typedef NSObject<OS_dispatch_queue> *dispatch_queue_t;
4、存取方法是良好的类接口必要组成部分
主要是针对 MRR 模式下的 setter 和 getter 方法的一个说明,下面举个例子来看下当今的 ARC 为我们做了哪些工作
- (NSString *)title {
return [[title retain] autorelease];
}
- (void)setTitle:(NSString *)newTitle {
if (title != newTitle) {
[title release];
title = [newTitle copy];
}
}
- 存取方法是外界访问对象属性的通道,并对对象实例数据的封装,防止实例数据被破坏;
- 方法返回的对象,需要保证对象在该作用于内不被释放或修改;
- 调用对象从某个方法(如存取方法)接收到对象时,不应该对其进行释放,除非它先前进行显示的保持或复制;
5、明晓类公共领域的方法都是虚方法
- Objective-C 中所有的方法都是虚方法,不需要显示地用 virtual 关键字声明;
- 实现纯虚方法依赖正式协议来实现;
- 协议并不是真正的类,它只能声明方法,不能添加数据(可以通过@synthesize合成setter和getter的方法添加属性);
- 非正式协议并不是真正的协议,它对代码没有约束;
6、初始化还是解码取决于是否支持归档还是解档
- 类的对象支持归档和解档,该类必须遵循NSCoding协议;必须实现对对象进行编码(encodeWithCoder:)和(initWithCoder:)的方法;
- 类的初始化方法 initWithCoder: 在角色上的并行性存在例外;
7、利用键-值机制访问类的私有成员变量和方法
- 在 Objective-C 中,类的成员变量或方法是没有绝对私有的,可以借助“编译运行时”机制,即“瞎子摸黑”机制来实现对它们的访问;
- KVC 和 KVO 在定制子类的设计时特别重要;
- KVC、KVO 和 KVB 都支持遍历;
- KVC 主要是通过 isa-swizzling 指针来实现其内部查找定位。KVO 其设计基于设计模式中的“观察者模式”。KVB 和 KVO 最明显的使用场景就是在一些界面实时显示很强的地方;
8、浅复制事宜指针而深复制事宜数据
- 浅复制,是将原始对象的指针值复制到副本中,从而达到原始对象和副本共享引用数据的目的;深复制,是复制指针所引用的数据,并将其付给副本的实例变量;
- 一般情况下,可以被视为数据容器的指针型实例变量旺旺被深复制,而更复杂的实例变量(如委托)则被浅复制;
- 实例变量的 setter 方法的实现要能够反映出需要使用的复制类型。如果相应的 setter 方法复制了新的值,那么就应该深复制这个实例变量;
- 如果实例变量的 setter 方法只是简单地将新的值赋给实例变量,而没有复制或保留它,那么其应该浅复制这个实例变量;
9、明智而审慎地使用 NSCoping
- 基类没有实现 NSCoping,那么子类的实现必须复制它所继承的实例变量,以及那些在类中声明的实例变量,最安全的方式是使用 alloc、init 和 set 方法;
- 类继承链 NSCoping 的行为,并声明了额外的实例变量,那么也需要实现 copyWithZone:;
- 入股哦积累使用了或者有可能使用过 NSCopyObject,那么必须使用有别于 alloc 和 init 函数的情况,用不同的方式处理实例变量;
10、使用协议来实现对匿名对象的提供
- 采用协议,可灵活实现。实现抽象不应该依赖于细节,而细节应依赖于抽象,降低“声明”和“实现”的耦合度;
- 设计程序采用协议,可减少集成类的复杂性;
Part2:实现
1、使用类别把类的实现拆分成不同的文件
如何拆分类别的注意事项:
- 确保多个业务之间没有任何关联,可以通过类别独立开;
- 如果为了拆分类别而需要主类公开过的的属性或者方法时,就不要拆分类别了;
- 使用类别的原则是:类别给主类提供服务,而不能由主类给类别提供服务;
- 如果类别中需要存储数据,通过关联对象管理类来存储,不要直接使用主类的属性;
- 使用类别的时,心里要清楚类别加载的原理,懒加载类、非懒加载类、懒加载分类与非懒加载分类之间的一些联系和区别;
- 利用类别机制,可将同一个类的实现,由一个常规的实现文件(.m)拆分成多个实现文件(.m);
- 把同一个类的实现文件(.m)拆分成多个实现文件(.m),适合类的实现文件大多比较庞大,业务比较多;
- 把同一个类的实现文件(.m)拆分成多个实现文件(.m),拆分标准多以同类型或同业务类型等来作为参照;
2. 明智地使用内省可使程序更加高效和健壮
什么是“内省”
对象揭示自己作为一个运行时对象的详细信息的一种能力。
内省有助于避免错误的进行消息的发送、错误的假设对象相等,以及类似的问题
常见的“内省”方法
-
评估继承关系
isKindOfClass: isMemberOfClass:
-
方法实现和协议遵循
respondsToSelector: conformsToProtocol:
-
对象的比较
hash isEqual:
-
字符串和时间的比较
isEqualToString: isEqualToTimeZone:
3、尽量使用不可变性对象而非可变性对象
- 尽量不要把可变对象存储到集合对象中,否则容易导致存储的可变对象被破坏或变为无效;
- 在开销上,可变对象比不可变对象要大,因为可变对象必须动态管理一个可变的辅助存储——在必要时分配或解除分配内存块,所以比相应的不可变版本效率低;
- 不能确定对象是否可变,则将它当做不可变处理;
4、利用复合能巧妙地把两个类和对象融合
- 在 OOP 编程中有两个技术用于描述类与类或者对象与对象之间的关系:一个是继承、另一个是复合;
- 复合是通过类中声明一个指向另一个类对象的指针作为实力变量,从而将这两个类进行复合;
- 使用 new 创建对象时,实际是 alloc + init 两个步骤的结合;
- 在 Objective-C 中所有对象之间的交互都是通过指针实现的;
5、使用类扩展来隐藏实现的细节
- 类扩展,从一定程度上可以看做是匿名类别;
- 利用类扩展隐藏私有信息;
- 类扩展是编译时的,如果某个类扩展没有被import到主类中,则程序中不会包含这个类扩展中的任何信息,类扩展里面的信息是被编译到 data->bits->ro 段中,属于只读;
6、使用内联块应注意避免循环引用
- 在内联块中能直接引用 self,但要注意避免导致循环引用,一定要看内联块有没有被某种链路的 self 持有;
- 避免直接在内联块中直接使用 self,通过 weakself-strongsell;
- 通过传参的形式在block使用 self;
示例:通知中心持有,导致的循环引用
NSObject* obj = [NSNotificationCenter.defaultCenter addObserverForName:@"" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
// self
}];
// obj -> block -> self
// center -> obj
解决办法:
- block 中使用 weakself
- dealloc 中移除对obj 的持有(如果self->obj的情况,那就只能使用weakself了)
7、利用类别把方法添加到现有的类
- 在OOP编程中有两个技术用于描述类与类或对象与对象之间的关系:一个是
继承
,另一个是复合
; - 复合是通过在类中声明一个指向另一个类对象的指针作为实例变量,从而将这两个类进行复合;
- 使用new创建对象的时候,实际等同于
alloc + init
两个步骤; - 在Objective-C中,所有对象之间的交互都是通过指针实现的;
8、通过强弱引用来管理对象的所有权
-
__weak
相对的是__strong
,后者不需要显示指定,因为它是默认的; - 引用原则是:父对象对子对象:
__strong
引用,子对象对父对象:__weak
引用; - 只要变量是在范围内(作用域),变量就会保持对对象的
__strong
引用,直到它被重新分配给另一个对象或置为nil; - 对一些不支持
__weak
引用的类,可通过Unsafe Unretained
引用来处理; - 使用
__weak
引用来避免循环引用;
Part3:继承与面向对象设计
这一章主要讲解对象和类的结构
1、明确 isa 在继承上的作用
- 在Objective-C中,每个对象都隐藏着一个数据结构——isa指针;
- isa指针指向的是对象的类,这个类也是一个对象,常说的类对象,它的isa指向对应的元类,元类是由系统管理的,开发者无法获取;
- 在Objective-C中,每个对象都保留一个超类的指针,用于调度自己的方法和调度基类;
2、利用类别和协议实现类似多重继承的机制
- 类别是实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中;
- 类别可以重载原始类的方法,但不推荐这么做,可能导致原始类的方法无法被调用;
- 协议是一系列不属于任何类的方法列表,其中声明的放阿飞可以被任何类实现。协议不是真正的类,只能声明方法,不能添加数据。协议中声明的属性,需要在遵循协议的类中定义别名以实现getter和setter方法;
3、类别和类扩展是类继承的延续性拓展
- 继承体现了类的上下级关系,而类别体现了类之间的平行关系;
- 类别具有替换特性,也就是说,如果类别犯法与类某个方法具有相同的方法签名,那么类别里的方法将会替换类的原有方法;
- 类别是为增加外部方法的话,类扩展就是用作类的内部扩展;
- 类别关注的中心是代码设计,把不同功能的方法分离开;
4、继承基类的实现行为勿忘调用 super
子类继承的所有父类的方法,都是父类预留的接口:
要么是提供子类某种遍历
要么是需要子类提供某种数据以便父类能顺利完成成某个功能
也可能是父类需要更加丰富的调用
总而言之,不能在父类中来区分子类的调用,也就是父类不能服务于子类,子类只能是父类的延续以及扩展,不能要求父类为自己而增加某些行为,一般子类自己去扩展行为即可
- 调用基类方法,以使用该类提供的服务;
- 覆盖基类的方法,以便将自己的代码引入到定义的程序模型中;
- 如果打算补充基类实现的行为,需要调用 super;
- 如果打算覆盖基类实现的行为,就不要调用 super;
Part4:设计模式与 Cocoa 编程
推荐书籍:《设计模式:可重用的面向对象的软件中元素》
设计模式:特定环境下的特定问题的解决方案
特定环境:一个经常出现的环境,也就是设计模式应用的地方
解决方案:一个可以在这个环境下达成目标、解决约束的一般性设计
1、设计模式是特定环境下的特定问题的解决方案
- 设计模式是某种特定设计的模板或指导原则;
- 如果将设计模式的概念定义为为类,那么某个设计模式的具体实现就是将这个类给实例化;
2、MVC模式是一种复合或聚合模式
- MVC(模型-视图-视图控制器),是一种高几倍的模式,关注的是应用程序的全局架构,并根据对象在程序中发挥的作用对齐进行分类;
- MVC是一个复合的设计模式,是由合成(Composite)、策略(Strategy)和观察者(Observe)模式组成;
3、对象建模在数据库中也广泛使用
-
实体-关系建模
是一种表现对象的方式,这里的对象通常用于介绍数据源的数据结构,使哪些数据结构可以被映射为面向对象系统中的对象; -
实体-关系建模
并不仅仅在Cocoa中使用,它在数据库技术中使用的属于和规则一起,是广泛使用的模式。这种对象表示有助于数据源中对象的存储和获取;
4、类簇可简化框架的公开架构而又不减少功能的丰富性
- 类簇(class cluster)基于抽象工厂设计模式;
- 类簇,可以用于隐藏实现的详细细节,为调用者提供一个简单的接口;
- 类簇也可以有多个基类,如NSArray、NSMutableArray,后者就是继承的前者。对一些“大同小异”的问题,往往会有不错的效果;
5、委托用于界面控制,而数据源用于数据控制
- 委托是一种对象,当想外委托任务的对象遇到程序中的事件时,它的委托可以代表它对事件进行处理,或者和它进行协调;
- 委托使一个对象有可能在没有进行集成的情况下改变另一个对象的行为;
- 数据源很像委托,区别在于委托处理的是用户界面的控制,而数据源处理的是数据的控制。
网友评论