Chapter 7. Poly-what-ism?

In This Chapter

  • Deciding whether to hide or override a base class method (so many choices!)

  • Building abstract classes — are you for real?

  • Declaring a method and the class that contains it to be abstract

  • Using ToString, the class business card

  • Sealing a class from being subclassed

In inheritance, one class "adopts" the members of another. Thus I can create a class SavingsAccount that inherits data members such as account id and methods such as Deposit() from a base class BankAccount. That's useful, but this definition of inheritance isn't sufficient to mimic what's going on out there in the business world.

Tip

See Chapter 6 of this minibook if you don't know (or remember) much about class inheritance.

A microwave oven is a type of oven, not because it looks like an oven but, rather, because it performs the same functions as an oven. A microwave oven may perform additional functions, but it performs, at the least, the base oven functions — most importantly, heating up my nachos when I say, "StartCooking." (I rely on my object of class Refrigerator to cool the beer.) I don't particularly care what the oven must do internally to make that happen, any more than I care what type of oven it is, who made it, or whether it was on sale when my wife bought it. (Hey, wait — I do care about that last one.)

From our human vantage point, the relationship between a microwave oven and a conventional oven doesn't seem like such a big deal, but consider the problem from the oven's point of view. The steps that a conventional oven performs internally are completely different from those that a microwave oven may take.

Note

The power of inheritance lies in the fact that a subclass doesn't have to inherit every single method from the base class just the way it's written. A subclass can inherit the essence of the base class method while implementing the details differently.

Overloading an Inherited Method

As described in Chapter 3 of this minibook (look up overloading in the index), two or more methods can have the same name as long as the number or type of arguments differs (or as long as both differ).

It's a simple case of method overloading

Note

Giving two methods the same name is overloading, as in "Keeping them straight is overloading my brain."

The arguments of a method become a part of its extended name, as this example demonstrates:

