Cocoa Design Patterns: Two-Stage Creation
Cocoa relies on conventions established by the NSObject base class to allocate and initialize new instances of Cocoa classes. The reliance on conventions for something as central to an object-oriented language as instance creation may seem problematic at first, but it works well in practice. Several interrelated patterns are used to assure correct allocation and initialization of instances.
Many languages, such as Java, C++, Ruby, and Smalltalk use a method named “new” to allocate and initialize new instances. Even though NSObject implements a +new method, Cocoa developers seldom, if ever, use it. The Two-Stage Creation pattern separates the first stage, object memory allocation, from the second stage, object initialization. The Two-Stage Creation pattern must be followed to effectively use Cocoa.
Motivation
Two-Stage Creation gives programmers control over how objects are allocated in memory and simultaneously provides flexibility when initializing instances. Two-Stage Creation simplifies instance initialization when creating subclasses of Cocoa classes and provides methods for the convenient creation and initialization of temporary objects.
This chapter describes Cocoa’s Two-Stage Creation and explains how it achieves the following goals:
- Enable the use of initializers regardless of the way memory is allocated
- Avoid the need to implement too many initializers when subclassing
- Simplify the creation and use of temporary instances
A little history helps to emphasize the reasons why Cocoa uses Two-Stage Creation. Very old versions of the Objective-C class libraries that evolved into Cocoa used class methods to handle both allocation and initialization.
If allocation and initialization are combined in one method, that method has to be a class method because until allocation is complete, there is no instance. The pattern for implementing a single class method to both allocate and initialize an instance looks like the following:
+ (id)circleWithCenter:(NSPoint)aPoint radius:(float)radius { // allocation and partial initialization are provided by superclass id newInstance = [[super new] autorelease]; if(nil != newInstance) // verify new instance was created { [newInstance setCenter:aPoint]; [newInstance setRadius:radius]; [newInstance setLabel:@”default”]; } return newInstance; }
Initializing instances within class methods has many drawbacks. Combined allocation and initialization results in a combinatorial explosion of methods that must be implemented to handle all of the different ways objects might be allocated and initialized. Consider a hypothetical MYImage object that can be initialized with the contents of a file, information downloaded using a network Uniform Resource Locator (URL), arbitrary binary data, the user’s current copy/paste buffer, or empty with a specified size. Now consider that storage for images might be allocated from just two different types of memory, regular memory or graphics card memory. The MYImage class could easily end up with all of the following methods:
+imageFromRegularMemoryWithContentsOfFile: +imageFromRegularMemoryWithContentsOfURL: +imageFromRegularMemoryWithData: +imageFromRegularMemoryWithPasteboard: +imageFromRegularMemoryWithSize: +imageFromGraphicsMemory:(MYCardID)aCard withContentsOfFile: +imageFromGraphicsMemory:(MYCardID)aCard withContentsOfURL: +imageFromGraphicsMemory:(MYCardID)aCard withData: +imageFromGraphicsMemory:(MYCardID)aCard withPasteboard: +imageFromGraphicsMemory:(MYCardID)aCard withSize:
Now imagine that it is also possible to store images in special memory shared between processes or in memory that is mapped into the computer’s virtual address space but not actually allocated until needed. There may be additional ways image data is obtained such as by copying an existing image or by screen capture. By the time methods for all of the combinations are created, there are tens if not hundreds of different methods.
The problem of too many methods that create initialized instances really raises its ugly head when you try to create a subclass. It may be necessary for the subclass to reimplement every one of the superclass’s instance creation methods and provide new variants for whatever additional parameters are used to create instances of the new class.