Chapter 2. The Real Truth about Object-Oriented Programming

In This Chapter

  • Recognizing the importance of modules

  • Getting a handle on objects

  • Understanding inheritance

  • Implementing inheritance

  • Understanding the connection between inheritance and polymorphism

  • Seeing encapsulation and polymorphism in action

  • Understanding the Model-View-Controller pattern

  • Refining the idea of "reusable code"

Albert Einstein once said that technological change is like an axe in the hands of a pathological criminal. Often a little change or bug-fix in one part of your program can have a disastrous impact on the rest of it. (Co-author Neal remembers a fellow programmer once lamenting, "but I only changed one line of code," after making changes to a program and then putting it into production without adequate testing — only to have it take down an entire mainframe complex.)

To minimize the side effects of "only changing one line of code," you need to divide your programs into modules so that a change you make in one module won't have an impact on the rest of your code. I refer to this Not-Rocking-the-Boat aspect of good code design as transparency.

A module is simply a self-contained, or independent, unit that can be combined with other units to get the job done. Modules are the solution to a rather knotty problem — even a simple program can run into hundreds of lines of instructions, and you need a way to break them into parts to make them understandable. But more importantly, you want to use modules because they make programs easier to modify.

Not All Modules Are Created Equal

The idea of dividing your program into modules is as old as programming itself, and you know how old that is. The programming style (or paradigm) of a particular programming language dictates the way you do that. You need to be concerned with two paradigms at this point, although with more experience you'll probably explore others.

Functions (or things like that) as well as groups of functions have historically been the basis of modularization. This way of dividing things into modules is used in the programming style or paradigm known as procedural programming. For example, in a vacation budget app that helps you track expenses in different currencies, you might write functions like spend dollars or charge foreign currency, which then operate on transaction and budget data.

In the last few years, however, the procedural paradigm has pretty much been supplanted, at least for commercial applications, by object-oriented programming. In Objective-C, objects (and, as you will see, their corresponding classes) are the way a program is divided up. In an object-oriented program, you find transaction objects and budget objects.

To give you some perspective, you can think of objects in an object-oriented program as working as a team necessary to reach a goal. Functions in a procedural program are more like the command and control structure of a large corporation (think GM) or the army. Which is more flexible?

For all practical purposes, the debate has been settled in favor of object-oriented programming for commercial applications (except for a few fanatics), and because you're learning Objective-C, which is an object-oriented language, it's time to get on with understanding objects.

Understanding How Objects Behave

An object-oriented program consists of a network of interconnected objects, essentially modules that call upon each other to solve a part of the puzzle. The objects work like a team. Each object has a specific role to play in the overall design of the program and is able to communicate with other objects. Objects communicate requests to other objects to do something using messages.

Object-oriented programmers think about objects as actors and talk about them that way. Objects have responsibilities. You ask them to do things, they decide what to do, and they behave in a certain way. You do this even with, for instance, a sandwich object or a shape object. You can, for example, tell a sandwich in an object-oriented program to go cut itself in half (ouch!), or tell a shape to draw itself.

This resemblance to real things gives objects much of their power and appeal. You can use them not only to represent things in the real world — a person, an airplane reservation, a credit card transaction — but also to represent things in a computer, such as a window, button, or slider.

But what gives object-oriented programming its power is that the way objects are defined and the way they interact with each other make it relatively easy to accomplish the goals of extensibility and enhance-ability — that is, achieve the transparency that is the hallmark of a good program. You can accomplish that by using two features that are the bedrock of object-oriented programming languages: encapsulation and polymorphism.

