Object-Oriented Concepts


Object-oriented programming is centered around six basic concepts:

  1. object

  2. message

  3. interface

  4. class

  5. inheritance

  6. polymorphism

People seem to have attached a wide range of meanings to these concepts, most of which are quite serviceable. We define some of these concepts perhaps a little more tightly than most people do because precision facilitates a better understanding of testing the concepts and eliminates some potential confusion about what needs to be tested. For example, while a distinction between operations and methods (or member functions) is not significant for most programmers, the distinction is significant to testers because the approach to testing an operation, which is part of a class specification and a way to manipulate an object, is somewhat different from testing a method, which is a piece of code that implements an operation. The distinction helps to differentiate the concerns of specification-based testing from the concerns of implementation-based testing.

We will review each of the basic object-oriented programming concepts and offer observations about them from a testing perspective. While we know that object-oriented programming languages support a variety of object-oriented programming models, we use the concepts as they are formulated for languages such as C++ and Java. Some of the variations between languages will affect the types of faults that are possible and the kinds of testing that are required. We try to note such differences throughout this book.

Object

An object is an operational entity that encapsulates both specific data values and the code that manipulates those values. For example, the data about a specific bank account and the operations needed to manipulate that data form an object. Objects are the basic computational entities in an object-oriented program, which we characterize as a community of objects that collaborate to solve some problem. As a program executes, objects are created, modified, accessed, and/or destroyed as a result of collaborations. Within the context of a good object-oriented design, an object in a program is a representation of some specific entity in the problem or in its solution. The objects within the program have relationships that reflect the relationships of their counterparts in the problem domain. Within the context of Brickles, many objects can be identified, including a paddle, pucks, a brick pile containing bricks, the play field, the play field boundaries (walls, ceiling, and floor), and even a player. A puck object will encapsulate a variety of attributes, such as its size, shape, location on a play field (if it is in play), and current velocity. It also supports operations for movement and for the puck's disappearance after it hits the floor. In the program that implements Brickles, we would expect to find an object for each of the pucks for example, at the start of a Brickles match, we see the puck in play and any that are in reserve. When in play, a puck object will collaborate with other objects the play field, paddle, and brick pile to implement Brickles physics, which are described in the game description (see page 11).

Objects are the direct target of the testing process during software development. Whether an object behaves according to its specification and whether it interacts appropriately with collaborating objects in an executing program are the two major focuses of testing object-oriented software.

An object can be characterized by its life cycle. The life cycle for an object begins when it is created, proceeds through a series of states, and ends when the object is destroyed.

Definitional versus Operational Semantics of Objects

There is a bit of confusion among many of our clients and students with respect to the distinction between those aspects of object-oriented programming that are concerned with the definition of classes and interface and those that are concerned with the use of objects. We refer to these as the definitional and operational aspects of object-oriented programming.

Definitional: The class definition provides what might, at first glance appear to be the extreme point on the definitional end of the continuum; however, in some languages such as CLOS, the structure and content of this definition is defined by a metaclass. The metaclass approach may theoretically extend the continuum indefinitely in the definitional direction since it is possible to have a metaclass for any class, including metaclasses. The dynamic dimension represents the possibility in some languages, such as Java, to define classes during program execution.

Operational: The operational end of this continuum corresponds to the concept that an object is the basis of the actions taken in the system. An object provides the mechanisms needed to receive messages, dispatch methods, and return results. It also associates instance attributes with methods. This information may be on the static end of that dimension (as in a C++ object), or it may be more dynamic in the case of a CLOS object that contains arbitrary slots.


graphics/02in01.gif


We make the following observations about objects from a testing perspective.

  • An object encapsulates. This makes the complete definition of the object easy to identify, easy to pass around in the system, and easy to manipulate.

  • An object hides information. This sometimes makes changes to the object hard to observe, thereby making the checking of test results difficult.

  • An object has a state that persists for the life of the object. This state can become inconsistent and can be the source of incorrect behavior.

  • An object has a lifetime. The object can be examined at various points in that lifetime to determine whether it is in the appropriate state based on its lifetime. Construction of an object too late or destruction of it too early is a common source of failures.

In Chapter 6 we will describe a variety of techniques for testing the interactions among objects. We will address other aspects of testing objects in Chapter 5 and Chapter 7.

Message

A message[1] is a request that an operation be performed by some object. In addition to the name of an operation, a message can include values actual parameters that will be used to perform that operation. A receiver can return a value to the sender.

