Object-Oriented Basics in C#

This chapter introduces object-oriented programming. Those who are familiar with object- oriented programming will probably want to skip this chapter.

You can take many approaches to object-oriented design, as evidenced by the number of books written about it. The following introduction takes a fairly pragmatic approach and doesn’t spend a lot of time on design, but the design-oriented approaches can be quite useful to newcomers.

1. What’s an Object?

An object is merely a collection of related information and functionality. An object can be something that has a corresponding real-world manifestation (such as an employee object), something that has some virtual meaning (such as a window on the screen), or just some convenient abstraction within a program (a list of work to be done, for example).

An object contains the data that describes the object and the operations that can be performed on the object. Information stored in an employee object, for example, might be various identi­fication information (name and address), work information (job title and salary), and so on. The operations performed might include creating an employee paycheck or promoting an employee.

When creating an object-oriented design, the first step is to determine what the objects are. When dealing with real-life objects, this is often straightforward, but when dealing with the virtual world, the boundaries become less clear. That’s where the art of good design shows up, and it’s why good architects are in such demand.

2. Inheritance

Inheritance is a fundamental feature of an object-oriented system, and it’s simply the ability to inherit data and functionality from a parent object. Rather than developing new objects from scratch, new code can be based on the work of other programmers,[1] adding only the new features that are needed. The parent object that the new work is based upon is known as a base class, and the child object is known as a derived class.

Inheritance gets a lot of attention in explanations of object-oriented design, but the use of inheritance isn’t particularly widespread in most designs. There are several reasons for this.

First, inheritance is an example of what’s known in object-oriented design as an “is-a” relationship. If a system has an animal object and a cat object, the cat object could inherit from the animal object because a cat “is-a” animal. In inheritance, the base class is always more generalized than the derived class. The cat class would inherit the eat function from the animal class and would have an enhanced sleep function. In real-world design, such relationships aren’t particularly common.

Second, to use inheritance, the base class needs to be designed with inheritance in mind. This is important for several reasons. If the objects don’t have the proper structure, inheritance can’t really work well. More important, a design that enables inheritance also makes it clear that the author of the base class is willing to support other classes inheriting from the class. If a new class is inherited from a class where this isn’t the case, the base class might at some point change, breaking the derived class.

Some less-experienced programmers mistakenly believe that inheritance is “supposed to be” used widely in object-oriented programming and therefore use it far too often. Inheritance should be used only when the advantages it brings are needed.[2] See the upcoming “Polymorphism and Virtual Functions” section.

In the .NET common language runtime (CLR), all objects are inherited from the ultimate base class named object, and there’s only single inheritance of objects (in other words, an object can be derived from only one base class). This prevents the use of some common idioms available in multiple-inheritance systems such as C++, but it also removes many abuses of multiple inheritance and provides a fair amount of simplification. In most cases, it’s a good trade-off. The .NET runtime allows multiple inheritance in the form of interfaces, which can’t contain implementation. We’ll discuss interfaces in Chapter 10.

3. Containment

So, if inheritance isn’t the right choice, what is?

The answer is containment, also known as aggregation. Rather than saying an object is an example of another object, an instance of that other object will be contained inside the object. So, instead of having a class look like a string, the class will contain a string (or an array or a hash table).

The default design choice should be containment, and you should switch to inheritance only if needed (in other words, if there really is an “is-a” relationship).

4. Polymorphism and Virtual Functions

Once, while writing a music system, we decided we wanted to be able to support both WinAmp and Windows Media Player as playback engines, but we didn’t want all the code to have to know which engine it was using. We therefore defined an abstract class, which is a class that defines the functions a derived class must implement and that sometimes provides functions that are useful to both classes.

In this case, the abstract class was called MusicServer, and it had functions such as Play(), NextSong(), Pause(), and so on. Each of these functions was declared as abstract so each player class would have to implement those functions themselves.

Abstract functions are automatically virtual functions, which allow the programmer to use polymorphism to make their code simpler. When there’s a virtual function, the programmer can pass around a reference to the abstract class rather than the derived class, and the compiler will write code to call the appropriate version of the function at runtime.

An example will probably make this clearer. The music system supports both WinAmp and Windows Media Player as playback engines. The following is a basic outline of what the classes look like:

using System;

public abstract class MusicServer {

public abstract void Play();

}

public class WinAmpServer: MusicServer {

public override void Play()

{

Console.WriteLine(“WinAmpServer.Play()”);

}

}

public class MediaServer: MusicServer {

public override void Play()

{

Console.WriteLine(“MediaServer.Play()”);

}

}

class Test {

public static void CallPlay(MusicServer ms)

{

ms.Play();

}

public static void Main()

{

MusicServer ms = new WinAmpServer();

CallPlay(ms);

ms = new MediaServer();

CallPlay(ms);

}

}

This code produces the following output:

WinAmpServer.Play()

MediaServer.Play()

Polymorphism and virtual functions are used in many places in the .NET runtime system. For example, the base object object has a virtual function called ToString() that’s used to convert an object into a string representation of the object. If you call the ToString() function on an object that doesn’t have its own version of ToString(), the version of the ToString() function that’s part of the object class will be called, which simply returns the name of the class. If you overload—write your own version of—the ToString() function, that one will be called instead, and you can do something more meaningful, such as writing out the name of the employee contained in the employee object. In the music system, this meant overloading functions for play, pause, next song, and so on.

5. Encapsulation and Visibility

When designing objects, the programmer gets to decide how much of the object is visible to the user and how much is private within the object. Details that aren’t visible to the user are said to be encapsulated in the class.

In general, the goal when designing an object is to encapsulate as much of the class as possible. These are the most important reasons for doing this:

  • The user can’t change private things in the object, which reduces the chance the user will either change or depend upon such details in their code. If the user does depend on these details, changes made to the object may break the user’s code.
  • Changes made in the public parts of an object must remain compatible with the previous version. The more that’s visible to the user, the fewer things that can be changed without breaking the user’s code.
  • Larger interfaces increase the complexity of the entire system. Private fields can be accessed only from within the class; public fields can be accessed through any instance of the class. Having more public fields often makes debugging much tougher.

Chapter 5 will explore this subject further.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *