Chapter 11
OOP Concepts

What’s in This Chapter

  • Properties, methods, and events
  • Inheritance, refinement, and abstraction
  • Hiding and overriding
  • Encapsulation, information hiding, and polymorphism

Wrox.com Downloads for This Chapter

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/csharp5programmersref on the Download Code tab.

This chapter describes the basic concepts behind object-oriented programming (OOP). It explains how to define classes and how to derive one class from another. It also describes the three fundamental features of OOP programming languages: encapsulation, inheritance, and polymorphism. It explains how C# provides those features and what benefits you can gain from using them properly.

Classes

A class is a programming entity that packages the data and behavior of some sort of programming abstraction. It encapsulates the idea that it represents in a package that has a well-defined interface to code that lies outside of the package. The interface determines how other pieces of code can interact with objects defined by the class. The interface determines which pieces of data are visible outside of the class and which pieces of data are hidden inside the class.

The three main sets of characteristics of a class are the properties, methods, and events that it defines. The public (externally visible) properties, methods, and events let the program work with the class:

  • A property is some sort of data value. It may be a simple value (such as a string or int), or it may be a more complex item (such as an array, list, or object containing its own properties, methods, and events). Properties determine some feature of an object such as its name, color, or behavior.
  • A method is a routine that performs some action. A method makes an object defined by the class do something.
  • An event provides notification that something happened to an object defined by the class. An event can invoke other pieces of code to tell other parts of the program that something has happened to the object.

For a concrete example, imagine a Job class that represents a piece of work to be done by an employee. This class might have the properties shown in the following table.

PropertyPurpose
JobDescriptionA string describing the job.
EstimatedHoursThe number of hours initially estimated for the job.
ActualHoursThe actual number of hours spent on the job.
StatusAn enumeration giving the job’s status (New, Assigned, InProgress, or Complete).
ActionTakenA string describing the work performed, parts installed, and so forth.
CustomerAn object of the Customer class that describes the customer for whom the job is performed. (That class has properties such as Name, Address, PhoneNumber, and ContractNumber.)
AssignedEmployeeAn object of the Employee class that describes the employee assigned to the job. (That class has properties such as Name, PhoneNumber, EmployeeId, and SocialSecurityNumber.)

The JobDescription, EstimatedHours, ActualHours, Status, and ActionTaken properties are relatively simple string and numeric values. The Customer and AssignedEmployee properties are objects themselves with their own properties, methods, and events.

This Job class might provide the methods shown in the following table.

MethodPurpose
AssignJobAssigns the Job to an Employee
PrintInvoicePrints an invoice for the Customer after the job is completed
EstimatedCostCalculates and returns an estimated cost based on the customer’s service contract type and EstimatedHours

The class could provide the events shown in the following table to keep the main program informed about the job’s progress.

EventPurpose
CreatedOccurs when the Job is first created
AssignedOccurs when the Job is assigned to an Employee
RejectedOccurs if an Employee refuses to do the job, perhaps because the Employee doesn’t have the right skills or equipment to do the work
CanceledOccurs if the Customer cancels the job before it is started
FinishedOccurs when the job is completed

The class packages the data and behavior of some programming abstraction such as a Job, Employee, Customer, Menu, SquashMatch, SoftwareProject, or anything else you might want to manipulate as a single entity.

After you have defined a class, you can create as many instances of the class as you like. An instance of the class is an object of the class type. For example, the Job class represents jobs in general. After you have defined the Job class, you can make instances of the class to represent specific jobs. You could create instances to represent building a brick wall, planting a tree, or repairing a telephone switch. The process of creating an instance of a class is called instantiation.

There are a couple common analogies to describe instantiation. One compares the class to a blueprint. After you define the class, you can use it to create any number of instances of the class, much as you can use the blueprint to make any number of similar houses (instances).

The different houses have much in common. For example, the blueprint defines the number, size, and relative placement of the houses’ rooms. These are analogous to features defined by the class that apply to all the instances. (In the Job class example, all instances have a PrintInvoice method that prints an invoice. Although exactly what is printed depends on the instance’s properties.)

The houses can also have differences such as different colors, front doors, and appliances. Those correspond to the class’s property values. For example, a House class could define ExteriorColor and ExteriorTrim properties that determine the color of each instance of the class.

A second analogy compares a class definition to a cookie cutter. After you create the cookie cutter, you can use it to make any number of cookies (instances). The cookie cutter (class) defines the cookies’ size and shape. Specific instances might have different properties such as thickness, dough type (chocolate chip, sugar, gingerbread, and so on), and frosting type (none, single color, and patterned).

