11.3 Organizing Objects

 < Day Day Up > 



11.3 Organizing Objects

Abstraction is the process of factoring out commonality in a program, storing it once, and reusing it. Once this commonality has been factored out, some way must be found to organize it so that it can be used. As was pointed out in Chapter 10, if the abstraction involves only stateless methods, static methods can be created and added to a library class and called as needed. Often, however, the commonality will require that a state be maintained between invocations of a method, and this will result in the need for object-oriented programming (OOP) techniques to maintain this state. OOP involves organizing objects around information (data) and behavior (methods that act on that data). The objects created to organize the data and methods are generally structured around objects that exist in the problem definition, with the objects in the problem definition being decomposed into objects in the program, thus this process is often called decomposition. In Chapter 10, the topic of how to handle this reuse of object state and behavior in utility classes was covered. This chapter addresses the reuse of an object state and behavior for objects represented in the problem to be solved. As will be shown, this type of design is more concerned with representing the objects to match their use in the problem definition than in the reuse possibilities, but the end result is still a design that factors out commonalities and enhances reuse.

As was pointed out in Chapter 10, information and behavior for objects can be organized in two ways: (1) build an object by including instance of other objects as variables, referred to as composition; or (2) build an object by using the Java extends clause to extend the behavior of some base object, called classification. Composition is normally thought of as a "has-a" relationship (see Section 11.3.1); the two major types of composition, aggregation and association, are defined and illustrated below. Classification is often thought of as an "is-a" relationship (see Section 11.3.2) and often involves using what are called abstract classes. Abstract classes are often confused with interfaces, but Section 11.3.3 demonstrates how they are different. Finally, Section 11.3.4 shows how classification can be used to implement an object that is made up of data and methods from other objects, using what are essentially "has-a" relationships that should be implemented as composition. Section 11.3.4 also discusses how this approach can result in an incorrect design and why this use of classification should normally be avoided.

Section 11.4 looks at why the simple "is-a" vs. "has-a" test must be carefully applied to a program design. Section 11.4.1 shows that an "is-a" relationship is really dependent on the definition of the problem to be solved. Section 11.4.2 goes on to show that, even when an object relationship is a true "is-a" relationship, mutable objects are not handled well in classification. When this occurs the "is-a" relationship really represents a state, which is better stored in a variable using composition rather than classification. Section 11.4.3 suggests that if classification is used to represent a role then it is best to use composition. Section 11.4.4 points out problems that can occur with objects if an object can take on multiple roles, which causes the number of possible classes needed to represent the possible objects to grow exponentially. Finally, Section 11.4.5 deals with how the choice of classification or composition can be impacted by the design of the program.

11.3.1 Composition: "Has-a" Relationships

Objects can be built by including information about the object as variables that are declared as part of the class definition for the object. These variables can be objects or primitives. For example a car "has an" engine size, "has a" driver, "has" four tires, "has a" model name, and "has an" ability to calculate the expectedTimeToService. The definition for this class is shown in Exhibit 1 (Program11.1a). Note that the composition relationship allows an object to be built from primitives (a float for engine size), other objects (a driver), collections of objects or primitives (an array of tires), and methods (expectedTime-ToService).

Exhibit 1: Program11.1a: Composition Example for a Car Class

