Chapter 5

Inheritance: Is That All I Get?

IN THIS CHAPTER

Bullet Including constructors in a hierarchy

Bullet Invoking the base-class constructor

Bullet Differentiating between is a and has a

Bullet Substituting one class object for another

Object-oriented programming is based on four principles: the capability to control access (encapsulation), inherit from other classes, respond appropriately (polymorphism), and refer from one object to another indirectly (interfaces).

Inheritance is a common concept. You are a human. You inherit certain properties from the class Human, such as your ability to converse and your dependence on air, food, and beverages. The class Human inherits its dependencies on air, water, and nourishment from the class Mammal, which inherits from the class Animal.

The capability to pass down properties is a powerful one. You can use it to describe items in an economical way. For example, if your son asks, “What's a duck?” you can say, “It’s a bird that quacks.” Despite what you may think, that answer conveys a considerable amount of information. Your son knows what a bird is, and now he knows all those same characteristics about a duck plus the duck’s additional property of “quackness.”

Object-oriented languages express this inheritance relationship by allowing one class to inherit properties from another. This feature enables object-oriented languages to generate a model that’s closer to the real world than the model generated by languages that don’t support inheritance. This chapter discusses C# inheritance in detail so that you can understand the relationships between the various classes that you use.

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 CSAIO4D2EBK02CH05 folder of the downloadable source. See the Introduction for details on how to find these source files.

Why You Need Inheritance

Inheritance serves several important functions. You may think, for example, that inheritance reduces the amount of typing. In a way, it does — you don’t need to repeat the properties of a Person when you're describing a Student class. A more important, related issue is the major buzzword reuse. Computer scientists have known for some time that starting from scratch with each new project and rebuilding the same software components makes little sense.

Compare the situation in software development to that of other industries. Think about the number of car manufacturers that start by building their own wrenches and screwdrivers before they construct a car. Of those who do that, estimate how many would start over completely and build all new tools for the next model. Practitioners in other industries have found that starting with existing screws, bolts, nuts, and even larger off-the-shelf components such as motors and compressors makes more sense than starting from scratch.

Inheritance enables you to tweak existing software components. You can adapt existing classes to new applications without making internal modifications. The existing class is inherited into — or, as programmers often say, extended by — a new subclass that contains the necessary additions and modifications. If someone else wrote the base class, you may not be able to modify it, so inheritance can save the day.

This capability carries with it a third benefit of inheritance. Suppose that you inherit from — extend — an existing class. Later, you find that the base class has a bug you must correct. If you modified the class to reuse it, you must manually check for, correct, and retest the bug in each application separately. If you inherited the class without changes, you can generally stick the updated class into the other application with little hassle.

But the biggest benefit of inheritance is that it describes the way life is. Items inherit properties from each other. There’s no getting around it.

Inheriting from a BankAccount Class (a More Complex Example)

A bank maintains several types of accounts. One type, the savings account, has all the properties of a simple bank account plus the ability to accumulate interest. The following sections discuss the BankAccount class and its subclass, SavingsAccount, starting with a basic application and then looking at how the constructors and other features work.

Working with the basic update

It's important to start with the basics. The BankAccount class acts as the base class for the SimpleSavingsAccount program:

// BankAccount -- Simulate a bank account, each of which
// carries an account ID (which is assigned
// on creation) and a balance.
internal class BankAccount // The base class
{
// Bank accounts start at 1000 and increase sequentially.
private static int _nextAccountNumber = 1000;

// Maintain the account number for each object.
private int _accountNumber;

// Constructor -- Initialize a bank account with the next account
// ID and the specified initial balance (default to zero).
internal BankAccount() : this(0) { }

internal BankAccount(decimal initialBalance)
{
_accountNumber = ++_nextAccountNumber;
Balance = initialBalance;
}

// Balance property.
protected decimal Balance
{ get; set; }

// Deposit -- any positive deposit is allowed.
public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
}
}

// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
public decimal Withdraw(decimal withdrawal)
{
if (Balance <= withdrawal) // Use Balance property.
{
withdrawal = Balance;
}
Balance -= withdrawal;
return withdrawal;
}

// ToString - Stringify the account.
public override string ToString()
{
return $"{_accountNumber} - {Balance:C}";
}
}

This example uses chaining to chain one constructor to another using this. When you see internal BankAccount() : this(0) { }, it means to call the next constructor in the chain using a value of 0 for the initialBalance parameter. Using this approach greatly reduces the amount of code you have to write, reduces the potential for error, and makes the code more maintainable. The second constructor obtains the next account number and sets the Balance property.

The Balance property has a protected access modifier because only the base class, BankAccount, and the derived class, SavingsAccount, will access it. Using this approach means that you can simplify the code and not have to maintain a separate internal variable of your own.

The three methods, Deposit(), Withdraw(), and ToString(), allow controlled public access of BankAccount features. ToString() overrides the base ToString() to display the account number and balance as a dollar amount.

The SavingsAccount class adds the ability to change the balance based on interest accumulation, as shown here:

internal class SavingsAccount : BankAccount // The subclass
{
private decimal _interestRate = 0;

// InitSavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
internal SavingsAccount(decimal interestRate) :
this(0, interestRate) { }

internal SavingsAccount(decimal initialBalance,
decimal interestRate) : base(initialBalance)
{
_interestRate = interestRate / 100;
}

// AccumulateInterest -- Invoke once per period.
public void AccumulateInterest()
{
Balance = Balance + (decimal)(Balance * _interestRate);
}

// ToString -- Stringify the account.
public override string ToString()
{
return $"{base.ToString()} ({_interestRate:P})";
}
}

As with BankAccount, SavingsAccount provides two constructors, one of which is chained to the other. SavingsAccount is a subclass of BankAccount, so it already has access to elements like a Balance. The only thing it needs to add is the ability to accumulate interest, which appears in AccumulateInterest(). Notice how the SavingsAccount class ToString() builds on the base class ToString(). Also note how the SavingsAccount() constructor calls initializeBalance() in the base class.

Main() does about as little as it can. It creates a BankAccount, makes a deposit, displays the account, creates a SavingsAccount, accumulates one period of interest, and displays the result, with the interest rate as shown here:

static void Main(string[] args)
{
// Create a bank account and display it.
BankAccount ba = new BankAccount(100M);
ba.Deposit(100M);
Console.WriteLine($"Account {ba.ToString()}");

// Now a savings account
SavingsAccount sa = new SavingsAccount(12.5M);
sa.Deposit(100M);
sa.AccumulateInterest();
Console.WriteLine($"Account {sa.ToString()}");
Console.Read();
}

You see the following output when you run this application:

Account 1001 - $200.00
Account 1002 - $112.50 (12.500%)

Tracking the BankAccount and SavingsAccount classes features

It can be hard to track precisely how the BankAccount and SavingsAccount classes work unless you single-step through them using the debugger. Another technique is to add some Console.WriteLine() method entries, as shown in the following listing (and in SimpleSavingsAccountTracked):

internal class BankAccount // The base class
{
// Bank accounts start at 1000 and increase sequentially.
private static int _nextAccountNumber = 1000;

// Maintain the account number for each object.
private int _accountNumber;

// Constructor -- Initialize a bank account with the next account
// ID and the specified initial balance (default to zero).
internal BankAccount() : this(0)
{
Console.WriteLine("*** Called BankAccount Default Constructor ***");
}

internal BankAccount(decimal initialBalance)
{
_accountNumber = ++_nextAccountNumber;
Balance = initialBalance;
Console.WriteLine("*** Called BankAccount Constructor ***");
}

// Balance property.
protected decimal Balance
{ get; set; }

// Deposit -- any positive deposit is allowed.
public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
Console.WriteLine("*** Called BankAccount Deposit() ***");
}
}

