Chapter 6

Poly-what-ism?

IN THIS CHAPTER

Bullet Hiding or overriding a base class method

Bullet Building abstract classes and methods

Bullet Using ToString()

Bullet Sealing a class from being subclassed

In inheritance, one class adopts the members of another. Thus it's possible to 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 5 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, such as heating food. It’s not important to know what the oven must do internally to make that happen, any more than it’s important to know what type of oven it is, who made it, or whether it was on sale when purchased.

From a 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.

Remember You don’t have to type the source code for this chapter manually. In fact, using the downloadable source is a lot easier. You can find the source for this chapter in the CSAIO4D2EBK02CH06 folder of the downloadable source. See the Introduction for details on how to find these source files.

Overloading an Inherited Method

As described in the “Overloading a method doesn’t mean giving it too much to do” section of Chapter 2 of this minibook, 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). If you need a quick, but complete, summary of overloading, check out the article at https://www.geeksforgeeks.org/c-sharp-method-overloading/ as well. The following sections extend the concept of method overloading to inherited methods.

It’s a simple case of method overloading

Remember Giving two methods the same name is overloading. 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.

Remember 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 nothing to do with UrClass.AMethod1().

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 that makes 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()
{
// Create a savings account:
BankAccount ba = new BankAccount(0, true);
}
}

Your method must tell the BankAccount whether the object you're instantiating is 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 in the SavingsAccount class. The BankAccount and SavingsAccount classes of the HidingWithdrawal example show how this approach works:

// BankAccount -- A very basic bank account
internal class BankAccount
{
internal BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}

internal decimal Balance
{ get; private set; }

internal 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
internal class SavingsAccount : BankAccount
{
private decimal InterestRate
{ get; set; }

// SavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
public SavingsAccount(decimal initialBalance, decimal interestRate)
: base(initialBalance)
{
InterestRate = interestRate / 100;
}

// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
internal 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 two classes provide some basics for creating the accounts and withdrawing money. Notice that both classes have a Withdraw() method with precisely the same signature, so the Withdraw() method in SavingsAccount completely hides the Withdraw() method in BankAccount. Here's the Main() method used to exercise these two classes.

static void Main(string[] args)
{
BankAccount ba;
SavingsAccount sa;

// Create a bank account and withdraw $100.
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);
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. Because of this approach, the set member of Balance is private, rather than internal, reducing security risks. 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. 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 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 should 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.

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

internal new 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);
}

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

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, the following example changes the previous HidingWithdrawal program in a seemingly innocuous way. (The HidingWithdrawalPolymorphically version streamlines the listing by leaving out the stuff that doesn't change.) The new version is shown here:

class Program
{
public static void MakeAWithdrawal(BankAccount ba, decimal amount)
{
ba.Withdraw(amount);
}

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

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 the “IS_A versus HAS_A — I'm So Confused_A” section of Chapter 5 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 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 term polymorphism comes from the Greek language: Poly means more than one, morph means transform, and ism relates to an ideology or philosophy. Consequently, polymorphism is the idea or concept of transforming a single object, BankAccount, into many different objects, BankAccount or SavingsAccount (in this case). 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 Programming (OOP). It’s so important that languages that don’t support it can’t advertise themselves as OOP languages.

Technicalstuff 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. As another example, suppose that you had written a great program that uses a class named Student. After months of design, coding, and testing, you release this application to rave reviews from colleagues and critics alike.

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(). 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 GraduateStudent. 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. (The “Avoiding invalid conversions with the is operator” section of Chapter 5 of this minibook introduces is and its cousin as.) 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 (as found in HidingWithdrawalPolymorphically2):

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

Technicalstuff As an alternative, the programmer could have performed the cast and the call for a SavingsAccount in the following single line:

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

You often see this technique used in programs written by experienced developers who hate typing any more than necessary. Although you can use this approach, it's more difficult to read than when you use multiple lines, as shown in the example code. 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.

The following example relies on polymorphism. It has output statements in the Withdraw() methods to prove that the proper methods are indeed being invoked. Here are the BankAccount and SavingsAccount classes of the PolymorphicInheritance program:

// BankAccount -- A very basic bank account
internal class BankAccount
{
internal BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}

internal decimal Balance
{ get; private set; }

internal virtual decimal Withdraw(decimal amount)
{
decimal amountToWithdraw = amount;

if (amountToWithdraw > Balance)
{
amountToWithdraw = Balance;
}
Console.WriteLine($"In BankAccount.Withdraw() for " +
$"${amountToWithdraw}.");

Balance -= amountToWithdraw;
return amountToWithdraw;
}
}

// SavingsAccount -- A bank account that draws interest
internal class SavingsAccount : BankAccount
{
private decimal InterestRate
{ get; set; }

// SavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
public SavingsAccount(decimal initialBalance, decimal interestRate)
: base(initialBalance)
{
InterestRate = interestRate / 100;
}

// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
internal override decimal Withdraw(decimal withdrawal)
{
// Take the $1.50 off the top.
Console.WriteLine("Deductng the SavingsAccount fee.");
base.Withdraw(1.5M);

// Now you can withdraw from what's left.
Console.WriteLine($"In SavingsAccount.Withdraw() for " +
$"${withdrawal}.");
return base.Withdraw(withdrawal);
}
}

The Withdraw() method is marked as virtual in the base class BankAccount. Likewise, you see it marked override in the subclass SavingsAccount. This version of the example also adds some Console.WriteLine() calls so you can see what's happening. Here’s the Program class code:

class Program
{
public static void MakeAWithdrawal(BankAccount ba, decimal amount)
{
ba.Withdraw(amount);
}

static void Main(string[] args)
{
BankAccount ba;
SavingsAccount sa;

// Create a bank account, withdraw $100, and
// display the results.
Console.WriteLine("Withdrawal: MakeAWithdrawal(ba, …)");
ba = new BankAccount(200M);
MakeAWithdrawal(ba, 100M);
Console.WriteLine("BankAccount balance is {0:C}", ba.Balance);

// Try the same trick with a savings account.
Console.WriteLine(" Withdrawal: MakeAWithdrawal(sa, …)");
sa = new SavingsAccount(200M, 12);
MakeAWithdrawal(sa, 100M);
Console.WriteLine("SavingsAccount balance is {0:C}", sa.Balance);
Console.Read();
}
}

Notice that all the decision-making code is removed from the HidingWithdrawalPolymorphically2 example. 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, …)
Deductng the SavingsAccount fee.
In BankAccount.Withdraw() for $1.5.
In SavingsAccount.Withdraw() for $100.
In BankAccount.Withdraw() for $100.
SavingsAccount balance is $98.50

