11. Categories, Posing, and Protocols

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.

Categories

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;
}


Program 11.1. Output


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 Notes About Categories

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.

Posing

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.

Program 11.2.


#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;
}


Program 11.2. Output


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

Protocols

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.

Informal Protocols

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.

Composite Objects

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

Exercises

  1. Extend the MathOps category from Program 11.1 to also include an invert method, which returns a Fraction that is an inversion of the receiver.
  2. Add a category to the 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.

  3. The functions 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;

  4. Assume the developer of the 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.
  5. Given the discussion on composite objects from this chapter and the following interface section:

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

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.158.230