Because all classes ultimately derive from the Object class, every instance of every class is in some sense an Object, so they are often simply called objects. If you don’t know or don’t care about an item’s class, you can simply refer to it as an object.

The following sections provide more details about the more important features provided by OOP languages in general and C# in particular.

Encapsulation

A class’s public interface is the set of properties, methods, and events that are visible to code outside of the class. The class may also have private properties, methods, and events that it uses to do its job. For example, the Job class described in the previous section provides an AssignJob method. That method might call a private FindQualifiedEmployee method that looks through an employee database to find someone who has the skills and equipment necessary to do the job. That routine is not used outside of the class, so it can be declared private.

The FindQualifiedEmployee method should be declared private to hide it from code outside of the class. That makes the interface easier to understand and use. Making FindQualifiedEmployee public would clutter the interface (and IntelliSense) with unnecessary information.

The class may also include private properties and events. These hidden properties, methods, and events are not part of the class’s public interface.

The class encapsulates the programming abstraction that it represents (a Job in this ongoing example). Its public interface determines what is visible to the application outside of the class. It hides the ugly details of the class’s implementation from the rest of the world. Because the class hides its internals in this way, encapsulation is also sometimes called information hiding.

By hiding its internals from the outside world, a class prevents exterior code from messing around with those internals. It reduces the dependencies between different parts of the application, allowing only those dependencies that are explicitly permitted by its public interface.

Removing dependencies between different pieces of code makes the code easier to modify and maintain. If you must change the way the Job class assigns a job to an employee, you can modify the AssignJob method appropriately. The code that calls the AssignJob routine doesn’t need to know that the details have changed. It simply continues to call the method and leaves the details up to the Job class.

Removing dependencies also helps break the application into smaller, more manageable pieces. A developer who calls the AssignJob method can concentrate on the job at hand, rather than on how the method works. This makes developers more productive and less likely to make mistakes while modifying the encapsulated code.

To make using a class as easy and safe as possible, you should hide as much information as possible about the class’s internals while still allowing outside code to do its job. If the external code doesn’t need to know something about how the class works, it shouldn’t.

To make public properties, methods, and events easy to use, you should make them simple (at least as seen from outside of the class) and tightly focused. A set of small methods that do a single simple task each is easier to use than a single method that can do a huge number of different things depending on its parameters.

For example, the Graphics class provides methods to draw and fill various shapes. The DrawEllipse, DrawRectangle, DrawLine, and DrawPolygon methods outline different kinds of shapes. Similarly, the FillEllipse, FillRectangle, FillRegion, and FillPolygon methods fill different kinds of shapes.

You could probably replace all those methods with a single DrawShape method that draws and outlines or fills various kinds of shapes depending on the parameters that it received. That would make the code harder to understand because you would have to carefully study the parameters in a specific call to figure out what it was doing. By using separate methods, the class makes the code obvious. If the program calls DrawRectangle, it is drawing a rectangle.

Making methods perform a single, tightly focused task can be a difficult concept for beginning programmers. Adding more features seems like it would give developers more power, so you might think it would make their jobs easier. However, it often makes development more confusing and difficult. Instead of thinking in terms of giving the developer more power, you should think about the fact that this approach gives the developer more things to worry about and more ways to make mistakes. Ideally, you should not expose any more features than the developer actually needs.

Inheritance

Inheritance lets you derive a child class from a parent class. The child class inherits all the properties, methods, and events defined by the parent class. It can then modify, add to, or subtract from the parent class. Making a child class inherit from a parent class is also called deriving the child class from the parent, and subclassing the parent class to form the child class.

For example, suppose you define a Person class that includes properties named FirstName, LastName, Street, City, State, Zip, Phone, and Email. It might also include a PrintEnvelope method that prints an envelope addressed to the person represented by the Person object.

Now you could derive the Employee class from Person. The Employee class inherits the FirstName, LastName, Street, City, State, Zip, Phone, and Email properties. It then adds new EmployeeId, SocialSecurityNumber, OfficeNumber, Extension, and Salary properties.

The Employee class might override the Person class’s PrintEnvelope method, so it addresses the envelope to the employee’s office instead of the home address.

Now you can derive other classes from those classes to create a whole hierarchy of classes. You could derive the Manager class from the Employee class and add fields such as Secretary that would refer to another Employee object that represents the manager’s secretary. Similarly, you could derive a Secretary class from Employee that includes a reference to a Manager object. You could derive ProjectManager, DepartmentManager, and DivisionManager from the Manager class; Customer from the Person class; and so on for other types of people that the application needs to use. Figure 11-1 shows an inheritance hierarchy containing these classes.