start example

 /**  *  A class to build a Car class  */ class Driver { } /**  *  A class to build a Car class  */ class Tire implements Clonable {   protected Object clone() {     return new Tire();   } } /**  *  An interface used to build the Car class  */ interface ServiceTimeCalculator {   public int expectedTimeToService(); } /**  *  A class to instantiate the interface  */ class myCarsCalculator implements ServiceTimeCalculator {   public int expectedTimeToService(){     return 0;   } } /**  *  This shows a class created using composition. All of the  *  different mechanisms for composition are included:  *      driver: association  *      modelName: aggregation (using immutable objects)  *      stc: composition using an interface  *      engineSize: aggregation (using a primitive)  *      tires[]: aggregation (using an encapsulated variable)  */ public class Car {   private Driver driver;   private String modelName();   private ServiceTimeCalculator stc;   private int engineSize;   private static final int NUMBER_OF_TIRES = 4;   private Tire tires[];   /**    *  Public constructor. It sets the values for the    *  variables. Note that the driver is association because    *  the driver object passed was saved. The tires are    *  aggregation, because new objects are created that do not    *  have scope outside of this object. Finally, methods can    *  be composited using interfaces.    */   public Car(Driver driver, ServiceTimeCalculator stc, Tire[]       tires) {     this.driver = driver;     this.stc = stc;     this.engineSize = 350;     for (int i = 0; i < NUMBER_OF_TIRES; i++) {       this.tires[i] = (Tire) tires[i].clone();     }   }   /**    *  Accessor method for tires, which are aggregate variables    *  using cloning of the object.    */   public void setTire(int tireNo, Tire tire) {     this.tires[tireNo] = (Tire)tire.clone();   }   /**    *  Accessor method for modelName, which is an aggregate    *  variable using an immutable object.    */   public void setModelName(String modelName) {     this.modelName = modelName;   }   /**    *  Accessor which returns the model name. Because the object is    *  immutable, this does not violate the aggregate property of    *  the object.    */   public String getModelName() {     return modelName;   }   /**    *  Accessor method for engineSize, which is an aggregate    *  variable using a primitive.    */   public void setEngineSize(int engineSize) {     this.engineSize = engineSize;   }   /**    *  Accessor method to return engineSize. Because it is a    *  primitive, this does not violate the aggregate property of    *  the object.    */   public int getEngineSize() {     return engineSize;   }   /**    *  Accessor method for the driver, an association variable.    *  Note that the actual mutable object is stored, meaning    *  that the object is shared with the calling method.    */   public Driver getDriver() {     return driver;   }   /**    *  Accessor method for driver, an association variable.    *  Note that the actual mutable object is returned.    */   public void setDriver(Driver driver) {     this.driver = driver;   } } 

end example

At some point during the life of the object, often when it is constructed, these values are set. This mechanism of building an object is referred to as composition because an object is composed of the objects and primitives. Composition can be used anytime a "has-a" relation exists. For example, a paragraph has at least one sentence, which has one or more words, or a person has an age. To build an object using composition, a variable is created to represent the property needed, and then an instance of that property is assigned.

One interesting thing to note is that not only is composition implemented for properties represented by data values, but it is also possible for properties that need to be calculated, in this case with the expectedTimeToService interface. Because each car object could have differing requirements for service that needs to be done, a class can be created that simply implements this function. An object instance of this class can then be stored with the car object, and the expectedTime-ToService method in the car object can delegate the actual calculation to that object. Delegation is the way in which reuse occurs in composite objects and simply means that a method in the encapsulating object calls the method in the encapsulated object to do the actual calculation. This relationship can be polymorphic, as shown in the car object through the use of interfaces.

Composition can be used in two ways to build an object, aggregation or association. These topics are discussed in the next three sections.

11.3.1.1 Aggregation

Aggregation applies to data for an object that is completely encapsulated inside of the object. This means that the data item cannot be changed outside of the object, and the data item exists only while the object exists. For example, a Car object has an Engine. The Engine is created when the Car is created and is only accessed from within the Car class. It has no existence or presence outside of the Car class. One big advantage of using aggregation is that, because the aggregated object cannot be modified outside of the object that encapsulates it, the encapsulating object can control any changes to the aggregated object. This is useful for monitoring any changes to the aggregated object or for providing synchronized access to an encapsulated aggregated object, as in the readers/writers program in Chapter 9.

Aggregate properties are normally maintained in a program in one of three ways. The first two ways are similar and use primitives and immutable variables. If primitives or immutable objects are used with private identifiers, it is not possible to change the values of these variables outside of the object in which they are contained. Exhibit 1 (Program11.1a) illustrates these two ways to specify an aggregate property using the engineSize and modelName identifiers. The engine-Size is a primitive and thus can only be referenced from within the current object; it is copied when it is returned to the calling program via the getEngineSize method. The modelName is an instance of a string and is thus immutable; it can be safely returned via the getModelName method.

The third way to specify an aggregate property is to create a new object for each aggregate property and to limit the scope of these objects to the encapsulating object. This means that only the methods in the encapsulating object have access to these aggregated objects, thus the encapsulating object can be used to control access to them. Exhibit 1 (Program11.1a) illustrates this concept through the array of tires. When the tires are stored in the Car object, the program always creates a new Tire object to represent it (via a call to the clone method). Unlike the use of primitives or immutable objects, no getTire method is used, because returning a copy of the Tire breaks the encapsulation (confinement) of the Tire object, so it can be changed outside of the Car object. In this way, the Tire object used by the Car object is always specific to the Car and has no visibility outside of the Car object.

