Chapter 3. Generalization, Inheritance, Genericity, and Polymorphism

So doth the greater glory dim the less: A substitute shines brightly as a king Until a king be by, and then his state Empties itself, as doth an inland brook Into the main of waters.

—Shakespeare, The Merchant of Venice

The basic operations of a Turing machine may be quite general in application and a boon to hardware developers, but it is extremely tedious to construct a program using only those basic operations. Most of the major advances related to languages and modeling have been directed at substituting abstract constructs that are more succinct for common combinations of Turing instructions.

That militant substitution began when BAL substituted instruction mnemonics for particular combinations of 1s and 0s. It gained steam as 3GLs added procedures, block structures, iteration formalisms, I/O constructs, and a host of other abstractions. Then graphical notations for high-level design concepts substituted simple bubbles and arrows that often represented collections of thousands of Turing instructions.

A primary contribution of the OO paradigm has been to formalize and standardize high-level abstraction, especially problem space abstractions, as spartan but rigorous substitutes for large collections of Turing instructions. Four important OO constructs are generalization, inheritance, genericity, and polymorphism.

Many people regard generalization, inheritance, and polymorphism as the hallmark characteristics of object orientation, which is why an entire chapter is devoted to them. Another view is that they are actually just mechanisms, albeit elegant, that implement the more fundamental OO ideas described in Chapter 2.

Generalization

Generalization is a very basic application of set theory. Recall that in Chapter 2 we defined a class as a set of entities that have the same properties. Set theory has the notion of subsets, as shown in Figure 3-1. The figure is called a Venn diagram, and the dots represent members of the set while the solid lines indicate the boundaries of set. Figure 3-1 represents the Venn diagram for a bank’s accounts. The dots represent individual bank accounts that are owned by the bank’s customers. In this case, all bank accounts must be either checking accounts or savings accounts, but not both. The boxes represent the boundaries of various sets of bank accounts: The outer box is the set of all of the bank’s accounts, called a superset, while the inner boxes bound subsets of those accounts for Savings and Checking. We could add other subsets, say, for IRA accounts.

Figure 3-1. Example of a Venn diagram for checking accounts in a bank. The overall set of all accounts can be subdivided into distinct groups of specialized accounts for checking and savings.

image

Each dot represents a uniquely identifiable member of every set whose boundary surrounds the dot. In this example, accounts are uniquely identifiable by a combination of the account’s owner and the type of account. Note that it is possible for both a checking account and a savings account to be owned by a single customer, so we can’t identify them uniquely just by owner.

If we look at a given dot in, say, subset Checking, that dot is also contained in the overall Account set. So we say that the entity is a member of set Checking and that it is a member of set Account. If we tidy up the terminology by using “A” as a shorthand for “member of Set A” and using “B” as a shorthand for “member of Set B,” we can then say that a B is an A since there is only one dot and it is contained in both sets. You will see references in the OOA/D literature indicating that generalization is an “is-a” relation. This is exactly what they mean. In a Venn diagram, any given dot represents a single entity that is contained in multiple sets. So we can say that entity is a B and that entity is an A.

We also mentioned in Chapter 2 that classes are defined in terms of a set of responsibilities. In a Venn diagram we can label the individual sets easily, as in the diagram, but it gets messy to try to also enumerate the particular responsibilities that define a particular set. In the OO paradigm we care a great deal about those responsibilities since we need them to solve the problem in hand. So we need a different notation for the Venn diagram, such as in Figure 3-2. Each box refers to one of the same sets from Figure 3-1, but instead of showing entity dots we enumerate the properties that define the set.

Figure 3-2. OO generalization of subsets of Account having different properties that define each set. The properties of Account are shared by every member while the Checking subset defines a subset of Account members that also have the property “overdraft.” Note that the Savings subset contains all the members of Account that do not have any other properties.

image

The line connecting the classes indicates that a generalization is being shown, where set Account is the full class (known as a root superclass) while the sets Checking, Savings, and IRA are subset classes that contain only a subset of members of set Account. The dots are not necessary because the special connector tells us that we are really looking at a Venn diagram, and we can infer that every member of a subclass is also a member of its parent superclass.

Note that the properties we have indicated define set Account. How do we reconcile them with the properties we used to define the subsets? That’s fairly straightforward. Every entity in the generalization (Venn diagram in tree form) is a member of set Account. Since since set Account is defined by the properties all of its members share, then all entities in set Account must have those properties. Similarly, the members of set Checking must have the properties that set Checking defines as shared in addition to the properties defined for membership in set Account. Since a member of set Checking is a member of set Account, it must have the all the properties defined for both sets.

The term generalization comes from the notion that set Account defines general or common properties for all of the entities in all descendant subsets. Conversely, another common term for the relation in OO contexts is specialization. Specialization captures the idea that a subset of the set Account have unique, specialized properties that not all members of the Account set have. You now know everything there is to know about OO generalization; it is a very simple organization of sets and subsets where the UML representation is just a Venn diagram in tree form.

All of the trickiness around generalization that you will see in the OOA/D literature deals with how we interpret generalization during problem space abstraction.1 For example, you might have noticed that there was no mention of the word object in this explanation. That’s because the generalization must exist for entities in the problem space before it can be abstracted into an OO solution. A common OOA mistake is to use generalization in the solution simply because it is elegant and makes life easier for the modeler. We will revisit this topic in great detail in Chapter 12.

Inheritance

If you thought generalization was simple, you are going to love inheritance. Inheritance is both one of the simplest OO concepts and one of the most overanalyzed. It is simple because it is just a suite of rules for resolving object properties in a generalization. In fact, in MBD it is a single, very simple rule that we already mentioned in passing:

The properties of an object member of a leaf subclass are the union of the properties of its class and the properties of every superclass in a direct line of ascent.

That’s it, folks.2 Not exactly a mind bender, and it is intuitively obvious to anyone who recognizes that classes are just sets of objects. We resolve subset properties exactly the way we would do it in a Venn diagram, and the OO paradigm has just renamed that technique inheritance.

Generalization, inheritance, and polymorphism are three entirely different things.

The confusion arises when authors start to talk about inheritance as if it were synonymous with generalization and polymorphism. As we shall see shortly, a special form of polymorphism is enabled by the combination of generalization and inheritance. If you keep these features separate in your mind, the paradigm will become a lot more intuitive.

Polymorphism

Polymorphism is a somewhat more complex topic than generalization and inheritance. Before getting into what it is, we need to dispel a common error in discussions of OO polymorphism. On online forums and even in the literature you will find people who refer to polymorphism as the substitution of implementations. While technically correct for some situations at the OOP level, this is not at all true for OOA/D.

A classic example of implementation substitution is a responsibility to sort an array. We can substitute a wide variety of sort algorithms (e.g., insertion, quicksort, etc.) for the implementation of the responsibility without affecting the results at all—the array will always come out in the designated order. That is, the observable results are unchanged when the implementations are substituted, which is because we employ encapsulation behind an interface expressly to hide the implementation, and we define the DbC contract in a manner that depends solely on the notion of ordering of a set.

OO polymorphism in generalizations is about substituting behaviors.

For example, consider the concept of Prey with a responsibility to respond when attacked. If the Prey in hand happens to be a Pheasant, its behavior when attacked will be to take flight. If the Prey in hand happens to be a Rodent, the behavior when attacked might be to scurry down the nearest hole in the ground. If the Prey in hand happens to be a Gazelle, its behavior response might be to bound away. These are quite different behaviors by any reasonable definition of behavior, and they have quite different observable results. But through the magic of OO polymorphism we can sometimes substitute them freely.

The trick lies in our notion of how our Prey entity collaborates with other entities like predators. Consider a lion feeding on a kill when some sort of prey critter wanders too close. The lion might growl or do a mock charge to drive the critter off. The lion seems to be attacking, but its goal is simply to make the interloper go away. From the lion’s perspective, it does not care how the critter goes away; it just cares that it goes away. So the lion is actually indifferent to responses of taking flight, scurrying down a hole, or bounding away when the lion sends the attack message. The lion’s DbC contract with the prey critter can be expressed solely in terms of the prey critter leaving the immediate scene.

In effect we have raised the level of abstraction about what the DbC contract between the lion and the prey critter is so that the details of specific prey behaviors do not matter. But from the prey critter’s perspective, each one actually responds in a very different way with different observable results.

Another key element is the separation of message and method. The lion sends an I’m attacking message to the prey. The lion sends that same message regardless of which particular flavor of prey critter happens to be there. So long as each flavor of prey critter responds to that message and leaves town in some fashion, the lion is satisfied.

The key ideas are

  1. The collaboration is at a high enough level of abstraction that differences in the observable results of potential behaviors are not important to the client.
  2. The DbC contract with the client is defined consistently with the collaboration’s level of abstraction.
  3. A mechanism exists to substitute different behaviors that satisfy (2) based on solution context.

This capability for substituting behavior in different solution contexts is a very powerful tool. However, as we shall see later in Chapter 12, we have to be quite careful how we use it lest we have Skyscrapers suddenly appear in our Pristine Landscapes. Managing that is tied to ensuring that the differences in observable results are acceptable in all collaboration contexts.3

OO polymorphism does this by combining abstraction and encapsulation in a very elegant way. First, the semantics of the behavior are generalized through abstraction. This enables unique but similar behaviors to be viewed at a higher, more general level of abstraction where the details that differentiate the implementations are no longer important—a perspective where they all look the same.

Second, polymorphism takes advantage of the encapsulation interfaces that hide implementations to provide access to those implementations in a uniform way. Specifically, polymorphism depends upon the existence of a common interface for all of the substitutable behaviors. This enables the client to access each behavior in exactly the same way. This provides a degree of anonymity for the specific behavior from the client’s view; the client doesn’t have to know which particular behavior will be accessed. What they don’t know about, they cannot depend upon, and we have effective decoupling.

The OO paradigm explicitly supports several ways to provide behavior substitution:

