Designing Object-Oriented Programs In C#

 

In This Chapter

  • Inheritance
  • Encapsulating Object Internals
  • Polymorphism
  •  

C# is a modern object-oriented programming language. As such it has manynew features to support object-oriented programming. The preceding chaptercovered the proper syntax of classes and their members. This chapter takes you astep further. It builds upon what has already been presented to createobject-oriented programs.

This chapter discusses inheritance, the capability to derive new classes fromexisting ones. It solidifies what has been presented about encapsulation. Thenit examines the nuances of polymorphism, allowing classes to dynamically modifytheir runtime behavior. This chapter provides a basis for how to do detaileddesign of object-oriented programs with C# as an implementation language.

Inheritance

Inheritance is an object-oriented term relating to how one class, a derivedclass, can share the characteristics and behavior from another class, a baseclass. There should be a natural parent/child relationship between the baseclass and the derived class, respectively. This can be thought of as an “isa” relationship, because the derived class can be identified by both itsclass type and its base class type. Essentially, any base class members withprotected or greater access also belong to a derived class.

The benefits gained by this are the ability to reuse the base class membersand also to add members to the derived class. The derived class then becomes aspecialization of the parent. This specialization can continue for as manylevels as necessary, each new level derived from the base class above it. In theopposite direction, going up the inheritance hierarchy, there is moregeneralization at each new base class traversed. Regardless of how many levelsbetween classes, the “is a” relationship holds.

Base Classes

Normal base classes may be instantiated themselves, or inherited. Derivedclasses inherit each base class member marked with protected or greater access.The derived class is specialized to provide more functionality, in addition towhat its base class provides.

A derived class declares that it inherits from a base class by adding a colon(:) and the base class name after the derived class name. Here’san example:

public class Contact
{
string name;
string email;
string address;

public Contact()
{
// statements …
}

public string Name
{
get
{
return name;
}
set
{
name = value;
}
}

public string Email
{
get
{
return email;
}
set
{
email = value;
}
}

public string Address
{
get
{
return address;
}
set
{
address = value;
}
}
}

public class Customer : Contact
{
string gender;
decimal income;

public Customer()
{
// statements …
}
}

In the example, the Contact class is inherited by theCustomer class. This means the Customer class possesses allthe same members as its base class (Contact) in addition to its own. Inthis case, Customer has the properties Name, Email,and Address.

Since Customer is a specialization of Contact, it has its own unique members:gender and income.

Abstract Classes

Abstract classes are a special type of base classes. In addition to normalclass members, they have abstract class members. These Abstract class membersare methods and properties that are declared without an implementation. Allclasses derived directly from abstract classes must implement these abstractmethods and properties.

Abstract classes can never be instantiated. This would be illogical, becauseof the members without implementations. So what good is a class that can’tbe instantiated? Lots! Abstract classes sit toward the top of a class hierarchy.They establish structure and meaning to code. They make frameworks easier tobuild. This is possible because abstract classes have information and behaviorcommon to all derived classes in a framework. Take a look at the followingexample:

abstract public class Contact
{
protected string name;

public Contact()
{
// statements…
}

public abstract void generateReport();

abstract public string Name
{
get;
set;
}}

public class Customer : Contact
{
string gender;
decimal income;
int numberOfVisits;

public Customer()
{
// statements
}

public override void generateReport()
{
// unique report
}

public override string Name
{
get
{
numberOfVisits++;
return name;
}
set
{
name = value;
numberOfVisits = 0;
}
}
}

public class SiteOwner : Contact
{
int siteHits;
string mySite;

public SiteOwner()
{
// statements…
}

public override void generateReport()
{
// unique report
}

public override string Name
{
get
{
siteHits++;
return name;
}
set
{
name = value;
siteHits = 0;
}
}
}

This example has three classes. The first class, Contact, is now anabstract class. This is shown as the first modifier of its class declaration.Contact has two abstract members, and it has an abstract method namedgenerateReport(). This method is declared with the abstract modifier infront of the method declaration. It has no implementation (no braces) and isterminated with a semicolon. The Name property is also declaredabstract. The accessors of properties are terminated with semicolons.

