CHAPTER 4

image

Base Classes and Inheritance

Class inheritance is a commonly used construct1 in object-oriented languages, and C# provides a full implementation.

The Engineer Class

The following class implements an Engineer class and methods to handle billing for that Engineer.

using System;
class Engineer
{
       // constructor
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
    }
       // figure out the charge based on engineer's rate
    public float CalculateCharge(float hours)
{
       return(hours * m_billingRate);
    }
       // return the name of this type
    public string TypeName()
    {
       return("Engineer");
    }
    private string m_name;
    private float m_billingRate;
}
class Test
{
    public static void Main()
    {
       Engineer engineer = new Engineer("Hank", 21.20F);
       Console.WriteLine("Name is: {0}", engineer.TypeName());
    }
}

Engineer will serve as a base class for this scenario. It contains private fields to store the name of the engineer and the engineer’s billing rate, along with a member function that can be used to calculate the charge based on the number of hours of work done.

Simple Inheritance

A CivilEngineer is a type of engineer and therefore can be derived from the Engineer class:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
    }
    public float CalculateCharge(float hours)
    {
       return(hours * m_billingRate);
    }
    public string TypeName()
    {
       return("Engineer");
    }
    private string m_name;
    protected float m_billingRate;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
    }
       // new function, because it's different than the
       // same as base version
    public new float CalculateCharge(float hours)
    {
       if (hours < 1.0F)
       {
            hours = 1.0F;       // minimum charge.
       }
       return(hours * m_billingRate);
    }
       // new function, because it's different than the
       // base version
    public new string TypeName()
    {
       return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
       Engineer e = new Engineer("George", 15.50F);
       CivilEngineer c = new CivilEngineer("Sir John", 40F);
       Console.WriteLine("{0} charge = {1}",
                   e.TypeName(),
                   e.CalculateCharge(2F));
       Console.WriteLine("{0} charge = {1}",
                   c.TypeName(),
                   c.CalculateCharge(0.75F));
    }
}

Because the CivilEngineer class derives from Engineer, it inherits all the data members of the class, and it also inherits the CalculateCharge() member function.

Constructors can’t be inherited, so a separate one is written for CivilEngineer. The constructor doesn’t have anything special to do, so it calls the constructor for Engineer, using the base syntax. If the call to the base class constructor was omitted, the compiler would call the base class constructor with no parameters.

CivilEngineer has a different way to calculate charges; the minimum charge is for one hour of time, so there’s a new version of CalculateCharge(). That exposes an issue; this new method needs to access the billing rate that is defined in the Engineer class, but the billing rate was defined as private and is therefore not accessible. To fix this, the billing rate is now declared to be protected. This change allows all derived classes to access the billing rate.

The example, when run, yields the following output:

Engineer Charge = 31
Civil Engineer Charge = 40

image Note  The terms inheritance and derivation are fairly interchangeable in discussions such as this. My preference is to say that class CivilEngineer derives from class Engineer, and, because of that, it inherits certain things.

Arrays of Engineers

The code above works fine in the early years of a company, when there are only a few employees. As the company grows, it’s easier to deal with an array of engineers.

Because CivilEngineer is derived from Engineer, an array of type Engineer can hold either type. This example has a different Main() function, putting the engineers into an array:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
    }
    public float CalculateCharge(float hours)
    {
       return(hours * m_billingRate);
    }
    public string TypeName()
    {
       return("Engineer");
    }
    private string m_name;
    protected float m_billingRate;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
    }
    public new float CalculateCharge(float hours)
    {
       if (hours < 1.0F)
       {
            hours = 1.0F;       // minimum charge.
       }
       return(hours * m_billingRate);
    }
    public new string TypeName()
    {
       return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
            // create an array of engineers
       Engineer[] engineers = new Engineer[2];
       engineers[0] = new Engineer("George", 15.50F);
       engineers[1] = new CivilEngineer("Sir John", 40F);

       Console.WriteLine("{0} charge = {1}",
                   engineers[0].TypeName(),
                   engineers[0].CalculateCharge(2F));
       Console.WriteLine("{0} charge = {1}",
                   engineers[1].TypeName(),
                   engineers[1].CalculateCharge(0.75F));
    }
}