[1] In C++ terminology, a message is referred to as a member function call. Java programmers and Smalltalk programmers refer to messages as method invocations. We will use these terms in discussions of C++ and Java code, but we will use the more generic term message in language-independent discussions. Keep in mind that a member function call is distinct from a member function. A method invocation is distinct from a method.

An object-oriented program is a community of objects that collaborate to solve a problem. This collaboration is achieved by sending messages to one another. We call the object originating a message the sender and the object receiving the message the receiver. Some messages result in some form of reply such as a return value or an exception being sent from the receiver to the sender.

The execution of an object-oriented program typically begins with the instantiation of some objects, and then a message being sent to one of the objects. The receiver of that message will send messages to other objects or possibly even to itself to perform computations. In some event-driven environments, the environment will repeatedly send messages and wait for replies in response to external events such as mouse clicks and key presses.

We make the following observations about messages from a testing perspective.

  • A message has a sender. The sender determines when to send the message and may make an incorrect decision.

  • A message has a receiver. The receiver may not be ready for the specific message that it receives. The receiver may not take the correct action when receiving an unexpected message.

  • A message may include actual parameters. These parameters will be used and/or updated by the receiver while processing the message. Objects passed as parameters must be in correct states before (and after) the message is processed, and they must implement the interfaces expected by the receiver.

These issues are the primary focus of interaction testing in Chapter 6.

Interface

An interface is an aggregation of behavioral declarations. Behaviors are grouped together because they define actions related by a single concept. For example, an interface might describe a set of behaviors related to being a moving object (see Figure 2.1).

Figure 2.1. A Java declaration for a Movable interface

graphics/02fig01.gif

An interface is a building block for specifications. A specification defines the total set of public behaviors for a class (we will define this next). Java contains a syntactic construct interface that provides this capability and does not allow the declaration of any state variables. You can produce the same result in C++ by declaring an abstract base class with only public, pure virtual methods.

We make the following observations about interfaces from a testing perspective.

  • An interface encapsulates operation specifications. These specifications incrementally build the specifications of larger groupings such as classes. If the interface contains behaviors that do not belong with the other behaviors, then implementations of the interface will have unsatisfactory designs.

  • An interface has relationships with other interfaces and classes. An interface may be specified as the parameter type for a behavior to allow any implementer of that interface to be passed as a parameter.

We will use the term interface to describe a set of behavior declarations whether or not you use the interface syntax.

Class

A class is a set of objects that share a common conceptual basis. Many people characterize a class as a template a "cookie cutter" for creating objects. While we understand that characterization makes apparent the role of classes in writing object-oriented programs, we prefer to think of a class as a set. The class definition then is actually a definition of what members of the set look like. This is also better than definitions that define a class as a type since some object-oriented languages don't use the concept of a type.

Objects form the basic elements for executing object-oriented programs, while classes are the basic elements for defining object-oriented programs. Any concept to be represented in a program must be done by first defining a class and then creating objects defined by that class. The process of creating the objects is referred to as instantiation and the result is referred to as an instance. We will use instance and object interchangeably.

The conceptual basis common to all the objects in a class is expressed in terms of two parts:

  • A class specification is the declaration of what each of the objects in the class can do.

  • A class implementation is the definition of how each of the objects in the class do what they can do.

Consider a C++ definition for a class PuckSupply from Brickles. Figure 2.2 shows a C++ header file and Figure 2.3 shows a source file for such a class. The use of a header file and one or more source files is a typical way to structure a C++ class definition.[2] In the context of C++, a header file contains the class specification as a set of operations declared in the public area of a class declaration. Unfortunately, as part of the implementation, the private (and protected) data attributes, must also be defined in the header file.

[2] Java prescribes that specification and implementation physically be in the same file. Nonetheless, there is a logical separation between what an object does and how it does it.

Figure 2.2. A C++ header file for the class PuckSupply

graphics/02fig02.gif

Figure 2.3. A C++ source file for the class PuckSupply

graphics/02fig03.gif

To create or manipulate an object from another class, a segment of code only needs access to the specification for the class of that object. In C++, this is typically accomplished by using an include directive naming the header file for the object's class:

 #include "PuckSupply.h" 

This, of course, gives access to all the information needed to compile the code, but it provides more information than is necessary to design the interactions between classes. In Chapter 4 we will discuss problems that can arise from designers having a view into possible implementations of a class and how to detect these problems during reviews.