Finally, note that all three types of aggregation can use setter methods (such as setModelName). This in no way breaks the encapsulation of the object; however, it can come with its own problems. For example, consider the case when an aggregated object is used as a notification object. If setter methods are used in an object, it is possible for a thread to wait on the aggregated notification object but then the variable in the encapsulating object to be changed. This leaves the thread waiting on the de-referenced object for a call to notify that will never happen. So, while the use of setter methods does not violate aggregation, it should be carefully considered.

11.3.1.2 Association

Association applies to data for an object that might have existence outside of an object. For example, the objects stored in a vector will be created externally to the vector, and then added to the vector. The object can later be modified, and this modification will be reflected in the object in the vector. Objects will thus only be associated with the vector. In Exhibit 1 (Program11.1a), association is illustrated by the driver of a car. A Car object could have multiple Driver objects over its lifetime, and the Driver object is normally maintained in the program outside of the Car object. Thus, attributes of the driver of the Car can be changed outside of the control of the Car object.

When association is used in a design it impacts concurrent operations. When an object is associated with the encapsulating object (in this case, the Driver with the Car), the encapsulating object cannot be used to ensure synchronized accessed to the associated object. Just because the Driver object is referenced in a synchronized method in the Car does not mean that some other thread that obtained a reference other than through the Car cannot change the Driver object and create a race condition.

As was shown, the choice between aggregation and association has important consequences when used with concurrent operation. It also affects other design considerations. For example, aggregate objects are garbage collected when the containing object goes out of scope, but the status of an associate object is unknown as there could be other references to it. When an object is created and exported by some mechanism, it could outlive the object that creates it. Likewise, if an object was created outside of the object and imported by some mechanism, the reference to it in the calling object could keep it from being garbage collected. All of this leads to the question of when to use aggregation and when to use association. Generally, this choice hinges on how the object is used in the program design.

11.3.1.3 Using Aggregation and Association

It seems obvious when to use aggregation or association. For example, it is obvious that when modeling a car, tires are always tracked with a car, are only of interest when they are part of the car, and thus should be aggregated. The driver of a car is created outside of the Car class, should be modifiable outside of the Car class, and thus should be associated with the car. That this design is obviously true can be shown by considering the modeling of a car rental company. Because drivers are changed each time the car is rented and because the driver record must be accessed outside of when the driver is renting the car (for functions such as billing), the driver must be associated (not aggregated) with the car. Likewise, the tires on a car are only of interest when they are on the car, and so represent an aggregation with the car object. This agrees with our basic intuition of how to model a car, that these relationships must always hold, and it stands to reason that a car is modeled with an associated driver and aggregate tires. But, is it really so obvious? Consider the problem of modeling a race car. A race car is always associated with a particular driver; however, during a race the tires on the car might be replaced several times, and these tires must be tracked when they are off the car to make sure that they are not accidentally used again. In this case of a race car, where the driver is an aggregate item and the tires are associated items, our preconceived notions about how to model a car really do not apply. A good rule that cannot be repeated enough is to solve the problem you are given, not the one you want to solve.

As this example shows, no easy rules exist for implementing a design. Designers need to understand the problems they are trying to implement and should remember that OOP is not about modeling the real world or, worse, the designer's intuition about how the real world behaves. A good design must reflect the realities of the problem being solved.

11.3.2 Classification: "Is-a" Relationships

Classification involves putting things into class hierarchies. A common example is in biological classification of living organisms, such as: "A human 'is-a' mammal 'is-an' animal 'is-a' living thing." Just as composition uses "has-a" relationships to build objects, classification uses "is-a" relationships to build objects. The difference is that in composition, an object is a composite of other objects that are included as variables in the definition of the larger object. In classification, a larger object is built by extending the properties of other base objects.

Classification is accomplished in Java using the extends clause, which tells a subclass to keep the functionality and data from a super class and add new functionality to it, extending the base class with the data and functionality of the new class. For example, Exhibit 2 (Program11.1b) shows an example of how the Car class in Exhibit 1 (Program11.1a) can be extended into specific types of cars. In this case, we have two types of cars, an SUV and a convertible. Both of these types have all the attributes of a car, but both extend that functionality to create more specific types of cars. Because each subclass inherits the functionality from the base class, classification is also called inheritance.

Exhibit 2: Program11.1b: Classification Example for a Car Class

start example

 class SUV extends car { } class Convertible extends car { } 

end example

Many novice programmers believe that because they can directly access data and methods from the super class in the subclass that classification designs are better than composition designs. However, the first rule should always be to implement a design consistent with the problem to be solved and within any constraints that might be imposed by the implementation details for the program. As will be shown, such an approach generally favors composition.

11.3.3 Abstract Classes versus Interfaces

Often, the base classes in a classification hierarchy are not meant to be instantiated as there is no instance of something that is strictly the base type. In the biological example, there are no animals that are just a mammal, but there are many different types of animals (such as dogs and people) which all share the characteristic of being a mammal. In Java, when a class is created to simply represent commonality between all objects that are subtypes of that class, the class created is declared to be abstract.

For example, consider the employee example provided in Exhibit 3 (Program11.2). Here, the base class Employee is created to define some variables and methods that are common to all types of employees, such as name, dependents, and calculating their weekly pay using the calculatePay method. The ability to calculate the pay of each employee is an important part of the definition of an employee; however, we have a number of different types of employees, and the way to calculate the salary is different for each type. It is not possible, then, to have an object that is an instance of a generic employee. The objects must be specific types of employees, so the employee class and calculatePay methods are declared as abstract, indicating that they must be defined in a subclass before objects of this type can be declared. In this case, each type of employee, salaried or hourly, has its own calculatePay method and variables necessary to support their pay type (salaried or hourly).

Exhibit 3: Program11.2: Employee Class

