The formal name for a structural relationship that exists between objects is an association. With respect to the Student Registration System (SRS), some sample associations might be as follows:
A Student is enrolled in a Course.
A Professor teaches a Course.
A DegreeProgram requires a Course.
Whereas an association refers to a relationship between classes, the term link can be used to refer to a structural relationship that exists between two specific objects (instances). Given the association "a Student is enrolled in a Course," we might have the following links:
Jackson Palmer (a particular Student object) is enrolled in Math 101 (a particular Course object).
Helmut Schmidt (a particular Student object) is enrolled in Basketweaving 972 (a particular Course object).
Mary Smith (a particular Student object) is enrolled in Basketweaving 972 (a particular Course object; as it turns out, the same Course object that Fred Schnurd is linked to).
Given any Student object X and any Course object Y, there is the potential for a link of type "is enrolled in" to exist between those two objects precisely because there is an "is enrolled in" association defined between the two classes that those objects belong to. In other words, associations enable links.
Most of the time, we define associations between two different classes; such associations are known as binary associations. The "is enrolled in" association, for example, is a binary association because it interrelates two different classes: Student and Course. A unary, or reflexive, association, on the other hand, is between two instances of the same class; for example:
Even though the two classes at either end of a reflexive association are the same, the objects are typically different instances of that class:
Math 101 (a Course object) is a prerequisite for Math 202 (a different Course object).
Professor Gupta (a Professor object) supervises Professors Jones and Green (other Professor objects).
And so forth. However, although somewhat rare, there can be situations in which the same object can serve in both roles of a reflexive relationship.
Higher-order associations are possible, but rare. A ternary association involves three classes; for example, a Student takes a Course from a particular Professor, as shown in Figure 5-1.
When describing associations, however, we usually decompose higher-order associations into an appropriate number of binary associations. We can, for example, represent the preceding three-way association as three binary associations instead (see Figure 5-2):
A Student attends a Course.
A Professor teaches a Course.
A Professor instructs a Student.
Within a given association, each participant class is said to have a role. In the instructs association (a Professor instructs a Student), the role of the Professor might be said to be instructor, and the role of the Student might be said to be instructee. We bother to assign names to the roles at either end of an association only if it helps to clarify the model. In the attends association (a Student attends a Course), there is probably no need to invent role names for the Student and Course ends of the association because they wouldn't add significantly to the clarity of the abstraction of which this association is a part.
For a given association type X between classes A and B, the term multiplicity refers to the number of objects of type A that may be associated with a given instance of type B. For example, a Student attends multiple Courses, but a Student has only one Professor in the role of advisor.
There are three basic categories of multiplicity, which we'll take a closer look at next: one-to-one, one-to-many, and many-to-many.
Exactly one instance of class A is related to exactly one instance of class B, no fewer, no more, and vice versa. For example:
A Student has exactly one Transcript (a multiplicity of one), and a Transcript belongs to exactly one Student.
A Professor chairs exactly one Department, and a Department has exactly one Professor in the role of chairperson.
We can further constrain an association by stating whether the participation of the class at either end is optional or mandatory. For example, we can change the preceding association to read as follows:
A given Professor might chair exactly one Department, but it is mandatory that a Department has exactly one Professor in the role of chairperson.
This revised version of the association is a more realistic portrayal of real-world circumstances than the previous version because although every department in a university typically does indeed have a chairperson, not every professor is a chairperson of a department—there aren't enough departments to go around! However, it's true that if a professor happens to be a chairperson of a department, the professor is a chairperson of only one department.
For a given single instance of class A, there can be many instances of class B related to it in a particular fashion; but from the perspective of an object of type B, there can only be one instance of class A that is so related. For example:
A Department employs many Professors (a multiplicity of many), but a Professor (usually) works for exactly one Department (a multiplicity of one).
A Professor advises many Students, but a given Student has exactly one Professor as an advisor.
Note that "many" in this case can be interpreted as either "zero or more (optional)" or as "one or more (mandatory)." To be a bit more specific, we can refine the previous one-to-many associations as follows:
A Department employs one or more ("many"; mandatory) Professors, but a Professor (usually) works for exactly one Department.
A Professor advises zero or more ("many"; optional) Students, but a given Student has exactly one Professor as an advisor.
In addition, as with one-to-one relationships, the "one" end of a one-to-many association may also be designated as mandatory or as optional. We may, for example, wish to "fine-tune" the previous association as follows, if we are modeling a university setting in which students aren't required to select an advisor:
A Professor advises many (zero or more; optional) students, but a given Student may optionally have at most one advisor.
For a given single instance of class A, there can be many instances of class B related to it, and vice versa. For example:
A Student enrolls in many Courses, and a Course has many Students enrolled in it.
A given Course can have many prerequisite Courses, and a given Course can in turn be a prerequisite for many other Courses. (This is an example of a many-to-many reflexive association.)
As with one-to-many associations, "many" can be interpreted as zero or more (optional) or as one or more (mandatory); for example:
A Student enrolls in zero or more ("many"; optional) Courses, and a Course has one or more ("many"; mandatory) Students enrolled in it.
Of course, the validity of a particular association—the classes that are involved, its multiplicity, and the optional or mandatory nature of participation in the association—is wholly dependent on the real-world circumstances being modeled. If you were modeling a university in which departments could have more than one chairperson or where students could have more than one advisor, your choice of multiplicities would differ from those used in our preceding examples.
Note that the concept of multiplicity pertains to associations, but not to links. Links always exist in a pairwise fashion between two objects (or, in rare cases, between an object and itself). Therefore, multiplicity in essence defines how many links of a certain association type can originate from a given object. This is best illustrated with an example.
Consider once again the many-to-many association:
"A Student enrolls in zero or more Courses, and a Course has one or more Students enrolled in it."
A specific Student object X can have zero, one, or more links to Course objects, but any one of those links is between exactly two objects—Student X and a single Course object. In Figure 5-3, for example:
Student X has one link (to Course A).
Student Y has four links (to Courses A, B, C, and D).
Student Z has no links to any Course objects whatsoever (Z is taking the semester off!).
Conversely, a specific Course object A must have one or more links to Student objects to satisfy the mandatory nature and multiplicity of the association, but again, any one of those links is between exactly two objects (a binary linkage): Course A and a single Student object. In Figure 5-2, for example:
Course A has two links (to Students X and Y).
Courses B, C, and D each have one link (to the same Student, Y).
Note, however, that once again every link is between precisely two objects: a Student and a Course. This example scenario does indeed uphold the many-to-many "is enrolled in" association between Student and Course; it's but one of a vast number of possible scenarios that may exist between the classes in question.
Just to make sure that this concept is clear, let's look at another example, this time using the one-to-one association.
"A Professor optionally chairs exactly one Department, and it is mandatory that a Department has exactly one Professor in the role of chairman. As illustrated in Figure 5-4:
Professor objects 1 and 4 each have one link—to Department objects A and B, respectively.
Professor objects 2 and 3 have no such links.
Moreover, from the Department objects' perspective, each Department does indeed have exactly one link to a Professor. Therefore, this example upholds the one-to-one "chairs" association between Professor and Department, while further illustrating the optional nature of the Professor class's participation in such links. Again, it's but one of a number of possible scenarios that may exist between the classes in question.
Aggregation is a special form of association, alternatively referred to as the "consists of," "is composed of," or "has a" relationship. Like an association, an aggregation is used to represent a relationship between two classes A and B. But with an aggregation, we're representing more than mere relationship: we're stating that an object belonging to a class A, known as an aggregate class, is composed of, or contains, one or more component objects belonging to a class B.
For example, a car is composed of an engine, a transmission, four wheels, and so on; so if Car, Engine, Transmission, and Wheel were all classes, we could form the following aggregations:
A Car contains one Engine.
A Car contains one Transmission.
A Car is composed of many (in this case, four) Wheels.
Or, as an example related to the SRS, we can say that
A University is composed of many Schools (the School of Engineering, the School of Law, etc.).
A School is composed of many Departments.
One wouldn't typically say, however, that a Department is composed of many Professors; instead, we'd probably state that a Department employs many Professors.
Note that these aggregation statements appear awfully similar to associations, where the name of the association just so happens to be "is composed of" or "contains." That's because an aggregation is an association! So why the fuss over trying to differentiate between aggregation and association? Do we even need to recognize that there is such a thing as an aggregation? It turns out that there are some subtle differences between aggregation and association that affect how an abstraction is rendered in code. Therefore, we'll defer further discussion of aggregation for now, but will return to discuss these subtleties in Chapter 14.
For now, use this simple rule of thumb: when you detect a relationship between two classes A and B, and the name you're inclined to give that association implies containment—"contains," "is composed of," "comprises," "consists of," and so forth—then it's probably really an aggregation that you're dealing with.
Let's assume that we've accurately and thoroughly modeled all the essential features of students via our Student class, and that we've actually programmed the class in C# (as you'll learn to do in Part Three). A simplified version of the Student class is shown here:
using System; public class Student { private string name; private string studentId; // etc. public string Name { get { return name; } set { name = value; } } public string StudentId { get { return studentId; } set {
studentId = value; } } // etc. }
In fact, let's further assume that our Student class code has been rigorously tested, found to be bug-free, and is actually being used in a number of applications: our SRS, for example, as well as perhaps a student billing system and an alumni relations system for the same university.
A new requirement has just arisen for modeling graduate students as a special type of student. As it turns out, the only features of a graduate student that we need to track above and beyond those that we've already modeled for a "generic student" are
What undergraduate degree the student previously received before entering their graduate program of study
What institution they received the undergraduate degree from
All the other features necessary to describe a graduate student—fields name, studentId, and so forth, along with properties to access them—are the same as those that we've already programmed for the Student class because a graduate student is a student, after all.
How might we approach this new requirement for a GraduateStudent class? If we weren't well-versed in object-oriented concepts, we might try one of the following approaches:
Modify the Student class to do "double duty."
"Clone" the Student class.
Use Inheritance to extend the existing Student class.
Let's talk about each of the three possible approaches in more detail.
We could add fields to reflect undergraduate degree information to our definition of a Student, along with properties to access them, and simply leave these fields empty when they are nonapplicable; that is, for an undergraduate student who hadn't yet graduated:
public class Student { private string name; private string studentId; private string undergraduateDegree; private string undergraduateInstitution; // etc.
Then, to keep track of whether these fields were supposed to contain values or not for a given Student object, we'd probably also want to add a bool field to note whether a particular student is a graduate student:
public class Student { private string name; private string studentId; private string undergraduateDegree; private string undergraduateInstitution; private bool isGraduateStudent; // etc.
In any new methods that we subsequently write for this class, we'll have to take the value of this bool field into account:
public void DisplayAllFields() { Console.WriteLine(name); Console.WriteLine(studentId); // If a particular student is NOT a graduate student, then the // values of the fields "'undergraduateDegree" and // "undergraduateInstitution" would be undefined, and so we would // only wish to print them if we are dealing with a graduate // student. if (isGraduateStudent) { Console.WriteLine(undergraduateDegree); Console.WriteLine(undergraduateInstitution); } // etc. }
This results in convoluted code, which is difficult to debug and maintain.
We could instead create a new GraduateStudent class by (a) making a duplicate copy of the Student class, (b) renaming the copy to be the GraduateStudent class, and (c) adding the extra features required of a graduate student to the copy.
This would be awfully inefficient because we'd then have much of the same code in two places, and if we wanted to change how a particular method worked or how a field was defined later on—say, a change of the type of the birthDate field from string to DateTime, with a corresponding change to the properties for that field—then we'd have to make the same changes in both classes.
Strictly speaking, either of the preceding two approaches would work, but the inherent redundancy in the code would make the application difficult to maintain. In addition, where these approaches both really break down is when we have to add a third, a fourth, or a fifth type of "special" student. For example, consider how complicated the DisplayAllFields method introduced in approach #1 would become if we wanted to use it to represent a third type of student: namely, continuing education students, who don't seek a degree, but instead are just taking courses for continuing professional enrichment.
We'd most likely need to add yet another bool flag to keep track of whether or not a degree was being sought:
public class Student { private string name; private string studentId; private string undergraduateDegree; private string undergraduateInstitution; private string degreeSought; private bool isGraduateStudent; private bool seekingDegree; // etc. // We'd also have to now take the value of this bool field // into account in the DisplayAllFields method: public void DisplayAllFields() { Console.WriteLine(name); Console.WriteLine(studentId); if (isGraduateStudent) { Console.WriteLine(undergraduateDegree); Console.WriteLine(undergraduateInstitution); } // If a particular student is NOT seeking a degree, then the value // of the field 'degreeSought' would be undefined, and so we // would only wish to print it if we are dealing with a degree- // seeking student. if (seekingDegree) { Console.WriteLine(degreeSought); } else { Console.WriteLine("NONE"); } // etc. }
This worsens the complexity issue!
We've had to introduce a lot of complexity in the logic of this one method to handle the various types of student; think of how much more "spaghetti-like" the code might become if we had dozens of different student types to accommodate! Unfortunately, with non-OO languages, these convoluted approaches would typically be our only options for handling the requirement for a new type of object. It's no wonder that applications have become so complicated and expensive to maintain as requirements inevitably evolve over time!
Fortunately, we do have yet another alternative!
With an object-oriented programming language (OOPL), we can solve this problem by taking advantage of inheritance, a powerful mechanism for defining a new class by stating only the differences (in terms of members) between the new class and another class that we've already established. Using inheritance, we can declare a new class named GraduateStudent that inherits all of the members of the Student class. The GraduateStudent class would then only have to take care of the two extra fields associated with a graduate student: undergraduateDegree and undergraduateInstitution. Inheritance is indicated in a C# class declaration using a colon followed by the name of the base class being extended.
public class GraduateStudent : Student { // Declare two new fields above and beyond // what the Student class declares... private string undergraduateDegree; private string undergraduateInstitution; //...and properties for each of these new fields. public string UndergraduateDegree { get { return undergraduateDegree; } set { undergraduateDegree = value; } } public string UndergraduateInstitution { get { return undergraduateInstitution; } set { undergraduateInstitution = value; } } }
That's all we need to declare in our new GraduateStudent class: two fields plus their associated properties! There is no need to duplicate any of the members of the Student class because we're automatically inheriting them. It's as if we had "borrowed" the code for the fields, properties, and methods from the Student class, and inserted it into GraduateStudent, but without the fuss of actually having done so.
When we take advantage of inheritance, the original class that we're starting from—Student, in this case—is called the base class. The new class—GraduateStudent—is called a derived class. A derived class is said to extend a base class. An alternative terminology is to say that a subclass inherits from (or extends) a superclass.
Inheritance is often referred to as the "is a" relationship between two classes because if a class B (GraduateStudent) is derived from a class A (Student), then B truly is a special case of A. Anything that we can say about a base class must also be true about all of its derived classes; that is:
A Student attends classes, so a GraduateStudent attends classes.
A Student has an advisor, so a GraduateStudent has an advisor.
A Student pursues a degree, so a GraduateStudent pursues a degree.
In fact, an "acid test" for the legitimate use of inheritance is as follows: if there is something that can be said about a base class A that can't be said about a proposed derived class B, then B really isn't a valid derived class of A.
Note, however, that the converse isn't true: because a derived class is a special case of its base class, it's possible to say things about the derived class that can't be said about the base class; for example:
A GraduateStudent has already attended an undergraduate institution, whereas a "general-purpose" Student might not have done so.
A GraduateStudent has already received an undergraduate degree, whereas a "general-purpose" Student might not have done so.
Because derived classes are special cases of their base classes, the term specialization is used to refer to the process of deriving one class from another. Generalization, on the other hand, is a term used to refer to the opposite process: namely, recognizing the common features of several existing classes and creating a new common base class for them all. Let's say we now wish to create the Professor class. Students and Professors have some fields in common (such as name, birthDate, and so on), and the methods that manipulate them. Yet they each have unique fields as well; the Professor class might require the fields title (a string) and worksFor (a reference to a Department), while the Student class's studentID, degreeSought, and majorField fields are irrelevant for a Professor. Because each class has fields that the other would find useless, neither class can be derived from the other. Nonetheless, to duplicate their shared field declarations and method code in two places would be horribly inefficient.
In such a circumstance, we may want to invent a new base class called Person, consolidate the member common to both Students and Professors in that class, and then have Student and Professor inherit these common members from Person. The resultant code in this situation appears here:
// Defining the base class: public class Person { // Fields common to Students and Professors. private string name; // See note about use of private accessibility private string address; // with inheritance after this code example. private string birthDate;
// Common properties. public string Name { get { return name; } set { name = value; } } // etc. } // Defining one derived class of Person... public class Student : Person { // Fields specific only to a Student. private string studentId; private string majorField; private string degreeSought; // Student-specific properties. public string StudentId { get { return studentId; } set { studentId = value; } } // etc. } //...and another derived class of Person! public class Professor : Person { // Fields specific only to a Professor. private string title; private Department worksFor; // Professor-specific properties. public string Title { get { return title; } set {
title = value; } } // etc. }
NOTE
You'll learn in Chapter 13 that there are a few extra complexities about inheriting private members, and how the "protected" accessibility level comes into play, which we aren't tackling just yet because we haven't covered enough ground to do them justice at this point.
Inheritance is perhaps one of the most powerful and unique aspects of an OOPL because
Derived classes are much more succinct than they would be without inheritance. Derived classes contain only the "essence" of what makes them different from their base classes. We know from looking at the GraduateStudent class definition, for example, that a graduate student is "a student who already holds an undergraduate degree from an educational institution." As a result, the total body of code for a given application is significantly reduced as compared with the traditional non-OO approach to developing the same application.
Through inheritance, we can reuse and extend code that has already been thoroughly tested without modifying it. As we saw, we were able to invent a new class—GraduateStudent—without disturbing the Student class code in any way. So, we can rest assured that any client code that relies on instantiating Student objects and invoking methods on them will be unaffected by the creation of derived class GraduateStudent, and thus we avoid having to retest huge portions of our existing application. (Had we used a non-OO approach of "tinkering" with the Student class code to try to accommodate graduate student fields, we would have had to retest our entire existing application to make sure that nothing had broken!)
Best of all, you can derive a new derived class from an existing class even if you don't own the source code for the latter! As long as you have the compiled version of a class, the inheritance mechanism works just fine; you don't need the original source code of a class in order to extend it. This is one of the most useful ways to achieve productivity with an OOPL: find a class (either written by someone else or one that is built into the language) that does much of what you need and create a derived class of that class, adding just those members that you need for your own purposes; or buy a third-party library of classes written by someone else, and do the same.
Finally, as you saw in Chapter 2, classification is the natural way that humans organize information; so, it only makes sense that we'd organize our software along the same lines, making it much more intuitive and hence easier to maintain and extend.
Inheritance is a powerful concept that is used throughout OO programming, but the coupling between base and derived classes can make it tricky to make any modifications to the base class because any changes made to the base class will "trickle down" to any and all derived classes. Potential problems can be avoided or minimized with careful design of a class hierarchy (a concept discussed in the next section and in more detail in Part Two of this book).
Over time, we build up an inverted tree of classes that are interrelated through inheritance; such a tree is called a class hierarchy. One such class hierarchy example is shown in Figure 5-5.
A bit of nomenclature:
Any given node in the hierarchy is said to be derived (directly or indirectly) from all the nodes above it in the hierarchy, known collectively as its ancestors.
The ancestor that is immediately above a given node in the hierarchy is considered to be that node's direct base class.
Conversely, all nodes below a given node in the hierarchy are said to be its descendants.
The node that sits at the top of the hierarchy is referred to as the root node.
A terminal, or leaf, node is one that has no descendants.
Two nodes that are derived from the same direct base class are known as siblings.
Note that arrows are used to point upward from each derived class to its direct base class.
Applying this terminology to the example hierarchy in Figure 5-5:
Class A (Person) is the root node of the entire hierarchy.
Classes B, C, D, E, and F are all said to be derived from class A, and are thus descendants of A.
Classes D, E, and F can be said to be derived from class B.
Classes D, E, and F are siblings; so are classes B and C.
Class D has two ancestors: A and B.
Classes C, D, E, and F are terminal nodes, in that they don't have any classes derived from them (as of yet, at any rate).
NOTE
In the C# language, the Object class (of the System namespace) serves as the ultimate base class for all other types, both user-defined as well as those built into the language. We'll talk about the Object class in more depth in Part Three of the book.
As with any hierarchy, this one may evolve over time:
It may widen with the addition of new siblings/branches in the tree.
It may expand downward as a result of future specialization.
It may expand upward as a result of future generalization.
Such changes to the hierarchy are made as new requirements emerge, or as our understanding of the existing requirements improves. For example, we may determine the need for MastersStudent and PhDStudent classes (as specializations of GraduateStudent) or of an Administrator class as a sibling to Student and Professor. This would yield the revised hierarchy shown in Figure 5-6.
As you've seen earlier in this chapter, association and aggregation (as a special form of association) are object relationships because they are implemented as object references. Two different objects are linked to one another by virtue of the existence of an association between their respective classes. Inheritance, on the other hand, is a class relationship because it's defined statically (that is, before the program is compiled) in the class definition. With inheritance, an object is simultaneously an instance of a derived class and all its base classes: a GraduateStudent is a Student that is a Person, all wrapped into one!
So, in looking once again at the hierarchy shown in Figure 5-6, we see that
All classes in the Person class hierarchy—Student, Professor, Graduate, and so on—share the qualities of, and are compatible with, the Person class. An array that is declared to hold Person references, for instance, can hold references to Professor, Continuing, and Student objects as well as Person references.
Subhierarchies work in the same manner; the Undergrad, GraduateStudent, and PhDStudent classes, for example, share the qualities of, and are compatible with, the Student class.
This notion of an object sharing the qualities of its base classes is a significant one that we'll revisit again and again throughout the book.
Once a class hierarchy is established and an application has been coded, changes to nonleaf classes (that is, those classes that have descendants) will introduce "ripple effects" down the hierarchy. For example, if after we establish the GraduateStudent class we go back and add a minorField field to the Student class, then GraduateStudent will inherit this new field once it has been recompiled. Perhaps this is what we want; on the other hand, we may not have anticipated the derivation of a GraduateStudent class when we first conceived of Student, and so this may not be what we want!
In an ideal world, developers of the Student class would speak with the developers of all derived classes—GraduateStudent, MastersStudent, and PhDStudent—to obtain their approval for any proposed changes to Student. But this isn't an ideal world, and often we may not even know that our class has been extended if, for example, our code is being distributed and reused on other projects or is being sold to clients. This evokes a general rule of thumb:
Whenever possible, avoid adding publicly accessible members to nonleaf classes once they have been established in code in an application to avoid "ripple effects" throughout an inheritance hierarchy.
This is easier said than done! However, it reinforces the importance of spending as much time as possible on requirements analysis before diving into the coding stage of an application development project. This won't prevent new requirements from emerging over time, but we should avoid oversights regarding the current requirements.