美文网首页
[iOS] Effective Objective-C ——协议

[iOS] Effective Objective-C ——协议

作者: 木小易Ying | 来源:发表于2019-10-12 14:37 被阅读0次

    23. 通过委托与数据源协议进行对象间通信

    委托模式主旨是:定义一套接口,某对象若想接受另一个对象的委托,则需要遵从此接口,以便成为其“委托对象”(delegate)。而这“另一个对象”则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。

    如果有些是可选实现的方法一定要标注optional,然后在使用[self.delegate xxx]之前需要先判断是不是delegate实现了这个可选方法(responseToSelector)。

    方法名同样也要清晰明确,并且第一个参数应该把定义delegate的对象传回去,因为有的时候可能两个object用同一个实现了的delegate的对象,这样就可以在方法内区分是哪个object触发了delegate方法。

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        if(tableView == tableViewA){
        ……
        }else if(tableView == tableViewA){
        ……
        }
    }
    

    在实现委托模式与数据源模式时,如果协议中的方法是可选的,那么就会写出一大批类似下面这样的代码来:

    if ([_delegate respondsToSelector:@selector(someClassDidSomething)]) {
      [_delegate someClassDidSomething];
    }
    

    但其实只有第一次检查是有意义的,之后每次检查的结果都是不变的,如果这个delegate是类似于网络progress更新会被频繁调用的时候,可以缓存起第一次检查的结果,这样就不用以后每次都查了,一般不会最开始不能响应某个方法突然变得可以响应。

    // 头文件 
    #import <Foundation/Foundation.h>
    @class EOCNetworkFetcher;
    
    @protocol EOCNetworkFetcherDelegate <NSObject>
    @optional
    -(void)networkFetcher:(EOCNetworkFetcher *)fetcher didReceiveData:(NSData *)data;
    
    -(void)networkFetcher:(EOCNetworkFetcher *)fetcher didFailWithError:(NSError *)error;
    
    -(void)networkFetcher:(EOCNetworkFetcher *)fetcher didUpdateProgressTo:(float)progerss;
    @end
    
    @interface EOCNetworkFetcher : NSObject
    
    @property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
    
    @end
    
    // 实现文件
    #import "EOCNetworkFetcher.h"
    
    @interface EOCNetworkFetcher() {
        // 使用含有位段的结构体
        struct {
            // 表示占用1个二进制位,可以表示0或1这两个值
            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:)];
    }
    
    - (void)didSomeThing
    {
        // 调用delegate的相关方法
        if (_delegateFlags.didUpdateProgressTo) {
            [_delegate networkFetcher:self didUpdateProgressTo:0.5];
        }
    }
    
    @end
    

    24. 将类的实现代码分散到便于管理的数个分类之中

    类中经常容易填满各种方法,而这些方法的代码则全部堆在一个巨大的实现文件里。如果过于繁杂,可以利用分类category把代码分区。

    #import <Foundation/Foundation.h>
    
    @interface EOCPerson : NSObject
    @property (nonatomic, copy, readonly) NSString *firstName;
    @property (nonatomic, copy, readonly) NSString *lastName;
    @property (nonatomic, strong, readonly) NSArray *friends;
    
    - (id)initWithFirstName:(NSString*)firstName 
                andLastName:(NSString*)lastName;
    @end
    
    @interface EOCPerson (Friendship)
    - (void)addFriend:(EOCPerson*)person;
    - (void)removeFriend:(EOCPerson*)person;
    - (BOOL)isFriendsWith:(EOCPerson*)person;
    @end
    
    @interface EOCPerson (Work)
    - (void)performDaysWork;
    - (void)takeVacationFromWork;
    @end
    
    @interface EOCPerson (Play)
    - (void)goToTheCinema;
    - (void)goToSportsGame;
    @end
    

    通过分类机制,可以把类代码分成很多个易于管理的小块,以便单独检视。使用分类机制之后,如果想用分类中的方法,那么要记得在引入EOCPerson.h时一并引入分类的头文件。虽然稍微有点麻烦,不过分类仍然是一种管理代码的好办法。


    这对于某些应该视为私有的方法来说更是极为有用。可以创建名为Private的分类,把这种方法全都放在里面。这个分类里的方法一般只会在类或框架内部使用,而无须对外公布。这样一来,类的使用者有时可能会在查看回溯信息时发现private一词,从而知道不应该直接调用此方法了。这可算作一种编写"自我描述式代码"(self-documenting code)的办法。

    在编写准备分享给其他开发者使用的程序库时,可以考虑创建Private分类。经常会遇到这样一些方法: 它们不是公共API的一部分,然而却非常适合在程序库之内使用。此时应该创建Private分类,如果程序库中的某个地方要用到这些方法,那就引入此分类的头文件。而分类的头文件并不随程序库一并公开,于是该库的使用者也就不知道库里面还有这些私有方法了。(库内部公开,但是又不对外公开)

    25. 总是为第三方类的分类名称加前缀

    如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了"主实现"中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。这样就完全取决于分类的加载时机了,非常不靠谱很容易出错,命名加了分类但是方法没有被调用。

    要解决此问题,一般的做法是: 以命名空间来区别各个分类的名称与其中所定义的方法。想在Objective-C中实现命名空间功能,只有一个办法,就是给相关名称都加上某个共用的前缀。与给类名加前缀时所应考虑的因素相似,给分类所加的前缀也要选得恰当才行。一般来说,这个前缀应该与应用程序或程序中其他地方所用的前缀相同。于是,我们可以给NSString分类加上ABC前缀:

    @interface NSString (ABC_HTTP)
    
    // Encode a string with URL encoding
    - (NSString*)abc_urlEncodedString;
    
    // Decode a URL encoded string
    - (NSString*)abc_urlDecodedString;
    
    @end
    

    而且如果不加前缀,可能覆盖苹果自己的方法,从而造成难于查找的bug。
    在给系统类或者第三方类库加category的时候,同理为了避免同名方法,都应该加上前缀。

    26. 勿在分类中声明属性

    为了用分类做代码分块(参考24),我们可能把相关属性的声明也放到category里面,那么为了不报错就要自己实现setter/getter,并且通过关联将属性值存起来,如果属性越多,这样的冗余代码越多,也不方便属性的内存管理。

    所以正常而言,如果用category做代码分类,属性也应该全部声明在主类中,不要在分类文件中声明,至于分类机制,则应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。

    27. 使用"class-continuation分类"隐藏实现细节

    "class-continuation分类"是一种无名的分类,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明的实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。与其他分类不同,"class-continuation分类"没有名字。比如,有个类叫做EOCPerson,其"class-continuation分类"写法如下:

    @interface EOCPerson ()
    //Methods here
    @end
    

    其中可以定义方法和实现变量,主要是为了隐藏不希望外部知道的方法和变量,当然你也可以在实现中声明不对外的变量,这个看偏好了,只是一般都是在class-continuation分类中声明会好一点。

    @interface ECOPerson() {
          NSString *_anInstanceVariable;
    }
    //Method declarations here
    @end
    
    等价于:
    @implementation EOCPerson {
          int _anotherInstanceVariable;
    }
    //Method implementations here
    @end
    

    ※ OC与c++混编

    Objective-C++是Objective-C与C++的混合体,其代码可以用这两种语言来编写。由于兼容性原因,游戏后端一般用C++来写。另外,有时候要使用的第三方库可能只有C++绑定,此时也必须使用C++来编码。

    假设某个类打算这样写:

    #import <Foundation/Foundation.h>
    
    #include "SomeCppClass.h"
    
    @interface EOCClass : NSObject {
    @private
        SomeCppClass _cppClass;
    }
    @end
    

    该类的实现文件可能叫做EOCClass.mm,其中.mm扩展名表示编译器应该将此文件按Objective-C++来编译,否则,就无法正确引入SomeCppClass.h了。然而请注意,名为SomeCppClass的这个C++类必须完全引入,因为编译器要完整地解析其定义方能得知_cppClass实例变量的大小。于是,只要是包含EOCClass.h的类,都必须编译为Objective-C++才行,因为它们都引入了SomeCppClass类的头文件。这很快就会失控,最终导致整个应用程序全部都要编译为Objective-C++。

    这么做确实完全可行,不过笔者觉得相当别扭,尤其是将代码发布为程序库供其他应用程序使用时,更不应该如此。要求第三方开发者将其源文件扩展名均改为.mm不是很合适。

    你可能认为解决此问题的办法是:不引入C++类的头文件,只是向前声明该类,并且将实例变量做成指向此类的指针。

    #import <Foundation/Foundation.h>
    
    class SomeCppClass;
    
    @interface EOCClass : NSObject {
    @private
        SomeCppClass *_cppClass;
    }
    @end
    

    现在实例变量必须是指针,若不是,则编译器无法得知其大小,从而会报错。但所有指针的大小确实都是固定的,于是编译器只需知道其所指的类型即可。不过,这么做还是会遇到刚才那个问题,因为引入EOCClass头文件的源码里都包含class关键字,而这是C++的关键字,所以仍然需要按Objective-C++来编译才行。这样做既别扭又无必要,因为该实例变量毕竟是private的,其他类为什么要知道它呢?这个问题还是得用"class-continuation分类"来解决。将刚才那个类改写之后,其代码如下:

    #import <Foundation/Foundation.h>
    
    @interface EOCClass : NSObject
    @end
    
    // EOCClass.mm
    #import "EOCClass.h"
    #include "SomeCppClass.h"
    
    @interface EOCClass () {
        SomeCppClass _cppClass;
    }
    @end
    
    @implementation EOCClass
    @end
    

    改写后的EOCClass类,其头文件里就没有C++代码了,使用头文件的人甚至意识不到其底层实现代码中混有C++成分。某些系统库用到了这种模式,比如网页浏览器框架WebKit,其大部分代码都以C++编写,然而对外展示出来的却是一套整洁的Objective-C接口。CoreAnimation里面也用到了此模式,它的许多后端代码都用C++写成,但对外公布的却是一套纯Objective-C接口。


    "class-continuation分类"还有一种合理用法,就是将public接口中声明为"只读"的属性扩展为"可读写",以便在类的内部设置其值。我们通常不直接访问实例变量,而是通过设置访问方法来做(参见第7条),因为这样能够触发"键值观测"(Key-Value Observing, KVO)通知,其他对象有可能正监听此事件。

    出现在"class-continuation分类"或其他类中的属性必须同类接口里的属性具备相同的特质(attribute),不过,其"只读"状态可以扩充为"可读写"。

    //.m文件
    #import <Foundation/Foundation.h>
    
    @interface EOCPerson : NSObject
    @property (nonatomic, copy, readonly) NSString *firstName;
    @property (nonatomic, copy, readonly) NSString *lastName;
    
    - (id)initWithFirstName:(NSString*)firstName
                   lastName:(NSString*)lastName;
    @end
    
    //.h文件
    @interface EOCPerson ()
    @property (nonatomic, copy, readwrite) NSString *firstName;
    @property (nonatomic, copy, readwrite) NSString *lastName;
    @end
    

    只会在类的实现代码中用到的私有方法也可以声明在"class-continuation分类"中。这么做比较合适,因为它描述了那些只在类实现代码中才会使用的方法。这些方法可以这样写:

    @interface EOCPerson()
    - (void)p_privateMethod;
    @end
    

    然而像上面这样在"class-continuation分类"中声明一下通常还是有好处的,因为这样做可以把类里所含的相关方法都统一描述于此。

    另外,若想使类所遵循的协议不为人所知,则可于"class-continuation分类"中声明。

    28. 通过协议提供匿名对象

    @property (nonatomic, weak) id <EOCDelegate> delegate;
    

    由于该属性的类型是id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性,即便该类不继承自NSObject也可以,只要遵循EOCDelegate协议就行。对于具备此属性的类来说,delegate就是"匿名的"(anonymous)。

    如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示,来隐藏类型名称(或类名)。

    协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法。

    • 例如当设计一个兼容各种数据库mysql,PostgreSQL等的manager,返回一个数据库连接,可能是各种底层数据库的类不是很确定,这种时候就可以用id来替代返回的connection类型。

      创建匿名对象把这些第三方类简单包裹一下,使匿名对象成为其子类,并遵从EOCDatabaseConnection协议。然后,用"connectionWithIdentifier:"方法来返回这些类对象。在开发后续版本时,无须改变公共API,即可切换后端的实现类。

    相关文章

      网友评论

          本文标题:[iOS] Effective Objective-C ——协议

          本文链接:https://www.haomeiwen.com/subject/kwhfmctx.html