c11f001.eps

Figure 11-1: You can derive classes from other classes to form complex inheritance relationships.

Inheritance Hierarchies

One of the key benefits of inheritance is code reuse. When you derive a class from a parent class, the child class gets to reuse the code you wrote for the parent class. For example, all the classes referred to in Figure 11-1 inherit their FirstName, LastName, Street, City, State, Zip, Phone, and Email properties from the Person class, so they don’t need to implement those properties separately.

The Consultant, Employee, Customer, and PreferredCustomer classes also inherit the PrintEnvelope method defined by the Person class. The Employee class overrides that method, so it doesn’t get to reuse the Person class’s version. However, the new version does something different from the original version, so you have no choice but to write new code. In addition, the six classes that inherit from Employee get to reuse the new version.

Code reuse not only saves you the effort needed to write the same code multiple times but also saves you time for debugging and maintenance. For example, if you find a bug in the PrintEnvelope method, you need to fix it only in one place instead of in each of the classes that inherits it. That saves time and prevents you from fixing the bug in one class but forgetting to fix it in another.

Code reuse also helps you make modifications. Suppose you decide to change the Zip property to store ZIP codes with the ZIP+4 format instead of the original 5-digit format. In that case you need to change only the representation in the Person class, and all the other classes inherit the change.

Of course, then you’ll probably want to change the PrintEnvelope method to use the new Zip format. Again, you have to make the change only in the Person class and the other classes inherit the change.

Similarly, if you need to add, modify, or delete a property or method, you need to make the change only in the class where it is defined, not in all the classes that inherit it. If you want to add a SendEmail method, you need to add it only to the Person class.

Refinement and Abstraction

You can think about the relationship between a parent class and its child classes in a top-down or bottom-up way. Those two points of view lead to the ideas of refinement and abstraction.

Refinement

Using a top-down view of inheritance, you can think of the child classes as refining the parent class. They provide extra detail that differentiates among different types of the parent class.

For example, suppose that you start with a class that covers a broad category such as Person. The Person class would need general fields that apply to all people such as FirstName, LastName, Address, and PhoneNumber.

Some kinds of people might need additional fields that don’t apply to every kind of person. For example, a school’s instructors might also need Title, CoursesAssigned, Advisees, Office, and OfficeHours properties. Students might need StudentId, Year, CoursesTaken, and GPA fields.

You could add all these fields to the Person class, but that would force the class to play two different roles. That would be complicated and confusing. It would also be somewhat wasteful because a Person acting as an instructor wouldn’t use any of the student properties, and a Person acting as a student wouldn’t use any of the instructor properties.

A better solution is to derive new Instructor and Student classes that refine the Person class’s definition and add their new properties. Now each class represents a specific kind of person.

Abstraction

Using a bottom-up view of inheritance, you can think of the parent as abstracting the features of its children. The parent class gathers together the common features of the child classes. Because the parent class is more general than the child classes (it includes a larger group of objects), abstraction is sometimes called generalization.

For example, suppose you’re making a drawing application and you define various classes to represent shapes such as Circle, Rectangle, Ellipse, and Polygon. When you build the program, you may discover that these classes have a lot in common. For example, at times the program may need to find bounding rectangles for each of the objects. You may find that the objects have common ForegroundColor and BackgroundColor properties. You may want each class to provide a PointIsOver method that returns true if the given point is over the shape. You might also want to make each class raise a Clicked event when the user clicks on its shape.

After you realize that these classes have so much in common, you might decide to extract those common features and move them into a common parent class named Drawable. That class would define the common features that the other classes need to implement. For example, the Drawable class could implement the ForegroundColor and BackgroundColor properties for the other classes to inherit.

Some of the child classes might override the default implementations in the Drawable class. It might even be impossible for the Drawable class to provide some of the features it defines. For example, each type of shape would need a different PointIsOver method because you need to use different techniques to determine whether a point is over a rectangle, circle, or polygon. You could give the Drawable class a default implementation, perhaps making it treat its shape as a rectangle, but you wouldn’t gain much by doing that. You would still need to override that method in every class except Rectangle. Providing the default implementation would also incorrectly imply that the method is useful for something other than a single child class.

In this case you’re probably better off marking the method as abstract and not providing a method body so that the child classes are required to override it. (For more information on the abstract keyword, see the section “abstract” in Chapter 6.)

Making a Drawable parent class also allows the program to treat all drawing objects uniformly as Drawables. For example, it can create a collection named AllDrawables to hold the current picture’s drawing objects. Then when the user moves the mouse, the program could loop through the collection calling each object’s PointIsOver method to determine whether the mouse was over an object. The section “Polymorphism” later in this chapter provides more details.

