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.
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.
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.
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).
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 voidAMethod()
{ // Do something. } public static voidAMethod(int)
{ // Do something else. } public static voidAMethod(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.
The return type isn't part of the extended name. You can't have two methods that differ only in their return types.
Not surprisingly, the class to which a method belongs is also a part of its extended name. Consider this code segment:
public classMyClass
{ public static void AMethod1(); public void AMethod2(); } public classUrClass
{
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.
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:
// 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.
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.
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.
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?
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.
Tell the C# compiler to treat warnings as errors, at least part of the time. To do so, choose Project
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 . . .
}
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!)
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.
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. returnbase.
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.
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:
// 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.
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.
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.)
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.
is
to access a hidden method polymorphicallyC# 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()
.
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.
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:
// 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...
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.
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.
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).
Much of the power of polymorphism springs from polymorphic objects sharing a common interface. For example, given a hierarchy of Shape
objects — Circle
s, Square
s, and Triangle
s, 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.
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.
Always override ToString()
.
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.
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 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.
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.
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.
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.
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.
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.
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:
// 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 stringabstract
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 { publicoverride
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.
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...
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.
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
.
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.