In this chapter you'll learn about how to add methods to a class in a modular fashion through the use of categories, how one class can substitute for another one, and how to create a standardized list of methods for others to implement.
Sometimes you might be working with a class definition and want to add some new methods to it. For example, you might decide for your Fraction
class that, in addition to the add:
method for adding two fractions, you'd like to have methods to subtract, multiply, and divide two fractions.
As another example, say you are working on a large programming project and as part of that project a new class is being defined containing many different methods. You have been assigned the task of writing methods for the class that work with the file system, for example. Other project members have been assigned methods responsible for creating and initializing instances of the class, performing operations on objects in the class, and drawing representations of objects from the class on the screen.
As a final example, suppose you've learned how to use a class from the library (for example, the Foundation framework's array class called NSArray
) and realize that there are one or more methods that you wish the class had implemented. Of course, you could write a new subclass of the NSArray
class and implement the new methods, but perhaps an easier way exists.
A practical solution for all these situations is one word: categories. A category provides an easy way for you to modularize the definition of a class into groups or categories of related methods. It also gives you an easy way to extend an existing class definition without even having access to the original source code for the class and without having to create a subclass. It is a powerful yet easy concept for you to learn.
Let's get back to the first case and show how to add a new category to the Fraction
class to handle the four basic math operations. We'll first show you the original Fraction
interface section:
#import <objc/Object.h>
#import <stdio.h>
// Define the Fraction class
@interface Fraction : Object
{
int numerator;
int denominator;
}
// setters
-(void) setNumerator: (int) n;
-(void) setDenominator: (int) d;
-(void) setTo: (int) n over: (int) d;
// getters
-(int) numerator;
-(int) denominator;
// utility
-(Fraction *) add: (Fraction *) f;
-(void) reduce;
-(double) convertToNum;
-(void) print;
@end
Next, let's remove the add:
method from this interface section and add it to a new category, along with the other three math operations you want to implement. Here's what the interface section would look like for your new MathOps
category:
#import "Fraction.h"
@interface Fraction (MathOps)
-(Fraction *) add: (Fraction *) f;
-(Fraction *) mul: (Fraction *) f;
-(Fraction *) sub: (Fraction *) f;
-(Fraction *) div: (Fraction *) f;
@end
Realize that, even though this is an interface section definition, it is an extension to an existing one. Therefore, you must include the original interface section so that the compiler knows about the Fraction
class (unless you incorporate the new category directly into the original Fraction.h
header file, which is an option).
After the #import
, you see the following line:
@interface Fraction (MathOps)
This tells the compiler you are defining a new category for the Fraction
class and that it's name is MathOps
. The category name is enclosed in a pair of parentheses after the class name. Notice that you don't list the Fraction
's parent class here; the compiler already knows it from Fraction.h
. Also, you don't tell it about the instance variables, as you've done in all the previous interface sections you've defined. In fact, if you try to list the parent class or the instance variables, you'll get a syntax error from the compiler.
This interface section tells the compiler you are adding an extension to the class called Fraction
under the category named MathOps
. The MathOps
category contains four instance methods: add:
, mul:
, sub:
, and div:
. Each method takes a fraction as its argument and returns one as well.
You can put the definitions for all your methods into a single implementation section. That is, you could define all the methods from the interface section in Fraction.h
plus all the methods from the MathOps
category in one implementations section. Alternatively, you could define your category's methods in a separate implementation section. In such a case, the implementation section for these methods must also identify the category to which the methods belong. As with the interface section, you do this by enclosing the category name inside parentheses after the class name, like so:
@implementation Fraction (MathOps)
// code for category methods
...
@end
In Program 11.1, the interface and implementation sections for the new MathOps
category are grouped together, along with a test routine, into a single file.
Program 11.1. MathOps Category and Test Program
#import "Fraction.h"
@interface Fraction (MathOps)
-(Fraction *) add: (Fraction *) f;
-(Fraction *) mul: (Fraction *) f;
-(Fraction *) sub: (Fraction *) f;
-(Fraction *) div: (Fraction *) f;
@end
@implementation Fraction (MathOps);
-(Fraction *) add: (Fraction *) f
{
// To add two fractions:
// a/b + c/d = ((a*d) + (b*c)) / (b * d)
Fraction *result = [[Fraction alloc] init];
int resultNum, resultDenom;
resultNum = (numerator * [f denominator]) +
(denominator * [f numerator]);
resultDenom = denominator * [f denominator];
[result setTo: resultNum over: resultDenom];
[result reduce];
return result;
}
-(Fraction *) sub: (Fraction *) f
{
// To sub two fractions:
// a/b - c/d = ((a*d) - (b*c)) / (b * d)
Fraction *result = [[Fraction alloc] init];
int resultNum, resultDenom;
resultNum = (numerator * [f denominator]) -
(denominator * [f numerator]);
resultDenom = denominator * [f denominator];
[result setTo: resultNum over: resultDenom];
[result reduce];
return result;
}
-(Fraction *) mul: (Fraction *) f
{
Fraction *result = [[Fraction alloc] init];
result setTo: numerator * [f numerator]
over: denominator * [f denominator]];
[result reduce];
return result;
}
-(Fraction *) div: (Fraction *) f
{
Fraction *result = [[Fraction alloc] init];
[result setTo: numerator * [f denominator]
over: denominator * [f numerator]];
[result reduce];
return result;
}
@end
int main (int argc, char *argv[])
{
Fraction *a = [[Fraction alloc] init];
Fraction *b = [[Fraction alloc] init];
Fraction *result;
[a setTo: 1 over: 3];
[b setTo: 2 over: 5];
[a print]; printf (" + "); [b print]; printf (" = ");
result = [a add: b];
[result print];
printf ("
");
[result free];
[a print]; printf (" - "); [b print]; printf (" = ");
result = [a sub: b];
[result print];
printf ("
");
[result free];
[a print]; printf (" * "); [b print]; printf (" = ");
result = [a mul: b];
[result print];
printf ("
");
[result free];
[a print]; printf (" / "); [b print]; printf (" = ");
result = [a div: b];
[result print];
printf ("
");
[result free];
[a free];
[b free];
return 0;
}
1/3 + 2/5 = 11/15
1/3 - 2/5 = -1/15
1/3 * 2/5 = 2/15
1/3 / 2/5 = 5/6
Realize once again that it is certainly legal in Objective-C to write a statement such as this:
[[a div: b] print];
This line directly prints the result of dividing Fraction a
by b
and thereby avoids the intermediate assignment to the variable result
, as was done in Program 11.1. However, you need to perform this intermediate assignment so you can capture the resulting Fraction
and subsequently release its memory. Otherwise, your program will leak memory every time you perform an arithmetic operation on a fraction.
Program 11.1 puts the interface and implementation sections for the new category into the same file with the test program. As mentioned previously, the interface section for this category could either go in the original Fraction.h
header file so that all methods would be declared in one place or in its own header file.
If you put your category into a master class definition file, all users of the class will have access to the methods in the category. If you don't have the ability to modify the original header file directly (consider adding a category to an existing class from a library, as shown in Part II, “The Foundation Framework”), you have no choice but to keep it separate.
Some points are worth mentioning about categories. First, although a category has access to the instance variables of the original class, it can't add any of its own. If you need to do that, consider subclassing.
Also, a category can override another method in the class, but this is typically considered poor programming practice. For one thing, after you override a method, you can no longer access the original method. Therefore, you must be careful to duplicate all the functionality of the overridden method in your replacement. If you do need to override a method, subclassing might be the right choice. If you override a method in a subclass, you can still reference the parent's method by sending a message to super
. So, you don't have to understand all the intricacies of the method you are overriding; you can simply invoke the parent's method and add your own functionality to the subclass's method.
You can have as many categories as you like, following the rules we've outlined here. If a method is defined in more than one category, the language does not specify which one will be used.
Unlike a normal interface section, you don't need to implement all the methods in a category. That's useful for incremental program development because you can declare all the methods in the category and implement them over time.
Remember that extending a class by adding new methods with a category affects not just that class, but all its subclasses as well. This can be potentially dangerous if you add new methods to the root object Object
, for example, because everyone will inherit those new methods, whether that was your intention.
The new methods you add to an existing class through a category can serve your purposes just fine, but they might be inconsistent with the original design or intentions of the class. Turning a Square
into a Circle
(admittedly an exaggeration), for example, by adding a new category and some methods muddies the definition of the class and is not good programming practice.
Also, object/category named pairs must be unique. Only one NSString (Private)
category can exist in a given Objective-C namespace. This can be tricky because the Objective-C namespace is shared between the program code and all the libraries, frameworks, and plug-ins. This is especially important for Objective-C programmers writing screensavers, preference panes, and other plug-ins because their code will be injected into application or framework code they do not control.
You can “fake out” the Objective-C system by pretending to be a class that you're not. This act of deception is known as posing, and it is supported by the poseAs:
method.1
For this substitution to occur, you have to be a subclass of the class you want to masquerade as. You also need to send the poseAs:
message to the class before you've allocated any instances of the class or sent it any messages. The subclass also can't add any new instance variables, which makes sense when you consider it will be filling in for the parent when it poses for it.
Program 11.2 shows how a class called FractionB
can be used to pose as the Fraction
class. We did this to override the print
method from the Fraction
class, but without having to change all references of the class named Fraction
to FractionB
. This is one of the qualities that makes posing so appealing.
#import "Fraction.h"
@interface FractionB: Fraction
-(void) print;
@end
@implementation FractionB;
-(void) print
{
printf (" (%i/%i) ", numerator, denominator);
}
@end
int main (int argc, char *argv[])
{
Fraction *a;
Fraction *b;
Fraction *result;
[FractionB poseAs: [Fraction class]];
a = [[Fraction alloc] init];
b = [[Fraction alloc] init];
[a setTo: 1 over: 3];
[b setTo: 2 over: 5];
[a print]; printf (" + "); [b print]; printf (" = ");
result = [a add: b];
[result print];
printf ("
");
[a free];
[b free];
[result free];
return 0;
}
(1/3) + (2/5) = (11/15)
FractionB
is defined as a subclass of Fraction
. It contains one method called print
, which is defined to override the parent's method. Of course, posing is not just used for overriding methods; new methods can be added as well. The new print
method puts parentheses around each fraction it displays, just for the heck of it!
Before you send any messages to the Fraction
class, you must invoke the poseAs:
method on it with the following message expression:
[FractionB poseAs: [Fraction class]];
The argument to poseAs:
is a class object, which you've seen how to obtain using the class
method.
After sending the poseAs:
message to the FractionB
class, all messages sent to the Fraction
class instead go to the FractionB
class. Naturally, because FractionB
is a subclass of Fraction
, it gets all its methods, except those it overrides.
Now when you allocate and initialize a Fraction
with
a = [[Fraction alloc] init];
the inherited methods in the FractionB
class are invoked. And when you print the result of the addition with
[result print];
the overridden print
method is invoked. The nice thing here is that you can define one class to replace another and simply plug it in by issuing the poseAs:
message. This eliminates the need to go through the program and change all the class uses to the name of the new subclass.
Categories and posing share much in common. One subtle difference, though, is that if you override a method with a category, you can't access the overridden method. When posing, however, you can access the overridden method by sending a message to super
. So, the print
method from FractionB
could have still invoked the print
method from Fraction
if it wanted to, as follows:
[super print];
Posing also comes in handy when you need to fix a bug in a method for which you don't have access to the source code. You can define a subclass, override the method, and then pose as the parent class.
A protocol is a list of methods that is shared among classes. The methods listed in the protocol do not have corresponding implementations; they're meant to be implemented by someone else (like you!). A protocol provides a way to define a set of methods with a specified name that are somehow related. The methods are typically documented so you know how they are to perform and so you can implement them in your own class definitions if desired.
If you decide to implement all of the methods for a particular protocol, you are said to conform to or adopt that protocol.
Defining a protocol is easy: You simply use the @protocol
directive followed by the name of the protocol, which is up to you. After that, you declare methods just as you have done with your interface section. All the method declarations, up to the @end
directive, become part of the protocol.
If you choose to work with the Foundation framework, you'll find that several protocols are defined. One of them is called NSCopying
, and it declares a method you need to implement if your class is to support copying of objects through the copy
(or copyWithZone:
) method. (The topic of copying objects is covered in detail in Chapter 18, “Copying Objects.”)
Here's how the NSCopying
protocol is defined in the standard Foundation header file NSObject.h
:
@protocol NSCopying
- (id)copyWithZone: (NSZone *)zone;
@end
If you adopt the NSCopying
protocol in your class, you must implement a method called copyWithZone:
. You tell the compiler you are adopting a protocol by listing the protocol name inside a pair of angular brackets (<
…>
) on the @interface
line. The protocol name comes after the name of the class and its parent class, as in the following:
@interface AddressBook: NSObject <NSCopying>
This says that AddressBook
is an object whose parent is NSObject
and which conforms to the NSCopying
protocol.2 Because the system already knows about the method(s) previously defined for the protocol (in this example it knows from the header file NSObject.h
), you don't declare the methods in the interface section. However, you need to define them in your implementation section.
So, in this example, in the implementation section for AddressBook
, the compiler expects to see the copyWithZone:
method defined.
If your class adopts more than one protocol, just list them inside the angular brackets, separated by commas, like so:
@interface AddressBook: NSObject <NSCopying, NSCoding>
This tells the compiler that the AddressBook
class adopts the NSCopying
and NSCoding
protocols. Again, the compiler will expect to see all the methods listed for those protocols implemented in the AddressBook
implementation section.
If you define your own protocol, you don't have to actually implement it yourself. However, you're alerting other programmers that if they want to adopt the protocol that they in fact do have to implement the methods. Those methods can be inherited from a superclass. Thus, if one class conforms to the NSCopying
protocol, its subclasses do as well (although that doesn't mean the methods are correctly implemented for that subclass).
A protocol can be used to define methods you want other people who subclass your class to implement. Perhaps a Drawing
protocol could be defined for your GraphicObject
class, and in it you could define paint
, erase
, and outline
methods, as in the following:
@protocol Drawing
-(void) paint;
-(void) erase;
-(void) outline;
@end
As the creator of GraphicObject
class, you don't necessarily want to implement these painting methods. However, you want to specify the methods that someone who subclasses the GraphicObject
class needs to implement to conform to a standard for drawing objects he's trying to create.
So, if you create a subclass of GraphicObject
called Rectangle
and advertise (that is, document) that your Rectangle
class conforms to the Drawing
protocol, users of the class will know that they can send paint
, erase
, and outline
messages to instances from that class.3 Notice that the protocol doesn't reference any classes; it's classless. Any class can conform to the Drawing
protocol, not just subclasses of GraphicObject
.
You can check to see whether an object conforms to a protocol by using the conformsTo:
method. For example, if you had an object called currentObject
and wanted to see whether it conformed to the Drawing
protocol so you could send it drawing messages, you could write this:
id currentObject;
...
if ([currentObject conformsTo: @protocol (Drawing)] == YES)
{
// Send currentObject paint, erase and/or outline msgs
...
}
The special @protocol
directive as used here takes a protocol name and produces a Protocol
object, which is what the conformsTo:
method expects as its argument.
You can enlist the aid of the compiler to check for conformance with your variables by including the protocol name inside angular brackets after the type name, like so:
id <Drawing> currentObject;
This tells the compiler that currentObject
will contain objects that conform to the Drawing
protocol. If you assign a statically typed object to currentObject
that does not conform to the Drawing
protocol (say you have a Square
class that does not conform), the compiler issues a warning message that looks like this:
prot1.m:61: warning: class 'Square' does not implement the 'Drawing' protocol
This is a compiler check here, so assigning an id
variable to currentObject
would not generate this message because the compiler has no way of knowing whether the object stored inside an id
variable conforms to the Drawing
protocol.
You can list more than one protocol if the variable will hold an object conforming to more than one protocol, as in this line:
id <NSCopying, NSCoding> myDocument;
When you define a protocol, you can extend the definition of an existing one. So, the protocol declaration
@protocol Drawing3D <Drawing>
says that the Drawing3D
protocol also adopts the Drawing
protocol. Thus, whichever class adopts the Drawing3D
protocol must implement the methods listed for that protocol as well as the methods from the Drawing
protocol.
Finally, a category can adopt a protocol too, like so:
@interface Fraction (Stuff) <NSCopying, NSCoding>
Here Fraction
has a category Stuff
(okay, not the best choice of names!) that adopts the NSCopying
and NSCoding
protocols.
As with class names, protocol names must be unique.
You might come across the notion of an informal protocol in your readings. This is really a category that lists a group of methods but does not implement them. Everyone (or just about everyone) inherits from the same root object, so informal categories are often defined for the root class. Sometimes informal protocols are also referred to as abstract protocols.
If you look at the header file objc/Object.h
, you might find some lines that look like this:
/* Abstract Protocol for Archiving */
@interface Object (Archiving)
- startArchiving: (void *)stream;
- finishUnarchiving;
@end
This defines a category called Archiving
for the Object
class. This informal protocol lists a group of methods (here, two are listed) that can be implemented as part of this protocol. An informal protocol is really no more than a grouping of methods under a name. This can help somewhat from the point of documentation and modularization of methods.
The class that declares the informal protocol doesn't implement the methods in the class itself, and a subclass that chooses to implement the methods needs to redeclare them in its interface section as well as implement one or more of them. Unlike formal protocols, the compiler gives no help with informal protocols; there's no concept of conformance or testing by the compiler.
If an object adopts a formal protocol, the object must conform to all the messages in the protocol. This can be enforced at runtime as well as compile time. If an object adopts an informal protocol, the object might not need to adopt all the methods in the protocol, depending on the protocol. Conformance to an informal protocol can be enforced at runtime (via respondsToSelector:
) but not at compile time.
You've learned several ways to extend the definition of a class through techniques such as subclassing, categories, and posing. Another technique involves defining a class that consists of one or more objects from other classes. An object from this new class is known as a composite object because it is composed of other objects.
As an example, consider the Square
class you defined in Chapter 8. You defined this as a subclass of a Rectangle
because you recognized that a square was just a rectangle with equal sides. When you define a subclass, it inherits all the instance variables and methods of the parent class. In some cases, this is undesirable—for example, some of the methods defined in the parent class might not be appropriate for use by the subclass. The Rectangle
's setWidth:andHeight:
method is inherited by the Square
class but really does not apply to a square (even though it will in fact work properly). Further, when you create a subclass, you must ensure that all the inherited methods work properly because users of the class will have access to them.
As an alternative to subclassing, you can define a new class that contains as one of its instance variables an object from the class you want to extend. Then you only have to define those methods in the new class that are appropriate for that class. Getting back to the Square
example, here's an alternative way to define a Square
:
@interface Square: Object
{
Rectangle *rect;
}
-(int) setSide: (int) s;
-(int) side;
-(int) area;
-(int) perimeter;
@end
The Square
class is defined here with four methods. Unlike the subclass version, which gives you direct access to the Rectangle
's methods (setWidth:
, setHeight:
, setWidth:andHeight:
, width
, and height
), those methods are not in this definition for a Square
. That makes sense here because those methods really don't fit in when you deal with squares.
If you define your Square
this way, it becomes responsible for allocating the memory for the rectangle it contains. For example, without overriding methods, the statement
Square *mySquare = [[Square alloc] init];
allocates a new Square
object but does not allocate a Rectangle
object stored in its instance variable, rect
.
A solution is to override init
or add a new method such as initWithSide:
to do the allocation. That method can allocate the Rectangle rect
and set its side appropriately. You'll also need to override the free
method (which you saw how to do with the Rectangle
class in Chapter 8) to release the memory used by the Rectangle rect
when the Square
itself is freed.
When defining your methods in your Square
class, you can still take advantage of the Rectangle
's methods. For example, here's how you could implement the area
method:
-(int) area
{
return [rect area];
}
Implementation of the remaining methods is left as an exercise for you (see exercise 5 that follows).
MathOps
category from Program 11.1 to also include an invert
method, which returns a Fraction
that is an inversion of the receiver.Fraction
class called Comparison
. In this category add two methods according to these declarations:
-(BOOL) isEqualTo: (Fraction *) f;
-(int) compare: (Fraction *) f;
The first method should return YES
if the two fractions are identical and return NO
otherwise. Be careful about comparing fractions (for example, comparing 3/4 to 6/8 should return YES
).
The second method should return –1
if the receiver compares less than the fraction represented by the argument, return 0
if the two are equal, and return 1
if the receiver is greater than the argument.
sin ()
, cos (),
and tan ()
are part of the Standard Library (like printf ()
and scanf ()
are). These functions are declared in the header file <math.h>
, which you should import into your program with the following line:
#import <math.h>
These functions can be used to calculate the sine, cosine, or tangent, respectively, of their double
argument, which is expressed in radians. The result is also returned as a double precision floating-point value. So
result = sin (d);
can be used to calculate the sine of d
, with the angle d
expressed in radians. Add a category called Trig
to the Calculator
class defined in Chapter 6, “Making Decisions.” Add methods to this category to calculate the sine, cosine, and tangent based on these declarations:
-(double) sin;
-(double) cos;
-(double) tan;
Trig
category from exercise 3 goofed and was supposed to write the methods to take arguments as angles expressed in degrees and not radians. Given that an angle can be converted from degrees to radians by multiplying it by π/180,4 override the three methods developed in exercise 3 and use poseAs:
to correct this mistake with the Calculator
class.@interface Square: Object
{
Rectangle *rect;
}
-(Square*) initWithSide: (int) s;
-(void) setSide: (int) s;
-(int) side;
-(int) area;
-(int) perimeter;
-(id) free; // Override to release the Rectangle object
@end
write the implementation section for a Square
and a test program to check its methods.
3.133.158.230