Using Refinement and Abstraction

Often a program’s classes are defined by using refinement. You know the general categories of things the program needs such as Person, Report, and Job. As you go through the project requirements, you can refine those to create the specific kinds of objects the program will need. Person becomes Employee, Manager, Secretary, Programmer, Customer, and Contractor. Report becomes TimeSheet, JobRequirements, ProgressReport, and Invoice.

During the project’s design phase, the classes tend to map naturally to the requirements, so it’s relatively easy to imagine their relationships.

Abstraction often arises during development. As you define and work with the classes, you may discover they have unexpected things in common. For example, suppose you’re working with the Person class and the descendant classes described earlier. After a while you realize that Manager and Programmer are salaried positions but Secretary is hourly. You could add a Salary property to Manager and Programmer, and add HourlyRate to Secretary. Unfortunately, that would require a duplicate Salary property. It would also mean you couldn’t treat Manager and Programmer objects uniformly.

The solution is to create a new SalariedEmployee class to act as a new parent for the Manager and Programmer classes. Figure 11-2 shows the new class hierarchy.

c11f002.eps

Figure 11-2: You can derive classes from other classes to form complex inheritance relationships.

Refinement is an important technique for building inheritance hierarchies, but it can sometimes lead to unnecessary refinement or over-refinement. For example, suppose that you define a Vehicle class. You then refine this class by creating Auto, Truck, and Boat classes. You refine the Auto class into Wagon and Sedan classes and further refine those for different drive types (four-wheel drive, automatic transmission, and so forth). If you really go crazy, you could define classes for specific manufacturers, body styles, and colors.

The problem with this hierarchy is that it captures a lot more detail than you need. If you’re building a program to manage a fleet of delivery vehicles, then you probably need to know the vehicle’s capacity, but you probably don’t need to keep track of its manufacturer, transmission type, and color. You may want to track some of that information so that you can identify each vehicle properly, but you don’t need to make these separate classes.

As far as a delivery scheduling application is concerned, the color is irrelevant. Creating lots of unnecessary classes makes the object model harder to understand and can lead to confusion and mistakes.

Avoid unnecessary refinement by refining a class only when doing so lets you capture new information that the application actually needs to know.

Just as you can take refinement to ridiculous extremes, you can also overdo abstraction. Because abstraction is driven by code rather than intuition, it sometimes leads to unintuitive inheritance hierarchies.

For example, suppose that your application needs to mail purchase orders to vendors and invoices to customers. If the PurchaseOrder and Invoice classes have enough in common, you might decide to create a more abstract MailableItem class that contains the code needed to create and mail a document to someone.

At some point you may discover that you also need to e-mail items to vendors or customers, so you create the idea of an EmailableItem class. The MailableItem and EmailableItem classes probably share some common features because they both represent sending something to someone, so you may then be tempted to create an even more abstract SendableItem class.

Although all this makes a sort of weird sense from a coding point of view, it doesn’t make much intuitive sense. That means programmers need to spend extra time figuring out what the classes are for and how to use them. It also means programmers are more likely to make mistakes that can slow development and create annoying bugs.

You can sometimes avoid over-abstraction by moving common features into libraries instead of creating separate classes to represent them. For example, you could make a library containing methods to send items via e-mail or postal mail. Then the program can call those methods as needed. Unless the program is some sort of a message tracking system, it probably doesn’t need an object to represent e-mails and letters.

Over-refinement and over-abstraction sometimes lead to inflated inheritance hierarchies. Sometimes the hierarchy grows tall and thin. Other times the design might include many separate but small inheritance hierarchies, a parent class with a single child, or a class that is never used.

If your inheritance hierarchy starts to take on one of these odd forms, you should spend some time to reevaluate your classes. Make sure each adds something meaningful to the application and that the relationships are reasonably intuitive. Too many classes with confusing relationships can drag a project to a halt as developers spend more time trying to understand the hierarchy than they spend writing code.

If you are unsure whether you should add a new class, leave it out. You can add it later if you discover that it is necessary after all. Usually it’s easier to add a new class than it is to remove an unnecessary class after developers have started using it.

Has-a and Is-a Relationships

In refinement you create child classes to differentiate among different kinds of objects. In abstraction you create a parent class to represent features that are common between two or more child classes. Both of these techniques create parent/child relationships between classes.

Another concept that sometimes masquerades as a parent/child relationship is containment. In containment, one object contains another object as an attribute.

