< Previous PageNext Page >

Basic Subclass Design

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.

In this section:

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


The Form of a Subclass Definition

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.


Figure 3-4  Where to put declarations in the interface file

Where to put declarations in the interface file

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.

Overriding Superclass Methods

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.

Instance Variables

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:

Entry and Exit Points

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):

  1. initializeThis 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.

  2. 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.

  3. 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.

  4. awakeFromNibWhen 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.

  5. 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.

  6. deallocImplement 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.)

Initialize or Decode?

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

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:

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.

Key-Value Mechanisms

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.

To make each property of a subclass compliant with the requirements of key-value coding, do the following:

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”.

Object Infrastructure

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:

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.

Error Handling

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.

For further information about NSError objects, handling errors, and displaying error alerts, see Error Handling Programming Guide For Cocoa.

Resource Management and Other Efficiencies

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:

Functions, Constants, and Other C Types

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:

When the Class Is Public

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:



< Previous PageNext Page >


© 2006 Apple Computer, Inc. All Rights Reserved. (Last updated: 2006-12-20)


Did this document help you?
Yes: Tell us what works for you.
It’s good, but: Report typos, inaccuracies, and so forth.
It wasn’t helpful: Tell us what would have helped.