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

249 阅读11分钟
原文链接: www.jianshu.com

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 字符串常量,麻烦了非常多,但是相信在项目代码量不断增加,以及工程变得越来越复杂以后,这样的做法对于代码管理上是非常有帮助的