Classes as Objects

Object-oriented programming languages typically support, either explicitly or implicitly within the semantics of the language, the notion that a class is itself an object, and as such can have operations and attributes defined for it. In both C++ and Java, operations and data values associated with a class are identified syntactically by the keyword static. We will refer to such operations as static operations. The presence of public static operations in a class specification implies that the class itself is an object that can be messaged. From a testing perspective, we must treat such a class as an object and create a test suite for the class as well as its instances. From a testing perspective, we should always be skeptical of nonconstant, static data associated with a class because such data can affect the behavior of instances.

Class Specification

A specification for a class describes what the class represents and what an instance of the class can do. A class specification includes a specification for each of the operations that can be performed by each of its instances. An operation is an action that can be applied to an object to obtain a certain effect. Operations fall into two categories:

  • Accessor (or inspector) operations provide information about an object for example, the value of some attribute or general state information. This kind of operation does not change the object on which the operation is being requested. In C++, accessor operations can and should be declared as const.

  • Modifier (or mutator) operations change the state of an object by setting one or more attributes to have new values.

We make this classification because testing accessors is different from testing modifiers. Within a class specification, some operations might both provide information and change it.[3] Some modifier operations might not make changes under all circumstances. In either case, we classify these operations as modifier operations.

[3] It is good object-oriented design practice for an operation to be one or the other, but not both.

There are two kinds of operations that deserve special attention:

  • A constructor is a class object operation used to create a new object, including the initialization of the new instance when it comes into existence.

  • A destructor is an instance object operation used to perform any processing needed just prior to the end of an object's lifetime.

Constructors and destructors are different from accessors and modifiers in that they are invoked implicitly as a result of the birth and death of objects. Some of these objects are visible in the program and some are not. The statement

graphics/02equ01.gif


in which a, b, c, and x are all objects from the same class, invokes the constructor of that class at least twice to create objects that hold intermediate results and die by the end of the statement, as follows:

graphics/02equ02.gif


graphics/02equ03.gif


graphics/02equ04.gif


A class represents a concept, either in the problem being solved by a software application or in the solution to that problem. We expect a description of what a class represents to be a part of a class specification. Consider, for example, that the class PuckSupply declared in Figure 2.2 probably does not have much meaning without an explanation that it represents the collection of pucks that a player has at the start of a Brickles match. As the player loses pucks to the floor during play, the program will replace it with another puck from a puck supply until that supply is exhausted, at which time the match ends with a loss for the player.

We also expect some meaning and constraints to be associated with each of the operations defined in a class specification for example, do the operations size() and get() for the class PuckSupply have any inherent meaning to you? Consequently, each operation should have a specification that describes what it does. A specification for PuckSupply is given in Figure 2.4.

Figure 2.4. A specification for the PuckSupply class based on contracts

graphics/02fig04.gif

  • The size() operation can be applied at any time (no preconditions) and returns the number of pucks in the receiver.

  • The get() operation can only be applied if at least one puck is left in the receiver that is, size() > 0. The result of the operation is to return a puck and to reduce the number of pucks by one.

Well-specified operation semantics are critical to both development and testing efforts and definitely worth the time and effort needed to express them well. You can use any form of notation to specify the semantics provided it is well-understood by all who must use it. We will specify semantics at several different points:

  • Preconditions for an operation prescribe conditions that must hold before the operation can be performed. Preconditions are usually stated in terms of attributes of the object containing the operation and/or attributes of any actual parameters included in the message requesting that the operation be performed.

  • Postconditions for an operation prescribe conditions that must hold after the operation is performed. Postconditions are usually stated in terms of (1) the attributes of the object containing the operation; (2) the attributes of any actual parameters included in the message that is requesting that the operation be performed; (3) in terms of the value of any reply; and/or (4) in terms of the exceptions that might be raised.

  • Invariants prescribe conditions that must always hold within the lifetime of the object. A class invariant describes a set of operating boundaries for an instance of a class. It is also possible to define interface invariants as well as operational invariants for segments of code. A class invariant can be treated as an implied postcondition for each operation. They must hold whenever an operation completes, although a method for an operation is allowed to violate invariants during its execution. Invariants are usually stated in terms of the attributes or states of an object.

