Effective Objective-C 2.0: Delegate and Data Source Protocols

来源:互联网 发布:matlab编程第四版答案 编辑:程序博客网 时间:2024/05/29 11:00

Delegate protocol and Data Source Protocol 结合另一篇:Delegate

http://blog.csdn.net/chuanyituoku/article/details/16890645

Item 23: Use Delegate and Data Source Protocols for Interobject Communication

Objects will often need to talk to each other and can do so in many ways. One such programming design pattern used extensively by Objective-C developers is known as the Delegate patternthe essence of which is to define an interface that any object can conform to in order to become the delegate of another objectThis other object then talks back to its delegate to get some information or to inform the delegate when interesting things happen.

Using this pattern enables the decoupling of data from business logic. For instance, a view in a user interface to display a list of data should be responsible only for the logic of how data is displayed, not for deciding what data should be displayed or what happens in data interaction. The view object can have properties that contain objects responsible for the data and event handling. These are known as the data source and delegate, respectively.

In Objective-C, the usual way of achieving this pattern is to use the protocol language feature, which is used throughout the frameworks that make up Cocoa. If you use this feature yourself, you’ll find that your own code will fit appropriately.

As an example, consider a class that is used to fetch data from the network. A class might do so to retrieve some data from a resource on a distant server. It might take a long time for the server to respond, and it would be rather bad practice to block while the data was being retrieved. So it’s common in this scenario to use the Delegate pattern, whereby the fetcher class has a delegate that it calls back once the data has been retrieved. Figure 4.1 illustrates this concept; the EOCDataModel object is the delegate of EOCNetworkFetcher. The EOCDataModel asks theEOCNetworkFetcher to perform a task asynchronously, and EOCNetworkFetcher tells its delegate, the EOCDataModel, when that task has completed.

Image

Figure 4.1 A delegate callback. Note that the EOCDataModel instance doesn’t necessarily have to be the delegate but could be another object.

This pattern is easy to implement using Objective-C through the use of a protocol. In the case of Figure 4.1, the protocol might look like this:

@protocol EOCNetworkFetcherDelegate
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didFailWithError:(NSError*)error;
@end

A delegate protocol is usually named as the class name followed by the worddelegate, using camel casing for the whole name. Following this naming pattern will make your delegate protocol feel familiar to anyone using it.

The protocol provides a property on the class that has the delegate. In our example, this is the EOCNetworkFetcher class. Therefore, the interface would look like this:

@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak)
    id <EOCNetworkFetcherDelegate> delegate;
@end

It’s important to make sure that the property is defined as weak and not strong, since it must be a nonowning relationship. Usually, the object that will be the delegate will also hold onto the object. An object wanting to use anEOCNetworkFetcher, for example, will keep hold of it until finished with it. If the property holding the delegate were an owning relationship using the strongattribute, a retain cycle would be introduced. For this reason, a delegate property will always be defined using either the weak attribute to benefit from autonilling (seeItem 6) or unsafe_unretained if autonilling is not required. The ownership diagram shown in Figure 4.2 illustrates this.

Image

Figure 4.2 Ownership diagram showing delegate being nonretained in order to avoid retain cycle

Implementing the delegate is a matter of declaring that your class implements the delegate protocol and then implementing any methods you want from the protocol.You can declare that a class implements a protocol in either the interface or the class-continuation category (see Item 27). Declaring it in the interface is useful if you want to advertise to others that you implement that protocol; however, in the case of delegates, it’s usual to care only about it internally. So it’s common to declare it in the class-continuation category like so:

@implementation EOCDataModel () <EOCNetworkFetcherDelegate>
@end

@implementation EOCDataModel
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didReceiveData:(NSData*)data {
    /* Handle data */
}
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didFailWithError:(NSError*)error {
    /* Handle error */
}
@end

Usually, methods within a delegate protocol will be optional, since the object being the delegate may not care about all the methods. In the example, the DataModelclass may not care that an error occurred, so might not implement thenetworkFetcher:didFailWithError: method. To indicate this, delegate protocols usually make most or all methods optional by applying the @optional keyword:

@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didFailWithError:(NSError*)error;
@end

Before any optional method is called on the delegate, introspection (see Item 14) should be used to determine whether the delegate responds to that selector. In the case of EOCNetworkFetcher, it would look like this:

NSData *data = /* data obtained from network */;
if ([_delegate respondsToSelector:
          @selector(networkFetcher:didReceiveData:)])
{
    [_delegate networkFetcher:self didReceiveData:data];
}

The respondsToSelector: method is used to determine whether the delegate has implemented that method. If yes, it’s called; otherwise, nothing happens. In that way, the delegate method is truly optional, and nothing will break if it’s not implemented. Even if no delegate is set, it will still function perfectly well, since sending a message to nil will make the if statement evaluate to false.

Getting the name of your delegate methods correct is also important. The name should indicate exactly what is happening and why the delegate is being told something. In the example, the delegate method reads very clearly, saying that a certain network fetcher object has just received some data. You should also always pass the instance that is delegating, just as in the example, so that the delegatemethod implementation can switch based on the specific instance. For example:

- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didReceiveData:(NSData*)data
{
    if (fetcher == _myFetcherA) {
        /* Handle data */
    } else if (fetcher == _myFetcherB) {
        /* Handle data */
    }
}

Here, the object being the delegate has two network fetchers and so must be told which network fetcher is telling it that data has been received. Without being told this information, the object would be able to use only one network fetcher at a time, which would not be ideal.