Tip Choose sparingly which methods to make virtual. Each one has a small cost in resource use and runtime speed, 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

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.

C# During Its Abstract Period

A duck is a type of bird. 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 a 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 the Bird subclasses — 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. For example, not all birds Fly() the same way (or possibly at all). Ducks have one style, cardinals another. The hummingbird's style is completely different. And ostriches are only interested in sticking their heads in the sand and not flying at all (or perhaps not — see https://www.scienceworld.ca/stories/do-ostriches-really-bury-their-heads-sand/). But if not all birds fly the same way and there’s no such thing as an instance of a generic Bird, what the heck is Bird.Fly()? The Bird.Fly() method would need to be different for each subclass of Bird. The following sections discuss this issue in detail.

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

Snapshot of a UML description of the HighSchool and University classes.

FIGURE 6-1: A UML description of the HighSchool and University classes.

High schools and universities have several similar properties (refer to Figure 6-1) — many more than you may think. Both schools offer a publicly available Enroll() method for adding Student objects to the school. In addition, both classes offer a private member, numStudents, which 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 described, but only one of each type of member is needed 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 students receive no federal grants.

Figure 6-1 is acceptable, as far as it goes, but some 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 6-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.

Snapshot of inheriting HighSchool simplifies the University class but introduces problems.

FIGURE 6-2: Inheriting HighSchool simplifies the University class but introduces problems.

You say, “So what? Inheriting works, and it saves effort.” True, but the problems are more than stylistic trivialities. 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. 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 student. 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. However, one method is 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.

Remember Inheritances of convenience lead to another problem. The way it’s written, Figure 6-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. 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.

To fix the source of the problem you must consider that a university isn’t a particular type of high school. A relationship exists between the two, but neither IS_A or HAS_A are the right ones. Instead, both high schools and universities are special types of schools. That’s what they have the most in common.

Figure 6-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.

Snapshot of Base both HighSchool and University on a common School class.

FIGURE 6-3: Base both HighSchool and University on a common School class.

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() and avgSAT 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, the example extracts 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. Adding the CommencementSpeech() method to the University class doesn't 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.

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 6-4.

Snapshot of class factoring usually results in added layers of inheritance hierarchy.

FIGURE 6-4: Class factoring usually results in added layers of inheritance hierarchy.

You can see that Figure 6-4 inserts a pair of new classes between University and School: HigherLearning and LowerLevel. It subdivides 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 subtle features of your solution.

Note, however, that no Unified Factoring Theory exists for any given set of classes. The relationship shown in Figure 6-4 seems natural, but suppose that an application cared more about differentiating types of schools administered by local politicians from those that aren't. This relationship, shown in Figure 6-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.

The abstract class: Left with nothing but a concept

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

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

FIGURE 6-5: Breaking down classes is partially a function of the problem being solved.

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. This is the conversation that could take 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 method Withdraw(). (After you delve into the details, you may find other properties that a simple account lacks.)

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. An abstract method is a method marked abstract and has no implementation because it has no method body. You create the method body when you subclass from the abstract class. Consider the classes of the AbstractInheritance program:

// 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 {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 {s}");
}
}

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 regardless of whether it has abstract members; however, a class can be concrete (not abstract) 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 send back to users. However, one converts the string to all caps before output and the other converts it to all-lowercase characters. Here is the Program class for this example:

class Program
{
public static void Test(AbstractBaseClass ba)
{
ba.Output("Test");
}

static void Main(string[] args)
{
// You can't create an AbstractBaseClass object because it's
// abstract. 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);
Console.Read();
}
}

This code looks much the same as the PolymorphicInheritance example earlier in the chapter. Main() instantiates an object of each of the subclasses and then calls the Test() method, which calls on the Output() method in each class. The following output from this program demonstrates the polymorphic nature of AbstractInheritance:

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

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

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. A sealed class cannot be used as the base class for any other class. Consider this code snippet:

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