// Withdraw -- You can withdraw any amount up to the
// balance; return the amount withdrawn.
public decimal Withdraw(decimal withdrawal)
{
if (Balance <= withdrawal) // Use Balance property.
{
withdrawal = Balance;
}
Balance -= withdrawal;
Console.WriteLine("*** Called BankAccount Withdrawal() ***");
return withdrawal;
}

// ToString - Stringify the account.
public override string ToString()
{
Console.WriteLine("*** Called BankAccount ToString() ***");
return $"{_accountNumber} - {Balance:C}";
}
}

// SavingsAccount -- A bank account that draws interest
internal class SavingsAccount : BankAccount // The subclass
{
private decimal _interestRate = 0;

// InitSavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
internal SavingsAccount(decimal interestRate) :
this(0, interestRate)
{
Console.WriteLine("*** Called SavingsAccount Constructor 1 ***");
}

internal SavingsAccount(decimal initialBalance,
decimal interestRate) : base(initialBalance)
{
_interestRate = interestRate / 100;
Console.WriteLine("*** Called SavingsAccount Constructor 2 ***");
}

// AccumulateInterest -- Invoke once per period.
public void AccumulateInterest()
{
Balance = Balance + (decimal)(Balance * _interestRate);
Console.WriteLine("*** Called SavingsAccount " +
"AccumulateInterest() ***");
}

// ToString -- Stringify the account.
public override string ToString()
{
Console.WriteLine("*** Called SavingsAccount ToString() ***");
return $"{base.ToString()} ({_interestRate:P})";
}
}

The code in Main() is unchanged, although, you could certainly play around with it to see what happens. When you run this version of the code, you see the following output:

*** Called BankAccount Constructor ***
*** Called BankAccount Deposit() ***
*** Called BankAccount ToString() ***
Account 1001 - $200.00
*** Called BankAccount Constructor ***
*** Called SavingsAccount Constructor 2 ***
*** Called SavingsAccount Constructor 1 ***
*** Called BankAccount Deposit() ***
*** Called SavingsAccount AccumulateInterest() ***
*** Called SavingsAccount ToString() ***
*** Called BankAccount ToString() ***
Account 1002 - $112.50 (12.50%)

Creating and using the BankAccount object ba is straightforward. The code calls the constructor that accepts an initial amount, deposits $100.00, and then prints the result.

However, creating and using the SavingsAccount object, sa, requires a little more work. In this case, the code:

  1. Calls the BankAccount constructor that accepts an initial amount first because that's the constructor called by the base(initialBalance) portion of the second SavingsAccount constructor.
  2. Performs the tasks in the second SavingsAccount constructor because that's the constructor called by the this(0, interestRate) portion of the first SavingsAccount constructor.
  3. Performs the tasks in the first SavingsAccount constructor (the one that accepts only an interest rate as input).
  4. Makes a deposit using the BankAccount.Deposit() method (despite the fact that the call appears as sa.Deposit(100M)).
  5. Accumulates interest using the SavingsAccount.AccumulateInterest() method because this method is unique to the SavingsAccount class.
  6. Calls the SavingsAccount.ToString() override, which calls the BankAccount.ToString() override to print the savings account string.

Remember It's important to understand the order used to call the various constructors and methods because you could make bad assumptions otherwise. When working through a class construction, you can always add statements to see what is getting called and when it’s getting called to better understand application flow. This approach really helps when trying to solve structural flaws in your application.

IS_A versus HAS_A — I’m So Confused_A

The relationship between SavingsAccount and BankAccount is the fundamental IS_A relationship in inheritance. In the following sections, you discover why. You also see what the alternative, the HAS_A relationship, would look like in comparison.

The IS_A relationship

The IS_A relationship between SavingsAccount and BankAccount is demonstrated by modifications to the class Program (shown in bold) in the SimpleSavingsAccount program from the preceding section (as found in SimpleSavingsAccount2):