public class MyClass
{
  public static void AMethod()
  {
    // Do something.
  }
  public static void AMethod(int)
  {
    // Do something else.
  }
  public static void AMethod(double d)
  {
    // Do something even different.
  }
  public static void Main(string[] args)
  {
    AMethod();
    AMethod(1);
    AMethod(2.0);
}

C# can differentiate the methods by their arguments. Each of the calls within Main() accesses a different method.

Warning

The return type isn't part of the extended name. You can't have two methods that differ only in their return types.

Different class, different method

Not surprisingly, the class to which a method belongs is also a part of its extended name. Consider this code segment:

public class MyClass
{
  public static void AMethod1();
  public void AMethod2();
}
public class UrClass
{
public static void AMethod1();
  public void AMethod2();
}
public class Program
{
  public static void Main(string[] args)
  {
    UrClass.AMethod1();  // Call static method.
    // Invoke the MyClass.AMethod2() instance method:
    MyClass mcObject = new MyClass();
    mcObject.AMethod2();
  }
}

The name of the class is a part of the extended name of the method. The method MyClass.AMethod1() has about as much to do with UrClass.AMethod1() as YourCar.StartOnAColdMorning() and MyCar.StartOnAColdMorning() — at least yours works.

Peek-a-boo — hiding a base class method

So a method in one class can overload another method in its own class by having different arguments. As it turns out, a method can also overload a method in its own base class. Overloading a base class method is known as hiding the method.

Suppose that your bank adopts a policy making savings account withdrawals different from other types of withdrawals. Suppose, just for the sake of argument, that withdrawing from a savings account costs $1.50.

Taking the procedural approach, you could implement this policy by setting a flag (variable) in the class to indicate whether the object is a SavingsAccount or just a simple BankAccount. Then the withdrawal method would have to check the flag to decide whether it needs to charge $1.50, as shown here:

public class BankAccount
{
  private decimal _balance;
  private bool _isSavingsAccount;  // The flag
  // Indicate the initial balance and whether the account
  // you're creating is a savings account.
  public BankAccount(decimal initialBalance, bool isSavingsAccount)
  {
    _balance = initialBalance;
    _isSavingsAccount = isSavingsAccount;
  }
  public decimal Withdraw(decimal amountToWithdraw)
  {
    // If the account is a savings account . . .
    if (_isSavingsAccount)
    {
      // ...then skim off $1.50.
      _balance -= 1.50M;
    }
// Continue with the usual withdraw code:
    if (amountToWithdraw > _balance)
    {
      amountToWithdraw = _balance;
    }
    _balance -= amountToWithdraw;
    return amountToWithdraw;
  }
}
class MyClass
{
  public void SomeMethod()
  {
    // I want create a savings account:
    BankAccount ba = new BankAccount(0, true);
  }
}

Your method must tell the BankAccount whether it's a SavingsAccount in the constructor by passing a flag. The constructor saves that flag and uses it in the Withdraw() method to decide whether to charge the extra $1.50.

The more object-oriented approach hides the method Withdraw() in the base class BankAccount with a new method of the same name, height, and hair color in the SavingsAccount class:

Note

// HidingWithdrawal -- Hide the withdraw method in the base
//    class with a method in the subclass of the same name.
using System;
namespace HidingWithdrawal
{
  // BankAccount -- A very basic bank account
  public class BankAccount
  {
    protected decimal _balance;
    public BankAccount(decimal initialBalance)
    {
      _balance = initialBalance;
    }
    public decimal Balance
    {
      get { return _balance; }
    }
    public decimal Withdraw(decimal amount)
    {
      // Good practice means avoiding modifying an input parameter.
      // Modify a copy.
      decimal amountToWithdraw = amount;
      if (amountToWithdraw > Balance)
      {
        amountToWithdraw = Balance;
      }
      _balance -= amountToWithdraw;
      return amountToWithdraw;
    }
  }
  // SavingsAccount -- A bank account that draws interest
  public class SavingsAccount : BankAccount
  {
public decimal _interestRate;
    // SavingsAccount -- Input the rate expressed as a
    //    rate between 0 and 100.
    public SavingsAccount(decimal initialBalance, decimal interestRate)
    : base(initialBalance)
    {
      _interestRate = interestRate / 100;
    }
    // AccumulateInterest -- Invoke once per period.
    public void AccumulateInterest()
    {
      _balance = Balance + (Balance * _interestRate);
    }
    // Withdraw -- You can withdraw any amount up to the
    //    balance; return the amount withdrawn.
    public decimal Withdraw(decimal withdrawal)
    {
      // Take the $1.50 off the top.
      base.Withdraw(1.5M);
      // Now you can withdraw from what's left.
      return base.Withdraw(withdrawal);
    }
  }
  public class Program
  {
    public static void Main(string[] args)
    {
      BankAccount ba;
      SavingsAccount sa;
      // Create a bank account, withdraw $100, and
      // display the results.
      ba = new BankAccount(200M);
      ba.Withdraw(100M);
      // Try the same trick with a savings account.
      sa = new SavingsAccount(200M, 12);
      sa.Withdraw(100M);
      // Display the resulting balance.
      Console.WriteLine("When invoked directly:");
      Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);
      Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);
      // Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
}

Main() in this case creates a BankAccount object with an initial balance of $200 and then withdraws $100. Main() repeats the trick with a SavingsAccount object. When Main() withdraws money from the base class, BankAccount.Withdraw() performs the withdraw function with great aplomb. When Main() then withdraws $100 from the savings account, the method SavingsAccount.Withdraw() tacks on the extra $1.50.

Tip

Notice that the SavingsAccount.Withdraw() method uses BankAccount. Withdraw() rather than manipulate the balance directly. If possible, let the base class maintain its own data members.

Making the hiding approach better than adding a simple test

On the surface, adding a flag to the BankAccount.Withdraw() method may seem simpler than all this method-hiding stuff. After all, it's just four little lines of code, two of which are nothing more than braces.

The problems are manifold. (I had to write several chapters just to be able to use that word.) One problem is that the BankAccount class has no business worrying about the details of SavingsAccount. More formally, it's known as "breaking the encapsulation of SavingsAccount." Base classes don't normally know about their subclasses, which leads to the real problem: Suppose that your bank subsequently decides to add a CheckingAccount or a CDAccount or a TBillAccount. All those likely additions have different withdrawal policies, each requiring its own flag. After adding three or four different types of accounts, the old Withdraw() method starts looking complicated. Each of those types of classes should worry about its own withdrawal policies and leave the poor old BankAccount.Withdraw() alone. Classes are responsible for themselves.

Accidentally hiding a base class method

Oddly enough, you can hide a base class method accidentally. For example, you may have a Vehicle.TakeOff() method that starts the vehicle rolling. Later, someone else extends your Vehicle class with an Airplane class. Its TakeOff() method is entirely different. In airplane lingo, "take off" means more than just "start moving." Clearly, this is a case of mistaken identity — the two methods have no similarity other than their identical name.

Fortunately, C# detects this problem.

C# generates an ominous-looking warning when it compiles the earlier HidingWithdrawal program example. The text of the warning message is long, but here's the important part:

'...SavingsAccount.Withdraw(decimal)' hides inherited member
  '...BankAccount.Withdraw(decimal)'.
  Use the new keyword if hiding was intended.

C# is trying to tell you that you've written a method in a subclass that has the same name as a method in the base class. Is that what you meant to do?

Tip

This message is just a warning — you don't even notice it unless you switch over to the Error List window to take a look. But you must sort out and fix all warnings. In almost every case, a warning is telling you about something that can bite you if you don't fix it.

Tip

Tell the C# compiler to treat warnings as errors, at least part of the time. To do so, choose Project

Accidentally hiding a base class method

The descriptor new, shown in the following sample code, tells C# that the hiding of methods is intentional and not the result of an oversight (and it makes the warning disappear):

// No withdraw() pains now.
new public decimal Withdraw(decimal withdrawal)
{
  // . . . no change internally . . .
}

Tip

This use of the keyword new has nothing to do with the same word new that's used to create an object. (C# even overloads itself!)

Calling back to base

Check out the SavingsAccount.Withdraw() method in the HidingWithdrawal example, shown earlier in this chapter. The call to BankAccount.Withdraw() from within this new method includes the new keyword base.

The following version of the method without the base keyword doesn't work:

new public decimal Withdraw(decimal withdrawal)
{
  decimal amountWithdrawn = Withdraw(withdrawal);
  amountWithdrawn += Withdraw(1.5);
  return amountWithdrawn;
}

This call has the same problem as this one:

void fn()
{
  fn(); // Call yourself.
}

The call to fn() from within fn() ends up calling itself (recursing) repeatedly. Similarly, a call to Withdraw() from within the method calls itself in a loop, chasing its tail until the program eventually crashes.

Somehow, you need to indicate to C# that the call from within SavingsAccount.Withdraw() is meant to invoke the base class BankAccount.Withdraw() method. One approach is to cast the this reference into an object of class BankAccount before making the call:

// Withdraw -- This version accesses the hidden method in the base
//    class by explicitly recasting the this object.
new public decimal Withdraw(decimal withdrawal)
{
  // Cast the this reference into an object of class BankAccount.
  BankAccount ba = (BankAccount)this;
  // Invoking Withdraw() using this BankAccount object
  // calls the method BankAccount.Withdraw().
  decimal amountWithdrawn = ba.Withdraw(withdrawal);
  amountWithdrawn += ba.Withdraw(1.5);
  return amountWithdrawn;
}

This solution works: The call ba.Withdraw() now invokes the BankAccount method, just as intended. The problem with this approach is the explicit reference to BankAccount. A future change to the program may rearrange the inheritance hierarchy so that SavingsAccount no longer inherits directly from BankAccount. This type of rearrangement breaks this method in a way that future programmers may not easily find. (Heck, I would never be able to find a bug like that one.)

You need a way to tell C# to call the Withdraw() method from "the class immediately above" in the hierarchy without naming it explicitly. That would be the class that SavingsAccount extends. C# provides the keyword base for this purpose.

Note

This keyword base is the same one that a constructor uses to pass arguments to its base class constructor.

The C# keyword base, shown in the following chunk of code, is the same sort of beast as this but is automatically recast to the base class no matter what that class may be:

// Withdraw -- You can withdraw any amount up to the
//    balance; return the amount withdrawn.
new public decimal Withdraw(decimal withdrawal)
{
  // Take the $1.50 off the top.
  base.Withdraw(1.5M);
  // Now you can withdraw from what's left.
  return base.Withdraw(withdrawal);
}

The call base.Withdraw() now invokes the BankAccount.Withdraw() method, thereby avoiding the recursive "invoking itself" problem. In addition, this solution doesn't break if the inheritance hierarchy is changed.

Polymorphism

You can overload a method in a base class with a method in the subclass. As simple as this process sounds, it introduces considerable capability, and with capability comes danger.

Here's a thought experiment: Should you make the decision to call BankAccount.Withdraw() or SavingsAccount.Withdraw() at compile-time or at runtime?

To illustrate the difference, I change the previous HidingWithdrawal program in a seemingly innocuous way. I call this new version Hiding WithdrawalPolymorphically. (I've streamlined the listing by leaving out the stuff that doesn't change.) The new version is shown here:

Note

// HidingWithdrawalPolymorphically -- Hide the Withdraw() method in the base
//    class with a method in the subclass of the same name.
public class Program
{
  public static void MakeAWithdrawal(BankAccount ba, decimal amount)
  {
    ba.Withdraw(amount);
  }
  public static void Main(string[] args)
  {
    BankAccount ba;
    SavingsAccount sa;

    // Create a bank account, withdraw $100, and
    // display the results.
    ba = new BankAccount(200M);
    MakeAWithdrawal(ba, 100M);

    // Try the same trick with a savings account.
    sa = new SavingsAccount(200M, 12);
    MakeAWithdrawal(sa, 100M);

    // Display the resulting balance.
    Console.WriteLine("When invoked through intermediary:");
    Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);
    Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);