The abstract base class Contact has two derived classes,Customer and SiteOwner. Both of these derived classesimplement the abstract members of the Contact class. ThegenerateReport() method in each derived class has an overridemodifier in its declaration. Likewise, the Name declaration contains anoverride modifier in both Customer and SiteOwner.

The override modifier for the overridden generateReport()method and Name property is mandatory. C# requires explicit declarationof intent when overriding methods. This feature promotes safe code by avoidingthe accidental overriding of base class methods, which is what actually doeshappen in other languages. Leaving out the override modifier generatesan error. Similarly, adding a new modifier also generates an error.Abstract methods must be overridden and cannot be hidden, which the newmodifier or the lack of a modifier would be trying to do.

Notice the name field in the Contact class. It has aprotected modifier. Remember, a protected modifier al
lowsderi
ved classes to access base class members. In this case, it enables theoverridden Name property to access the name field in theContact class.

The most famous of all abstract classes is the Object class. It maybe referred to as object or Object, but it’s still thesame class. Object is the base class for all other classes in C#.It’s also the default base class when a base class is not specified. Thefollowing class declarations produce the same exact results:

abstract public class Contact : Object
{
// class members
}

abstract public class Contact
{
// class members
}

Object is implicitly included as a base class if it is not alreadydeclared. Besides providing the abstract glue to hold together the C# classframework, object includes built-in functionality, some of which isuseful for derived classes to implement. Table 8.1 lists each objectmethod and its purpose.

Table 8.1 Object Class Methods

Method Purpose
Equals() Compares object references for equality.
GetHashCode() Returns a hash code for an object.
GetType() Returns the type of the object.
ToString() Returns a string representation of an object.
Finalize() Same as a destructor.
MemberwiseClone() Performs shallow copy of an object.


All of the methods in Table 8.1 are public, except forFinalize() and MemberwiseClone(), which are protected. TheGetType() and MemberwiseClone() methods may not be overridden,but all others may. Listing 8.1 shows an example of using object methods.

Listing 8.1 Object Class Member Implementations in a Derived Class

using System;

public class WebSite
{
public string SiteName;
public string URL;
public string Description;
public WebSite()
{
}

public WebSite( string strSiteName, string strURL, string strDescription )
{
SiteName = strSiteName;
URL = strURL;
Description = strDescription;
}
}

abstract public class Contact
{
protected string name;

public Contact()
{
// initialization code…
}

public abstract string generateReport();

abstract public string Name
{
get;
set;
}
}