class Program
{
public static void DirectDeposit(BankAccount ba, decimal pay)
{
ba.Deposit(pay);
}

static void Main(string[] args)
{
// Create a bank account and display it.
BankAccount ba = new BankAccount(100M);
DirectDeposit(ba, 100M);
Console.WriteLine($"Account {ba.ToString()}");

// Now a savings account
SavingsAccount sa = new SavingsAccount(12.5M);
DirectDeposit(sa, 100);
sa.AccumulateInterest();
Console.WriteLine($"Account {sa.ToString()}");
Console.Read();
}
}

In effect, nothing has changed. The only real difference is that all deposits are now being made through the local method DirectDeposit(), which isn't part of class BankAccount. The arguments to this method are the bank account and the amount to deposit.

Remember Notice that Main() could pass either a bank account or a savings account to DirectDeposit() because a SavingsAccount IS_A BankAccount and is accorded all the same rights and privileges. Because SavingsAccount IS_A BankAccount, you can assign a SavingsAccount to a BankAccount-type variable or method argument.

Gaining access to BankAccount by using containment

The class SavingsAccount could have gained access to the members of BankAccount in a different way, as shown in the following code (and in SimpleSavingsAccount3):

internal class SavingsAccount
{
private BankAccount _bankAccount;
private decimal _interestRate = 0;

// InitSavingsAccount -- Input the rate expressed as a
// rate between 0 and 100.
internal SavingsAccount(decimal interestRate) :
this(0, interestRate)
{ }

internal SavingsAccount(decimal initialBalance,
decimal interestRate)
{
_bankAccount = new BankAccount(initialBalance);
_interestRate = interestRate / 100;
}

// AccumulateInterest -- Invoke once per period.
public void AccumulateInterest()
{
_bankAccount.Balance = _bankAccount.Balance +
(decimal)(_bankAccount.Balance * _interestRate);
}

public void Deposit(decimal amount)
{
_bankAccount.Deposit(amount);
}

public decimal Withdraw(decimal withdrawal)
{
return _bankAccount.Withdraw(withdrawal);
}

// ToString -- Stringify the account.
public override string ToString()
{
return $"{_bankAccount.ToString()} ({_interestRate:P})";
}
}

In this case, the class SavingsAccount_ contains a data member _bankAccount (as opposed to inheriting from BankAccount). The _bankAccount object contains the balance and account number information needed by the savings account. The SavingsAccount_ class retains the data unique to a savings account and delegates to the contained BankAccount object as needed. That is, when the SavingsAccount needs, say, the balance, it asks the contained BankAccount for it. Notice that this strategy requires the inclusion of instantiating _bankAccount within the second constructor and using _bankAccount anywhere that you might have seen base used in the past.

Warning This approach has several drawbacks. For one thing, you must change the access modifier for Balance in BankAccount to public, which means that it's no longer protected. Every time you make a class member more accessible, you add the potential for security issues. In addition, this version of SavingsAccount includes its own version of Deposit() and Withdraw(), which leads to code replication and potential errors, again increasing the potential for security issues. Here are some reasons you might use containment rather than inheritance:

  • The new class doesn't need access to all the existing class members.
  • The application requires loose coupling so that changes in the existing class don’t ripple through to the new class.
  • Runtime changes to the existing class won’t affect the new class (an advanced programming technique not fully discussed in this minibook; see Book 3, Chapter 6), but you can read about it at https://www.dotnetcurry.com/csharp/dynamic-class-creation-roslyn).
  • It’s essential to limit access to private and protected members, and the new class won’t require access to these members (actually improving security).

In this case, you say that the SavingsAccount_HAS_A BankAccount. Hard-core object-oriented jocks say that SavingsAccount composes a BankAccount. That is, SavingsAccount is partly composed of a BankAccount.

The HAS_A relationship

The HAS_A relationship is fundamentally different from the IS_A relationship. This difference doesn't seem so bad in the following application-code segment example:

// Create a new savings account.
BankAccount ba = new BankAccount()

// HAS_A version of SavingsAccount
SavingsAccount_ sa = new SavingsAccount_(ba, 5);

// And deposit 100 dollars into it.
sa.Deposit(100M);

// Now accumulate interest.
sa.AccumulateInterest();