This version yields the following output:

Engineer Charge = 31
Engineer Charge = 30

That’s not right.

Because CivilEngineer is derived from Engineer, an instance of CivilEngineer can be used wherever an instance of Engineer is required.

When the engineers were placed into the array, the fact that the second engineer was really a CivilEngineer rather than an Engineer was lost. Because the array is an array of Engineer, when CalculateCharge() is called, the version from Engineer is called.

What is needed is a way to correctly identify the type of an engineer. This can be done by having a field in the Engineer class that denotes what type it is. In the following (contrived) example, the classes are rewritten with an enum field to denote the type of the engineer:

using System;
enum EngineerTypeEnum
{
    Engineer,
    CivilEngineer
}
class Engineer
{
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
       m_type = EngineerTypeEnum.Engineer;
    }
    public float CalculateCharge(float hours)
    {
       if (m_type == EngineerTypeEnum.CivilEngineer)
       {
            CivilEngineer c = (CivilEngineer) this;
            return(c.CalculateCharge(hours));
       }
       else if (m_type == EngineerTypeEnum.Engineer)
       {
            return(hours * m_billingRate);
       }
       return(0F);
    }
    public string TypeName()
    {
       if (m_type == EngineerTypeEnum.CivilEngineer)
       {
            CivilEngineer c = (CivilEngineer) this;
            return(c.TypeName());
       }
       else if (m_type == EngineerTypeEnum.Engineer)
       {
            return("Engineer");
       }
       return("No Type Matched");
    }
    private string m_name;
    protected float m_billingRate;
    protected EngineerTypeEnum m_type;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
       m_type = EngineerTypeEnum.CivilEngineer;
    }
    public new float CalculateCharge(float hours)
    {
       if (hours < 1.0F)
       {
            hours = 1.0F;       // minimum charge.
       }
       return(hours * m_billingRate);
    }
    public new string TypeName()
    {
       return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
       Engineer[] engineers = new Engineer[2];
       engineers[0] = new Engineer("George", 15.50F);
       engineers[1] = new CivilEngineer("Sir John", 40F);

       Console.WriteLine("{0} charge = {1}",
                   engineers[0].TypeName(),
                   engineers[0].CalculateCharge(2F));
       Console.WriteLine("{0} charge = {1}",
                   engineers[1].TypeName(),
                   engineers[1].CalculateCharge(0.75F));
    }
}

By looking at the type field, the functions in Engineer can determine the real type of the object and call the appropriate function.

The output of the code is as expected:

Engineer Charge = 31
Civil Engineer Charge = 40

Unfortunately, the base class has now become much more complicated; for every function that cares about the type of a class, there is code to check all the possible types and call the correct function. That’s a lot of extra code, and it would be untenable if there were 50 kinds of engineers.

Worse is the fact that the base class needs to know the names of all the derived classes for it to work. If the owner of the code needs to add support for a new engineer, the base class must be modified. If a user who doesn’t have access to the base class needs to add a new type of engineer, it won’t work at all.

Virtual Functions

To make this work cleanly, object-oriented languages allow a function to be specified as virtual. Virtual means that when a call to a member function is made, the compiler should look at the real type of the object (not just the type of the reference) and call the appropriate function based on that type.