The aggregate of the specifications of all of the operations in a class provides part of the description of the behavior of its instances. Behavior can be difficult to infer from operation specifications alone, so behavior is typically designed and represented at a higher form of abstraction using states and transitions (See State Diagrams on page 49). Behavior is characterized by defining a set of states for an instance and then describing how various operations effect transitions from state to state. The states associated with a puck supply in Brickles define whether it is empty or not empty. Being empty is determined by the size attribute of a puck supply. If the size is zero, then it is empty, otherwise it is not empty. You can remove a puck from a supply only if that supply is not empty that is, if its size is not zero.

When you write a specification for an operation, you can use one of two basic approaches to define the interface between the receiver and the sender. Each approach has a set of rules about how to define the constraints and responsibilities of the sender and the receiver when an operation is to be performed. A contract approach is embedded in the specification in Figure 2.4. A defensive programming approach underlies the specification in Figure 2.5. The contract approach emphasizes preconditions, but has simpler postconditions, while the defensive programming approach is just the reverse.

Figure 2.5. A specification for the class PuckSupply based on defensive programming

graphics/02fig05.gif

Under the contract approach, which is a design technique developed by Bertrand Meyer [Meye94], an interface is defined in terms of the obligations of the sender and the receiver involved in an interaction. An operation is defined in terms of the obligations of each party. Typically, these are set forth in preconditions and postconditions for an operation and a set of invariant conditions that must hold across all operations, thereby acting as postconditions required of all operations. The preconditions prescribe the obligation of the sender that is, before the sender can make a request for a receiver to perform an operation, the sender must ensure that all preconditions are met. If preconditions have been met, then the receiver is obligated to meet the requirements set forth in the postconditions as well as those in any class invariant. Under the contract approach, care must be taken in the design of a class interface to ensure that preconditions are sufficient to allow a receiver to meet postconditions (if not, you should add additional preconditions) and to ensure that a sender can determine whether all preconditions are met before sending a message. Typically, a set of accessor methods allow for checking specified conditions. Furthermore, care must be taken to ensure that postconditions address all possible outcomes of an operation, assuming preconditions are met.

Under the defensive programming approach, an interface is defined primarily in terms of the receiver, and any assumptions it makes on its own state and the values of any inputs (arguments or global data values) at the time of the request. Under this approach, an operation typically returns some indication concerning the status of the result of the request success or failure for a particular reason, such as a bad input value. This indication is traditionally in the form of a return code that associates a value with each possible outcome. However, a receiver can provide to a sender an object that encapsulates the status of the request. Furthermore, exceptions are being used more frequently because many object-oriented programming languages now support them. Some operations are defined so that no status is returned in case of failure, but instead, execution is terminated when a request cannot be met. Certainly this action cannot be tolerated in most software systems.

The primary goal of defensive programming is to identify "garbage in" and hence eliminate "garbage out." A member function checks for improper values coming in and then reports the status of processing the request to a sender. The approach tends to increase the complexity of software because each sender must follow a request for an operation with code to check the processing status and then, for each possible outcome, provide code to take an appropriate recovery action. The approach tends to increase both the size of code and to increase execution time because inputs are checked on every call, even though the sender may have already checked them.[4]

[4] It is curious that code we have seen that was written using a defensive programming approach rarely checks to ensure the receiver actually performed the requested operation that is, the mistrust is only on the part of a receiver. Perhaps this practice arises from the fact that the code for the receiver has usually been tested and is considered trustworthy enough to work correctly. Misuse can come only on the sender's side; we'll address this in Chapter 5.

The contract and defensive programming approaches represent two opposite views of software specification. As the name implies, defensive programming reflects a lack of trust of a sender on the part of a receiver. By contrast, a contract reflects a mutual responsibility shared by both a sender and a receiver. A receiver processes a request based on inputs believed to meet stated preconditions. A sender assumes that conditions have been met after the request has been processed. It is not uncommon for the approaches to be mixed in specifying the operations within a single class because they each have advantages and disadvantages.

Interface design based on contracts eliminates the need for a receiver to verify preconditions on each call.[5] It makes for better software engineering and better program (and programmer) efficiency. However, it introduces one important question: "In the context of an executing program, how are contracts enforced?" Clearly, the contract places obligations on both sender and receiver. Nonetheless, can a receiver truly trust every sender to meet preconditions? The consequences of "garbage in" can be disastrous. A program's execution in the presence of a sender's failure to meet a precondition would most likely result in data corruption that would in turn have serious consequences! It is critical that all interactions under contracts be tested to ensure compliance with a contract.