The ideas of inheritance and containment are sometimes referred to as is-a and has-a relationships.

For example, a Student is-a specific type of Person object. The is-a relation maps naturally into inheritance hierarchies. Because a Student is-a Person, it makes sense to derive the Student class from the Person class.

In contrast a Person object has-a street address, city, state, and ZIP code. The has-a relation maps most naturally to embedded items. For example, you could give the Person class the Street, City, State, and Zip properties.

To see why the difference between the is-a and has-a relationships is important, suppose your program works with the Person and Student classes. Suppose it also works with FinancialAidPayment, RegistrationFee, and other classes that have street, city, state, and ZIP code information. Using abstraction, you might make a HasPostalAddress class that contains those values. Then you could derive the Person, FinancialAidPayment, and RegistrationFee classes as children of HasPostalAddress. Unfortunately, that makes a rather unintuitive inheritance hierarchy. Deriving all those classes from the same parent class also makes them seem closely related when they are actually related only coincidentally.

A better solution is to encapsulate the postal address data in its own Address class and then include an instance of that class in the Person, FinancialAidPayment, and RegistrationFee classes.

You make a parent class through abstraction in part to avoid duplication of code. The parent class contains a single copy of the common variables and code, so the child classes don’t need to have their own separate versions for you to debug and maintain. Placing an instance of the Address class in each of the other classes provides the same benefit without complicating the inheritance hierarchy.

Sometimes you can use either is-a or has-a to describe a relationship. For example, a Person has-an address, but at the same time a Person is-a thing that has an address. In cases like this, you need to use your common sense and intuition to decide which makes more sense. One hint is that it is easy to describe something that “has an address” but the phrase “is a thing that has an address” is more awkward and ill-defined.

You can also think about how a relationship might affect other classes. Are the Person, FinancialAidPayment, and RegistrationFee classes truly closely related? Or do they just share some common information?

Adding and Modifying Class Features

Adding new properties, methods, and events to a child class is easy. You simply declare them as you would in any other class. The parent class knows nothing about them, so the new items are added only to the child class.

The following code shows how you could implement the Person and Employee classes in C#.

// A general person.
public class Person
{
    public string FirstName, LastName, Street, City, State, Zip, Phone, Email;

    // Dial the phone.
    public void DialPhone()
    {
        // Dial the number Phone...
    }
}

// An employee.
public class Employee : Person
{
    public string EmployeeId, SocialSecurityNumber, OfficeNumber, Extension;
    public decimal Salary;

    // Print a timesheet for the employee.
    public void PrintTimesheet()
    {
        // Print the timesheet...
    }
}

The Person class defines name and address values. For simplicity, they are implemented as fields, but in practice you might want to make them properties. This class also defines a DialPhone method that dials the person’s phone number.

The Employee class is derived from the Person class. (That’s what “: Person” means at the end of the class declaration.) This class adds some new values and then defines a new PrintTimesheet method.

There are two ways a child class can modify the behavior of a method defined in its parent class (or any ancestor class): hiding and overriding.

Hiding and Overriding

First, the child class can hide the parent class’s version of a method. To do that, add the keyword new to indicate that you want to use a new version of the method. The following code shows how the Employee class could hide the DialPhone method to create a new version that includes the Employee’s Extension. The new keyword is highlighted in bold.

// Dial the phone + extension.
public new void DialPhone()
{
    // Dial the number Phone + Extension...
}

The second way a child class can modify a parent method is to override it.

This method requires some cooperation from the parent class. The parent class must mark the method with the virtual or abstract keyword. (The parent class can also have overridden the method. In that case, some ancestor class declared the original method with the virtual or abstract keyword.)

The following code shows the new version of the Person class’s DialPhone method with the virtual keyword highlighted in bold.

// Dial the phone.
public virtual void DialPhone()
{
    // Dial the number Phone...
}

The virtual keyword indicates that this method can be overridden by descendant classes. The following code shows how the Employee class could override the DialPhone method.

// Dial the phone + extension.
public override void DialPhone()
{
    // Dial the number Phone + Extension...
}

Overriding a method in this way is a powerful technique. When you invoke an overridden method for an object, you get the version defined by the object’s true class, even if you are referring to the object with a variable of an ancestor class.

In contrast, when you hide a method, you get only that version if you use a variable of the class that defined the new version.

These are confusing concepts, so here’s a detailed example. Consider the following stripped-down Person and Employee classes.

public class Person
{
    public string Name;

    public virtual void IsVirtual()
    {
        Console.WriteLine(Name + ": Person.IsVirtual");
    }