Ad hoc polymorphism. Multiple behaviors are provided within a single responsibility, and one of those behaviors is selected at runtime when the method is invoked. This was the most common form of polymorphism in procedural applications, and it was manifested in “switch” statements within procedures where each “case” of the switch held a unique behavior. We can do the same thing within individual OO methods.

Inclusion polymorphism (aka inherent polymorphism). Different behavior responses can be bound to a single message dynamically at runtime. This is the most common form of polymorphism seen at the OOPL level, and it is enabled by generalization. Essentially, the superclass provides an interface shared by the subclasses, but members of different subclasses provide their own unique behaviors. The Prey example demonstrates the inclusion polymorphism mechanism: The lion’s messages are sent to the Prey superclass, and the individual subclass in hand provides a specific behavior such as takeFlight.

Overload polymorphism. Substitution is achieved by overloading the same name with different behaviors. This most commonly appears in an OOP context when arithmetic or logical operators have their mechanisms substituted based upon the context of the objects to which they are applied. This is explicitly defined in the OOPLs, but it is usually implicitly defined at the OOA/D level. That is, in OOA/D it is assumed that fundamental operations on attribute ADTs are appropriate for the ADTs and will be implemented accordingly during OOP.

Parametric polymorphism (aka genericity). This polymorphism occurs when behavior substitution is parameterized. That is, observable results are modified based on the values of state variables. This differs from ad hoc polymorphism in that there is usually just a single generic behavior that is executed, but the results of that execution can be drastically different based upon state variable values that the behavior eats. This is the most important form of polymorphism at the OOA/D level because it enables us to encode invariants while relegating details to configuration data. We will talk about this extensively in Chapter 5.

Ad hoc polymorphism is relatively straightforward and intuitive for anyone who has programmed in a procedural language. Overload polymorphism isn’t of much interest to OOA/D because that view already abstracts the sorts of differences that overloading of names supports. We are going to concentrate here on the other two forms of polymorphism.

Inclusion (or Inherent) Polymorphism

Inclusion polymorphism is enabled by generalization. Conversely, it is inclusion polymorphism that adds power to OO generalization. Inclusion polymorphism employs superclasses to achieve both the necessary generality and the interface commonality. The generality is achieved through abstraction while the interface commonality is achieved through interface inheritance. By definition, the superclass represents the common characteristics of its subclasses. So, we collect similar but unique subclasses under a superclass, conceptually generalize their shared characteristics in the superclass, and then provide a common interface to those characteristics at the superclass level.

In Figure 3-2, the superclass class defines the basic responsibilities of Deposit, Withdrawal, and GetBalance. Each subclass implements the superclass’ responsibilities. But in doing so, the subclasses can provide quite different behaviors with different observable results. The incoming message to the superclass is then dispatched to the specific behavior provided by the subclass in hand. Unless we were in the banking business we might expect those behaviors to be exactly the same.

However, at the detailed behavior level there are issues like legal restrictions (e.g., early withdrawal penalties for IRAs), policy restrictions (e.g., different posting delays for deposits), rules restrictions (e.g., overdraft protection for Checking but not Savings), reporting requirements (e.g., the bank’s P&L and Balance Sheet), and lots of other stuff. This stuff inevitably results in different behaviors with different observable results because different business rules and policies apply for each subclass context.

Yet many clients have a much more basic view of Account characteristics like Deposit, Withdrawal, and GetBalance where those detailed differences are not relevant. It is the “Here’s a pile of money, deposit it and don’t bug me with the details of what that involves” sort of view. Inclusion polymorphism enables this by letting the client send a generic Deposit message to the Account superclass rather than the individual subclass.

Genericity

Genericity is one of those things that everybody uses without realizing it. Genericity is about substituting different behavior through parameterization. Essentially, we obtain quite different results depending upon the values (or presence) of input parameters to a single behavior responsibility. This idea has been around since Assembly macros, so we won’t spend a lot of time on it. In fact, it has another name—parametric polymorphism4—that suggests it is just a special case of polymorphism.

Any method where different behaviors are possible depending upon the values of the method’s arguments technically demonstrates genericity, so any method with an IF test of a parameter value could be deemed an example of genericity. However, most OO people would think of genericity in terms of significant behavior differences. It is left as an exercise for the student to come up with a good definition of significant in this context.

The unique thing that the OO paradigm brings to the table is expansion of the notion of what constitutes a parameter. The traditional view is that it is an input to a procedure. In an OO context the notion of parameter is expanded to include potentially any state variable that is accessible to a behavior method. Thus in the OO context, any knowledge attribute of any reachable object is potentially a parameter to a behavior responsibility.

This introduces a very powerful design pattern where we define generic behavior responsibilities in one object and relate that object to another object—a “specification object”—whose attributes parameterize the generic behavior. This enables us to instantiate the relationship between the objects dynamically at runtime. That is, we decide which specification object is the “right” one for the context dynamically. This is an enormously powerful technique, but it is so underutilized in today’s OO development that we devote an entire chapter, Chapter 5, to it.

..................Content has been hidden....................

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