With that in mind, the example can be modified as follows:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
    }
       // function now virtual
    virtual public float CalculateCharge(float hours)
    {
       return(hours * m_billingRate);
    }
       // function now virtual
    virtual public string TypeName()
    {
       return("Engineer");
    }
    private string m_name;
    protected float m_billingRate;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
    }
       // overrides function in Engineer
    override public float CalculateCharge(float hours)
    {
       if (hours < 1.0F)
       {
            hours = 1.0F;       // minimum charge.
       }
       return(hours * m_billingRate);
    }
       // overrides function in Engineer
    override public string TypeName()
    {
       return("Civil Engineer");
    }
}
class Test
{
    public static void Main()
    {
       Engineer[] engineers = new Engineer[2];
       engineers[0] = new Engineer("George", 15.50F);
       engineers[1] = new CivilEngineer("Sir John", 40F);

       Console.WriteLine("{0} charge = {1}",
                   engineers[0].TypeName(),
                   engineers[0].CalculateCharge(2F));
       Console.WriteLine("{0} charge = {1}",
                   engineers[1].TypeName(),
                   engineers[1].CalculateCharge(0.75F));
    }
}

The CalculateCharge() and TypeName() functions are now declared with the virtual keyword in the base class, and that’s all that the base class has to know. It needs no knowledge of the derived types, other than to know that each derived class can override CalculateCharge() and TypeName() if desired. In the derived class, the functions are declared with the override keyword, which means that they are the same function that was declared in the base class. If the override keyword is missing, the compiler will assume that the function is unrelated to the base class’s function, and virtual dispatching won’t function.2

Running this example leads to the expected output:

Engineer Charge = 31
Civil Engineer Charge = 40

When the compiler encounters a call to TypeName() or CalculateCharge(), it goes to the definition of the function and notes that it is a virtual function. Instead of generating code to call the function directly, it writes a bit of dispatch code that at runtime will look at the real type of the object and call the function associated with the real type, rather than just the type of the reference. This allows the correct function to be called even if the class wasn’t implemented when the caller was compiled.

For example, if there was payroll processing code that stored an array of Engineer, a new class derived from Engineer could be added to the system without having to modify or recompile the payroll code.

VIRTUAL BY DEFAULT OR NOT?

In some languages, the use of "virtual" is required to make a method virtual, and in other languages all methods are virtual by default. VB, C++, and C# are in the “required” camp, and Java, Python, and Ruby are in the “default” camp.

The desirability of one behavior over the other has spawned numerous lengthy discussions. The default camp says “we don’t know how users might use our classes, and if we restrict them, it just makes things harder for no good reason.” The required camp say “if we don’t know how users might use our classes, how can we make them predictable, and how can we guide users toward overriding the virtual methods we want them to use if all methods are virtual?”

My opinion has tended toward those who make it required, simply because if code can be extended in multiple ways, users will extend it in multiple ways, and I’m not a fan of the resultant mess and confusion. However, if you are writing unit tests, it’s very inconvenient to have to write wrapper classes around existing classes merely so you can write your tests,3 so I’m not as close to the required camp as I have been in the past.

Abstract Classes

There is a small problem with the approach used so far. A new class doesn’t have to implement the "TypeName() function, since it can inherit the implementation from Engineer. This makes it easy for a new class of engineer to have the wrong name associated with it.

If the ChemicalEngineer class is added, for example:

using System;
class Engineer
{
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
    }
    virtual public float CalculateCharge(float hours)
    {
       return(hours * m_billingRate);
    }
    virtual public string TypeName()
    {
       return("Engineer");
    }
    private string m_name;
    protected float m_billingRate;
}
class ChemicalEngineer: Engineer
{
    public ChemicalEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
    }

    // overrides mistakenly omitted
}
class Test
{
    public static void Main()
    {
       Engineer[] engineers = new Engineer[2];
       engineers[0] = new Engineer("George", 15.50F);
       engineers[1] = new ChemicalEngineer("Dr. Curie", 45.50F);

       Console.WriteLine("{0} charge = {1}",
                   engineers[0].TypeName(),
                   engineers[0].CalculateCharge(2F));
       Console.WriteLine("{0} charge = {1}",
                   engineers[1].TypeName(),
                   engineers[1].CalculateCharge(0.75F));
    }
}