The problem is that this modified SavingsAccount_ cannot be used as a BankAccount because it doesn't inherit from BankAccount. Instead, it contains a BankAccount — not the same concept. For example, this code example fails:

// DirectDeposit -- Deposit my paycheck automatically.
void DirectDeposit(BankAccount ba, int pay)
{
ba.Deposit(pay);
}

void SomeMethod()
{
// The following example fails.
SavingsAccount_ sa = new SavingsAccount_(sa, 100);
// … continue …
}

Remember DirectDeposit() can't accept a SavingsAccount_ in lieu of a BankAccount. No obvious relationship between the two exists, as far as C# is concerned, because inheritance isn't involved. Don’t think, though, that this situation makes containment a bad idea. You just have to approach the concept a bit differently in order to use it.

When to IS_A and When to HAS_A

The distinction between the IS_A and HAS_A relationships is more than just a matter of software convenience. This relationship has a corollary in the real world.

For example, a Ford Explorer IS_A car. An Explorer HAS_A motor. If your friend says, “Come on over in your car” and you show up in an Explorer, he has no grounds for complaint. He may have a complaint if you show up carrying your Explorer’s engine in your arms, however. (Or at least you will.) The class Explorer should extend the class Car, not only to give Explorer access to the methods of a Car but also to express the fundamental relationship between the two.

Unfortunately, the beginning programmer may have Car inherit from Motor, as an easy way to give the Car class access to the members of Motor, which the Car needs in order to operate. For example, Car can inherit the method Motor.Go(). However, this example highlights a problem with this approach: Even though humans become sloppy in their speech, making a car go isn't the same thing as making a motor go. The car’s go operation certainly relies on that of the motor’s, but they aren’t the same thing — you also have to put the transmission in gear, release the brake, and complete other tasks. Perhaps even more than that, inheriting from Motor misstates the facts. A car simply isn’t a type of motor.

Remember Elegance in software is a goal worth achieving in its own right. It enhances understandability, reliability, and maintainability.

Other Features That Support Inheritance

C# implements a set of features designed to support inheritance. The following sections discuss these features.

Substitutable classes

A program can use a subclass object where a base-class object is called for. In fact, you may have already seen this concept in one of the examples. SomeMethod() can pass a SavingsAccount object to the DirectDeposit() method, which expects a BankAccount object. You can make this conversion more explicit:

BankAccount ba;

// The original, not SavingsAccount_
SavingsAccount sa = new SavingsAccount();

// OK:
ba = sa; // Implicitly convert subclass to base class.
ba = (BankAccount)sa; // But the explicit cast is preferred.
sa = (SavingsAccount)ba; // An explicit cast is allowed.

// Not OK:
sa = ba; // No implicit conversion of base class to subclass

The first line stores a SavingsAccount object into a BankAccount variable. C# converts the object for you. The second line uses a cast to explicitly convert the object.

The final two lines attempt to convert the BankAccount object back into SavingsAccount. You can complete this operation explicitly, but C# doesn't do it for you. It’s like trying to convert a larger numeric type, such as double, to a smaller one, such as float. C# doesn't do it implicitly because the process may involve a loss of data.

Remember The IS_A property isn’t reflexive. That is, even though an Explorer is a car, a car isn’t necessarily an Explorer. Similarly, a BankAccount isn’t necessarily a SavingsAccount, so the implicit conversion isn't allowed. The explicit conversion is allowed because the programmer has indicated a willingness to “chance it.”

Invalid casts at runtime

Generally, casting an object from BankAccount to SavingsAccount is a dangerous operation. Consider this example from SimpleSavingsAccount4:

class Program
{
public static void ProcessAmount(BankAccount bankAccount)
{
// Deposit a large sum to the account.
bankAccount.Deposit(10000.00M);

// If the object is a SavingsAccount, collect interest now.
SavingsAccount savingsAccount = (SavingsAccount)bankAccount;
savingsAccount.AccumulateInterest();
}

static void Main(string[] args)
{
SavingsAccount sa = new SavingsAccount(100M, 12.5M);
ProcessAmount(sa);
Console.WriteLine(sa.ToString());

BankAccount ba = new BankAccount(100M);
ProcessAmount(ba);
Console.WriteLine(ba.ToString());
Console.Read();
}
}