    public void HideMe()
    {
        Console.WriteLine(Name + ": Person.HideMe");
    }
}

public class Employee : Person
{
    public override void IsVirtual()
    {
        Console.WriteLine(Name + ": Employee.IsVirtual");
    }
    public new void HideMe()
    {
        Console.WriteLine(Name + ": Employee.HideMe");
    }
}

The Person class defines two methods. The IsVirtual method is defined with the virtual keyword. The HideMe method is defined without the virtual keyword.

The Employee class overrides IsVirtual and hides HideMe.

All four of these methods simply display their object’s name, the method’s class, and the method’s name in the Console window.

Now consider the following code that uses these methods.

Person ann = new Person() { Name = "Ann" };
Employee bob = new Employee() { Name = "Bob" };
Person person = bob;

ann.IsVirtual();
bob.IsVirtual();
person.IsVirtual();
Console.WriteLine();
ann.HideMe();
bob.HideMe();
person.HideMe();

The code creates a Person object and an Employee object. It then creates a Person variable and makes it refer to the Employee object it just created. (You can do that because an Employee is a kind of Person.)

Next, the code calls each of the objects’ IsVirtual methods. The object ann is a Person, so it calls the Person version of IsVirtual. The object bob is an Employee, so it calls the Employee version of IsVirtual.

The person object has type Person, but it actually refers to the Employee object bob. Because the Employee class overrode the definition of this method, the person object uses the version defined by the object’s true class, in this case Employee. Even though it looks like the code is invoking Person.IsVirtual, it actually invokes Employee.IsVirtual.

The case with the HideMe method is somewhat simpler. As before, the ann and bob objects call their respective class’s versions of the method.

The person object has type Person. Because this method is hidden in the Employee class, the Person class doesn’t know anything about that version. When the code calls the Person object’s HideMe method, it gets the Person class’s version.

The following text shows the code’s output.

Ann: Person.IsVirtual
Bob: Employee.IsVirtual
Bob: Employee.IsVirtual
Ann: Person.HideMe
Bob: Employee.HideMe
Bob: Person.HideMe

The object person invokes the Employee class’s version of IsVirtual (the third line of output) but it invokes the Person class’s version of HideMe (the last line of output).

There are two other keywords that affect overriding: abstract and sealed.

abstract

An abstract method is one that doesn’t have a method body. It defines the name, parameters, and return type of the method but doesn’t provide an implementation. (This is similar to the way interfaces define method signatures but don’t provide an implementation.)

If you give a class an abstract method, there’s a sort of placeholder in the class for the method. Because the placeholder is empty, you cannot create an instance of the class because it is incomplete. For that reason, if a class contains an abstract method, you must also mark the class as abstract.

Note that the class could define other non-abstract properties, methods, and events. (Some people call a non-abstract method, class, or other item concrete.) If it contains even one abstract method, then the class must be abstract.

An abstract method is also considered virtual, so a child class can override it to give it a method body. Then the child class can be concrete and you can create instances of it.

For an example, consider the following code.

public abstract class Report
{
    public abstract void GenerateReport();
    public abstract void DistributeReport();
}
public abstract class PersonnelReport : Report
{
    public override void DistributeReport()
    {
        // Code to distribute the report to the personnel department...
    }
}
public class Timesheet : PersonnelReport
{
    public override void GenerateReport()
    {
        // Code to generate a timesheet.
    }
}

The idea here is that the Report class defines broad features that should be provided by any report. The class defines two abstract methods: GenerateReport and DistributeReport. Because the class contains an abstract method, it must also be abstract. (In this case that’s okay. The Report class doesn’t represent a specific kind of report, so it doesn’t make sense to create an instance of one anyway.)

The PersonnelReport class derived from Report represents a report that should be sent to everyone in the personnel department. It overrides the DistributeReport method to send the report to everyone in the department. The class still contains an abstract method (GenerateReport) so it must be marked abstract.

The Timesheet class derived from PersonnelReport overrides the GenerateReport method to actually create a report. This class has no abstract methods so this can be a concrete class.

sealed

The final keyword that affects overriding is sealed. If you mark an overridden method as sealed in a child class, then further descendant classes cannot override that method.

Sealed methods have a couple of odd quirks. First, you cannot seal a method in the class where it is originally defined, only in a class that overrides it. Second, you cannot override a sealed method in a descendant class but you can hide it, so sealing a method doesn’t completely protect it from later tampering.

For example, consider the following code.

public abstract class Animal
{
    public abstract string FoodType();
}

public class Herbivore : Animal
{
    public override sealed string FoodType()
    {
        return "Vegetation";
    }
}