The ChemicalEngineer class will inherit the CalculateCharge() function from Engineer, which might be correct, but it will also inherit TypeName(), which is definitely wrong. What is needed is a way to force ChemicalEngineer to implement TypeName().

This can be done by changing Engineer from a normal class to an abstract class. In this abstract class, the TypeName() member function is marked as an abstract function, which means that all classes that derive from Engineer will be required to implement the TypeName() function.

An abstract class defines a contract that derived classes are expected to follow.4 Because an abstract class is missing “required” functionality, it can’t be instantiated, which for the example means that instances of the Engineer class cannot be created. So that there are still two distinct types of engineers, the ChemicalEngineer class has been added.

Abstract classes behave like normal classes except for one or more member functions that are marked as abstract:

using System;
abstract class Engineer
{
    public Engineer(string name, float billingRate)
    {
       m_name = name;
       m_billingRate = billingRate;
    }
    virtual public float CalculateCharge(float hours)
    {
       return(hours * m_billingRate);
    }
    abstract public string TypeName();

    private string m_name;
    protected float m_billingRate;
}
class CivilEngineer: Engineer
{
    public CivilEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
    }
    override public float CalculateCharge(float hours)
    {
       if (hours < 1.0F)
       {
            hours = 1.0F;       // minimum charge.
       }
       return(hours * m_billingRate);
    }
       // This override is required, or an error is generated.
    override public string TypeName()
    {
       return("Civil Engineer");
    }
}
class ChemicalEngineer: Engineer
{
    public ChemicalEngineer(string name, float billingRate) :
       base(name, billingRate)
    {
    }
    override public string TypeName()
    {
       return("Chemical Engineer");
    }
}
class Test
{
    public static void Main()
    {
       Engineer[] engineers = new Engineer[2];
       engineers[0] = new CivilEngineer("Sir John", 40.0F);
       engineers[1] = new ChemicalEngineer("Dr. Curie", 45.0F);

       Console.WriteLine("{0} charge = {1}",
                   engineers[0].TypeName(),
                   engineers[0].CalculateCharge(2F));
       Console.WriteLine("{0} charge = {1}",
                   engineers[1].TypeName(),
                   engineers[1].CalculateCharge(0.75F));
    }
}

The Engineer class has changed by the addition of abstract before the class, which indicates that the class is abstract (i.e., has one or more abstract functions), and the addition of abstract before the TypeName() virtual function. The use of abstract on the virtual function is the important one; the one before the name of the class makes it clear that the class is abstract, since the abstract function could easily be buried among the other functions.

The implementation of CivilEngineer is identical, except that now the compiler will check to make sure that TypeName() is implemented by both CivilEngineer and ChemicalEngineer.

In making the class abstract, we have also ensured that instances of the Engineer class cannot be created.

Sealed Classes and Methods

Sealed classes are used to prevent a class from being used as a base class. It is primarily useful to prevent unintended derivation:

// error
sealed class MyClass
{
    MyClass() {}
}
class MyNewClass : MyClass
{
}

This fails because MyNewClass can’t use MyClass as a base class because MyClass is sealed.

Sealed classes are useful in cases where a class isn’t designed with derivation in mind or where derivation could cause the class to break. The System.String class is sealed because there are strict requirements that define how the internal structure must operate, and a derived class could easily break those rules.

A sealed method lets a class override a virtual function and prevents a derived class from overriding that same function. In other words, placing sealed on a virtual method stops virtual dispatching. This is rarely useful, so sealed methods are rare.

1 Too commonly used, in my opinion, but that discussion would be another book.

2 For a discussion of why this works this way, see Chapter 10.

3 Yes, I know, there are some mocking technologies that can get around this limitation. I’m not sure, however, that you should; many times writing the wrapper gives some useful encapsulation.

4 A similar effect can be achieved by using interfaces. See Chapter 9 for a comparison of the two techniques.

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

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