The delegate methods can also be used to obtain information from the delegate. For example, the network fetcher class might want to provide a mechanism such that if it encounters a redirect while fetching the data, it asks its delegate whether the redirect should occur. The delegate method for this may look like the following:

- (BOOL)networkFetcher:(EOCNetworkFetcher*)fetcher
        shouldFollowRedirectToURL:(NSURL*)url;

This example should make it easy to see why it’s called the Delegate pattern, since the object is delegating responsibility for an action to another class.

Protocols can also be used to provide an interface through which the data that a class requires is obtained. This other use of the Delegate pattern is referred to as the Data Source pattern because its aim is to provide data to the class. The flow of information is toward the class; with a normal delegate, the flow of information is away from the class. This flow is illustrated in Figure 4.3.

Image

Figure 4.3 Flow of information is out of the class for a delegate and into the class for a data source.

For example, a list view object in a user interface framework might use a data source protocol to provide the data to show in the list. The list view may also have a delegate to handle user interaction with the list. The separation of the data source and delegate protocols provides a cleaner interface by separating distinct portions of logic. In addition, you could have one object be the data source and another be the delegate. However, the same object usually ends up being both.

With the Delegate and Data Source patterns where implementing any of the methods is optional, you will have a lot of code that looks like the following:

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

Checking whether the delegate responds to a certain selector is pretty quick, but if you do this repeatedly, responses after the first one are potentially redundant. If the delegate hasn’t changed, it’s extremely unlikely that it has suddenly started or stopped responding to the given selector. For this reason, it is common to make the optimization of caching whether the delegate responds to the methods in the protocol. For example, the network fetcher in the example has a delegate method that is called back as a progress indicator, telling the delegate every so often how far the network fetch has progressed. This method may get called many times during the life cycle of the fetcher, and checking each time whether the delegate responds to the selector is redundant.

Consider the expanded delegate protocol for the selector defined like so:

@protocol EOCNetworkFetcherDelegate
@optional
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didReceiveData:(NSData*)data;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didFailWithError:(NSError*)error;
- (void)networkFetcher:(EOCNetworkFetcher*)fetcher
        didUpdateProgressTo:(float)progress;
@end

The single, optional method networkFetcher:didUpdateProgressTo: has been called in here. The best way for the caching to be done is to use the bitfield data type. This feature from C is often overlooked but is excellent for this purpose. It allows you to define that a certain field of a structure should be sized a certain number of bits. It looks like this:

struct data {
    unsigned int fieldA : 8;
    unsigned int fieldB : 4;
    unsigned int fieldC : 2;
    unsigned int fieldD : 1;
};

In this structure, fieldA will use exactly 8 bits, fieldB will use 4 bits, fieldC will use 2 bits, and fieldD will use 1 bit. So fieldA will be able to hold values from 0 to 255, and fieldD will be able to hold either 0 or 1. The latter is of interest for caching what methods a delegate implements. If you create a structure that uses only 1-bit bitfields, you can pack in a lot of Booleans into a small amount of data. For the example of the network fetcher, you would make the instance have a structure as one of its instance variables containing the bitfield, one variable for each delegatemethod. The structure would look like this:

@interface EOCNetworkFetcher () {
    struct {
        unsigned int didReceiveData      : 1;
        unsigned int didFailWithError    : 1;
        unsigned int didUpdateProgressTo : 1;
    } _delegateFlags;
}
@end

Here, I have used the class-continuation category to add the instance variable, as explained in Item 27: The instance variable that has been added is a structure containing three fields, one for each of the optional delegate methods. The structure can be queried and set by using the following from within the EOCNetworkFetcherclass:

// Set flag
_delegateFlags.didReceiveData = 1;

// Check flag
if (_delegateFlags.didReceiveData) {
    // Yes, flag set
}

This structure is used to cache whether the delegate responds to the given selectors. This can be done from within the setter accessor method for the delegate property:

- (void)setDelegate:(id<EOCNetworkFetcher>)delegate {
    _delegate = delegate;
    _delegateFlags.didReceiveData =
      [delegate respondsToSelector:
                @selector(networkFetcher:didReceiveData:)];
    _delegateFlags.didFailWithError =
      [delegate respondsToSelector:
                @selector(networkFetcher:didFailWithError:)];
    _delegateFlags.didUpdateProgressTo =
      [delegate respondsToSelector:
                @selector(networkFetcher:didUpdateProgressTo:)];
}

Then, instead of checking whether the delegate responds to the given selector each time a delegate method is called, the flags are checked instead:

if (_delegateFlags.didUpdateProgressTo) {
    [_delegate networkFetcher:self
          didUpdateProgressTo:currentProgress];
}

If this is called many times, it’s a worthwhile optimization to make. The need to make this optimization depends on your code. You should instrument your code and determine where the hot spots are and keep this under your belt as a potential for a speed improvement. It’s most likely to make an impact in data source protocols in which the data source is asked repeatedly for each individual piece of data.

Things to Remember

Image Use the Delegate pattern to provide an interface to your objects that need to tell other objects about pertinent events.

Image Define a protocol with potentially optional methods to define the interface that your delegate should support.

Image Use the Delegate pattern when an object needs to obtain data from another object. In this case, it is often referred to as a “data source protocol.”

Image If required, implement the bitfield structure approach to cache which methods a delegate responds to from the protocol.

原创粉丝点击