    // Wait for user to acknowledge the results.
    Console.WriteLine("Press Enter to terminate...");
    Console.Read();
  }
}

The following output from this program may or may not be confusing, depending on what you expected:

When invoked through intermediary
BankAccount balance is $100.00
SavingsAccount balance is $100.00
Press Enter to terminate...

This time, rather than perform a withdrawal in Main(), the program passes the bank account object to the method MakeAWithdrawal().

The first question is fairly straightforward: Why does the MakeAWithdrawal() method even accept a SavingsAccount object when it clearly states that it's looking for a BankAccount? The answer is obvious: "Because a SavingsAccount IS_A BankAccount." (See Chapter 6 of this minibook.)

The second question is subtle. When passed a BankAccount object, MakeAWithdrawal() invokes BankAccount.Withdraw() — that's clear enough. But when passed a SavingsAccount object, MakeAWithdrawal() calls the same method. Shouldn't it invoke the Withdraw() method in the subclass?

The prosecution intends to show that the call ba.Withdraw() should invoke the method BankAccount.Withdraw(). Clearly, the ba object is a BankAccount. To do anything else would merely confuse the state. The defense has witnesses back in Main() to prove that although the ba object is declared BankAccount, it is in fact a SavingsAccount. The jury is deadlocked. Both arguments are equally valid.

