Effective Objective-C 2.0: Class-Continuation Category == Class Extention

Item 27: Use the Class-Continuation Category to Hide Implementation Detail

Often, you will want a class to contain more methods and instance variables than are exposed externally. You could expose them externally and document that they are private and shouldn’t be relied on. After all, no method or instance variable is truly private in Objective-Cowing to the way the dynamic messaging system works (see Item 11). However, it is good practice to expose publicly only what needs to be exposed. So what do you do about methods and instance variables that should not be exposed publicly but whose existence you still want to document? That’s where a special category—the “class-continuation category”—can come in handy.

The class-continuation category, unlike normal categories, must be defined in the implementation file of the class for which it is a continuation. Importantly, it is the only category that is allowed to declare extra instance variables. Also, this category doesn’t have a specific implementation. Any method defined within it is expected to appear in the main implementation of the class. Unlike other categories, a class-continuation category has no name. A class-continuation category for a class named EOCPerson would look like this:

@interface EOCPerson ()
// Methods here
@end

Why is such a category useful? It’s useful because both methods and instance variables can be defined there. This is possible only because of the nonfragile ABI (see Item 6 for more detail about this), which means that the size of an object does not have to be known in order to use it. Therefore, instance variables do not have to be defined in the public interface, since consumers of the class do not have to know their layout. For this reason, adding instance variables to a class was made possible in the class-continuation category, as well as in the implementation of a class. To do this, you simply need to add some braces in the right place and put the instance variables in there:

@interface EOCPerson () {
    NSString *_anInstanceVariable;
}
// Method declarations here
@end

@implementation EOCPerson {
    int _anotherInstanceVariable;
}
// Method implementations here
@end

What is the point of doing this? You could define instance variables in the public interface. But a benefit to hiding them away in the class-continuation category or implementation block is that they are known about only internally. Even if you mark them as private in the public interface, you are still leaking implementation detail. For example, suppose that you don’t want others to know about the existence of a supersecret class used only internally. If one of your classes owns an instance of this class and you were to declare the instance variable in the public interface, it would look like this:

#import <Foundation/Foundation.h>

@class EOCSuperSecretClass;

@interface EOCClass : NSObject {
@private
    EOCSuperSecretClass *_secretInstance;
}
@end

The fact that a class named EOCSuperSecretClass exists has been leaked. You could get around this by not strongly typing the instance variable and declaring it of id type instead ofEOCSuperSecretClass*. However, this would be less than ideal because you would lose any help from the compiler when using it internally. And why should you miss out on help like that just because you don’t want to expose something? This is where the class-continuation category can help. Now it can be declared like so:

// EOCClass.h
#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
@end

// EOCClass.m
#import "EOCClass.h"
#import "EOCSuperSecretClass.h"

@interface EOCClass () {
    EOCSuperSecretClass *_secretInstance;
}
@end

@implementation EOCClass
// Methods here
@end

Similarly, the instance variable could have been added to the implementation block, semantically equivalent to adding it to the class-continuation category and more a matter of preference. I prefer adding it to the category because it keeps all data definitions in the same place. You may also have properties defined in the class-continuation category, so it’s good to declare extra instance variables here also. This instance variable is not truly private, since it is always possibleto hack around using methods from the runtime, but to all intents and purposes, it is private. Also, since it is not declared in the public header, it is much more hidden if you were to ship this code as part of a library.

Another place where this is particularly useful is with Objective-C++ code. In this hybrid of Objective-C and C++, code written in both languages can be used. Often, back ends to games are written in C++ for portability reasons. Other times, you may need to use C++ because you’re interfacing with a third-party library that has only C++ bindings. For these times, the class-continuation category can come in handy as well. Suppose that previously, you would have written the class like so:

#import <Foundation/Foundation.h>
#include "SomeCppClass.h"

@interface EOCClass : NSObject {
@private
    SomeCppClass _cppClass;
}
@end

The implementation file for this class would be called EOCClass.mm, where the .mm extension indicates to the compiler that the file should be compiled as Objective-C++. Without this, the inclusion of SomeCppClass.h would be impossible. However, note that the C++ classSomeCppClass has had to be imported fully because the definition needs to be fully resolved such that the compiler knows how big the _cppClass instance variable is. So any file that includes EOCClass.h to use the class also needs to be compiled as Objective-C++, since it will also be including the SomeCppClass header file. This can easily spiral out of control and end up with your entire application being compiled as Objective-C++. This is perfectly fine, but I consider this to be fairly ugly, especially if the code is being shipped as a library for use in other applications. It’s not very nice for a third-party developer to have to rename all files to have the.mm extension.

You may think that one way to get around the problem described is to forward declare the C++ class instead of importing its header and then make the instance variable a pointer to an instance, as follows:

#import <Foundation/Foundation.h>