Okay, I know both terms sound either like bad sci-fi or advanced bioengineering, but they're actually not that complicated. Here's what they really mean:

  • Encapsulation is about keeping the details of how an object works hidden from the other objects that use it. Many people have no idea how a computer works but can effectively browse the Internet, create documents, and receive and send e-mail. Most people who can successfully drive cars have no idea of how the engine works. I refer to this as the I-Don't-Care-And-Please-Don't-Tell-Me approach.

    Encapsulation makes possible the ability to change how an object carries out its responsibilities or behaves (enhance-ability) without having to disturb the existing code that uses the object. It also makes possible the ability to add new responsibilities to an object (extensibility) without disturbing the existing code. One of the primary things that objects encapsulate is their data, and another thing they do is transparently add new objects, as you see later in this chapter.

  • Polymorphism is about cultivating more of the same. Your friend's refrigerator is not the same as yours, but you know how to open it and find the beer. Different refrigerator models offer different features, but they operate the same way. Your objects shouldn't have to care about how one object is different from another as long as the object does what the requesting object needs it to do. I refer to this as the More-Of-The-Same approach.

    This feature in object-oriented languages makes it possible to add new objects of the same type, and have your program handle them without making any changes to the code that uses them. For example, you can create a program that processes cash and credit card transactions, and then sometime later you can add an ATM transaction and have the program process that new kind of transaction without having to make any changes to the processing logic.

    With respect to all the new ideas you have to learn, this concept is usually the hardest one for most people to grasp right away (the name polymorphism doesn't help), although everyone gets it after seeing it in action. I give you a good example later in this chapter, and I promise you that after you use it in your program, you'll wonder why you thought it was so hard in the first place.

Seeing the Concepts in Action

To understand these concepts, you need some concrete examples from the real world. Imagine living in Minneapolis (where Neal lived briefly), where it can be not just cold, but really cold. You want a handheld device (something like Figure 2-1, which I call a "uPhone") that lets you start your car and turn on the heater before you leave the house in the morning.

The imaginary uPhone.

Figure 2-1. The imaginary uPhone.

Encapsulation

You may be happily using your uPhone until one day your mechanic finds a new heater that works better and is plug-compatible with your old heater — it has the same controls. And guess what? Adding this great new heater doesn't faze your (old) uPhone one bit. It just keeps on truckin'.

The reason is that the application (including the uPhone, uPhone Interface, and Component Interface — refer to Figure 2-1) knows nothing about heaters. All the application really cares about is the heater switch (car heater control). As long as that stays the same, everything works. If the uPhone had to interact directly with the heater components without these interfaces, it wouldn't work.

To make your programs enhance-able, you want to depend on the implementation details as little as possible. You'll remember that the programming term for this I-Don't-Care-And-Please-Don't-Tell-Me approach is encapsulation. What you're doing is hiding how things are being done from what is being done.

In a program, that means hiding the internal mechanisms and data structures of a software component behind a defined interface in such a way that users of the component (other pieces of software) only need to know what the component does and don't have to make themselves dependent on the details of how the component does what it promises to do. This means the following:

  • The internal mechanisms of a module can be improved without having to make any changes in any of the modules that use it.

  • The component is protected from user meddling (like trying to rewire a heater).

  • Things are less complex because the interdependencies between modules have been reduced as much as possible.

This is the way modules, or objects, should work in an object-oriented program. You want the objects to limit their knowledge of other objects to what those objects can do — like turn on and off. That way, if you change something, you don't have to go digging through a zillion lines of code to figure out whether any code in your program is depending on something being done a particular way and then changing that dependent code to work with the new way it will be done. Ignorance is bliss . . . for the programmer that is.

Polymorphism

To carry the uPhone metaphor further, imagine that your spouse or friend wants one, but she or he has a different kind of car with a different heater control. All you have to do is change the Component Interface to the heater, keeping the uPhone Interface the same — which means no changes are required to the uPhone itself.

What you're looking for is a situation in which the requestor doesn't even care who receives the message, as long as it can get what it wants. So the uPhone doesn't care whether it's sending the message to a heater in a 1959 Cadillac, or a 1958 Corvette, or even an SSC Ultimate Aero TT, as long as the heater can respond to the message.

The capability of different objects to respond, each in its own way, to identical messages is called polymorphism.

Although encapsulation allows you to ignore how things are done, polymorphism allows you to escape the specific details of differences between objects that do the same thing in different ways. In the real world, if you can drive a Chevy, you can drive a Caddy or any other car, as long as the controls are more or less the same. It isn't that a 1959 Cadillac and a 1958 Corvette are the same; if they were, what would be the point? What's important is that they are different, but you can go about using them in the same way.

In a program, different objects might perform the same methods in different ways — if the user spends cash, a cash transaction object subtracts that amount from the user's budget. If the user uses a credit card, a credit card transaction will first have to convert the amount in foreign currency that the user charged to dollars and then subtract it from the budget.

How Inheritance Works

As I point out in "Understanding How Objects Behave" previously in this chapter, an object has responsibilities. Other objects ask it to do some action (such as draw a shape, or make a transaction), and it decides what to do and behaves in a certain way. The other objects don't meddle with the encapsulated details of how the action is accomplished, as long as the expected result occurs. They work as a team: Each object has a specific role to play in the overall design of the program and is able to communicate with other objects.

If an object needs to be modified, encapsulation ensures that the modifications don't interfere with existing code. Each object can also be replaced by a better, more functional object that does the same thing — polymorphism ensures that it's possible to add new objects of the same type, and have your program handle them without making any changes to the code that uses them.

What makes this scenario work is the fact that with object-oriented programming, you can essentially copy all the characteristics of an existing class to make a new class — the new class inherits the methods and data structure of the existing class. Inheritance is the reason why you can replace an old refrigerator with a new one that not only does the same thing, but does it better. The new refrigerator inherits all the best capabilities of the old one and improves them (at least you hope it does).

To show how inheritance works, I'm diving back into the vacation budget app from Chapter 1 of this minibook (you may want to take another look at declaring a class interface and defining class methods and instance variables). Let's say you want to create a transaction object that knows the difference between a cash transaction and a credit-card transaction (which includes an extra fee). First, look at the old-school way of programming the problem: You could take a C-like procedural approach to ensure that the transaction object sends the right message to the Budget object to record the credit-card or cash transaction properly. Consider how you might code this Transaction object and use a switch statement to manage more than one kind of transaction in a single array. The Transaction.h file would contain the code in Listing 2-1.

Example 2-1. Transaction.h

#import <Cocoa/Cocoa.h>

typedef enum {cash, charge} transactionType;

@interface Transaction : NSObject {

  transactionType type;
  double amount;
}

- (void) createTransaction: (double) theAmount
                      ofType: (transactionType) aType;
- (double) returnAmount;
- (transactionType) returnType;
@end

This Transaction class stores both an amount and its type. To know what kind of transaction it is, the typedef creates a type, transactionType, and an instance variable typecash for the dollar transaction and charge for the credit card ones. The instance variable amount is the value of the transaction. Three methods are also defined: The first method (createTransaction) simply initializes the object with a type and amount. The second (returnAmount) and third (returnType) methods return the amount of the transaction and type of transaction (cash or charge), respectively.

In the Transaction.m file you find the implementation code, shown in Listing 2-2.

Example 2-2. Transaction.m

#import "Transaction.h"

@implementation Transaction
- (void) createTransaction: (double) theAmount ofType: (transactionType) aType{

  type = aType;
  amount = theAmount;
}
- (double) returnAmount{

  return amount;
}

- (transactionType) returnType {

  return type;
};

@end

This implements the methods declared in the Transaction.h interface in Listing 2-1.

With a transaction object that has an amount and knows what kind of transaction it is, you can put both cash and charge transactions in the same array and use a switch statement to ensure that the right message is sent to the Budget object. A switch statement is a kind of control statement that allows the value of a variable or expression to control the flow of program execution. In this case, you would use returnType (the type of transaction) in a switch statement, as shown in Listing 2-3.

Example 2-3. The switch Statement

for (Transaction * aTransaction in transactions) {
switch ([aTransaction returnType]) {
      case cash:
        [europeBudget spendDollars:
                             [aTransaction returnAmount]];
        break;
      case charge:
        [europeBudget chargeForeignCurrency:
                             [aTransaction returnAmount]];
        break;
      default:
        break;
      }
  }

The problem with that approach is that the switch statements can rapidly get very complicated, and a program with switch statements scattered throughout becomes difficult to extend and enhance.

Quite frankly, this kind of complex control structure is characteristic of the procedural programming paradigm. Object-oriented programming and Objective-C do not "improve" this control structure, but they let you avoid using it unless absolutely necessary. You can use one of Objective-C's extensions to C — inheritance — to take advantage of polymorphism. Inheritance greatly simplifies your code, and you end up with a program that's a great deal easier to understand and extend. (The two actually go hand in hand.)

Objective-C, like other object-oriented programming languages, permits you to base a new class definition on a class already defined. The base class is called a superclass; the new class is its subclass. The subclass is defined only by its extension to its superclass; everything else remains the same. For the vacation budget app, then all you need to do is create a Transaction Base superclass that encapsulates what is the same between a cash and credit card transaction, and then create Cash Transaction subclass and Credit Card Transaction subclass that implement the differences.

Tip

The terms superclass and subclass can be confusing. When most people think of super, they think of something with more functionality, not less. In some languages, the term used is base class, which I think does a better job of conveying what it all actually means. But it is what it is, so keep this in mind.

In Figure 2-2, you see an example of a class diagram used by programmers to describe their classes. The diagram uses the UML (Unified Modeling Language) notation. (The superclass and subclass arrows and terms are not part of the notation; they're there to illustrate the hierarchy of the Transaction classes in the program.) The name of the class is at the top of the box, the middle section describes the instance variables, and the bottom box shows you the methods of that (sub)class.

Figure 2-2 shows that both CashTransaction and CreditCardTransaction classes are subclasses of Transaction. Each subclass inherits all the methods and all the instance variables of its superclass. You therefore don't have to include references to re-implementing a method like spend in the interfaces for CashTransaction and CreditCardTransaction. All you have to do is implement spend in the @implementation.

Every class but NSObject (the root of all your classes) can thus be seen as another stop on the road to increasing specialization. As you add subclasses, you're adding to the cumulative total of what's inherited. The CashTransaction class defines only what's needed to turn a Transaction into a CashTransaction.

Note

Incidentally, if you think about it, inheritance also implements a kind of encapsulation. You can extend the behavior of an existing object without impacting the existing code that already works — remember, it's all about enhance-ability and extensibility.

The Transaction class hierarchy.

Figure 2-2. The Transaction class hierarchy.

Note

In Objective-C, every class has only one superclass, but can have an unlimited number of subclasses. In some languages, however, a class can have multiple superclasses. This is known as multiple inheritance. Although Objective-C doesn't support multiple inheritance, it does provide some features not found in those languages that enable you to get many of the benefits of multiple inheritance, without the accompanying disadvantages. These features include categories and protocols, both of which are covered in Chapter 5 of this minibook.

Knowing what inheritance enables you to do

Inheritance allows you to do a number of things that make your programs more extensible and enhance-able. What kinds of things? Well, to be more specific, in a subclass you can make three kinds of changes to what you inherit from a superclass. (Think of this section as describing the mechanics of creating a subclass.)

  • You can add new methods and instance variables. This is one of the most common reasons for defining a subclass in general.

  • You can refine or extend the behavior of a method. You do this by adding a new version of the same method, while still continuing to use the code in the old method. To add a new version, you implement a new method with the same name as one that's inherited. The new version overrides the inherited version. In the body of the new method, you send a message to execute the inherited version. I illustrate this later in the "Deriving classes" section of this chapter.

  • You can change the behavior of a method you inherit. You do this by replacing an existing method with a new version — by overriding the old method as described in the preceding bullet point. In this case, however, you don't send a message to execute the inherited version. The old implementation is still used for the class that defined it and other classes that inherit it, although classes that inherit from the new class use your implementation. Changing behavior is not unusual, although it does make your code harder to follow. If you find yourself frequently overriding a method to completely change its behavior, you should question your design.

Note

Even though you may override a method in a class, subclasses of the class still do have access to the original. For obvious reasons, this is generally not a good idea, and again should have you questioning your design.

Although a subclass can override inherited methods, it can't override inherited instance variables. If you try to declare a new one with the same name as an inherited one, the compiler complains.

Using inheritance effectively

Given the preceding possibilities, here are some ways you can use inheritance in your programs:

  • Create a protocol. The Transaction class is an example. A protocol in this sense is a list of method(s) that subclasses are expected to implement. The superclass might have skeletal versions of the methods with no real functionality (as Transaction does), or it might implement partially functional versions that you use in the subclass methods. In either case, the superclass's declaration (its list of methods) defines a protocol that all its subclasses must follow.

    When different classes implement similarly named methods, a program is better able to make use of polymorphism.

  • Reuse code. Reusing code has traditionally been a poster child for inheritance use. There are three approaches:

    • Increasing specialization: If classes have some things in common, but also differ in key ways, the common functionality can be put in a superclass that all classes can inherit. Transaction is a good example of that.

    • Implementing generic functionality: (This is often coupled with the protocol approach.) In the AppKit and UIKit frameworks, user interface objects have been created for your using pleasure. They implement as much generic functionality as they can, but it's up to you to add the specific functionality to make it so they do something useful in your application. For example, a view can display itself on the screen, scroll, and so on, but you need to implement methods that display what you want displayed.

    • Modifying a class that more or less does what you want it to do: There may be a class that does most of what you want it to, but you need to change some things about how it works. You can make the changes in a subclass.

Implementing Inheritance in a Program

To realize how inheritance can work in a program, you can do what you did in the last section: Create an abstract superclass, Transaction, which creates a protocol for subsequent subclasses, then create two subclasses of Transaction, CashTransaction and CreditCardTransaction. They inherit all the methods and instance variables of the Transaction class, but each implements its own spend: method. (I just talked about creating Transaction in the last section; now you can actually get down to doing it.)

Deriving classes

To create the Transaction superclass, you need to change the Transaction.h and Transaction.m files from earlier in Listing 2-1 and Listing 2-2 so they look like what's shown in Listing 2-4 and Listing 2-5.

Example 2-4. Transaction.h

#import <Cocoa/Cocoa.h>
@class Budget;

@interface Transaction : NSObject {

Budget *budget;
  double  amount;
}

- (void) createTransaction: (double) theAmount forBudget:
    (Budget*) aBudget;
- (void) spend;
- (void) trackSpending: (double) theAmount;
@end

Example 2-5. Transaction.m

#import "Transaction.h"
#import "Budget.h"
@implementation Transaction
- (void) createTransaction: (double) theAmount forBudget:
   (Budget*) aBudget {
budget = aBudget;
  amount = theAmount;
}
- (void) spend {

// Fill in the method in subclasses
}

- (void) trackSpending: (double) theAmount {

  NSLog (@"You are about to spend another %.2f", theAmount);
}
@end

Go ahead and compare Listing 2-4 with Listing 2-1 (Transaction.h), and compare Listing 2-5 with Listing 2-2 (Transaction.m). You change the arguments used in createTransaction:: by deleting aType, and using aBudget instead. Each transaction sends the right message to the Budget object. You also create a skeletal spend method as a placeholder, which will be implemented in the subclasses, and delete returnAmount and returnType because you won't need them any more — you only need that information if you decide to stick with the switch statement (shown earlier in Listing 2-3).

You also add a new method, trackSpending:, which tracks your spending and uses NSLog to display a message about it (and as an example, shows you how to send messages to inherited methods):

- (void) trackSpending: (double) theAmount;

Also included in the new Transaction.h file (Listing 2-4) is the @class statement coded here as @class Budget. The compiler needs to know certain things about the classes that you're using — such as what methods you defined and so on — and the #import statement in the implementation (the new Transaction.m file in Listing 2-5) solves that problem. But when you get into objects that point at other objects, you also need to provide that information in the interface (Transaction.h file), which can cause a problem if you end up with what are known as circular dependencies (which sounds cool but is beyond the scope of this book).

To solve that problem, Objective-C introduces the @class keyword as a way to tell the compiler that the budget instance variable, whose type Budget the compiler knows nothing about (yet), is a pointer to that class. Knowing that is enough for the compiler, at least in the interface files. However, you still have to do the #import in the implementation file when you refer to methods of that class.

Next, you can take advantage of what you just did and create two subclasses of Transaction, CashTransaction (CashTransaction.h in Listing 2-6, and CashTransaction.m in Listing 2-7), and CreditCardTransaction (CreditCardTransaction.h in Listing 2-8, and CreditCardTransaction.m in Listing 2-9). They inherit all the methods and instance variables of the Transaction class, but each implements its own spend: method. You can also have both methods send a message to their superclass's trackSpending: method, which shows how to send messages to a superclass.

Tip

Object-oriented programmers like to think of subclasses like CashTransaction as having an "is-a" relationship to their superclasses. For example, a cash transaction is-a transaction.

Example 2-6. CashTransaction.h

#import <Cocoa/Cocoa.h>
#import "Transaction.h"

@interface CashTransaction : Transaction {
}
@end

Example 2-7. CashTransaction.m

#import "CashTransaction.h"
#import "Budget.h"

@implementation CashTransaction
- (void) spend {

  [self trackSpending:amount];
  [budget spendDollars:amount];
}
@end

Example 2-8. CreditCardTransaction.h

#import <Cocoa/Cocoa.h>
#import "Transaction.h"

@interface CreditCardTransaction : Transaction {
}
@end

Example 2-9. CreditCardTransaction.m

#import "CreditCardTransaction.h"
#import "Budget.h"

@implementation CreditCardTransaction
- (void) spend {
[super trackSpending:amount];
  [budget chargeForeignCurrency:amount];
}
@end

To add the two new subclasses, all you have to do is declare the unique behavior in each class. You use @interface statements to specify Transaction as the superclass:

@interface CreditCardTransaction : Transaction {
@interface CashTransaction : Transaction {

Note

When you add a new class to a project, Xcode doesn't know what its subclass is, so it uses NSObject unless you specify a particular subclass — it's up to you to change the NSObject default to the right superclass.

Your new subclasses inherit all of the methods and instance variables of the Transaction class, which includes all the instance variables and methods it inherits from its superclass and so on up the inheritance hierarchy. (In this case, as you can see in Listing 2-4, the Transaction superclass is NSObject, so it ends there.) So you're cool when it comes to being able to behave like a good Objective-C object. And while you didn't do it here, you can also add instance variables to a subclass as well, and as many methods as you need.

You also need to import both interface files so the compiler can understand what Transaction and Budget are, so you add the #imports for the Transaction and Budget interface files because both are used by the methods in the CashTransaction and CreditCardTransaction classes.

The superclass's method trackSpending: displays that you're about to spend some money. You can have CashTransaction and CreditCardTransaction send a message to trackSpending:, in two different ways:

[self trackSpending:amount];

or

[super trackSpending:amount];

You'd use the first statement to send messages to methods that are part of your class, which includes those that you inherit. As you can see, even though trackSpending: is defined only in the Transaction superclass, your class inherited trackSpending: and the message to self works fine. Unless you have overridden the inherited trackSpending: method in your class, you should really use [super trackSpending: amount], because that makes sure that you are sending the message to the superclass method. In this case self and super are interchangeable, but as you see when you initialize objects in Chapter 3 of this minibook, that isn't always the case.

You can now use that inheritance-based Transaction class design in your program. In Objective-C programs, the instructions contained within main (the mother of all functions and the place where all Objective-C programs start their execution) are always the first ones to be executed. The main function is in the Vacation.m file (named after the Xcode project Vacation), as shown in Listing 2-10. Note that it starts off with two #import statements: The second refers to the Budget.h header file, which I show in Chapter 1 of this minibook.

Example 2-10. Vacation.m

#import <Foundation/Foundation.h>
#import "Budget.h"

 #import "Transaction.h"
 #import "CashTransaction.h"
 #import "CreditCardTransaction.h"

int main (int argc, const char * argv[]) {

 Budget *europeBudget = [Budget new];
 [europeBudget createBudget:1000.00
                                withExchangeRate:1.2500];
 Budget *englandBudget = [Budget new];
 [englandBudget createBudget:2000.00
                                withExchangeRate:1.5000];

NSMutableArray *transactions = [[NSMutableArray alloc]
   initWithCapacity:10];
  Transaction *aTransaction ;
  for (int n = 1; n < 2; n++) {
    aTransaction = [CashTransaction new];
    [aTransaction createTransaction:n*100
                                forBudget:europeBudget];
    [transactions addObject:aTransaction];
    aTransaction = [CashTransaction new];
    [aTransaction createTransaction:n*100
                               forBudget:englandBudget];
    [transactions addObject:aTransaction];
  }

  int n =1;
  while (n < 4) {
    aTransaction = [CreditCardTransaction new];
    [aTransaction createTransaction:n*100
                                forBudget:europeBudget];
    [transactions addObject:aTransaction];
aTransaction = [CreditCardTransaction new];
    [aTransaction createTransaction:n*100
                               forBudget:englandBudget];
    [transactions addObject:aTransaction];
    n++;
  }

  for (Transaction* aTransaction in transactions) {
    [aTransaction spend];
  }

  return 0;
}

Stepping through all this code, you see that the first thing you need to do is add the necessary #import statements so the compiler knows what to do with the new classes.

#import "Transaction.h"
#import "CashTransaction.h"
#import "CreditCardTransaction.h"

Next, use Budget as an argument when you initialize the Transaction:

Budget *europeBudget = [Budget new];
[europeBudget createBudget:1000.00
                    withExchangeRate:1.2500];

Budget  *englandBudget = [Budget new];
[englandBudget createBudget:2000.00
                     withExchangeRate:1.5000];

The code that follows this sets up an array with NSMutableArray. As you find out in Chapter 4 of this minibook, NSMutableArray arrays are ordered collections that can contain any sort of object. The code creates the cash and credit card transactions for both Europe and England in both the for and while loops:

aTransaction = [CreditCardTransaction new];
[aTransaction createTransaction:n*100
                          forBudget:europeBudget];
[transactions addObject:aTransaction];
aTransaction = [CreditCardTransaction new];
[aTransaction createTransaction:n*100
                         forBudget:englandBudget];
[transactions addObject:aTransaction];

The code then sends the spend message to each Transaction object in the transactions array:

[aTransaction spend];

This is something you find in many applications — a set of instructions that send the same message to a list of objects. This is what polymorphism is all about — a program architecture that makes your program easier to extend. As long as your new transaction is a subclass of Transaction, it can be used immediately in your program without any changes to the rest of your program (except, of course, to create and implement the transaction itself)!

You can see how easy it is to add a new kind of transaction to the mix. All you have to do is create the new transaction type and add it to the array, and it makes itself at home with the rest of the transactions.

Note

Keep in mind that the for and while loops are there only to generate transactions — think of them as simulating a user interface.

Replacing a control structure with polymorphism

Revisiting Listing 2-3, you have code that iterates through an array — accesses each array object in a for loop — and uses a switch statement to decide whether to send the sendDollars: or chargeForeignCurrency: message to a budget for that kind of transaction, passing the transaction as an argument.

In the object-oriented universe, you would set up your code to have two kinds of transaction objects — cash and credit card. Both kinds respond to a spend message, and every transaction has a pointer to the budget it is associated with. You iterate through the array and send the spend message to each transaction. If it is a cash transaction in Europe, for example, it has a reference to the europeBudget object and sends it the spendDollars: message. If it is a credit card transaction in England, it sends the chargeForeignCurrency: message to englandBudget. No fuss, no bother, and no control structure. This means you have one array that holds every transaction for every country you visit — much better than managing multiple arrays for different countries and different transaction types. This enables you to change the entire switch structure in Listing 2-3 with the following:

for (Transaction*  aTransaction in transactions) {
  [aTransaction spend];
}

If you want a new transaction, all you need to do is code it up and add it to the array. And, if you wanted to visit a new country, all you'd have to do is create a budget for that country and attach it to the transactions that occurred in that country. You can see that illustrated in Figure 2-3.

Transactions and budgets.

Figure 2-3. Transactions and budgets.

Figure 2-3 gives you the bird's-eye view. To see how things actually work down on the ground, start with what a transaction object would look like. You'd need two instance variables:

Budget *budget;
double  amount;

You'd also need two methods:

- (void) createTransaction: (double) theAmount
                             forBudget: (Budget*) aBudget;
- (void) spend;

As you can see, besides an initialization method, you have a method named spend. You also have an instance variable, budget, which enables the Transaction object to send a message to its budget; and another instance variable, amount, which holds the amount of this transaction. Because every type of transaction has a spend method, you can enumerate through the array and send each object a spend message, and each object, depending on its type, turns around and sends the right message to its budget.

So far, both cash and credit card transactions look the same; the only difference is in the implementation of spend. The cash transaction implements spend as:

- (void) spend {
[budget spendDollars:amount];
  }

The credit card transaction looks like this:

- (void) spend {
[budget chargeForeignCurrency:amount];
  }

This ability for different objects to respond to the same message each in its own way is an example of polymorphism, and is one of the cornerstones of enhance-able and extensible programs.

The final word on polymorphism and inheritance

The preceding sections tell you how to use one of the Objective-C extensions to C — inheritance — to implement polymorphism (or More-Of-The-Same). Polymorphism is the ability of different object types to respond to the same message, each one in its own way. Because each object can have its own version of a method, a program becomes easier to extend and enhance because you don't have to change the message to add functionality. All you have to do is create a new subclass, and it responds to the same messages in its own way.

Polymorphism allows you to isolate code in the methods of different objects rather than gathering them in a single function that has to know all the possible cases as well as in control structures such as if and switch statements. When a new case comes along, you won't have to re-code all those if and switch statements — you need only add a new class with a new method, leaving well enough alone as far as the code that you've already written, tested, and debugged is concerned.

Note

Using inheritance together with polymorphism is one of the extensions to C that is hard to implement without language support. For this to really work, the exact behavior can be determined only at runtime (this is called late binding or dynamic binding).

When a message is sent, the Objective-C runtime looks at the object you're sending the message to, finds the implementation of the method matching the name, and then invokes that method.

Encapsulating Objects

As I point out at the beginning of this chapter, using encapsulation enables you to safely tuck data behind an object's walls. You can keep the data safe and reduce the dependencies of other parts of your program on what the data is and how it is structured.

Encapsulation is also useful when you apply it to application functionality. When you limit what your objects know about other objects in your application, changing objects or their functionality becomes much easier because it reduces the impact of those changes on the rest of your application.

Getting to know the Model-View-Controller (MVC) pattern

The Cocoa framework is designed around certain programming paradigms, known as design patterns — a commonly used template that gives you a consistent way to get a particular task done.

I talk about these design patterns in Chapter 5 of Book I. The Model-View-Controller (MVC) design pattern implements the kind of object encapsulation that reduces the impact of changes to an application. This design pattern is not unique to Cocoa; a version of it has been in use since the early days of Smalltalk — the programming language that Objective-C bases its extensions to the C language on. The MVC design pattern goes a long way back, and the fact that it is still being used tells you something about its value.

MVC divides your application into three groups of objects and encourages you to limit the interaction between objects to others in its own group as much as possible. It creates, in effect, a world for the application, placing objects in one of three categories — model, view, and controller (described in the following list) — and specifies roles and responsibilities for all three kinds of objects as well as the way they're supposed to interact with each other.

Here's that list I mentioned:

  • Model objects: Model objects make up the content engine of your application. This is where all the objects are (as opposed to the code in the main function). In the examples in this chapter, they process transactions and compute what you have left in your budget. If you were to add things such as hotel objects, train objects, and the like, this is where they would belong. Model objects are very generous with what they can do and are happy to share what they know with the rest of your application. But not only do they not care about what other objects use them, or what these other objects do with the information they provide; being good objects, they really don't want to know.

    Think of the experience of watching programs on TV. The model (which may be one object or several objects that interact) would be a particular program — one that does not give a hoot about what TV set it is being shown on.

  • View objects: These objects display things on the screen and respond to user actions. This is what is generally thought of as the user interface, and pretty much anything you see on the screen is part of the View group of objects. View objects are pros at formatting and displaying data, as well as handling user interactions, such as allowing the user to enter a credit card transaction, make a new hotel reservation, and add a destination or even create a new trip. But they don't care about where that data comes from and are unaware of the model.

    Back to watching TV: You can think of the view as a television screen that doesn't care about what program it's showing or what channel you just selected.

    If you create an ideal world where the view knows nothing about the model and the model knows nothing about the view, you need to add one more set of objects. These objects connect the model and view — making requests for data from the model and sending that data back for the view to display. This is the collective responsibility of controller objects, described next.

  • Controller objects: These objects connect the application's view objects to its model objects. They deliver to the view the data that needs to be displayed — getting it from the model. They deliver user requests to the model for current data (such as, how much money do you have left in your budget?), as well as new data (for instance, you just spent 300 euros).

    Once again, think of watching TV: Picture the controller as the circuitry that pulls the show off the cable and sends it to the screen or that can request a particular pay-per-view show.

    One of the advantages of using this application model is that it allows you to separate these three parts to your application and work on them separately. You just need to make sure each group has a well-defined interface. When the time is right, you just connect the parts — and you have an application.

Note

A category of functionality that is not handled by the MVC pattern exists at the application level, or all the nuts and bolts and plumbing needed to run an application. These objects are responsible for startup, shut down, file management, event handling, and everything else that is not M, V, or C.

Implementing the MVC pattern

What makes the separation between models, views, and controllers possible is a well-defined interface. You can create an interface between the model and a sometime-in-the-future-to-be-developed controller by using a technique called composition, which is a useful way to create interfaces. Composition is another way to hide what's really going on behind the Wizard's curtains — it keeps the objects that use the composite object ignorant of the objects the composite object uses, and actually makes the components ignorant of each other, allowing you to switch components in and out at will.

You can create a composite object — call it Destination — that interfaces to main (which is used for the time being as a surrogate for both the views and controllers). Its design is really in the @interface for Destination. To act as an interface to be used by a controller, the Destination class needs to declare methods that do the following:

  • Create Transaction objects from the transaction amounts that will be sent from the user interface.

  • Return the data the user interface needs to display.

In Listing 2-11, you can see the Destination class interface that accomplishes both of these tasks.

Example 2-11. Destination.h — the Destination Design

#import <Cocoa/Cocoa.h>
@class Budget;
@interface Destination : NSObject {

  NSString       *country;
  NSMutableArray *transactions;
  Budget         *theBudget;
}
- (void) createWithCountry: (NSString*) theCountry andBudget: (double) budgetAmount withExchangeRate: (double) theExchangeRate;
- (void) spendCash: (double) aTransaction;
- (void) chargeCreditCard: (double) aTransaction;
- (double) leftToSpend;

@end

The createWithCountry::: method declaration's arguments let you initialize a new Destination with the country you are heading to, the amount you want to budget, and the current exchange rate.

You now have taken the creation of the Budget classes for each leg of your trip out of main (where they are placed in Chapter 1 of this minibook) and put them in Destination. You also have taken the creation and management of the Transaction array out of main and put that in Destination also. You enable main (the controller surrogate) to send transaction amounts to the Destination object (by sending the spendCash: and chargeCreditCard: messages). The Destination object can then, in turn, create and manage the appropriate Transaction objects and send them the spend: message.

You also enable the main method (by sending the leftToSpend) to ask the model for the information it needs to deliver to the surrogate user interface. This displays how much money remains in the budget.

Notice the instance variables that reference other objects — the transactions array and theBudget. This is a model for what makes a composite object and how it gets its work done — using other objects to distribute the work.

Tip

Object-oriented programmers like to think of composite objects like Destination as having a "has-a" relationship to their parts. The destination has-a budget, for example.

Listing 2-12 shows how you would implement these methods in the Destination.m file.

Example 2-12. Destination.m

#import "Destination.h"
#import "CashTransaction.h"
#import "CreditCardTransaction.h"
#import "Budget.h"
#import "Transaction.h"
@implementation Destination

- (void) createWithCountry: (NSString*) theCountry andBudget:
   (double) budgetAmount withExchangeRate: (double)
   theExchangeRate{

  transactions = [[NSMutableArray alloc]
                                     initWithCapacity:10];
  theBudget = [Budget new];
  [theBudget createBudget:budgetAmount
                        withExchangeRate:theExchangeRate];
  country = theCountry;
  NSLog (@"I'm off to %@", theCountry);
}


-(void) spendCash: (double) amount {

  Transaction *aTransaction = [CashTransaction new];
  [aTransaction createTransaction:amount
                                    forBudget:theBudget];
  [transactions addObject:aTransaction];
  [aTransaction spend];
}

-(void) chargeCreditCard: (double) amount {

  Transaction *aTransaction = [CreditCardTransaction new];
  [aTransaction createTransaction:amount
                                     forBudget:theBudget];
  [transactions addObject:aTransaction];
  [aTransaction spend];
}

- (double) leftToSpend {

  return [theBudget returnBalance];
}

@end

The method leftToSpend (near the very end of Listing 2-12) provides the user interface with the data it needs to display. (It also requires adding a new method to Budget, as you see next.) The NSLog statement (in the middle of Listing 2-12) isn't intended to be part of the user interface — you are just including it to trace program execution. It uses the country instance variable.

Because the Destination object is responsible for reporting to the controller the amount left to spend, it needs to get the amount from the Budget object, requiring you to code a method, returnBalance, in the Budget class. The Budget.h interface is shown in Listing 2-13, and the Budget.m implementation can be seen in Listing 2-14.

Example 2-13. Budget.h

#import <Cocoa/Cocoa.h>

@interface Budget : NSObject {

  float  exchangeRate;
  double budget;
  double transaction;
}

- (void) createBudget: (double) aBudget
                withExchangeRate: (float) anExchangeRate;


- (void) spendDollars: (double) dollars ;

- (void) chargeForeignCurrency: (double) euros;
- (double) returnBalance;
@end

Example 2-14. Budget.m

#import "Budget.h"

@implementation Budget

- (void) createBudget: (double) aBudget
                withExchangeRate: (float) anExchangeRate {

  exchangeRate = anExchangeRate;
  budget = aBudget;
}

- (void) spendDollars: (double) dollars {

  budget -= dollars;
}

- (void) chargeForeignCurrency: (double) foreignCurrency {

  transaction = foreignCurrency*exchangeRate;
  budget -= transaction;
}
- (double) returnBalance {
   return budget;
}
@end

Looking Ahead

Up until now, you've been doing initialization on an ad hoc basis, using initialization methods such as the one I rolled out back in Listing 2-1:

- (void) createTransaction: (double) theAmount
                            forBudget: (Budget*) aBudget;

Or this one in Listing 2-14:

- (void) createBudget: (double) aBudget
                withExchangeRate: (float) anExchangeRate;
exchangeRate = anExchangeRate;
  budget = aBudget;
}

There's a standard way to do initialization, however — one designed to work in a class hierarchy that ensures all the super- and subclasses are initialized properly. I show you how to implement these standard initialization methods in Chapter 3 of this minibook.

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

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