[5] It is still useful for debugging to include code to check preconditions. This code can be "removed" from the executable after a system is debugged, but before the final testing. Eiffel [Meye00] has language-level support for contract checking and compiler switches to enable and disable the checking.

From a testing perspective, the approach used in an interface determines the types of testing that need to be done. The contract approach simplifies class testing, but complicates interaction testing because we must ensure that any sender meets preconditions. The defensive programming approach complicates class testing (because test cases must address all possible outcomes) and interaction testing (because we must ensure all possible outcomes are produced and that they are properly handled by a sender).

Tip

Review pre- and postconditions and invariants for testability during design. Are the constraints clearly stated? Does the specification include means by which to check preconditions?


Class Implementation

A class implementation describes how an object represents its attributes and carries out operations. It comprises several components:

  • A set of data values stored in data members, which are sometimes referred to as instance variables or variables. The data values store some or all of the values associated with the attributes of an object. There is not necessarily a one-to-one mapping of attributes to data values. Some attributes can be derived from others for example, the direction a puck is moving in the horizontal direction can be deduced from its velocity. Some redundant representation of derivable attributes is sometimes desirable in order to improve the performance of member functions with respect to time. In some cases, an attribute identified for an object might not be represented at all because the attribute is not needed in an application. By removing the attribute, we can reduce the memory space needed to hold such an object.

  • A set of methods, referred to as member functions in C++ or methods in Java, constitutes code that will be used to implement an algorithm that accomplishes one operation declared in the public or private class specification. The code typically uses or sets an object's variables. It processes any actual parameter values, checks for exceptional conditions, and computes a return value if one is specified for the operation.

  • A set of constructors to initialize a new instance (at the start of its lifetime). A constructor is really an operation on a class object.

  • A destructor that handles any processing associated with the destruction of an instance (when it reaches the end of its lifetime).

  • A set of private operations in a private interface.[6] Private operations provide support for the implementation of public operations.

    [6] For simplicity, we will generally use the term private to refer to any aspect of a class that is not public. C++ supports private and protected components of a class and Java supports even more levels of access to components.

Class testing is an important aspect of the total testing process because classes define the building blocks for object-oriented programs. Since a class is an abstraction of the commonalities among its instances, the class testing process must ensure that a representative sample of members of the class are selected for testing.

By viewing classes from a testing perspective, we can identify the following potential causes of failures within their design and implementation.

  • A class specification contains operations to construct instances. These operations may not properly initialize the attributes of new instances.

  • A class relies on collaboration with other classes to define its behaviors and attributes. For example, an instance variable might be an instance of another class or a method might send a message to a parameter (which is an instance of another class), to do some part of its computation. These other classes may be implemented incorrectly and contribute to the failure of the class using them in a definition.

Subsystems and Classes

A class is a very interesting concept in terms of systems and subsystems. In many ways, a system or a subsystem can be specified as a class one associated with a complex behavior, but a class nonetheless that has states and transitions and an interface. Much of the focus of this book is on class testing. Many of the techniques we discuss could be scaled up to system and subsystem testing if the system is indeed specified as a class. Note, however, that the complexity of such a class far exceeds that of a class such as Point. This is an indication of some of the issues we must address with respect to class testing.

  • A class's implementation "satisfies" its specification, but that is no guarantee that the specification is correct. The implementation may violate a higher requirement, such as accepted design criteria, or it may simply incorrectly model the underlying concept.

  • The implementation might not support all required operations or may incorrectly perform operations.

  • A class specifies preconditions to each operation. The class might not provide a way for that precondition to be checked by a sender before sending a message.

The design approach used, contract or defensive, gives rise to different sets of potential problems. Under a contract approach, we only need to test situations in which the preconditions are satisfied. Under a defensive programming approach, we must test every possible input to determine that the outcome is handled properly.

Inheritance

Inheritance is a relationship between classes that allows the definition of a new class based on the definition of an existing class.[7] This dependency of one class on another allows the reuse of both the specification and the implementation of the preexisting class. An important advantage of this approach is that the preexisting class does not have to be modified or made aware in any way of the new class. The new class is referred to as a subclass or (in C++) a derived class. If a class inherits from another, the other class is referred to as its superclass or (in C++) base class. The set of classes that inherit either directly or indirectly from a given class form an inheritance hierarchy. Within that hierarchy, we can refer to the root, which is the class from which all others inherit directly or indirectly. Each class in a hierarchy, except the root, has one or more ancestors, the class(es) from which it inherits directly or indirectly. Each class in a hierarchy has zero or more descendents, which are the classes that inherit from it directly or indirectly.

