Effective Objective-C 2.0 Tips 总结 Chapter 3 & Chapter 4

简介: Chapter 3 接口与 API 设计Tips 15 使用前缀避免明明空间冲突Objective-C 没有命名空间,所以我们在起名时要设法避免命名冲突避免命名冲突的方法就是使用前缀应用中的所有名称都需要加前缀(包括实现文件中的全局变量和...

Chapter 3 接口与 API 设计

  • Tips 15 使用前缀避免明明空间冲突

    • Objective-C 没有命名空间,所以我们在起名时要设法避免命名冲突
    • 避免命名冲突的方法就是使用前缀
    • 应用中的所有名称都需要加前缀(包括实现文件中的全局变量和纯 C 函数)
  • Tips 16 提供“全能(designated)初始化方法”

    • 一个会被所有初始化方法调用到的初始化方法
    • 当底层数据存储机制变化时,只需要修改这个方法就可以了,不需要改动其他初始化方法
    • 如果超类的全能初始化方法不适用于子类,或是与超类不同,那么需要覆盖这个超类方法
    • 子类的全能初始化方法都应该调用超类的对应方法,逐级向上
  • Tips 17 实现 description 方法

    • 在数组字典等集合对象打印时,都会调用对象的 description 方法,方便调试
    • 系统默认的 description 方法对于自定义的对象并没有输出较为有用的内容,所以可以实现这个方法方便我们显示对象
    • 在调试时会调用 debugDescription 方法(也就是在调试时 lldb 中输入 po 时调用的将会是 debugDescription),所以实现他可以帮助我们调试时获得更多的信息
    • 可以使用 NSDictionary 来实现 description 方法,这样显示和输出都会比较方便,例如:
      // Header File
      // 这里我略微修改了下原书中的示例代码
      @interface EOCLocation : NSObject
      @property (nonatomic, copy) NSString *title;
      @property (nonatomic) CGFloat latitude;
      @property (nonatomic) CGFloat longitude;
      @end
      // 我们要是可以使用 NSLog(@"%@", eoc_location) 直接输出这个对象的经纬度(也就是所有属性)就好了,那么可以参考下面的写法实现 description 方法
      @implementation EOCLocation
      - (NSString *)description {
          return [NSString stringWithFormat:@"<%@: %p, %@>",
                  [self class],
                  self,
                  @{
                      @"title": self.title,
                      @"latitude": @(self.latitude),
                      @"longitude": @(self.longitude),
                  }];
      }
      @end
      
  • Tips 18 尽量使用不可变对象

    • 减少 side effect,在使用了一段时间的 RAC 和学习函数式思想后,一定程度上理解了不可变对象的好处
    • 具体开发实践中,应尽量把对外公布的属性设为只读,并且有必要时才对外公布,否则使用私有属性
    • 对于只读属性,可以不用指定内存管理语义(也就是 strong,weak,copy)
    • 对外只读的属性可以在对象内部,也就是类扩展(Class-Extension 也叫 Class-Continuation)中重新声明为可读写的
    • 可以使用 GCD 来设置读写操作为同步操作
    • 就算属性设置为只读,在外部仍可以使用 KVC 来访问这些属性,例如:[object setValue:@"value" forKey:@"propertyName"]
    • 集合属性(Array,Set,Dictionary)可以提供只读属性供外界使用(内部保存可变类型的变量,返回该变量的不可变拷贝),并提供操相应的操作方法,例如下面例子中,使用 -addFriend:-removeFriend: 方法来实现对 friends 集合的操作,这样保证了添加或删除盆友的操作对象是知情的。对于直接修改 friends 集合的操作对象是不知情的,这样可能会导致对象内各数据的不一致。
      @interface EOCPerson : NSObject
      @property (nonatomic, strong, readonly) NSSet *friends;
      @end
      
      @implementation EOCPerson {
          NSMutableSet *_internalFriends;
      }
      
      - (NSSet *)friends {
          return [_internalFriends copy];
      }
      
      - (void)addFriend:(EOCPerson *)person {
          [_internalFriends addObject:person];
      }
      
      - (void)removeFriend:(EOCPerson *)person {
          [_internalFriends removeObject:person];
      }
      @end
      
    • 不要在返回的对象上查询其是否是可变对象并对其进行操作,同上条这样对对象集合属性的直接修改,容易产生 bug
  • Tips 19 使用清晰而协调的命名方式

    • 方法名的风格要保证与自己的代码或是需要集成的框架一致,也就是上下文需要一致,这点最重要放第一
    • 起名遵循 Objective-C 的命名规范,这样的接口名字一定程度上提示了接口的作用
    • 方法名言简意赅,从左到右读起来最好像一个日常用于中的句子
    • 方法名里不要使用缩略后的类型名称
    • Objective-C 的方法名相较其他语言要长一些,但是可以更好地表达方法的作用,以及各个参数的意义,比如:
    Rectangle *recgangle = new Rectangle(5.0f, 10.0f);
    // 不如下面的命名方式
    Rectangle *recgangle = [Rectangle initWithSize:(float)width :(float)height];
    // 不如下面的命名方式
    Rectangle *recgangle = [Rectangle initWithWidth:(float)width andHeight:(float)height];
    
  • Tips 20 为私有方法名加前缀

    • 因为在 Objective-C 中没有私有方法,所有对象都可以响应任意消息,并且可以通过 runtime 获取对象可以相应的消息,所以我们使用特定的命名来区分私有方法
    • 在使用 Category 或继承系统中或第三方库中的类的时候,可以防止命名冲突
    • C 语言中使用 _ 下划线作为系统内部函数的开头所以我们不能使用 _ 作为私有方法的前缀(苹果的官方库也使用 _
    • 原书作者建议使用 p_ 来作为私有方法的前缀,个人建议使用开发中项目使用的前缀小写来作为类前缀,比如上文的 EOCPerson 中添加私有方法可以使用 eco_privateMethodName:,这样的前缀在第三方类库中出现重复的概率比较小
  • Tips 21 理解 Objective-C 错误模型

    • ARC 在默认情况下并不是异常安全的,也就是抛出异常的时候,在作用域末尾应该释放的对象将不会被释放
    • 可以使用 -fobjc-arc-exceptions 来告诉编译器需要生成异常安全的代码,但是这样会引入一些额外代码,并且在不抛出异常时也会执行这部分代码
    • 就算不使用 ARC 使用异常也很容易写出内存泄漏的代码,因为需要在抛出异常前清理所有申请的资源,所以现在我们只在非常罕见(严重错误,比如:抽象类中的方法没有实现)的情况下抛出异常,抛出之后不需要考虑回复的问题,并且退出应用,这样就不用编写复杂的异常安全代码
    • 对于不严重的错误,我们通过返回 nil/0 或是使用 NSError 来处理,NSError 中包含了错误处理所需的各种信息,我们自己的错误需要规划和设置好对应的 Error Domain,Error Code
    • 一般通过 delegate 来传递错误 - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error 或是输入参数返回错误 - (BOOL)doSomething:(NSError **)error
  • Tips 22 理解 NSCopying 协议

    • 实现 NSCopying 接口可以让类实现拷贝(copy)方法,- (id)copyWithZone:(NSZone *)zone 中的 zone 是以前开发时使用的内存区参数,目前已经不使用了,可以不用考虑他
    • 实现 NSMutableCopying 协议支持可变拷贝(mutableCopy)方法
    • 对象拷贝时需要决定是深拷贝还是浅拷贝,一般情况下用浅拷贝
    • 绝大多数情况下 NSCopying 实现的都是浅拷贝,所以如果使用深拷贝,建议创建一个单独的方法来完成

Chapter 4 协议(Protocol)和分类(Category)

  • Tips 23 使用委托(delegate)和数据源(data source)协议进行对象间通信

    • 委托模式(delegate pattern):对象把应对某个行为的责任委托给了另一个类
    • 类似我们经常使用的 UITableViewUITableViewDelegateUITableViewDataSource 分别定义了如何处理事件的接口和如何提供数据的接口,实现这两个接口为 UITableView 提供交互逻辑和显示数据,UITableView 本身只负责显示获取到的数据
    • 委托模式同样适用于异步事件,比如网络请求完成后,回调委托对象将结果传递回去,实现事件的异步处理
    • 使用委托对象的对象中的委托对象属性需要设置为 weak,防止循环引用
    • 使用委托中的方法时,使用 respondsToSelector: 先查询委托对象是否实现了该方法,特别是在协议中使用 @option 关键字标注的可选方法
    • 委托中的方法名要清晰明确,需要说明事件的来源,当前的事件,以及为什么委托对象需要获取这个事件,所有委托方法都需要将发起委托的对象发送到委托对象(作为第一个参数),让委托对象判断事件来源
    • 针对需要进行多次调用的委托对象(例如网络加载时下载进度),可以通过结构体等方法,在设置委托对象的时候,一次检查需要响应的方法并记录,之后在使用的时候,直接通过记录结果来判断是否实现了某个方法,不用每次都使用 respondsToSelector: 方法来查询是否实现,例:
    @interface EOCNetworkFetcher() {
        struct {
            unsigned int didReceiveData       : 1;
            unsigned int didFailWithError     : 1;
            unsigned int didUpdateProgressTo  : 1;
        } _delegateFlags;
    }
    @end
    
    @implementation EOCNetworkFetcher
    - (void)setDelegate:(id<EOCNetworkFetcherDelegate>)delegate {
        _delegate = delegate;
        _delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
        _delegateFlags.didFailWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailWithError:)];
        _delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
    }
    @end
    
    // 在需要调用 delegate 方法的时候
    if (_delegateFlags.didUpdateProgressTo) {
        [_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
    }
    
  • Tips 24 将类的实现代码分散到便于管理的多个 Category 中

    • 在开发的过程中,类的代码只会越来越大,那么我们可以通过分类机制将类的代码打散,根据业务分散到不同的分类中
    • 应该把私有方法放到叫(Private)的分类中,隐藏实现细节
  • Tips 25 总是为第三方分类的分类名称加前缀

    • 如果分类中出现同名方法,容易出现奇怪的 bug,所以在为其他类添加分类的时候,分类名称和分类中的方法需要添加你自己使用的前缀
  • Tips 26 勿在分类中声明属性

    • 分类中可以定义方法(包括 getter 和 setter),但是不要定义属性,因为在分类中定义的属性不会生成实例变量
    • 虽然有 objc_setAssociatedObject 魔法可以用,但是这容易导致内存管理问题,因为无法使用属性记录内存管理语义,但是建议一般情况下不使用
    • 分类的主要作用是扩张类的功能,而不是封装数据
  • Tips 27 使用 Class-Continuation 分类,隐藏实现细节

    • Class-Continuation 分类必须定义在该类的实现文件中,并且可以声明实例变量,并且建议仅以此种方式增加实例变量
    • 头文件中声明为只读的属性,可以在实现文件中的 Class-Continuation 分类中扩展为可读写
    • 私有方法原型,和私有属性,都可以放到 Class-Continuation 分类中
    • 在 Class-Continuation 分类中可以声明实现的接口,并且外部不会知道
    • 可以通过私有属性很好的封装 C++/Objective-C++ 的代码,提供 Objective-C 的接口给其他代码使用
  • Tips 28 通过协议提供匿名对象

    • 使用类似 @property(nonatomic, weak) id<ProtocolName> delegate; 提供匿名类型对象作为 delegate,可以隐藏类名
    • 对于类型不重要,只需要提供可向应方法的对象,可以使用匿名对象,隐藏实现细节

对于 Chapter 1 的补充

第一章第四条中,多用类型常量,少用 #define 预处理指令中,建议大家使用类型常量而不是 #define 来定义常量,这里增加一个补充内容,swift 中,我们可以使用 struct 中的静态变量来声明常量,这样带来的一个好处是使用和分类管理非常方便

Xcode 8.0 带的 clang 4.0 后开始支持类常量,也就是定义属性的时候,可以加入 class 来修饰属性,这样这个属性是属于类的,于是乎,我们可以这样使用常量了

NSString *notificationName = XXXConstant.notificationNames.XXXUserDidLoginNotificationName;

看上去比类型常量长一些,不过似乎还算比较好看

定义的时候需要这样定义:

@interface XXXConstantNotificationNames : NSObject

@property(nonatomic, readonly) NSString *XXXUserDidLoginNotificationName;

@end

@interface XXXConstant : NSObject

@property(nonatomic, class, copy) XXXConstantNotificationNames *notificationNames;

@end

并且,类常量是不会被 synthesize 的,也就是说编译器不会自动为类常量创建相应的变量,所以在实现文件中,我们需要这么写

@implementation XXXConstantNotificationNames

- (NSString *)XXXUserDidLoginNotificationName {
    return @"XXXUserDidLoginNotificationName";
}

@end

@implementation XXXConstant
static XXXConstantNotificationNames *_notificationNames = nil;

+ (void)load {
    _notificationNames = [[XXXConstantNotificationNames alloc] init];
}

- (XXXConstantNotificationNames *) {
    reutrn _notificationNames;
}

@end

看上去比定义一个 kXXXUserDidLoginNotificationName 字符串常量,麻烦了非常多,但是相信在项目代码量不断增加,以及工程变得越来越复杂以后,这样的做法对于代码管理上是非常有帮助的

目录
相关文章
|
iOS开发 开发者 C++
Effective Objective-C 2.0 Tips 总结 Chapter 5,6,7
Effective Objective-C 2.0 Tips 总结 Chapter 5,6,7 Chapter 5 内存管理 Tips 29 理解引用计数 引用计数是 Objective-C 内存管理的基础,包括 ARC 也是建立在引用计数的基础之...
1290 0
|
安全 iOS开发 编译器
Effective Objective-C 2.0
本书是iOS开发进阶的必读书籍之一。文中部分名词的中文翻译略坑,比如对block和GCD的翻译。其他整体还好,原作者写的比较用心。代码规范讲了不少,底层原理讲了一点点,且主要集中在第二章。
1392 0
|
iOS开发 编译器 C语言
Effective Objective-C 2.0 Tips 总结 Chapter 1 & Chapter 2
下面只是对读到的所有 Tips 结合我平时开发中遇到的问题进行总结,每一个 Tips 和书中的每一条对应,本文的目的是去掉书中的大部分讨论的内容,让人能够马上使用这些 Tips,建议阅读过原书后食用更佳。
1167 0
|
C++ iOS开发 编译器
《Effective Objective-C 2.0》3、枚举类型表示状态、选项
第五条:使用枚举类型表示状态和选项,可以使代码更加清晰,可读性更好。 枚举类型使用关键字enum定义,通常与typedef相结合,定义一组状态或选项: typedef enum CustomState { CustomStateNon...
981 0
|
iOS开发 编译器 自然语言处理
《Effective Objective-C 2.0》2、数据的定义方法
第三条:多用字面量语法,少用等价的方法 这部分所描述的是创建foundation类的对象时的技巧。Foundation框架是iOS中至关重要的框架,iOS应用中大部分数据都可以用foundation类表示。
1028 0
|
C++ iOS开发 编译器
《Effective Objective-C 2.0》1、熟悉Objective-C
该系列是《Effective Objective-C 2.0——编写高质量iOS与OS X代码的52个有效方法》的读书笔记。 第一条:了解Objective-C语言的起源 同C++类似,Objective-C也是C语言进行面相对象化的扩展。
1006 0
|
iOS开发
Effective Objective-C 2.0 Reading Notes
1. Literal Syntax   NSString *someString = @"Effective Objective-C 2.0";   NSNumber *someNumber = [NSNumber numberWithInt:1]; NSNumber *someNum...
728 0
|
6月前
|
安全 编译器 Swift
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
IOS开发基础知识: 对比 Swift 和 Objective-C 的优缺点。
388 2
|
4月前
|
开发工具 iOS开发 容器
【Azure Blob】关闭Blob 匿名访问,iOS Objective-C SDK连接Storage Account报错
iOS Objective-C 应用连接Azure Storage时,若不关闭账号的匿名访问,程序能正常运行。但关闭匿名访问后,上传到容器时会出现错误:“Public access is not permitted”。解决方法是将创建容器时的公共访问类型从`AZSContainerPublicAccessTypeContainer`改为`AZSContainerPublicAccessTypeOff`,以确保通过授权请求访问。
【Azure Blob】关闭Blob 匿名访问,iOS Objective-C SDK连接Storage Account报错
|
6月前
|
缓存 开发工具 iOS开发
优化iOS中Objective-C代码调起支付流程的速度
优化iOS中Objective-C代码调起支付流程的速度
102 2