ProcessAmount() performs a few operations, including invoking the AccumulateInterest() method. The cast of ba to a SavingsAccount is necessary because the bankAccount parameter is declared to be a BankAccount. The program compiles properly because all type conversions are made by explicit cast.

All goes well with the first call to ProcessAmount() from within Main(). The SavingsAccount object sa is passed to the ProcessAmount() method. The cast from BankAccount to SavingsAccount causes no problem because the ba object was originally a SavingsAccount anyway.

The second call to ProcessAmount() isn't as lucky, however. The cast to SavingsAccount cannot be allowed. The ba object doesn't have an AccumulateInterest() method. When you run this example, you see the following output when you use the Debug⇒  Start Without Debugging command:

1001 - $11,362.50 (12.50%)

Unhandled Exception: System.InvalidCastException: Unable to cast object of
type 'SimpleSavingsAccount.BankAccount' to type
'SimpleSavingsAccount.SavingsAccount'. at
SimpleSavingsAccount.Program.ProcessAmount(BankAccount bankAccount) in
E:CSAIO4D2EBK02CH05SimpleSavingsAccount4Program.cs:line 96 at
SimpleSavingsAccount.Program.Main(String[] args) in
E:CSAIO4D2EBK02CH05SimpleSavingsAccount4Program.cs:line 107
Press any key to continue …

Warning An incorrect conversion generates an error during the execution of the program (a runtime error in the form of an exception). Runtime errors are much more difficult to find and fix than compile-time errors. Worse, they can happen to a user other than you, which users tend not to appreciate.

Avoiding invalid conversions with the is operator

The ProcessAmount() method would work if it could ensure that the object passed to it is a SavingsAccount object before performing the conversion. C# provides two keywords for this purpose: is and as.

The is operator accepts an object on the left and a type on the right. The is operator returns true if the runtime type of the object on the left is compatible with the type on the right. Use it to verify that a cast is legal before you attempt the cast. You can modify ProcessAmount() in the previous section (as shown in SimpleSavingsAccount5) to avoid the runtime error by using the is operator:

public static void ProcessAmount(BankAccount bankAccount)
{
// Deposit a large sum to the account.
bankAccount.Deposit(10000.00M);

// If the object is a SavingsAccount
if (bankAccount is SavingsAccount)
{
// then collect interest now.
SavingsAccount savingsAccount = (SavingsAccount)bankAccount;
savingsAccount.AccumulateInterest();
}
}

The added if statement checks the bankAccount object to ensure that it's of the class SavingsAccount. The is operator returns true when ProcessAmount() is called the first time. When passed a BankAccount object in the second call, however, the is operator returns false, avoiding the illegal cast. This version of the program doesn't generate a runtime error as shown here.

1001 - $11,362.50 (12.50%)
1002 - $10,100.00

Tip A best practice is to protect all casts with the is operator to avoid the possibility of a runtime error. However, you should avoid casts altogether, if possible.

Avoiding invalid conversions with the as operator

The as operator works a bit differently from is. Rather than return a bool if the cast should work (but doesn't), it converts the type on the left to the type on the right. It safely returns null if the conversion fails — rather than cause a runtime error. You should always use the result of casting with the as operator only if it isn't null. So, using as looks like this:

SavingsAccount savingsAccount = bankAccount as SavingsAccount;
if (savingsAccount != null)
{
// Go ahead and use savingsAccount.
}
// Otherwise, don't use it: generate an error message yourself.

Remember Generally, you should prefer as because it’s more efficient. The conversion is already done with the as operator, whereas you must complete two steps when you use is: First test with is and then complete the cast with the cast operator. Unfortunately, as doesn't work with value-type variables, so you can’t use it with types such as int, long, or double or with char. When you're trying to convert a value-type object, prefer the is operator.

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

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