[7] In a programming language that supports multiple inheritance, a new class can be defined in terms of one or more existing classes. C++ supports multiple inheritance, but most other object-oriented programming languages do not. Most designers tend to avoid the use of multiple inheritance because of its complexity. Sometimes multiple inheritance is very useful, especially in modeling similarities between two subclasses at the same level in an inheritance hierarchy. We will focus primarily on single inheritance, but we will address multiple inheritance in important areas of testing.

Good object-oriented design requires that inheritance be used only to implement an is a (or is a kind of) relationship. The best use of inheritance is with respect to specifications and not implementation. This requirement becomes evident in the context of inclusion polymorphism (see page 34).

Viewed from the testing perspective, inheritance does the following:

  • Provides a mechanism by which bugs can be propagated from a class to its descendents. Testing a class as it is developed eliminates faults early before they are passed on to other classes.

  • Provides a mechanism by which we can reuse test cases. Because a subclass inherits part of its specification and implementation from its superclass, we can potentially reuse test cases for the superclass in testing the subclass.

  • Models an is a kind of relationship. Use of inheritance solely for code reuse will probably lead to maintenance difficulties. This is chiefly a design quality issue, but we argue that it is such a common mistake in object-oriented development that testers can make a significant contribution to a project's success by checking that inheritance is used properly. Besides, proper use of inheritance in design leads to benefits in execution testing of classes (see Chapter 7).

Polymorphism

Polymorphism is the ability to treat an object as belonging to more than one type. The typing system in a programming language can be defined to support a number of different type-conformance policies. An exact match policy may be the safest policy, but a polymorphic typing system supports designs that are flexible and easy to maintain.

Substitution Principle

Inheritance should be used only to model the is a (or is a kind of ) relationship. That is, if D is a subclass of C, then it should be understood that D is a kind of C. Based on the substitution principle [LiWi94], an instance of a subclass D can be used whenever an instance of the class C is expected. In other words, if a program is designed to work with an instance of the class C in some context, then an instance of the class D could be substituted in that same context and the program still could work. In order for that to happen, the behavior associated with D must somehow conform to that which is associated with C.

One way to enforce "substitutability" is to constrain behavior changes from class to subclass. The behavior associated with a class can be defined in terms of the observable states of an instance and the semantics associated with the various operations defined for an instance of that class. The behavior associated with a subclass can be defined in terms of incremental changes to the observable states and operations defined by its base class.

Under the substitution principle, only the following changes are allowed in defining the behavior associated with a new subclass:

  • The preconditions for each operation must be the same or weaker that is, less constraining from the perspective of a client.

  • The postconditions for each operation must be the same or stronger that is, must do at least as much as defined by the superclass.

  • The class invariant must be the same or stronger that is, add more constraints.

These constraints on behavior changes must be enforced by the developers. Viewed from the perspective of observable states, we can show that

  • The observable states and all transitions between them associated with the base class must be preserved by the subclass.

  • The subclass may add transitions between these states.

  • The subclass may add observable states as long as each is either concurrent or a substate of an existing state.

Inclusion Polymorphism

Inclusion polymorphism is the occurrence of different forms in the same class. Object-oriented programming language support for inclusion polymorphism[8] gives programmers the ability to substitute an object whose specification matches another object's specification for the latter object in a request for an operation. In other words, a sender in an object-oriented program can use an object as a parameter based on its implementation of an interface rather than its full class.

[8] Some people refer to this support as dynamic binding. Dynamic binding is an association at runtime between the operation specified in a message and a method to process the requested operation. However, dynamic binding is the mechanism by which inclusion polymorphism is implemented by runtime environments. In C++, dynamic binding must be requested by the keyword virtual in a member function declaration.

In C++, inclusion polymorphism arises from the inheritance relationship. A derived class inherits the public interface of its base class[9] and thus instances of the derived class can respond to the same messages as the base class.[10] A sender can manipulate an instance of either class with a value that is either a reference or a pointer whose target type is the base class. A member function call can be made through that value.

[9] We assume public inheritance is used. We believe protected and private inheritance should be used only under rare circumstances.

[10] Instances of the derived class can potentially respond to additional messages because the derived class defines additional operations in its public interface.