In this case, C# comes down on the side of the prosecution: The safer of the two possibilities is to go with the declared type because it avoids any miscommunication. The object is declared to be a BankAccount and that's that. However, that may not be what you want.

Using the declared type every time (Is that so wrong?)

In some cases, you don't want to choose the declared type. What you want (or, "what you really, really want," to quote the popular Spice Girls song) is to make the call based on the real type — the runtime type — as opposed to the declared type. For example, you want to use the SavingsAccount stored in a BankAccount variable. This capability to decide at runtime is known as polymorphism, or late binding. Using the declared type every time is called early binding because it sounds like the opposite of late binding.

Note

The ridiculous term polymorphism comes from the Greek language: Poly means "more than one," morph means "transform," and ism is a fairly useless Greek term. But we're stuck with it.

Polymorphism and late binding aren't exactly the same concept — but the difference is subtle:

  • Polymorphism refers to the general ability to decide which method to invoke at runtime.

  • Late binding refers to the specific way a language implements polymorphism.

Polymorphism is the key to the power of object-oriented (OO) programming. It's so important that languages that don't support it can't advertise themselves as OO languages. (I think it's an FDA regulation: You can't label a language that doesn't support it as OO unless you add a disclaimer from the surgeon general, or something like that.)

Note

Languages that support classes but not polymorphism are object-based languages. Visual Basic 6.0 (not VB.NET) is an example of such a language.