public class Koala : Herbivore
{
    public new string FoodType()
    {
        return "Eucalyptus";
    }
}

The Animal class defines an abstract FoodType method.

The Herbivore class derived from Animal overrides FoodType to return the string "Vegetation".

The Herbivore class marks the method sealed so the Koala class, which is derived from Herbivore, cannot override FoodType. It can, however, use the new keyword to hide the Herbivore implementation of the method with a new version.

Polymorphism

Loosely speaking, polymorphism is the ability to treat one object as if it were an object of a different type. In OOP terms, it means that you can treat an object of one class as if it were from an ancestor class.

For example, suppose Employee and Customer are both derived from the Person class. Then you can treat Employee and Customer objects as if they were Person objects because, in a sense, they are. They are specific types of Person objects. They inherited all the properties, methods, and events of a Person object, so they should act as Person objects.

C# enables you to make a variable of one class refer to an object of a derived class. In this example, you can use a Person variable to hold a reference to an Employee or Customer object, as shown in the following code.

Employee employee = new Employee();     // Make an Employee
Customer customer = new Customer();     // Make a Customer
Person person = new Person();           // Make a Person.

person = employee;   // Okay because an Employee is a type of Person.
person = customer;   // Okay because a Customer is a type of Person.
employee = person;   // Not okay because a Person is not necessarily an Employee.

One common reason to use polymorphism is to treat a collection of objects in a uniform way that makes sense in the context of the parent class. For example, suppose that the Person class defines the FirstName and LastName properties. The program could define a collection named AllPeople and add references to Customer and Employee objects to represent all the people that the program needs to manage. The code could then loop through the collection, treating each object as a Person, as shown in the following code.

foreach (Person person in AllPeople)
{
    Console.WriteLine(person.FirstName + " " + person.LastName);
}

When you use an object polymorphically, you can access only the features defined by the type of variable you actually use to refer to an object. For example, if you use a Person variable to refer to an Employee object, you can use only the features defined by the Person class, not those added by the Employee class.

If you know an object has a specific type, you can convert the object into that type before you work with it. For example, the following code loops through the AllPeople list and takes special action for objects that are Employees.

foreach (Person person in AllPeople)
{
    if (person is Employee)
    {
        // Do something Employee-specific with the person.
        Employee employee = person as Employee;
        ...
    }
}

The code uses the statement if (person is Employee) to determine whether the variable person can be treated as an Employee object. It is important to realize that this doesn’t mean person actually is an Employee. It means person is an Employee or a class derived from Employee, so it can be treated as an Employee. For example, if the Manager class is derived from Employee, then person is Employee returns true for Manager objects.

The code uses the following statement to convert the Person looping variable into an Employee variable.

Employee employee = person as Employee;

The as keyword converts the variable person into an Employee if possible. If person cannot be converted into an Employee, then as returns null. (In this example, you know person can be converted into an Employee because the code just checked.) The as statement is roughly equivalent to the following code.

Employee employee = null;
if (person is Employee) employee = (Employee)person;

Sometimes you might want to invoke the base class’s version of an overridden method. For example, suppose you override the Person class’s ToString method but you also want to be able to call the version defined in the Object class from which Person is derived. In that case you could add the following BaseToString method to the Person class.

public string BaseToString()
{
    return base.ToString();
}

Here the keyword base tells the program to use the version of ToString defined in the parent class. Now the program can use the Object class’s version of ToString by calling the BaseToString method.

The code inside the Person class can invoke the parent class’s version of ToString as shown here, but there is no way for the program to invoke an object’s overridden base class methods directly. If you don’t define a method similar to BaseToString, the program cannot call the Object class’s version of the method.

Also note that there is no way to call further up the inheritance chain.

Summary

Classes are programming abstractions that group data and related behavior in tightly encapsulated packages. After you define a class, you can create instances of that class.

Inheritance lets you derive a child class from a parent class, possibly adding, hiding, or overriding the parent’s behavior. The new, virtual, abstract, override, and sealed keywords give you a fair amount of control over how methods are inherited and modified.

Interfaces give you another method for defining behavior that doesn’t follow an inheritance hierarchy. Like an abstract class, an interface lets you determine a class’s behavior without providing an implementation. Because interfaces don’t need to follow a derivation hierarchy, you can use them to implement nonhierarchical relationships such as multiple inheritance (interface inheritance).

Polymorphism enables you to treat an object as if it were of an ancestor’s type. For example, the following text shows the inheritance hierarchy for the Windows Forms PictureBox control.

