Fundamentals of Object
Fundamentals of Object
The goals of this tutorial are to guide you through the terminology of object-oriented
programming (OOP) and to give you an understanding of the importance of object-
oriented concepts to programming. Many languages, such as C++ and Microsoft
Visual Basic, are said to "support objects," but few languages actually support all the
principles that constitute object-oriented programming. C# is one of these
languages: it was designed from the ground up to be a truly object-oriented,
component-based language. So, to get the absolute maximum out of this book, you
need to have a strong grasp of the concepts presented here.
I know that conceptual tutorials like this are often skipped over by readers who want
to dive into the code right away, but unless you consider yourself an "object guru," I
encourage you to read this tutorial. For those of you only somewhat familiar with
object-oriented programming, you should reap benefits from doing so. Also, keep in
mind that the tutorials that follow this one will refer back to the terminology and
concepts discussed here.
Before we get started, I'd like to add that object-oriented programming is much
more than a marketing phrase (although it has become that for some people), a new
syntax, or a new application programming interface (API). Object-oriented
programming is an entire set of concepts and ideas. It's a way of thinking about the
problem being addressed by a computer program and tackling the problem in a more
intuitive and therefore more productive manner.
My first job involved using the Pascal language to program the box-office reporting
and itinerary applications for Holiday on Ice. As I moved on to other jobs and other
applications, I programmed in PL/I and RPG III (and RPG/400). After a few more
years, I started programming applications in the C language. In each of these
instances, I was easily able to apply knowledge I had learned from prior experience.
The learning curve for each successive language was shorter regardless of the
complexity of the language I was learning. This is because until I started
programming in C++, all the languages I had used were procedural languages that
mainly differed only in syntax.
Everything Is an Object
In a true object-oriented language, all problem domain entities are expressed
through the concept of objects. (Note that in this book I'll be using the Coad/Yourdon
definition for "problem domain"-that is, that a problem domain is the problem you're
attempting to solve, in terms of its specific complexities, terminology, challenges,
and so on.) As you might guess, objects are the central idea behind object-oriented
programming. Most of us don't walk around thinking in terms of structures, data
packets, function calls, and pointers; instead, we typically think in terms of objects.
Let's look at an example.
If you were writing an invoicing application and you needed to tally the detail lines of
the invoice, which of the following mental approaches would be more intuitive from
the client's perspective? -
Non-object-oriented approach I'll have access to a data structure representing an
invoice header. This invoice header structure will also include a doubly linked list of
invoice detail structures, each of which contains a total line amount. Therefore, to
get an invoice total, I need to declare a variable named something like
totalInvoiceAmount and initialize it to 0, get a pointer to the invoice header
structure, get the head of the linked list of detail lines, and then traverse the linked
list of detail lines. As I read each detail line structure, I'll get its member variable
containing the total for that line and increment my totalInvoiceAmount variable.
Object-oriented approach I'll have an invoice object, and I'll send a message to that
object to ask it for the total amount. I don't need to think about how the information
is stored internally in the object, as I had to do with the non-object-oriented data
structure. I simply treat the object in a natural manner, making requests to it by
sending messages. (The group of messages that an object can process are
collectively called the object's interface. In the following paragraph, I'll explain why
thinking in terms of interface rather than implementation, as I have done here, is
justifiable in the object-oriented approach.)
Obviously, the object-oriented approach is more intuitive and closer to how many of
us would think our way through a problem. In the second solution, the invoice object
probably iterates through a collection of invoice detail objects, sending a message to
each one requesting its line amount. However, if what you're looking for is the total,
you don't care how it's done. You don't care because one of the main tenets of
object-oriented programming is encapsulation-the ability of an object to hide its
internal data and methods and to present an interface that makes the important
parts of the object programmatically accessible. The internals of how an object
carries out its job are unimportant as long as that object can carry out that job. You
are simply presented with an interface to the object, and you use that interface to
make the object perform a given task on your behalf. (I'll further explain the
concepts of encapsulation and interfaces later in this chapter.) The point here is that
programs written to simulate the real-world objects of the problem domain are much
easier to design and write because they allow us to think in a more natural way.
Notice that the second approach required an object to perform work on your behalf-
that is, to total the detail lines. An object doesn't contain data only, as a structure
does. Objects, by definition, comprise data and the methods that work on that data.
This means that when working with a problem domain we can do more than design
the necessary data structures. We can also look at which methods should be
associated with a given object so that the object is a fully encapsulated bit of
functionality. The examples that follow here and in the coming sections help illustrate
this concept.
NOTE
The code snippets in this chapter present the concepts of object-oriented
programming. Keep in mind that while I present many example code snippets in C#,
the concepts themselves are generic to OOP and are not specific to any one
programming language. For comparison purposes in this chapter, I'll also present
examples in C, which is not object-oriented.
Let's say you're writing an application to calculate the pay of your new company's
only employee, Amy. Using C, you would code something similar to the following to
associate certain data with an employee: -
struct EMPLOYEE
{
char szFirstName[25];
char szLastName[25];
int iAge;
double dPayRate;
};
Here's how you'd calculate Amy's pay by using the EMPLOYEE structure: -
void main()
{
double dTotalPay;
if (pEmp)
{
pEmp->dPayRate = 100;
strcpy(pEmp->szFirstName, "Amy");
strcpy(pEmp->szLastName, "Anderson");
pEmp->iAge = 28;
dTotalPay = pEmp->dPayRate * 40;
printf("Total Payment for %s %s is %0.2f",
pEmp->szFirstName, pEmp->szLastName, dTotalPay);
}
free(pEmp);
}
In this example, the code is based on data contained in a structure and some
external (to that structure) code that uses that structure. So what's the problem?
The main problem is one of abstraction: the user of the EMPLOYEE structure must
know far too much about the data needed for an employee. Why? Let's say that at a
later date you want to change how Amy's pay rate is calculated. For example, you
might want to factor in FICA and other assorted taxes when determining a net
payment. Not only would you have to change all client code that uses the EMPLOYEE
structure, but you would also need to document-for any future programmers in your
company-the fact that a change in usage had occurred.
class Employee
{
public Employee(string firstName, string lastName,
int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate;
}
class EmployeeApp
{
public static void Main()
{
Employee emp = new Employee ("Amy", "Anderson", 28, 100);
Console.WriteLine("\nAmy's pay is $" + emp.CalculatePay(40));
}
}
In the C# version of the EmployeeApp example, the object's user can simply call the
object's CalculatePay method to have the object calculate its own pay. The
advantage of this approach is that the user no longer needs to worry about the
internals of exactly how the pay is calculated. If at some time in the future you
decide to modify how the pay is calculated, that modification will have no impact on
existing code. This level of abstraction is one of the basic benefits of using objects.
Now, one valid comment might be that I could have abstracted the C client's code by
creating a function to access the EMPLOYEE structure. However, the fact that I'd
have to create this function completely apart from the structure being worked on is
exactly the problem. When you use an object-oriented language such as C#, an
object's data and the methods that operate on that data (its interface) are always
together.
Keep in mind that only an object's methods should modify an object's variables. As
you can see in the previous example, each Employee member variable is declared
with the protected access modifier, except for the actual CalculatePay method, which
is defined as public. Access modifiers are used to specify the level of access that
derived class and client code has to a given class member. In the case of the
protected modifier, a derived class would have access to the member, but client code
would not. The public modifier makes the member accessible to both derived classes
and client code. I'll go into more detail on access modifiers in Chapter 5, "Classes,"
but the key thing to remember for now is that modifiers enable you to protect key
class members from being used incorrectly.
My point is that we're doing exactly what we shouldn't be doing. We're expending
mental energy thinking about the language and the machine-in terms of how much
memory to allocate and when to allocate it-instead of concentrating on the problem
domain. Using objects, we can focus on the business logic instead of the machinery
needed to solve the problem.
There are many ways to define a class and distinguish it from an object. You can
think of a class as simply a type (just like char, int, or long) that has methods
associated with it. An object is an instance of a type or class. However, the definition
I like best is that a class is a blueprint for an object. You, as the developer, create
this blueprint as an engineer would create the blueprint of a house. Once the
blueprint is complete, you have only one blueprint for any given type of house.
However, any number of people can purchase the blueprint and have the same
house built. By the same token, a class is a blueprint for a given set of functionality,
and an object created based on a particular class has all the functionality of the class
built right in.
Instantiation
To instantiate this class and use it, we have to declare an instance of it in a method
similar to this: -
public static void Main()
{
Employee emp = new Employee ("Amy", "Anderson", 28, 100);
}
In this example, emp is declared as type Employee and is instantiated using the new
operator. The variable emp represents an instance of the Employee class and is
considered an Employee object. After instantiation, we can communicate with this
object through its public members. For example, we can call the emp object's
CalculatePay method. We can't do this if we don't have an actual object. (There is
one exception to this, and that's when we're dealing with static members. I'll discuss
static members in both Chapter 5 and Chapter 6, "Methods.") -
Here we have two instances-emp and emp2-of the same Employee class. While
programmatically each object has the same capabilities, each instance will contain its
own instance data and can be treated separately. By the same token, we can create
an entire array or collection of these Employee objects. Chapter 7, "Properties,
Arrays, and Indexers," will cover arrays in detail. However, the point I want to make
here is that most object-oriented languages support the ability to define an array of
objects. This, in turn, gives you the ability to easily group objects and iterate
through them by calling methods of the object array or by subscripting the array.
Compare this to the work you'd have to do with a linked list, in which case you'd
need to manually link each item in the list to the item that comes before and after it.
Encapsulation
As I mentioned earlier, encapsulation, sometimes called information hiding, is the
ability to hide the internals of an object from its users and to provide an interface to
only those members that you want the client to be able to directly manipulate.
However, I also spoke of abstraction in the same context, so in this section, I'll clear
up any confusion regarding these two similar concepts. Encapsulation provides the
boundary between a class's external interface-that is, the public members visible to
the class's users-and its internal implementation details. The advantage of
encapsulation for the class developer is that he can expose the members of a class
that will remain static, or unchanged, while hiding the more dynamic and volatile
class internals. As you saw earlier in this chapter, encapsulation is achieved in C# by
virtue of assigning an access modifier-public, private, or protected-to each class
member.
Designing Abstractions
An abstraction refers to how a given problem is represented in the program space.
Programming languages themselves provide abstractions. Think about it like this:
When was the last time you had to worry about the CPU's registers and stack? Even
if you initially learned how to program in assembler, I'll bet it's been a long time
since you had to worry about such low-level, machine specific details. The reason is
that most programming languages abstract you from those details such that you can
focus on the problem domain.
Object-oriented languages enable you to declare classes whose names and interfaces
closely mimic real-world problem domain entities such that using theobjects have a
more natural "feel" to them. The result of removing the elements not directly related
to solving the problem at hand is that you're able to focus specifically on the problem
and greater productivity. In fact, paraphrasing Bruce Eckel in Thinking in Java
(Prentice Hall Computer Books, 2000), the ability to solve most problems will
generally come down to the quality of the abstraction being used.
However, that's one level of abstraction. If you take that a step further, as a class
developer you need to think in terms of how you can best design abstractions for
your class's clients to allow the client to focus on the task at hand and not be mired
in the details of how your class works. At this point, a good question might be, "How
does a class's interface relate to abstraction?" The class's interface is the
implementation of the abstraction.
I'll use a somewhat familiar analogy from programming courses to help crystallize
these concepts: the internal workings of vending machines. The internals of a
vending machine are actually quite involved. To fulfill its responsibilities, the machine
has to accept cash and coinage, make change, and dispense the selected item.
However, the vending machine has a finite set of functions it needs to express to its
users. This interface is expressed through a coin slot, buttons for selecting the
desired item, a lever to request change, a slot that holds the returned change, and a
shoot to dispense the selected item. Each of these items represents a part of the
machine's interface. Vending machines have, by and large, remained much the same
since their invention. This is because despite the fact that the internals have changed
as technology has evolved, the basic interface has not needed to change much. An
integral part of designing a class's interface is having a deep enough understanding
of the problem domain. This understanding will help you create an interface that
gives the user access to the information and methods that they need yet insulates
them from the internal workings of the class. You need to design an interface not
only to solve today's problems but also to abstract sufficiently from the class's
internals so that private class members can undergo unlimited changes without
affecting existing code.
Designing the abstraction of your classes in a way most useful to the programmers
using them is paramount in developing reusable software. If you can develop a
stable, static interface that persists across implementation changes, less of your
application will need modification over time. For example, think of our earlier payroll
example code. In the case of an Employee object and the payroll functionality, only a
few methods are going to be relevant, such as CalculatePay, GetAddress, and
GetEmployeeType. If you know the problem domain of a payroll application, you can
easily determine, to a fairly high degree, the methods that the users of this class are
going to need. Having said that, if you combine intimate knowledge of the problem
domain with forethought and planning in the design of this class, you can be
reasonably assured that the majority of your interface for this class will remain
unchanged despite future changes in the actual implementation of the class. After
all, from a user's perspective, it's only an Employee class. From the user's vantage
point, almost nothing should change from version to version.
The decoupling of user and implementation detail is what makes an entire system
easier to understand and therefore easier to maintain. Contrast this with procedural
languages such as C, in which each module needs to explicitly name and access the
members of a given structure. In that case, each time the structure's members
change, every single line of code referring to the structure must also change.
Inheritance
Inheritance relates to the programmer's ability to specify that one class has a kind-of
relationship with another class. Through inheritance, you can create (or derive) a
new class that's based on an existing class. You can then modify the class the way
that you want and create new objects of the derived type. This ability is the essence
of creating a class hierarchy. Outside of abstraction, inheritance is the most
significant part of a system's overall design. A derived class is the new class being
created, and the base class is the one from which the new class is derived. The
newly derived class inherits all the members of the base class, thereby enabling you
to reuse previous work.
NOTE
In C#, the issue of which base class members are inherited is controlled by the
access modifiers used to define the member . I'll get into that level of detail in
Chapter 5. For the purposes of this discussion, you can assume that a derived class
will inherit all its base class members.
As an example of when and how to use inheritance, let's look back at our
EmployeeApp example. In that example, we would almost certainly have different
types of employees, such as salaried, contractor, and hourly. While all of these
Employee objects would have a similar interface, they would in many cases function
differently internally. For instance, the CalculatePay method would work differently
for a salaried employee than it would for a contractor. However, you want the same
CalculatePay interface for your users regardless of employee type.
if (pEmployee->type == SALARIED)
{
// Do W-2 employee processing.
}
else if (pEmployee->type == CONTRACTOR)
{
// Do 1099 processing.
}
else if (pEmployee-> == HOURLY)
{
// Do hourly processing.
}
else
{
// Do corp-to-corp processing.
}
This code has a couple of problems. First, the success of the CalculatePay function is
tightly linked to the EMPLOYEE structure. As I mentioned earlier, tight coupling like
this is a problem because any modification to the EMPLOYEE structure will break this
code. As an object-oriented programmer, the last thing you want to do is burden the
users of your class with needing to know the intricate details of your class's design.
That would be like a vending machine manufacturer requiring you to understand the
internal mechanics of the vending machine before you can purchase a soda.
Second, the code doesn't promote reuse. Once you begin to see how inheritance
promotes reuse, you realize that classes and objects are good things. In this case,
you would simply define all the members for the base class that would function the
same regardless of employee type. Any derived class would then inherit this
functionality and change anything necessary. Here's how that would look in C#: -
class Employee
{
public Employee(string firstName, string lastName,
int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate;
}
You've seen in this small example that inheritance enables you to reuse code by
inheriting functionality from base classes. And it goes even further, allowing you to
extend the class above and beyond that point by adding your own variables and
methods.
To address the all-important issue of proper inheritance, I'll use a term from Marshall
Cline and Greg Lomow's C++ FAQs (Addison-Wesley, 1998):
substitutability.Substitutability means that the advertised behavior of the derived
class is substitutable for the base class. Think about that statement for a moment-
it's the single most important rule you'll learn regarding building class hierarchies
that work. (By "work," I mean stand the test of time and deliver on the OOP
promises of reusable and extendable code.) -
Another rule of thumb to keep in mind when creating your class hierarchies is that a
derived class should require no more and promise no less than its base class on any
inherited interfaces. Not adhering to this rule breaks existing code. A class's interface
is a binding contract between itself and programmers using the class. When a
programmer has a reference to a derived class, the programmer can always treat
that class as though it is the base class. This is called upcasting. In our example, if a
client has a reference to a ContractEmployee object, it also has an implicit reference
to that object's base, an Employee object. Therefore, by definition,
ContractEmployee should always be able to function as its base class. Please note
that this rule applies to base class functionality only. A derived class can choose to
add behavior that is more restrictive regarding its requirements and promises as
little as it wants. Therefore, this rule applies only to inherited members because
existing code will have a contract with only those members.
Polymorphism
The best and most concise definition I've heard for polymorphism is that it is
functionality that allows old code to call new code. This is arguably the biggest
benefit of object-oriented programming because it allows you to extend or enhance
your system without modifying or breaking existing code.
Let's say you write a method that needs to iterate through a collection of Employee
objects, calling each object's CalculatePay method. That works fine when your
company has one employee type because you can then insert the exact object type
into the collection. However, what happens when you start hiring other employee
types? For example, if you have a class called Employee and it implements the
functionality of a salaried employee, what do you do when you start hiring contract
employees whose salaries have to be computed differently? Well, in a procedural
language, you would modify the function to handle the new employee type, since old
code can't possibly know how to handle new code. An object-oriented solution
handles differences like this through polymorphism.-
Using our example, you would define a base class called Employee. You then define a
derived class for each employee type (as we've seen previously). Each derived
employee class would then have its own implementation of the CalculatePay method.
Here's where the magic occurs. With polymorphism, when you have an upcasted
pointer to an object and you call that object's method, the language's runtime will
ensure that the correct version of the method is called. Here's the code to illustrate
what I'm talking about: -
using System;
class Employee
{
public Employee(string firstName, string lastName,
int age, double payRate)
{
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.payRate = payRate;
}
class PolyApp
{
protected Employee[] employees;
Console.WriteLine("\n");
}
app.LoadEmployees();
app.CalculatePay();
}
}
Compiling and running this application will yield the following results: -
c:\>PolyApp
Loading employees...
SalariedEmployee.CalculatePay
ContractorEmployee.CalculatePay
HourlyEmployee.CalculatePay
Note that polymorphism provides at least two benefits. First, it gives you the ability
to group objects that have a common base class and treat them consistently. In the
example above, although technically I have three different object types-
SalariedEmployee, ContractorEmployee,and HourlyEmployee-I can treat them all as
Employee objects because they all derive from the Employee base class. This is how
I can stuff them in an array that is defined as an array of Employee objects. Because
of polymorphism, when I call one of those object's methods, the runtime will ensure
that the correct derived object's method is called.
The second advantage is the one I mentioned at the beginning of this section: old
code can use new code. Notice that the PolyApp.CalculatePay method iterates
through its member array of Employee objects. Because this method extracts the
objects as implicitly upcasted Employee objects and the runtime's implementation of
polymorphism ensures that the correct derived class's method is called, I can add
other derived employee types to the system, insert them into the Employee object
array, and all my code continues working without me having to change any of my
original code! –
Summary
This chapter has taken you on a whirlwind tour through terminology and concepts
that fall under the umbrella of object-oriented programming. A full discourse on the
subject would require several chapters and would, therefore, take away from the
focus of this book. However, a firm grasp of object-oriented fundamentals is
imperative to getting the most out of the C# language.