In Java, inclusion polymorphism is supported both through inheritance between classes and an implementation relationship between interfaces and classes. A sender can manipulate objects with a reference declared for either a class or an interface. If a reference is associated with a class, then the reference can be bound to an instance of that class or any of its descendents. If a reference is associated with an interface, then the reference can be bound to an instance of any class that is declared to implement that interface.

Our definition of a class as a set of objects that share a common conceptual basis (see page 22) is influenced primarily by the association of inheritance and inclusion polymorphism. The class at the root of a hierarchy establishes a common conceptual basis for all objects in the set. A descendent of that root class refines the behavior established by that root class and any of its other ancestors. The objects in the descendent class are still contained in the set that is the root class. Thus, a descendent class defines a subset of each of the sets that are its ancestors. Suppose that the Brickles specification is extended to incorporate additional kinds of bricks say, some that are hard and have to be hit twice with a puck before they disappear, and some that break with a considerable force that increases the speed of any puck that hits it. The HardBrick and PowerBrick classes could each be defined as a subclass of Brick. The relationship between the sets are illustrated in Figure 2.6. Note how in a polymorphic sense, the class Brick contains 24 elements 10 "plain" bricks, 8 hard bricks, and 6 power bricks. Hard bricks and power bricks have special properties, but they also respond to the same messages as "plain" bricks, although probably in different ways.

Figure 2.6. A set diagram for a Brick inheritance hierarchy

graphics/02fig06.gif

Sets representing classes can be considered from two perspectives:

  1. From a class's perspective, each set contains all instances. Conceptually, the size of the set could be infinite, as is the case with bricks since, in theory, we can create bricks for any number of Brickles matches or even for any other arcade games because the Brick class is not necessarily tied to Brickles. Infinite sets are most easily represented using Venn diagrams.

  2. From an executing program's perspective, each set is drawn with one element per instance in existence. The sets in Figure 2.6 are drawn from this perspective.

Both perspectives are useful during testing. When a class is to be tested outside the context of any application program (see Chapter 5 and Chapter 6), we will test it by selecting arbitrary instances using the first perspective. When the use of a class is to be tested in the context of an executing application program or in the context of object persistence, then we can utilize the second perspective to ensure that the size of the set is correct and that elements correspond to appropriate objects in the problem or in its solution.

Inclusion polymorphism provides a powerful capability. You can perform all design and programming to interfaces, without regard to the exact class of the object that is sent a message to perform an operation. Inclusion polymorphism takes design and programming to a higher level of abstraction. In fact, it is useful to define classes for which no instances exist, but for which its subclasses do have instances. An abstract class is a class whose purpose is primarily to define an interface that is supported by all of its descendents.[11] In terms of the example extending the kinds of bricks in Brickles, an alternate formulation is to define an abstract class called Brick and define three subclasses for it: PlainBrick, HardBrick, and PowerBrick (see Figure 2.7).

[11] An abstract class might also define portions of the implementation for its descendents. Both C++ and Java provide syntax for the definition of abstract classes and ensure that instances of them cannot be created in a running program.

Figure 2.7. A set diagram for a Brick class inheritance hierarchy

graphics/02fig07.gif

Among the abstract classes that we used in the design of Brickles are the following:

  • Sprite to represent the things that can appear on a play field.

  • MovableSprite, which is a subclass of Sprite, to represent sprites that can move in a play field.

  • StationarySprite, which is a subclass of Sprite, to represent sprites that cannot move in a play field.

Puck and Paddle are concrete subclasses of MovableSprite, while Brick is a subclass of StationarySprite. The use of abstractions allows polymorphism to be exploited during design. For example, we can design at the level of a play field containing sprites without detailed knowledge of all the various kinds of sprites. We can design at the level of movable sprites moving in a play field and colliding with other sprites both movable and stationary. If the game specification were extended to incorporate hard bricks and power bricks, most parts of the program would not need to be changed because, after all, hard bricks and power bricks are just stationary sprites. The parts of the program that are affected should be limited to those that construct the actual instances of the classes.

Subclassing and Subtyping

Consider a design solution that involves inclusion polymorphism. In the diagram below, classes C and D inherit from class B. Instances of class A think they are sending messages to an instance of class B (the type of formal parameter B). The polymorphic attribute of the typing system allows instances of C and D in place of the instance of B. Each class has a different implementation of the doIt() method.


graphics/02in02.gif


Designing software well, within the context of inheritance and inclusion polymorphism, requires a disciplined use of inheritance (and interfaces in Java). It is important that behavior is preserved as classes are added to extend a class hierarchy. If, for example, bricks can move, then they are not really classifiable as stationary sprites. Good design requires that each subclass be a subtype that is, the specification for the subclass must fully meet all specifications of its direct ancestor. This is an enforceable design requirement when the following rules are applied with respect to pre- and postconditions for each inherited operation:

  • The tryIt() method of A is written to satisfy the preconditions of the doIt() operation of B before it calls doIt(). If an instance of C or D is to be substituted, the preconditions for C::doIt() or D::doIt() must not add any new conditions to those for B::doIt() or we would have to modify A to accommodate C and D.

  • The tryIt() method of A is written to satisfy the preconditions of the doIt() operation of B before it calls doIt(). If an instance of C or D is to be substituted, the preconditions for C::doIt() or D::doIt() must not add any new conditions to those for B::doIt() or we would have to modify A to accommodate C and D.

  • The invariant defined for B must still be true in instances of C and D. Additional invariants may be added.

These requirements are easy to understand in the context of a software contract (see page 27). Preconditions set forth the obligations of any sender and the postconditions and class invariants set forth the obligations of a receiver in any interaction. The requirement for same or weaker (less strict) preconditions means that in meeting its obligations in terms of the contract for A, a sender still meets its obligations for B, which is not as constraining. The requirement for the same or stronger (more strict) postconditions and invariants means that a receiver still can meet a sender's expectations in terms of the contract for A, even though that receiver might do more than the sender expects.

A polymorphic reference hides the actual class of a referent. All referents are manipulated through their common interface. C++ and Java provide support for determining the actual class of a referent at runtime. Good object-oriented design requires that such runtime type inspections should be held to a minimum, primarily because they create a maintenance point since the extension of a class hierarchy introduces more types to be inspected. However, situations arise in which such inspections can be justified.

The following are the functions of inclusion polymorphism viewed from a testing perspective:

  • Inclusion polymorphism allows systems to be extended incrementally by adding classes rather than modifying existing ones. Unanticipated interactions can occur in the extensions.

  • Inclusion polymorphism allows any operation to have one or more parameters of a polymorphic reference. This increases the number of possible kinds of actual parameters that should be tested.

  • Inclusion polymorphism allows an operation to specify replies that are polymorphic references. The actual class of the referent could be incorrect or unanticipated by the sender.

This dynamic nature of object-oriented languages places more importance on testing a representative sample of runtime configurations. Static analyses can provide the potential interactions that might occur, but only the runtime configuration can illustrate what actually happens. In Chapter 6 we consider a statistical technique that assists in determining which configurations will expose the most faults for the least cost of resources.

Parametric Polymorphism

Parametric polymorphism is the capability to define a type in terms of one or more parameters.

Templates in C++ provide a compile-time ability to instantiate a "new" class. It is new in the sense that an actual parameter is provided for the formal parameter in the definition. Instances of the new class can then also be created. This capability has been used extensively in the C++ Standard Template Library. The interface of a simple list class template is shown in Figure 2.8.

Figure 2.8. A C++ List class template

graphics/02fig08.gif

From a testing perspective, parametric polymorphism supports a different type of relationship from inheritance. If the template works for one instantiation, there is no guarantee it will work for another because the template code might assume the correct implementations of operations such as making (deep) copies and destructors. This should be checked during inspection. It is possible to write templated drivers for testing many parts of templates.

Abstraction

We have referred to the concept of abstraction throughout this chapter. Abstraction is the process of removing detail from a representation. Abstraction allows us to look at a problem or its solution in various levels of detail, thereby letting us leave out any considerations that are irrelevant to the current level of interest. Object-oriented technologies make extensive use of abstraction for example, the root class in an inheritance hierarchy models a concept more abstract than its descendents. In the next section we will see a number of system models that are developed in order of increasing detail.MORE TO COME We need to talk about abstraction and testing at various levels at some point. This chapter?

Viewed from the testing perspective, layers of abstraction in the development products are paralleled by layers of test analysis. That is, by beginning with the highest levels of abstraction, we can provide a more thorough examination of the development product and, therefore, a more effective and accurate set of tests.



A Practical Guide to Testing Object-Oriented Software
A Practical Guide to Testing Object-Oriented Software
ISBN: 0201325640
EAN: 2147483647
Year: 2005
Pages: 126

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net