System.Object
  System.MarshalByRefObject
    System.ComponentModel.Component
      System.Windows.Forms.Control
        System.Windows.Forms.PictureBox

This means you can treat a PictureBox as if it is a PictureBox, Control, Component, MarshalByRefObject, or Object.

This chapter described these features and briefly mentioned how you can implement many of them in C#. The next chapter explains the syntax for creating classes and structures in greater detail. It also explains the differences between the two and how to decide when to use classes and when to use structures.

Exercises

For the exercises that require you to build an inheritance hierarchy, draw abstract classes with dashed outlines and concrete classes with solid outliines.

  1. Consider the inheritance hierarchy shown Figure 11-2. Suppose you decide you also need a PartTimeProgrammer class to represent programmers who are paid hourly. How would you update the hierarchy?
  2. Consider the following code.
    foreach (Person person in AllPeople)
    {
        if (person is Employee)
        {
            // Do something Employee-specific with the person.
            Employee employee = person as Employee;
            ...
        }
    }

    Rewrite the code so that it doesn’t use is. (Hint: Place the as statement before the new if statement.) Which version is better?

  3. Draw an inheritance hierarchy for a pet store application that includes the classes shown in the following table. Add parent classes as needed. (Hint: If you include the properties defined by each class, there shouldn’t be a lot of duplication.)
    ClassProperties
    JanitorName, Address, EmployeeId, Hours, HourlyPay
    ShiftManagerName, Address, EmployeeId, Hours, Salary
    CustomerName, Address, CustomerId, Pets
    GroomerName, Address, Hours, HourlyRate
    SupplierName, Address, SupplierId, Products
    StoreManagerName, Address, EmployeeId, Hours, Salary
    SalesClerkName, Address, EmployeeId, Hours, HourlyPay
    TrainerName, Address, Classes, Hours, HourlyRate

    Are there some classes that should be abstract? Why? How would you make them abstract?

  4. Build a program that implements the hierarchy you designed for Exercise 3. For properties with non-obvious data types such as Pets and Classes, use string as a placeholder. (If you’re not sure how to build the classes, read the next chapter and then come back to this exercise.)
  5. Suppose you’re building a fantasy role-playing game. Each player has one of three races: Human, Elf, or Dwarf. Each player also has a weapon. Right now you have defined Sword, Bow, and Wand, but you plan to add other weapons later such as other bladed weapons (Spear, Dagger, and Axe), missile weapons (Sling, Atlatl, and Dart), and magic weapons (Potion, Pendant, and VoodooDoll).

    How would you design the Player class to handle all this? Draw an inheritance hierarchy to show the relationships among the classes.

  6. Suppose you decide to modify the game described in Exercise 5 so that players have a profession. The basic professions are Fighter and MagicUser. A player can be one of the basic professions or can be a specialist. The initial Fighter specialties are Knight, Ranger, and Archer. The initial MagicUser specialties are Illusionist, Witch, and Chemist. How would you modify the definition of the Player class? Draw the class inheritance hierarchy.
  7. Suppose software developers are assigned to departments. They may also be assigned to a primary development project and up to two secondary projects. How would you design the Developer class to handle this?
  8. Suppose you’re building a program for a college and you need the classes and properties shown in the following table.
    ClassProperties
    StudentName, Address, StudentId, CurrentClasses, PastClasses
    InstructorName, Address, EmployeeId, CurrentClasses, PastClasses
    TeachingAssistantName, Address, StudentId, EmployeeId, CurrentClasses, PastClasses, CurrentClassesTaught, PastClassesTaught
    ResearchAssistantName, Address, StudentId, EmployeeId, Sponsor

    (The Sponsor property is the Instructor for whom a ResearchAssistant works.)

    How would you implement these classes? Draw the inheritance hierarchy. (Hint: Change the names of some properties if that helps.)

  9. Build a program that implements the hierarchy you designed for Exercise 8. For properties with non-obvious data types such as CurrentClasses, use string as a placeholder. (If you’re not sure how to build the classes, read the next chapter and then come back to this exercise.) Make classes that implement an interface do so directly without delegation.
  10. Modify the classes you built for Exercise 9 so that they use delegation to implement the IStudent interface. What are the advantages and disadvantages of the approaches used in Exercises 9 and 10?
  11. In Exercise 10, why shouldn’t you make the Student class implement the IStudent interface directly and then make the TeachingAssistant and ResearchAssistant classes delegate the interface to a Student object?
  12. In the model you built for Exercises 10 and 11, suppose you want to derive the LabAssistant class from ResearchAssistant. How would the new class handle the IStudent interface?
..................Content has been hidden....................

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