public class SiteOwner : Contact
{
int siteHits;
WebSite mySite;

public SiteOwner()
{
mySite = new WebSite();
siteHits = 0;
}

public SiteOwner(string aName, WebSite aSite)
{
mySite = new WebSite(aSite.SiteName,
aSite.URL,
aSite.Description);

Name = aName;
}

public override string generateReport()
{
return this.ToString();
}

public override string Name
{
get
{
siteHits++;
return name;
}
set
{
name = value;
siteHits = 0;
}
}

public override string ToString()
{
return “[" +
Name +
", " +
siteHits.ToString() +
"]“;
}

public override bool Equals(Object anOwner)
{
return this.ToString().Equals(anOwner.ToString());
}

public override int GetHashCode()
{
return this.ToString().GetHashCode();
}

public SiteOwner Clone()
{
return (SiteOwner) this.MemberwiseClone();
}
}

public class Test
{
public Test() {}

public static void Main()
{
WebSite mySite = new WebSite(“Le Financier”,
“http://www.LeFinancier.com”,
“Fancy Financial Site”);

SiteOwner firstOwner = new SiteOwner(“Jack”, mySite);
SiteOwner secondOwner = firstOwner.Clone();

Console.WriteLine(“Report: {0}”,
firstOwner.generateReport());
Console.WriteLine(“To String: {0}”,
firstOwner.ToString());
Console.WriteLine(“Hash Code: {0}”,
firstOwner.GetHashCode());

Console.WriteLine(“Report: {0}”,
secondOwner.generateReport());
Console.WriteLine(“To String: {0}”,
secondOwner.ToString());
Console.WriteLine(“Hash Code: {0}”,
secondOwner.GetHashCode());

Console.WriteLine(
“1stOwner: {0} equals: {1} 2ndOwner: {2}.”,
firstOwner.Name,
firstOwner.Equals(secondOwner),
secondOwner.Name);

Console.WriteLine(
“2nd Equality Check: {0}”,
firstOwner.Equals(secondOwner));

Console.WriteLine(“Report: {0}”,
firstOwner.generateReport());
Console.WriteLine(“To String: {0}”,
firstOwner.ToString());
Console.WriteLine(“Hash Code: {0}”,
firstOwner.GetHashCode());

Console.WriteLine(“Report: {0}”,
secondOwner.generateReport());
Console.WriteLine(“To String: {0}”,
secondOwner.ToString());
Console.WriteLine(“Hash Code: {0}”,
secondOwner.GetHashCode());
}
}

And here’s its output:

Report: [Jack, 1]
To String: [Jack, 2]
Hash Code: 179554879
Report: [Jack, 1]
To String: [Jack, 2]
Hash Code: 179554879
1stOwner: Jack equals: False 2ndOwner: Jack.
2nd Equality Check: True
Report: [Jack, 7]
To String: [Jack, 8]
Hash Code: 179555189
Report: [Jack, 7]
To String: [Jack, 8]
Hash Code: 179555189

Listing 8.1 contains three classes to show implementation of objectmethods. The Contact class is the base class for the SiteOwnerclass. Although the Contact class possesses functionality and abstractdefinitions, it does not contain overridden methods from the Objectclass. The SiteOwner class does have the overridden methods from theObject class. This shows that a class doesn’t have to inheritdirectly from a base class to override its members.

The SiteOwner class overrides three of Object’s methods,ToString(), Equals(), and GetHashCode(). Each ofthese methods has the override modifier in its declaration. TheObject class method definitions for these members are not invokedbecause the Object class method definitions are overridden bySiteOwner.

The ToString() method returns a string representation of anobject’s contents. This is often useful for debugging where the contents ofan object are dumped to an error log file or perhaps to the console. In thiscase, ToString() concatenates Name and siteHits andformats them into a string to be returned to t

he calling program. This exampleuses the ToString() method extensively.

The Equals() method compares the current SiteOwner‘sToString() method to the value of the parameter’sToString() method. This comparison takes advantage of the built-incapabilities of the string type.

The GetHashCode() method executes the ToString() method ofthe current SiteOwner and uses that to get a hash code. A hash value isnormally calculated from a key value that is not expected to change within anobject. In this case, the process is simplified by using the string type’sbuilt-in GetHashCode() method. Hash codes are useful for any functionrequiring a unique integer value from a class. The most common use of this iswith the HashTable collection class.

The Clone() method uses Object‘sMemberwiseClone() method. The Object class’sMemberwiseClone() method can’t be overridden and has protectedaccess. Therefore, other objects cannot call this method on an instance ofSiteOwner. This is why this method call is wrapped in theClone() method.

The output of this program is somewhat strange. The first comparison, wherefirstOwner is compared to secondOwner, fails. However, thesecond comparison, immediately after that, passes. What gives?

Fortunately, the ToString() printouts provide some clues. The secondparameter of the ToString() method output increments from 1 to 2 in thefirst pair of printouts before the equality checks. After the equality checks,the numbers increment from 7 to 8. Going back to the ToString() methodshows this second parameter is the siteHits field.

Further investigation reveals that the only place where the siteHitsfield is modified is in the get accessor of the Name property.This shows why the number is changing. Every time ToString() executes,it uses the Name property. This invokes the get accessor ofName, which increments siteName. Since, during processing ofthe ToString() method, the get accessor of Nameexecutes before siteHits is read, the printout never showssiteHits as being 0.

So, to explain the printouts: ToString() is called twice for eachobject, firstOwner and secondOwner, incrementingsiteHits twice. This leaves siteHits at 2 on both objects. Inthe equality check, firstOwner.Name is accessed, leavingfirstOwner.siteHits at 3. Now, secondOwner.siteHits is still 2because its Name property has not been accessed. Therefore, when theEquals() method is called, these two objects produce different stringsand are, in fact, not equal. Finally, secondOwner.Name is accessed,incrementing its siteHits to 3. Now both objects produce the samestrings, so when Equals() is called again on the next line it returnstrue. The rest of the printouts should be understandable after thisexplanation.


Warning

Properties can have sideeffects. In the ToString() method of this chapter, it seemed prettyslick to update the siteHit every time the Name property wasread. Perhaps some motivation for this would be that every time a site wasvisited, the Site Owner’s name would be referenced. This was a narrow viewof how this class could be used. The choice to use the Name property inthe ToString() method seemed natural, but the side effect ofincrementing the siteHits field caused a potentially serious bug. Whenbuilding a class, think about how properties will be used.


A couple of Object methods not shown here are GetType() andFinalize(). The GetType() method is shown later in thechapter, when polymorphism is discussed. The Finalize() method isnormally never used in a class declaration. The destructor syntax is usedinstead. Destructors and Finalizers are the same thing. During compilation, C#converts all destructors to the Finalize() method, for compatibilitywith other languages conforming to Common Language Infrastructure (CLI)standards. This enables the garbage collector to work with Finalize()methods instead of language specific syntax.

Calling Base Class Members

Derived classes can access the members of their base class if those membershave protected or greater access. Simply use the member name in the appropriatecontext, just as if that member were a part of the derived class itself.Here’s an example:

abstract public class Contact
{
private string address;
private string city;
private string state;
private string zip;

public string FullAddress()
{
string fullAddress =
address + ‘\n’ +
city + ‘,’ + state + ‘ ‘ + zip;

return fullAddress;
}
}

public class Customer : Contact
{
public string GenerateReport()
{
string fullAddress = FullAddress();
// do some other stuff…
return fullAddress;
}
}

In this example, the GenerateReport() method of theCustomer class calls the FullAddress() method in its baseclass, Contact. All classes have full access to their own memberswithout qualification. Qualification refers to using a class name with the dotoperator to access a class member—MyObject.SomeMethod(), forinstance. This shows that a derived class can access its base class members inthe same manner as its own.

Base class constructors can be called from derived classes. To call a baseclass constructor, use the base() constructor reference. This isdesirable when it’s necessary to initialize a base class appropriately.

Here’s an example that shows the derived class constructor with anaddress parameter:

abstract public class Contact
{
private string address;

public Contact(string address)
{
this.address = address;
}
}

public class Customer : Contact
{
public Customer(string address) : base(address)
{
}
}

In this code, the Customer class does not have an address, so itpasses the parameter to its base class constructor by adding a colon and thebase keyword with the parameter to its declaration. This calls theContact constructor with the address parameter, where theaddress field in Contact is initialized.


Warning

Depending on the design of a class hierarchy, failure to initialize base class constructors may leave code in an inconsistent state.


The following example will not compile. It illustrates the effects of notincluding a default constructor in a class definition:

abstract public class Contact
{
private string address;

public Contact(string address)
{
this.address = address;
}
}

publ

ic class Customer : Contact
{
public Customer(string address)
{
}
}

In this example, the Customer constructor does not call the baseclass constructor. This is obviously a bug, since the address fieldwill never be initialized.

When a class has no explicit constructor, the system assigns a defaultconstructor. The default constructor automatically calls a default orparameterless base constructor. Here’s an example of automatic defaultconstructor generation that would occur for the preceding example:

public Customer() : Contact()
{
}

When a class does not declare any constructors, the code in this example isautomatically generated. The default base class constructor is called implicitlywhen no derived class constructors are defined. Once a derived class constructoris defined, whether or not it has parameters, a default constructor will not beautomatically defined, as the preceding code showed.
Continues…

Pages: 1 2

Twitter Digg Delicious Stumbleupon Technorati Facebook Email

No comments yet... Be the first to leave a reply!