start example

 /**  *  The base class to define a person.  */ class Person {   private String name;   publicPerson(Stringname) {     this.name = name;   }   public Person(Person person) {     this.name = person.name;   }   public void setName(String name) {     this.name = name;   }   public String getName() {     return name;   } } /**  *  The base class for an employee. The data to define an  *  employee, as well as the methods that must be defined in the  *  subclasses, are defined here.  */ abstract class Employee extends Person {   protected Employee(String name) {     super(name);   }   protected Employee(Employee employee) {     super(employee);   }   abstract public float calculatePay();   public static void main(String args[]) {     Employee employee = new HourlyEmployee("Cindy," 12.23f,         40.00f);     System.out.println(employee.calculatePay());     // Here the employee is changed to a SalariedEmployee.     // Note that a new object was created.     employee = new SalariedEmployee(employee, 2075.00f);     System.out.println(employee.calculatePay());   } } /**  *  A type of employee that is paid based on the number of hours  *  worked. To calculate the pay, the hours worked and the rate  *  per hour need to be entered.  */ class HourlyEmployee extends Employee {   private float ratePerHour;   private float hoursWorked;   public HourlyEmployee(String name, float ratePerHour,     float hoursWorked) {     super(name);     this.ratePerHour = ratePerHour;     this.hoursWorked = hoursWorked;   }   public HourlyEmployee(Employee employee, float ratePerHour,     float hoursWorked) {     super(employee);     this.ratePerHour = ratePerHour;     this.hoursWorked = hoursWorked;   }   public float calculatePay() {     return ratePerHour * hoursWorked;   } } /**  *  A type of employee that is paid a fixed amount each week  */ class SalariedEmployee extends Employee {   private float weeklySalary;   public SalariedEmployee(String name, float weeklySalary) {     super(name);     this.weeklySalary = weeklySalary;   }   public SalariedEmployee(Employee employee, float weeklySalary) {     super(employee);     this.weeklySalary = weeklySalary;   }   public float calculatePay() {     return weeklySalary;   } } 

end example

This implementation of the Employee class shows that just as with interfaces, classification using extends clauses is polymorphic. The specific calculatePay method that is called is not determined until the program is actually run and the specific object type chosen. So, what is the difference between an abstract class and an interface? In C++, no difference exists, so why did Java make the distinction? The first reason why the distinction is made is that abstract classes and interfaces are not semantically the same. Abstract classes are used when some base functionality can be extended using the Java extends clause. This base functionality is found in either variable definitions or implementation (not definition) of methods. An interface does not have any functionality to be extended, as it is simply an agreement between objects that some behavior will be available. What is nice about the Java keywords extend and implement is that they make this distinction clear.

The second reason to make the distinction is that it makes handling of objects easier. In Java, multiple interfaces can be implemented in a single object; however, because these interfaces are simply agreements, if two interfaces define the same method the implementing class must implement the method only once to meet the requirements of both interfaces. Thus, no confusion exists as to which method is to be used, as one method satisfies all interfaces. When some functionality, be it data or methods, is extended in Java, only single inheritance is allowed, so it is not possible to have conflicts regarding what behavior to use.

If, however, abstract classes are used for interfaces, subclasses must be able to extend (not simply implement) multiple super classes, even if those abstract classes are only used to implement true interfaces. This is because the abstract class could implement variables and methods, as there is no restriction to doing this in the language. Because multiple parent classes can define the same variables and methods, the language must implement the capability to decide how to clarify the methods and variables. Other problems, some very complex, including possible inheritance structures, lead to rules for handling conflicts that can be very confusing. Thus, the use of abstract classes to define a base behavior and interfaces to define promises of behavior in objects is actually a very nice feature in Java. It more correctly defines the semantics of what is happening and is much easier to implement and understand.

11.3.4 Using Classification to Mimic Composition

This section closes with an example of how not to use classification in a program. Nearly every problem that can be solved with classification can be solved with composition, and vice versa, but that does not mean that the solutions are equally as good. This was made clear to me in a class when I gave a design problem similar to the following to be done during a break in class. The problem, intended to illustrate composition, was to design a program for a social club. This club was to have a president, who was a person, and a roster of members, and it was to be headquartered in a building that had an address consisting of a street address, city, and state, all stored as strings. Each person was to have a name, consisting of a first name and last name, and an age. I thought this would be a trivial assignment and anticipated designs similar to the one in Exhibit 4 (Program11.3).

Exhibit 4: Program11.3: Club

start example

 /**  *  A class to define a name for a person  */ class Name {   String firstName;   String lastName; } /**  *  A class to define a person who has a name and age  */ class Person {   Name name;   int age; } /**  *  A class to define a building  */ class Building {   String streetAddress;   String city;   String state; } /**  *  This class shows that a club has a clubhouse,  *  a president, and a list of members.  */ class Club {   Person president;   Building clubHouse;   Person members[]; } 

end example

When the groups of students presented their designs, one stood out, which is shown in Exhibit 5 (Program11.4). This design used inheritance to collect all the data values needed to create the social club. This struck me as wrong. After all, the relationships were obviously "has-a" relationships, yet the students had implemented them as "is-a" relationships. Further, they had created a very deep inheritance hierarchy that I felt was confusing. No amount of discussion, however, could convince even one student that Exhibit 5 (Program11.4) was a better design than Exhibit 4 (Program11.3). They felt that the classification design allowed for more reuse and felt that the deep hierarchy was more a result of Java not allowing multiple inheritance, which would have allowed the social club to directly extend the Building class and the Person class.

Exhibit 5: Program11.4: Club

start example

 /**  *  A class that defines a name, consisting of a first name and  *  a last name  */ class Name {   String firstName;   String lastName; } /**  *  A person object still has a name and age, but the  *  name comes to the object through inheritance.  */ class Person extends Name {   int age; } /**  *  A building is still an address, but because of single  *  inheritance we include the attributes of a person so  *  that a club can be built from a building.  */ class Building extends Person {   String streetAddress;   String city;   String state; } /**  *  All the data needed for a club is now present except  *  the list of members. Note that this class has the same  *  data as the class presented in Program11.3.  */ class Club extends Building {   Person members[]; } 

end example

When it was pointed out that this example would also be limited if the application were to be changed to add an address to the person class, the students argued that this was more a limitation of Java than a valid criticism of their design. Having thought long and hard about this problem, I still feel that classification is an invalid solution to this problem that leads to a more confusing and potentially less extensible solution; however, the issue is left open to discussion as a problem at the end of the chapter.



 < Day Day Up > 



Creating Components. Object Oriented, Concurrent, and Distributed Computing in Java
The .NET Developers Guide to Directory Services Programming
ISBN: 849314992
EAN: 2147483647
Year: 2003
Pages: 162

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