Without polymorphism, inheritance has little meaning. Let me spring another example on you to show you why. Suppose that you had written a boffo program that uses a class named (just to pick a name out of the air) Student. After months of design, coding, and testing, you release this application to rave reviews from colleagues and critics alike. (You've even heard talk of starting a new Nobel Prize category for software, but you modestly brush such talk aside.)

Time passes and your boss asks you to add to this program the capability of handling graduate students, who are similar but not identical to undergraduate students. (The graduate students probably claim that they aren't similar in any way.) Suppose that the formula for calculating the tuition amount for a graduate student is completely different from the formula for an undergrad. Now, your boss doesn't know or care that, deep within the program, are numerous calls to the member method CalcTuition(). (A lot of things happen that your boss doesn't know or care about, by the way.) The following example shows one of those many calls to CalcTuition():

void SomeMethod(Student s)  // Could be grad or undergrad
{
  // . . . whatever it might do . . .
  s.CalcTuition();
  // . . . continues on . . .
}

If C# didn't support late binding, you would need to edit someMethod() to check whether the student object passed to it is a GraduateStudent or a Student. The program would call Student.CalcTuition() when s is a Student and GraduateStudent.CalcTuition() when it's a graduate student.

Editing someMethod() doesn't seem so bad, except for two problems:

  • You're assuming use by only one method. Suppose that CalcTuition() is called from many places.

  • CalcTuition() might not be the only difference between the two classes. The chances aren't good that you'll find all items that need to be changed.

Using polymorphism, you can let C# decide which method to call.

Using is to access a hidden method polymorphically

C# provides one approach to manually solving the problem of making your program polymorphic, using the keyword is. (I introduce is, and its cousin as, in Chapter 6 of this minibook.) The expression ba is SavingsAccount returns true or false depending on the runtime class of the object. The declared type may be BankAccount, but which type is it really? The following code chunk uses is to access the SavingsAccount version of Withdraw() specifically:

public class Program
{
  public static void MakeAWithdrawal(BankAccount ba, decimal amount)
  {
    if(ba is SavingsAccount)
    {
      SavingsAccount sa = (SavingsAccount)ba;
      sa.Withdraw(amount);
    }
    else
    {
      ba.Withdraw(amount);
    }
  }
}

Now, when Main() passes the method a SavingsAccount object, MakeAWithdrawal() checks the runtime type of the ba object and invokes SavingsAccount.Withdraw().

Note

Just as an aside, the programmer could have performed the cast and the call in the following single line:

((SavingsAccount)ba).Withdraw(amount);  // Notice locations of parentheses.

I mention this technique only because you often see it in programs written by show-offs. (You can use it, but it's more difficult to read than when you use multiple lines. Anything written confusingly or cryptically tends to be more error-prone, too.)

The is approach works, but it's a bad idea. It requires MakeAWithDrawal() to be aware of all the different types of bank accounts and which of them is represented by different classes. That puts too much responsibility on poor old MakeAWithdrawal(). Right now, your application handles only two types of bank accounts, but suppose that your boss asks you to implement a new account type, CheckingAccount, and it has different Withdraw() requirements. Your program doesn't work properly if you don't search out and find every method that checks the runtime type of its argument.

Declaring a method virtual and overriding it

As the author of MakeAWithdrawal(), you don't want to know about all the different types of accounts. You want to leave to the programmers who use MakeAWithdrawal() the responsibility to know about their account types and just leave you alone. You want C# to make decisions about which methods to invoke based on the runtime type of the object.

You tell C# to make the runtime decision of the version of Withdraw() by marking the base class method with the keyword virtual and marking each subclass version of the method with the keyword override.

I used polymorphism to rewrite the program example from the previous section. I added output statements to the Withdraw() methods to prove that the proper methods are indeed being invoked. (I also cut out any duplicated information.) Here's the PolymorphicInheritance program:

Note

// PolymorphicInheritance -- Hide a method in the
//    base class polymorphically. Show how to use
//    the virtual and override keywords.
using System;
namespace PolymorphicInheritance
{
  // BankAccount -- A basic bank account
  public class BankAccount
  {
    protected decimal _balance;
    public BankAccount(decimal initialBalance)
    {
      _balance = initialBalance;
    }
    public decimal Balance
    {
      get { return _balance; }
    }
    public virtual decimal Withdraw(decimal amount)
    {
      Console.WriteLine("In BankAccount.Withdraw() for ${0}...", amount);
      decimal amountToWithdraw = amount;
      if (amountToWithdraw > Balance)
      {
        amountToWithdraw = Balance;
      }
      _balance -= amountToWithdraw;
      return amountToWithdraw;
    }
  }
  // SavingsAccount -- A bank account that draws interest
  public class SavingsAccount : BankAccount
  {
    public decimal _interestRate;
    // SavingsAccount -- Input the rate expressed as a
    //    rate between 0 and 100.
    public SavingsAccount(decimal initialBalance, decimal interestRate)
                           : base(initialBalance)
    {
_interestRate = interestRate / 100;
    }
    // AccumulateInterest -- Invoke once per period.
    public void AccumulateInterest()
    {
      _balance = Balance + (Balance * _interestRate);
    }
    // Withdraw -- You can withdraw any amount up to the
    //    balance; return the amount withdrawn.
    override public decimal Withdraw(decimal withdrawal)
    {
      Console.WriteLine("In SavingsAccount.Withdraw()...");
      Console.WriteLine("Invoking base-class Withdraw twice...");
      // Take the $1.50 off the top.
      base.Withdraw(1.5M);
      // Now you can withdraw from what's left.
      return base.Withdraw(withdrawal);
    }
  }
  public class Program
  {
    public static void MakeAWithdrawal(BankAccount ba, decimal amount)
    {
      ba.Withdraw(amount);
    }
    public static void Main(string[] args)
    {
      BankAccount ba;
      SavingsAccount sa;
      // Display the resulting balance.
      Console.WriteLine("Withdrawal: MakeAWithdrawal(ba, ...)");
      ba = new BankAccount(200M);
      MakeAWithdrawal(ba, 100M);
      Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);
      Console.WriteLine("Withdrawal: MakeAWithdrawal(sa, ...)");
      sa = new SavingsAccount(200M, 12);
      MakeAWithdrawal(sa, 100M);
      Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);
      // Wait for user to acknowledge the results.
      Console.WriteLine("Press Enter to terminate...");
      Console.Read();
    }
  }
}

The output from executing this program is shown here:

Withdrawal: MakeAWithdrawal(ba, ...)
In BankAccount.Withdraw() for $100...
BankAccount balance is $100.00
Withdrawal: MakeAWithdrawal(sa, ...)
In SavingsAccount.Withdraw()...
Invoking base-class Withdraw twice...
In BankAccount.Withdraw() for $1.5...
In BankAccount.Withdraw() for $100...
SavingsAccount balance is $98.50
Press Enter to terminate...

Note

The Withdraw() method is flagged as virtual in the base class BankAccount, and the Withdraw() method in the subclass is flagged with the keyword override. The MakeAWithdrawal() method is unchanged, yet the output of the program is different because the call ba.Withdraw() is resolved based on the ba runtime type.

Tip

To get a good feel for how this works, step through the program in the Visual Studio 2005 debugger. Just build the program as normal and then repeatedly press F11 to watch the program walk through its paces. Watch the Withdraw() calls carefully. Watching the same call end up in two different methods at two different times is impressive.

Tip

Choose sparingly which methods to make virtual. Each one has a cost, so use the virtual keyword only when necessary. It's a trade-off between a class that's highly flexible and can be overridden (lots of virtual methods) and a class that isn't flexible enough (hardly any virtuals).

Getting the most benefit from polymorphism — the do-to-each trick

Much of the power of polymorphism springs from polymorphic objects sharing a common interface. For example, given a hierarchy of Shape objects — Circles, Squares, and Triangles, for example — you can count on all shapes having a Draw() method. Each object's Draw() method is implemented quite differently, of course. But the point is that, given a collection of these objects, you can freely use a foreach loop to call Draw() or any other method in the polymorphic interface on the objects. I call it the "do-to-each" trick.

The Class Business Card: ToString()

All classes inherit from a common base class that carries the clever name Object. However, it's worth mentioning here that Object includes a method, ToString(), that converts the contents of the object into a string. The idea is that each class should override the ToString() method to display itself in a meaningful way. I used the method GetString() until now because I didn't want to begin discussing inheritance issues until Chapter 6 of this minibook. After you understand inheritance, the virtual keyword, and overriding, we can describe ToString(). By overriding ToString() for each class, you give each class the ability to display itself in its own way. For example, a useful, appropriate Student.ToString() method may display the student's name and ID.

Most methods — even those built into the C# library — use the ToString() method to display objects. Thus overriding ToString() has the useful side effect of displaying the object in its own, unique format, no matter who does the displaying.

Note

Always override ToString().

C# During Its Abstract Period

The duck is a type of bird, I think. So are the cardinal and the hummingbird. In fact, every bird out there is a subtype of bird. The flip side of that argument is that no bird exists that isn't some subtype of Bird. That statement doesn't sound profound, but in a way, it is. The software equivalent of that statement is that all bird objects are instances of some subclass of Bird — there's never an instance of class Bird. What's a bird? It's always a robin or a grackle or another specific species.

Different types of birds share many properties (otherwise, they wouldn't be birds), yet no two types share every property. If they did, they wouldn't be different types. To pick a particularly gross example, not all birds Fly() the same way. Ducks have one style. The cardinal's style is similar but not identical. The hummingbird's style is completely different. (Don't even get me started about emus and ostriches or the rubber ducky in my tub.)

But if not all birds fly the same way and there's no such thing as a Bird, what the heck is Bird.Fly()? The subject of the following sections, that's what it is.

Class factoring

People generate taxonomies of objects by factoring out commonalities. To see how factoring works, consider the two classes HighSchool and University, shown in Figure 7-1. This figure uses the Unified Modeling Language (UML), a graphical language that describes a class along with the relationship of that class to others. UML has become universally popular with programmers and is worth learning (to a reasonable extent) in its own right.

A UML description of the HighSchool and Univer-sity classes.

Figure 7-1. A UML description of the HighSchool and Univer-sity classes.

Note

Note

A Car IS_A Vehicle but a Car HAS_A Motor.

High schools and universities have several similar properties — many more than you may think (refer to Figure 7-1). Both schools offer a publicly available Enroll() method for adding Student objects to the school. In addition, both classes offer a private member numStudents that indicates the number of students attending the school. Another common feature is the relationship between students: One school can have any number of students — a student can attend only a single school at one time. Even high schools and most universities offer more than I describe, but one of each type of member is all I need for illustration.

In addition to the features of a high school, the university contains a method GetGrant() and a data member avgSAT. High schools have no SAT entrance requirements and receive no federal grants (unless I went to the wrong one).

Figure 7-1 is acceptable, as far as it goes, but lots of information is duplicated, and duplication in code (and UML diagrams) stinks. You can reduce the duplication by allowing the more complex class University to inherit from the simpler HighSchool class, as shown in Figure 7-2.

Inheriting HighSchool simplifies the Univer-sity class but introduces problems.

Figure 7-2. Inheriting HighSchool simplifies the Univer-sity class but introduces problems.

The HighSchool class is left unchanged, but the University class is easier to describe. You say that "a University is a HighSchool that also has an avgSAT and a GetGrant() method." But this solution has a fundamental problem: A university isn't a high school with special properties.

You say, "So what? Inheriting works, and it saves effort." True, but my reservations are more than stylistic trivialities. (My reservations are at some of the best restaurants in town — at least, that's what all the truckers say.) This type of misrepresentation is confusing to the programmer, both now and in the future. Someday, a programmer who is unfamiliar with your programming tricks will have to read and understand what your code does. Misleading representations are difficult to reconcile and understand.

In addition, this type of misrepresentation can lead to problems down the road. Suppose that the high school decides to name a "favorite" student at the prom — not that I would know anything about that sort of thing. The clever programmer adds the NameFavorite() method to the HighSchool class, which the application invokes to name the favorite Student object.

But now you have a problem: Most universities don't name a favorite anything, other than price. However, as long as University inherits from HighSchool, it inherits the NameFavorite() method. One extra method may not seem like a big deal. "Just ignore it," you say.

One extra method isn't a big deal, but it's just one more brick in the wall of confusion. Extra methods and properties accumulate over time, until the University class is carrying lots of extra baggage. Pity the poor software developer who has to understand which methods are "real" and which aren't.

"Inheritances of convenience" lead to another problem. The way it's written, Figure 7-2 implies that a University and a HighSchool have the same enrollment procedure. As unlikely as that statement sounds, assume that it's true. The program is developed, packaged up, and shipped off to the unwitting public — of course, I've embedded the requisite number of bugs so that they'll want to upgrade to Version 2.0 with all its bug fixes — for a small fee, of course.

Months pass before the school district decides to modify its enrollment procedure. It isn't obvious to anyone that modifying the high school enrollment procedure also modifies the sign-up procedure at the local college.

How can you avoid these problems? Not going to school is one way, but another is to fix the source of the problem: A university isn't a particular type of high school. A relationship exists between the two, but IS_A isn't the right one. (HAS_A doesn't work either. A university HAS_A high school? A high school HAS_A university? Come on!) Instead, both high schools and universities are special types of schools. That's what they have most in common.

Figure 7-3 describes a better relationship. The newly defined class School contains the common properties of both types of schools, including the relationship they both have with Student objects. School even contains the common Enroll() method, although it's abstract because HighSchool and University usually don't implement Enroll() the same way.

The classes HighSchool and University now inherit from a common base class. Each contains its unique members: NameFavorite() in the case of HighSchool, and GetGrant() for the University. In addition, both classes override the Enroll() method with a version that describes how that type of school enrolls students. In effect, I've extracted a superclass, or base class, from two similar classes, which now become subclasses.

Both HighSchool and Univer-sity should be based on a common School class.

Figure 7-3. Both HighSchool and Univer-sity should be based on a common School class.

The introduction of the School class has at least two big advantages:

  • It corresponds with reality. A University is a School, but it isn't a HighSchool. Matching reality is nice but not conclusive.

  • It isolates one class from changes or additions to the other. When my boss inevitably requests later that I introduce the commencement exercise to the university, I can add the CommencementSpeech() method to the University class and not affect HighSchool.

This process of culling common properties from similar classes is known as factoring. This feature of object-oriented languages is important for the reasons described earlier in this minibook, plus one more: reducing redundancy. Let me repeat: Redundancy is bad.

Warning

Factoring is legitimate only if the inheritance relationship corresponds to reality. Factoring together a class Mouse and Joystick because they're both hardware pointing devices is legitimate. Factoring together a class Mouse and Display because they both make low-level operating-system calls is not.

Factoring can and usually does result in multiple levels of abstraction. For example, a program written for a more developed school hierarchy may have a class structure more like the one shown in Figure 7-4.

You can see that I have inserted a pair of new classes between University and School: HigherLearning and LowerLevel. For example, I've subdivided the new class HigherLearning into College and University. This type of multitiered class hierarchy is common and desirable when factoring out relationships. They correspond to reality, and they can teach you sometimes subtle features of your solution.

Class factoring can (and usually does) result in added layers of inheritance hierarchy.

Figure 7-4. Class factoring can (and usually does) result in added layers of inheritance hierarchy.

Note, however, that no Unified Factoring Theory exists for any given set of classes. The relationship shown in Figure 7-4 seems natural, but suppose that an application cared more about differentiating types of schools that are administered by local politicians from those that aren't. This relationship, shown in Figure 7-5, is a more natural fit for that type of problem. No "correct" factoring exists: The proper way to break down the classes is partially a function of the problem being solved.

Breaking down classes is partially a function of the problem being solved.

Figure 7-5. Breaking down classes is partially a function of the problem being solved.

The abstract class: Left with nothing but a concept

As intellectually satisfying as factoring is, it introduces a problem of its own. Visit (or revisit) BankAccount, introduced at the beginning of this chapter. Think about how you may go about defining the different member methods defined in BankAccount.

Most BankAccount member methods are no problem to refactor because both account types implement them in the same way. You should implement those common methods in BankAccount. Withdraw() is different, however. The rules for withdrawing from a savings account differ from those for withdrawing from a checking account. You have to implement SavingsAccount.Withdraw() differently from CheckingAccount.Withdraw(). But how are you supposed to implement BankAccount.Withdraw()?

Ask the bank manager for help. I imagine this conversation taking place:

  • "What are the rules for making a withdrawal from an account?" you ask, expectantly.

  • "Which type of account? Savings or checking?" comes the reply.

  • "From an account," you say. "Just an account."

  • [Blank look.] (You might say a "blank bank look." Then again, maybe not.)

The problem is that the question doesn't make sense. No such thing as "just an account" exists. All accounts (in this example) are either checking accounts or savings accounts. The concept of an account is abstract: It factors out properties common to the two concrete classes. It's incomplete because it lacks the critical property Withdraw(). (After you delve into the details, you may find other properties that a simple account lacks.)

The concept of a BankAccount is abstract.

How do you use an abstract class?

Abstract classes are used to describe abstract concepts.

An abstract class is a class with one or more abstract methods. (Oh, great. That helps a lot.) Okay, an abstract method is a method marked abstract. (We're moving now!) Let me try again: An abstract method has no implementation — now you're really confused.

Consider the following stripped-down demonstration program:

Note

// AbstractInheritance -- The BankAccount class is abstract because
//    there is no single implementation for Withdraw.
namespace AbstractInheritance
{
  using System;
  // AbstractBaseClass -- Create an abstract base class with nothing
  //    but an Output() method. You can also say "public abstract."
  abstract public class AbstractBaseClass
  {
    // Output -- Abstract method that outputs a string
    abstract public void Output(string outputString);
  }
// SubClass1 -- One concrete implementation of AbstractBaseClass
  public class SubClass1 : AbstractBaseClass
  {
    override public void Output(string source) // Or "public override"
    {
      string s = source.ToUpper();
      Console.WriteLine("Call to SubClass1.Output() from within {0}", s);
    }
  }
  // SubClass2 -- Another concrete implementation of AbstractBaseClass
  public class SubClass2 : AbstractBaseClass
  {
    public override void Output(string source)  // Or "override public"
    {
      string s = source.ToLower();
      Console.WriteLine("Call to SubClass2.Output() from within {0}", s);
    }
  }
  class Program
  {
    public static void Test(AbstractBaseClass ba)
    {
      ba.Output("Test");
    }
    public static void Main(string[] strings)
    {
      // You can't create an AbstractBaseClass object because it's
      // abstract -- duh. C# generates a compile-time error if you
      // uncomment the following line.
      // AbstractBaseClass ba = new AbstractBaseClass();
      // Now repeat the experiment with Subclass1.
      Console.WriteLine("
creating a SubClass1 object");
      SubClass1 sc1 = new SubClass1();
      Test(sc1);
      // And, finally, a Subclass2 object
      Console.WriteLine("
creating a SubClass2 object");
      SubClass2 sc2 = new SubClass2();
      Test(sc2);
      // Wait for user to acknowledge.
      Console.WriteLine("Press Enter to terminate... ");
      Console.Read();
    }
  }
}

The program first defines the class AbstractBaseClass with a single abstract Output() method. Because it's declared abstract, Output() has no implementation — that is, no method body.

Two classes inherit from AbstractBaseClass: SubClass1 and SubClass2. Both are concrete classes because they override the Output() method with "real" methods and contain no abstract methods themselves.

Tip

A class can be declared abstract whether it has abstract members or not; however, a class can be concrete only when all abstract methods in any base class above it have been overridden with full methods.

The two subclass Output() methods differ in a trivial way: Both accept input strings, which they regurgitate to users. However, one converts the string to all caps before output and the other converts it to all-lowercase characters.

The following output from this program demonstrates the polymorphic nature of AbstractBaseClass:

Creating a SubClass1 object
Call to SubClass1.Output() from within TEST

Creating a SubClass2 object
Call to SubClass2.Output() from within test
Press Enter to terminate...

Tip

An abstract method is automatically virtual, so you don't add the virtual keyword to an abstract method.

Creating an abstract object — not!

Notice something about the AbstractInheritance program: It isn't legal to create an AbstractBaseClass object, but the argument to Test() is declared to be an object of the class AbstractBaseClass or one of its subclasses. It's the "subclasses" clause that's critical here. The SubClass1 and SubClass2 objects can be passed because each one is a concrete subclass of AbstractBaseClass. The IS_A relationship applies. This powerful technique lets you write highly general methods.

Sealing a Class

You may decide that you don't want future generations of programmers to be able to extend a particular class. You can lock the class by using the keyword sealed.

Note

A sealed class cannot be used as the base class for any other class.

Consider this code snippet:

using System;
public class BankAccount
{
  // Withdrawal -- You can withdraw any amount up to the
  //    balance; return the amount withdrawn
  virtual public void Withdraw(decimal withdrawal)
  {
    Console.WriteLine("invokes BankAccount.Withdraw()");
  }
}
public sealed class SavingsAccount : BankAccount
{
override public void Withdraw(decimal withdrawal)
  {
    Console.WriteLine("invokes SavingsAccount.Withdraw()");
  }
}
public class SpecialSaleAccount : SavingsAccount  // Oops!
{
  override public void Withdraw(decimal withdrawal)
  {
    Console.WriteLine("invokes SpecialSaleAccount.Withdraw()");
  }
}

This snippet generates the following compiler error:

'SpecialSaleAccount' : cannot inherit from sealed class 'SavingsAccount'

You use the sealed keyword to protect your class from the prying methods of a subclass. For example, allowing a programmer to extend a class that implements system security enables someone to create a security back door.

Sealing a class prevents another program, possibly somewhere on the Internet, from using a modified version of your class. The remote program can use the class as is, or not, but it can't inherit bits and pieces of your class while overriding the rest.

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

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