| < Previous PageNext Page > |
All subclasses, regardless of ancestor or role, share certain characteristics when they are well designed. A poorly designed subclass is susceptible to error, hard to use, difficult to extend, and a drag on performance. A well-designed subclass is quite the opposite. This article offers suggestions for designing subclasses that are efficient, robust, and both usable and reusable.
Further Reading: Although this document describes some of the mechanics of crafting a subclass, it focuses primarily on basic design. It does not describe how you can use the development applications Xcode and Interface Builder to automate part of class definition. To start learning about these applications, read Xcode Quick Tour Guide. The design information presented in this section is particularly applicable to model objects. Model Object Implementation Guide discusses the proper design and implementation of model objects at length.
The Form of a Subclass Definition
Overriding Superclass Methods
Instance Variables
Entry and Exit Points
Initialize or Decode?
Accessor Methods
Key-Value Mechanisms
Object Infrastructure
Error Handling
Resource Management and Other Efficiencies
Functions, Constants, and Other C Types
When the Class Is Public
An Objective-C class consists of an interface and an implementation. By convention, these two parts of a class definition go in separate files. The interface file (as with ANSI C header files) has an extension of .h; the implementation file has an extension of .m. The name of the file up to the extension is typically the name of the class. Thus for a class named Controller, the files would be:
Controller.h |
Controller.m |
The interface file contains a list of method (and function) declarations that establish the public interface of the class. It also holds declarations of instance variables, constants, string globals, and other data types. The directive @interface introduces the essential declarations of the interface, and the @end directive terminates the declarations. The @interface directive is particularly important because it identifies the name of the class and the class directly inherited from, using the form:
@interfaceClassName : Superclass
Listing 3-2 gives an example of the interface file for the hypothetical Controller class, before any declarations are made.
Listing 3-2 The basic structure of an interface file
#import <Foundation/Foundation.h> |
@interface Controller : NSObject { |
} |
@end |
To complete the class interface, you must make the necessary declarations in the appropriate places in this interface structure, as shown in Figure 3-4.
See “Instance Variables” and “Functions, Constants, and Other C Types” for more information about these types of declarations.
You begin the class interface by importing the appropriate Cocoa frameworks as well as the header files for any types that appear in the interface. The #import preprocessor command is similar to #include in that it incorporates the specified header file. However, #import improves on efficiency by including the file only if it was not previously included, directly or indirectly, for compilation. The angle brackets (<...>) following the command identify the framework the header file is from and, after a slash character, the header file itself. Thus the syntax for an #import line takes the form:
#import <Framework/File.h>
The framework must be in one of the standard system locations for frameworks. In the example in Listing 3-2, the class interface file imports the file Foundation.h from the Foundation framework. As in this case, the Cocoa convention is that the header file having the same name as the framework contains a list of #import commands that include all the public interfaces (and other public headers) of the framework. Incidentally, if the class you’re defining is part of an application, all you need to do is import the Cocoa.h file of the Cocoa umbrella framework.
If you must import a header file that is part of your project, use quotation marks rather than angle brackets as delimiters; for example:
#import "MyDataTypes.h" |
The class implementation file has a simpler structure, as shown by Listing 3-3. It is important that the class implementation file begin by importing the class interface file.
Listing 3-3 The basic structure of an implementation file
#import "Controller.h" |
@implementation Controller |
@end |
You must write all method implementations between the @implementation and @end directives. The implementations of functions related to the class can go anywhere in the file, although by convention they are also put between the two directives. Declarations of private types (functions, structures, and so on) typically go between the #import commands and the @implementation directive.
Custom classes, by definition, modify the behavior of their superclass in some program-specific way. Overriding superclass methods is usually the way to effect this change. When you design a subclass, an essential step is identifying the methods you are going to override and thinking about how you are going to re-implement them.
Although “When to Override a Method” offers some general guidelines, identifying which superclass methods to override requires you to investigate the class. Peruse the header files and documentation of the class. Also locate the designated initializer of the superclass because, for initialization to succeed, you must invoke the superclass version of this method in your class’s designated initializer. (See “Object Creation” for details.)
Once you have identified the methods, you can start—from a purely practical standpoint—by copying the declarations of the methods to be overridden into your interface file. Then copy those same declarations into your .m file as skeletal method implementations by replacing the terminating semicolon with enclosing braces.
Remember, as discussed in “When to Override a Method”, a Cocoa framework (and not your own code) usually invokes the framework methods that you override. In some situations you need to let the framework know that it should invoke your overridden version of a method and not the original method. Cocoa gives you various ways to accomplish this. The Interface Builder application, for example, allows you to substitute your class for a (compatible) framework class in its Info (inspector) window. If you create a custom NSCell class, you can associate it with a particular control using the NSControl setCell: method.
The reason for creating a custom class, aside from modifying the behavior of the superclass, is to add properties to it. “Properties” in this context means both the attributes of an instance of the subclass and the references that instance has to other objects (that its, its relationships). Given a hypothetical Circle class, if you are going to make a subclass that adds color to the shape, the subclass must somehow carry the attribute of color. To accomplish this, you might add a color instance variable (probably typed as an NSColor object) to the class interface. Instances of your custom class encapsulate the new attribute, holding it as a persistent, characterizing piece of data.
Instance variables in Objective-C are declarations of objects, structures, and other data types that are part of the class definition. If they are objects, the declaration can either dynamically type (using id) or statically type the object. The following example shows both styles:
id delegate; |
NSColor *color; |
Generally, you dynamically type an object instance variable when the class membership of the object is indeterminate or unimportant. Objects held as instances variables should be created, copied, or explicitly retained—unless they are already retained by a parent object (as is the case with delegates). If an instance is unarchived, it should retain its object instance variables as it decodes and assigns them in initWithCoder:. (For more on object archiving and unarchiving, see “Entry and Exit Points”.)
The convention for naming instance variables is to make them a lowercase string containing no punctuation marks or special characters. If the name contains multiple words, run them together, but make the first letter of the second and each succeeding word uppercase. For example:
NSString *title; |
NSColor *backgroundColor; |
NSRange currentSelectedRange; |
When the tag IBOutlet precedes the declarations of instance variables, it identifies an outlet with (presumably) a connection archived in a nib file. The tag also enables Interface Builder and Xcode to coordinate their activities. Outlets unarchived from an Interface Builder nib file are automatically retained.
Instance variables can hold more than an object’s attributes, vendable to clients of the object. Sometimes instance variables can hold private data that is used as the basis for some task performed by the object; a backing store or a cache are examples. (If data is not per-instance, but is to be shared among instances of a class, use global variables instead of instance variables.)
When you add instance variables to a subclass, it’s worthwhile to heed a few guidelines:
Add only instance variables that are absolutely necessary. The more instance variables you add, the more the instance size swells. And the more instances of your class that are created, the more of a concern this becomes. If it’s possible, compute a critical value from existing instance variables rather than add another instance variable to hold that value.
For the same reason of economy, try to represent the instance data of the class efficiently. For example, if you want to specify a number of flags as instance variables, use a bit field instead of a series of Boolean declarations. (Be aware, however, that there are archiving complications with bit fields). You might use an NSDictionary object to consolidate a number of related attributes as key-value pairs; if you do this, make sure that the keys are adequately documented.
Give the proper scope to your instance variables. Never scope a variable as @public as this violates the principle of encapsulation. Use @protected for instance variables if you know which are the likely subclasses of your class (such as the classes of your application) and you require efficient access of data. Otherwise, @private is a reasonable choice for the high degree of implementation hiding it provides. This implementation hiding is especially important for classes exposed by a framework and used by applications or other frameworks; it permits the modification of the class implementation without the need to recompile all clients.
Make sure there are accessor methods for those instance variables that are essential properties of the class. (Accessor methods get and set the values of instance variables.) See “Accessor Methods” for more information on this subject.
If you intend your subclass to be public—that is, you anticipate that others might subclass it—pad the end of the instance variable list with a reserved field, usually typed as id. This reserved field helps to ensure binary compatibility if, at any time in the future, you need to add another instance variable to the class. See “When the Class Is Public” for more information on preparing public classes.
The Cocoa frameworks send an object messages at various points in the object’s life. Almost all objects (including classes, which are really objects themselves) receive certain messages at the beginning of their runtime life and just before their destruction. The methods invoked by these messages (if implemented) are “hooks” that allow the object to perform tasks relevant to the moment. These methods are (in the order of invocation):
initialize—This class method enables a class to initializes itself before it or its instances receive any other messages. Superclasses receive this message before subclasses. Classes that use the old archiving mechanism can implement initialize to set their class version. Other possible uses of initialize are registering a class for some service and initializing some global state that is used by all instances. However, sometimes it’s better to do these kinds of tasks lazily (that is, the first time they’re needed) in an instance method rather than in initialize.
init (or other initializer)—You must implement init or some other primary initializer to initialize the state of the instance unless the work done by the superclass designated initializer is sufficient for your class. See “Object Creation” for information on initializers, including designated initializers.
initWithCoder:—If you expect objects of your class to be archived—for example, yours is a model class— you should adopt (if necessary) the NSCoding protocol and implement its two methods, initWithCoder: and encodeWithCoder: . In these methods you must decode and encode, respectively, the instance variables of the object in a form suitable for an archive. You can use the decoding and encoding methods of NSCoder for this purpose or the key-archiving facilities of NSKeyedArchiver and NSKeyedUnarchiver. When the object is being unarchived instead of being explicitly created, the initWithCoder: method is invoked instead of the initializer. In this method, after decoding the instance-variable values, you assign them to the appropriate variables, retaining or copying them if necessary. For more on this subject, see Archives and Serializations Programming Guide for Cocoa.
awakeFromNib—When an application loads a nib file, an awakeFromNib message is sent to each object loaded from the archive, but only if it can respond to the message, and only after all the objects in the archive have been loaded and initialized. When an object receives an awakeFromNib message, it’s guaranteed to have all its outlet instance variables set. Typically, the object that owns the nib file (File’s Owner) implements awakeFromNib to perform programmatic initializations that require outlet and target-action connections to be set.
encodeWithCoder:—Implement this method if instances of your class should be archived. This method is invoked just prior to the destruction of the object. See the description of initWithCoder: above.
dealloc—Implement this method to release instance variables and deallocate any other memory claimed by an instance of your class. The instance is destroyed soon after this method returns. See “Object Creation” for further information on the dealloc method.
An application’s global application object (identified as NSApp) also sends messages to an object—if it’s the NSApp delegate and implements the appropriate methods—at the beginning and end of the application’s life. Just after the application launches, NSApp sends its delegate the messages applicationWillFinishLaunching: and applicationDidFinishLaunching: (the former is sent just before any double-clicked document is opened, the latter just afterwards). In either of these methods the delegate can specify global application logic that needs to happen just once early in the application’s runtime existence. Just before it terminates, NSApp sends applicationWillTerminate:. to its delegate, which can implement the method to gracefully handle program termination by saving documents and application state.
The Cocoa frameworks give you numerous other hooks for events ranging from window closing to application activation and deactivation. These are usually implemented as delegation messages and require your object to be the delegate of the framework object and to implement the required method. Sometimes the hooks are notifications. (See “Communicating With Objects” for more on delegation.)
If you want objects of your class to be archived and unarchived, the class must conform to the NSCoding protocol; it must implement the methods that encode its objects (encodeWithCoder:) and decode them (initWithCoder:). Instead of an initializer method or methods, the initWithCoder: method is invoked to initialize an object being created from an archive.
Because the class's initializer method and initWithCoder: might be doing much of the same work, it makes sense to centralize that common work in a helper method called by both the initializer and initWithCoder:. For example, if as part of its set-up routine, an object specifies drag types and dragging source, it might implement something such as shown in Listing 3-4.
Listing 3-4 Initialization helper method
(id)initWithFrame:(NSRect)frame { |
self = [super initWithFrame:frame]; |
if (self) { |
[self setTitleColor:[NSColor lightGrayColor]]; |
[self registerForDragging]; |
} |
return self; |
} |
- (id)initWithCoder:(NSCoder *)aCoder { |
self = [super initWithCoder:aCoder]; |
titleColor = [[aCoder decodeObject] copy]; |
[self registerForDragging]; |
return self; |
} |
- (void)registerForDragging { |
[theView registerForDraggedTypes: |
[NSArray arrayWithObjects:DragDropSimplePboardType, NSStringPboardType, |
NSFilenamesPboardType, nil]]; |
[theView setDraggingSourceOperationMask:NSDragOperationEvery forLocal:YES]; |
} |
An exception to the parallel roles of a class initializer and initWithCoder: is when you create a custom subclass of a framework class whose instances appear on an Interface Builder palette. The general procedure is to define the class in your project, drag the object from the Interface Builder palette into your interface, and then associate that object with your custom subclass using the Custom Class pane of the Interface Builder Info window. However, in this case the initWithCoder: method for the subclass is not invoked during unarchiving; an init message is sent instead. You should perform any special set-up tasks for your custom object in awakeFromNib, which is invoked after all objects in a nib file have been unarchived.
Accessor methods (or, simply, “accessors”) get and set the values of instance variables. They are an essential part of the interface of a well-designed class. They function as if they were the gates to an object’s properties, regulating access to them and enforcing the encapsulation of the object’s instance data.
For reasons of convention—and because it enables classes to be compliant with key-value coding (see “Key-Value Mechanisms”)—the names of accessor methods must have a specific form. For a method that returns the value of an instance variable (sometimes called the getter), the name of the method is simply the name of the instance variable. For a method that sets the value of an instance variable (the setter), the name begins with “set” and is immediately followed by the name of the instance variable (with the first letter uppercase). For example, if you had an instance variable named “color,” the declarations of the getter and setter accessors would be:
- (NSColor *)color; |
- (void)setColor:(NSColor *)aColor; |
If an instance variable is a scalar C type, such as int or float, the implementations of accessor methods tend to be very simple. Assuming an instance variable named currentRate, of type float, Listing 3-5 shows how to implement the accessor methods.
Listing 3-5 Implementing accessors for a non-object instance variable
- (float)currentRate { |
return currentRate; |
} |
- (void)setCurrentRate:(float)newRate { |
currentRate = newRate; |
} |
When instance variables hold objects, the situation becomes more nuanced. Because they are instance variables, these objects must be persistent and so must be created, copied, or retained when they are assigned. When a setter accessor changes the value of an instance variable, it is responsible not only for ensuring persistence, but for properly disposing of the old value. The getter accessor vends the value of the instance variable to the object requesting it. The operations of both types of accessors have implications for memory management given two assumptions from the Cocoa object-ownership policy:
Objects returned from methods (such as from getter accessors) are valid in the scope of the invoking object. In other words, it is guaranteed that the object will not be released or change value within that scope (unless documented otherwise).
When an invoking object receives an object from a method such as an accessor, it should not release the object unless it has explicitly retained (or copied) it first.
With these two assumptions in mind, let’s look at two possible implementations of a getter and setter accessor for an NSString instance variable named title. Listing 3-6 shows the first.
Listing 3-6 Implementing accessors for an object instance variable—good technique
- (NSString *)title { |
return title; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (title != newTitle) { |
[title autorelease]; |
title = [newTitle copy]; |
} |
} |
Note that the getter accessor simply returns a reference to the instance variable. The setter accessor, on the other hand, is busier. After verifying that the passed-in value for the instance variable isn’t the same as the current value, it autoreleases the current value before copying the new value to the instance variable. (Sending autorelease to the object is “more thread-safe” than sending it release.) However, there is still a potential danger with this approach. What if a client is using the object returned by the getter accessor and meanwhile the setter accessor autoreleases the old NSString object and then, soon after, that object is released and destroyed? The client object’s reference to the instance variable would no longer be valid.
Listing 3-7 shows another implementation of the accessor methods that works around this problem by retaining and then autoreleasing the instance variable value in the getter method.
Listing 3-7 Implementing accessors for an object instance variable—better technique
- (NSString *)title { |
return [[title retain] autorelease]; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (title != newTitle) { |
[title release]; |
title = [newTitle copy]; |
} |
} |
In both examples of the setter method above (Listing 3-6 and Listing 3-7), the new NSString instance variable is copied rather than retained. Why isn’t it retained? The general rule is this: When the object assigned to an instance variable is a value object—that is, an object that represents an attribute such as a string, date, number, or corporate record—you should copy it. You are interested in preserving the attribute value, and don't want to risk having it mutate from under you. In other words, you want your own copy of the object.
However, if the object to be stored and accessed is an entity object, such as an NSView or an NSWindow object, you should retain it. Entity objects are more aggregate and relational, and copying them can be expensive. One way to determine whether an object is an value object or entity object is to decide whether you’re interested in the value of the object or in the object itself. If you’re interested in the value, it’s probably a value object and you should copy it (assuming, of course, the object conforms to the NSCopying protocol).
Another way to decide whether to retain or copy an instance variable in a setter method is to determine if the instance variable is an attribute or relationship. This is especially true for model objects, which are objects that represent the data of an application. An attribute is essentially the same thing as a value object: an object that is a defining characteristic of the object that encapsulates it, such as a color (NSColor object) or title (NSString object). A relationship on the other hand is just that: a relationship with (or reference to) one or more other objects. Generally, in setter methods you copy the values of attributes and you retain relationships. However, relationships have a cardinality; they can be one-to-one or one-to-many. One-to-many relationships, which are typically represented by collection objects such as NSArray instances or NSSet instances, may require setter methods to do something other than simply retain the instance variable. See Model Object Implementation Guide for details. For more on object properties as either attributes or relationships, see “Key-Value Mechanisms”.
If the setter accessors of a class are implemented as shown in either Listing 3-6 or Listing 3-7, to deallocate an instance variable in the class’s dealloc method, all you need to do is invoke the appropriate setter method with an argument of nil.
Several mechanisms with “key-value” in their names are fundamental parts of Cocoa: key-value binding, key-value coding, and key-value observing. They are essential ingredients of Cocoa technologies such as bindings, which automatically communicate and synchronize values between objects. They also provide at least part of the infrastructure for making applications scriptable—that is, responsive to AppleScript commands. Key-value coding and key-value observing are especially important considerations in the design of a custom subclass.
The term “key-value” refers to the technique of using the name of a property as the key to obtain its value. The term is part of the vocabulary of the object modeling pattern, which, in turn, derives from the entity-relationship modeling used for describing relational databases. In object modeling, objects—particularly the data-bearing model objects of the Model-View-Controller pattern—have properties, which usually (but not always) take the form of instance variables. A property can either be an attribute, such as name or color, or it can be a reference to one or more other objects. These references are known as relationships, and relationships can be one-to-one or one-to-many. The network of objects in a program form an object graph through their relationships with each other. In object modeling you can use key paths—strings of dot-separated keys—to traverse the relationships in an object graph and access the properties of objects.
Note: For a full description of object modeling, see “Object Modeling”.
Key-value binding, key-value coding, and key-value observing are enabling mechanisms for this traversal.
Key-value binding (KVB) establishes bindings between objects, and also removes and advertises those bindings. It makes use of several informal protocols. A binding for a property must specify the object and a key path to the property.
Key-value coding (KVC), through an implementation of the NSKeyValueCoding informal protocol, makes it possible to get and set the values of an object’s properties through keys without having to invoke that object’s accessor methods directly. (Cocoa provides a default implementation of the protocol.) A key typically corresponds to the name of an instance variable or accessor method in the object being accessed.
Key-value observing (KVO), through an implementation of the NSKeyValueObserving informal protocol, allows objects to register themselves as observers of other objects. The observed object directly notifies its observers when there are changes to one of its properties. Cocoa implements automatic observer notifications for each property of a KVO-compliant object.
To make each property of a subclass compliant with the requirements of key-value coding, do the following:
For an attribute or a to-one relationship named key, implement accessor methods named key (getter) and setKey: (setter). For example, if you have a property named salary, you would have accessor methods salary and setSalary:.
For a to-many relationship, if the property is based on an instance variable that is a collection (for example, an NSArray object) or an accessor method that returns a collection, give the getter method the same name as the property (for example, employees). If the property is mutable, but the getter method doesn’t return a mutable collection (such as NSMutableArray), you must implement insertObject:inKeyAtIndex: and removeObjectFromKeyAtIndex:. If the instance variable is not a collection and the getter method does not return an collection, you must implement other NSKeyValueCoding methods.
To make your object KVO-compliant, simply ensure that your object is KVC-complaint if you are satisfied with automatic observer notification. However, you may chose to implement manual key-value observing, which requires additional work.
Further Reading: Read Key-Value Coding Programming Guide and Key-Value Observing Programming Guide to learn more about these technologies. For a summary of the Cocoa bindings technology, which uses KVC, KVO, and KVB, see “Bindings” in “Communicating With Objects”.
If a subclass is well-designed, instances of that class will behave in ways expected of Cocoa objects. Code using the object can compare it to other instances of the class, discover its contents (for example, in a debugger), and perform similar basic operations with the object.
Custom subclass should implement most, if not all, of the following root-class and basic protocol methods:
isEqual: and hash—Implement these NSObject methods to bring some object-specific logic to their comparison. For example, if what individuates instances of your class is their serial number, make that the basis for equality. For further information, see “Introspection”.
description—Implement this NSObject method to return a string that concisely describes the properties or contents of the object. This information is returned by the print object command in the gdb debugger and is used by the %@ specifier for objects in formatted strings. For example, say you have a hypothetical Employee class with attributes for name, date of hire, department, and position ID. The description method for this class might look like the following:
- (NSString *)description { |
return [NSString stringWithFormat:@"Employee:Name = %@, |
Hire Date = %@, Department = %@, Position = %i\n", [self name], |
[[self dateOfHire] description], [self department], |
[self position]]; |
} |
copyWithZone:—If you expect clients of your class to copy instances of it, you should implement this method of the NSCopying protocol. Value objects, including model objects, are typical candidates for copying; objects such as NSWindow and NSColorPanel are not. If instances of your class are mutable, you should instead conform to the NSMutableCopying protocol.
initWithCoder: and encodeWithCoder:—If you expect instances of your class to be archived (for example, a model class), you should adopt the NSCoding protocol (if necessary) and implement these two methods. See “Entry and Exit Points” for more on the NSCoding methods.
If any of the ancestors of your class adopts a formal protocol, you must also ensure that your class properly conforms to the protocol. That is, if the superclass implementation of any of the protocol methods is inadequate for your class, your class should re-implement the methods.
It is axiomatic for any programming discipline that the programmer should properly handle errors. However, what is “proper” often varies by programming language, application environment, and other factors. Cocoa has its own set of conventions and prescriptions for handling errors in subclass code.
If the error encountered in a method implementation is a system-level or Objective-C runtime error, create and raise an exception, if necessary, and handle it locally, if possible.
In Cocoa, exceptions are typically reserved for programming or unexpected runtime errors such as out-of-bounds collection access, attempts to mutate immutable objects, sending an invalid message, and losing the connection to the window server. You usually take care of these errors with exceptions when an application is being created rather than at runtime. Cocoa predefines several exceptions that you can catch with an exception handler. For information on predefined exceptions and the procedure and API for raising and handling exceptions, see Exception Programming Topics for Cocoa.
For other sorts of errors, including expected runtime errors, return nil, NO, NULL, or some other type-suitable form of zero to the caller. Examples of these errors include the inability to read or write a file, a failure to initialize an object, the inability to establish a network connection, or a failure to locate an object in a collection. Use an NSError object if you feel it necessary to return supplemental information about the error to the sender.
An NSError object encapsulates information about an error, including an error code (which can be specific to the Mach, POSIX, or OSStatus domains) and a dictionary of program-specific information. The negative value that is directly returned (nil, NO, and so on) should be the principal indicator of error; if you do communicate more specific error information, return an NSError object indirectly in an argument of the method.
If the error requires a decision or action from the user, display an alert dialog.
Use an instance of the NSAlert class (and related facilities) for displaying an alert dialog and handling the user’s response. See Dialogs and Special Panels for information.
For further information about NSError objects, handling errors, and displaying error alerts, see Error Handling Programming Guide For Cocoa.
There are all sorts of things you can do to enhance the performance of your objects and, of course, the application that incorporates and manages those objects. These procedures and disciplines range from multithreading to drawing optimizations and techniques for reducing your code footprint. You can learn more about these things by reading Cocoa Performance Guidelines and other performance documents.
Yet you can, before even adopting a more advanced performance technique, improve the performance of your objects significantly by following three simple, common-sense guidelines:
Don’t load a resource or allocate memory until you really need it.
If you load a resource of the program, such as a nib file or an image, and don’t use it until much later, or don’t use it at all, that is a gross inefficiency. Your program’s memory footprint is bloated for no good reason. You should load resources or allocate memory only when there is an immediate need.
For example, if your application’s preferences window is in a separate nib file, don’t load that file until the user first chooses Preferences from the application menu. The same caution goes for allocating memory for some task; hold off on allocation until the need for that memory arises. This technique of lazy-loading or lazy-allocation is easily implemented. For example, say your application has an image that it loads, upon the first user request, for displaying in the user interface. Listing 3-8 shows one approach: loading the image in the getter accessor for the image.
Listing 3-8 Lazy-loading of a resource
- (NSImage *)fooImage { |
if (!fooImage) { // fooImage is an instance variable |
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"Foo" ofType:@"jpg"]; |
if (!imagePath) return nil; |
fooImage = [[NSImage alloc] initWithContentsOfFile:imagePath]; |
} |
return fooImage; |
} |
Use the Cocoa API; don’t dig down into lower-level programming interfaces.
Much effort has gone into making the implementations of the Cocoa frameworks as robust, secure, and efficient as possible. Moreover, these implementations manage interdependencies that you might not be aware of. If, instead of using the Cocoa API for a certain task, you decide to “roll your own” solution using lower-level interfaces, you’ll probably end up writing more code, which introduces a greater risk of error or inefficiency. Also, by leveraging Cocoa, you are better prepared to take advantage of future enhancements while at the same time you are more insulated from changes in the underlying implementations. So use the Cocoa alternative for a programming task if one exists. For example, instead of using BSD routines to seek through an open file, use the NSFileHandle class; or use NSBezierPath and NSColor methods for drawing rather than calling the related Quartz (Core Graphics) functions.
Practice good memory-management techniques.
Perhaps the most important single thing you can do to improve the efficiency and robustness of your custom objects is to practice good memory-management techniques. Make sure every object allocation, copy, or retain is matched with a release. Become familiar with the policies and techniques for proper memory management and practice, practice, practice them. See Memory Management Programming Guide for Cocoa for complete details.
Because Objective-C is a superset of ANSI C, it permits any C type in your code, including functions, typedef structures, enum constants, and macros. An important design issue is when and how to use these types in the interface and implementation of custom classes.
The following list offers some guidance on using C types in the definition of a custom class:
Define a function rather than a method for functionality that is often requested but doesn’t need to be overridden by subclasses. The reason for doing this is performance. In these cases, it’s best for the function to be private rather then being part of the class API. You can also implement functions for behavior that is not associated with any class (because it is global) or for operations on simple types (C primitives or structures). However, for global functionality it might be better—for reasons of extensibility—to create a class from which a singleton instance is generated.
Define a structure type rather than a simple class when the following conditions apply:
You don’t expect to augment the list of fields.
All the fields are public (for performance reasons).
None of the fields is dynamic (dynamic fields might require special handling, such as retaining or releasing).
You don’t intend to make use of object-oriented techniques, such as subclassing.
Even when all these conditions apply, the primary justification for structures in Objective-C code is performance. In other words, if there is no compelling performance reason, a simple class is preferable.
Declare enum constants rather than #define constants. The former are more suitable for typing, and you can discover their values in the debugger.
When you are defining a custom class for your own use, such as a controller class for an application, you have great flexibility because you know the class well and can always redesign it if you have to. However, if your custom class will likely be a superclass for other developers—in other words, if it’s a public class—the expectations others have of your class mean that you have to be more careful of your design.
Here are few guidelines for developers of public Cocoa classes:
Make sure those instance variables that subclasses need to access are scoped as @protected.
Add one or two reserved instance variables to help ensure binary compatibility with future versions of your class (see “Instance Variables”).
Implement your accessor methods to properly handle memory management of vended objects (see “Accessor Methods”). Clients of your class should get back autoreleased objects from your getter accessors.
Observe the naming guidelines for public Cocoa interfaces as recommended in Coding Guidelines for Cocoa.
Document the class, at least with comments in the interface file.
| < Previous PageNext Page > |
© 2006 Apple Computer, Inc. All Rights Reserved. (Last updated: 2006-12-20)
|