class SomeCppClass;

@interface EOCClass : NSObject {
@private
    SomeCppClass *_cppClass;
}
@end

The instance variable needs to be a pointer now, since if it were a nonpointer, the compiler wouldn’t be able to work out the size the instance variable needed to be and would cause an error. Pointers are all of fixed size, so the compiler simply needs to know the type to which the pointer is pointing. But this exhibits the same problem in that any class importing the EOCClassheader will meet the class keyword, which is a C++ keyword and therefore will need to be compiled as Objective-C++. This is ugly and unnecessary, since the instance variable is privateanyway, so why should other classes even care that it exists? Well, class-continuation category to the rescue once again. Using it makes the class look like so:

// EOCClass.h
#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
@end

// EOCClass.mm
#import "EOCClass.h"
#include "SomeCppClass.h"

@interface EOCClass () {
    SomeCppClass _cppClass;
}
@end

@implementation EOCClass
@end

Now the EOCClass header is free from any C++, and consumers of that header will not even be aware that underneath, it is riddled with C++. This pattern is seen in some of the system libraries, such as where WebKit, the web browser framework, is written largely in C++ and exposed through a clean Objective-C interface. This pattern is also seen in CoreAnimation, where a lot of the back-end code is written in C++ but exposed through a pure Objective-C interface.

Another good use of the class-continuation category is to expand properties that are read-only in the public interface but need to be set internally. Usually, you will want to set via the setter accessor method rather than direct access to the instance variable (see Item 7) because it will trigger Key-Value Observing (KVO) notifications, to which another object may be listening. Aproperty that appears in the class-continuation or any other category and in the class interface must have the exact same attributes, with the exception that the read-only status can be expanded to read-write. For example, consider a class representing a person with a public interface like this:

#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

Usually, a class-continuation category would be defined to expand the status of the two properties to read-write:

@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end

That is all that needs to be done. Now the implementation of EOCPerson is free to use the setter accessor either by calling setFirstName: or setLastName: or by using the property dot syntax. Doing so can be a very useful way to keep your objects immutable publicly but still be able to manage the data internally as required. Therefore, the data encapsulated by the class is controlled by the instance itself rather than being able to be changed externally. See Item 18 for more about this topic. Note that there is a potential for race conditions to be introduced with this approach if an observer reads a property at the same time as the property is written internally. Sensible use of synchronization (see Item 41) will mitigate this problem.

Another good use of the class-continuation category is to declare private methods that are going to be used only inside the implementation of the class. This is useful because it documents the methods available inside the class implementation. It looks like this:

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

Here, the prefix idea from Item 20 is used to indicate a private method. You don’t strictly need to declare methods before using them in recent compiler versions. However, it is still often a good idea to do so in a class-continuation category like this, as it is a way to document what methods exist in one place. I often like to write the method prototypes like this first, before implementing the class. Then I go through and implement the methods. It is a great way to improve readability of a class.

Finally, the class-continuation category is a good place to state that your object conforms to protocols that should be considered private. Often, you don’t want to leak information that you conform to a certain protocol in the public interface, maybe because the protocol is part of your private API. For example, consider that EOCPerson conformed to a protocol calledEOCSecretDelegate. Using the public interface, it would look like this:

#import <Foundation/Foundation.h>
#import "EOCSecretDelegate.h"

@interface EOCPerson : NSObject <EOCSecretDelegate>
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;

- (id)initWithFirstName:(NSString*)firstName
               lastName:(NSString*)lastName;
@end

You may think that you could just forward declare the EOCSecretDelegate protocol rather than importing it (or rather, the header file in which it is defined). You would forward declare it like this, instead of the import:

@protocol EOCSecretDelegate;

However, any place that the EOCPerson header would be imported, the compiler would emit the following warning:

warning: cannot find protocol definition for 'EOCSecretDelegate'

It’s warning you because it knows that it will never be able to know what methods are included in the protocol without being able to see its definition. But since this is a private internal protocol, you don’t even want to leak its name. Class-continuation category to the rescue again! Instead of declaring that EOCPerson conforms to EOCSecretDelegate in the public interface, you simply do it in the class-continuation category:

#import "EOCPerson.h"
#import "EOCSecretDelegate.h"

@interface EOCPerson () <EOCSecretDelegate>
@end

@implementation EOCPerson
/* ... */
@end

The public interface can have all references to EOCSecretDelegate removed. The private protocol is no longer exposed, and consumers would have to do deep introspection to find out that it exists.

Things to Remember

Image Use the class-continuation category to add instance variables to a class.

Image Redeclare properties in the class-continuation category as read-write if they are read-only in the main interface, if the setter accessor is required internally within the class.

Image Declare method prototypes for private methods within the class-continuation category.

Image Use the class-continuation category to declare protocols that your class conforms to that you want to keep private.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值