11.4 Choosing Composition or Classification

 < Day Day Up > 



11.4 Choosing Composition or Classification

As with deciding whether to use aggregation or association, trying to figure out whether to use classification or composition is not as straightforward as figuring out whether the relationship is a "has-a" or an "is-a" relationship. As with everything, the best answer is to do what produces the best solution to the problem to be solved. As stated before, nearly any problem that can be implemented as a "has-a" relationship can be implemented as an "is-a" relationship, and vice versa. So, the choices made should reflect what is most appropriate for the problem.

As discussed in Chapter 10, solutions that use classification allow easier access to the underlying definition in the base class but lead to less robust designs. So, a good general rule to follow is to first look at the problem to determine the presence of "is-a" or "has-a" relationships. For "has-a" relationships, choose composition, as composition has very few problems as compared to classification. Examine "is-a" relationships more closely. Make sure that choosing classification does bring with it hidden problems; if no problems appear to exist, classification should be used, as it will provide the easiest reuse for the underlying class.

This section describes five checks to identify problems in classification relationships. If these problems exist, using classification will likely result in poor designs. The first check is to make sure the relationship being described is, in fact, an "is-a" relationship in the context of the problem being solved. The second is to make sure that that the object type is not mutable, that it cannot change its type. The third check is to make sure that the class is created to represent a true type and not simply a role for an object. The fourth check is to make sure that an object cannot take on multiple types. Finally, a check should be made to ensure that no other constraints in the problem definition or program implementation would be hindered by using classification.

11.4.1 Check to Ensure That the Relationship Is an "Is-a" Relationship

The "is-a" and "has-a" checks on a relationship are useful because they are so easy to understand. The larger question is whether they are generally correct when applied to a problem. For example, in the employee problem shown in Exhibit 3 (Program11.2), it might seem obvious that an hourly employee (HourlyEmployee) "is an" employee, thus classification should be used in the design. To suggest that HourlyEmployee "has an" employee flies in the face of the intuition and experience of most designers, and so is simply discarded out of hand. However, are the intuition and experience of a designer relevant to the question of how to design this particular program? The answer should obviously be "no," as it is the context of the problem that should determine the approach. Too often designers and programmers bring their own view of the world into solving problems and make choices based on their preconceived ideas and biases. This is what makes this check so easy to ignore, but absolutely vital.

For example, consider the HourlyEmployee and SalariedEmployee example. Some company (a temporary employment firm) might need to solve a problem where a certain number of employee slots (some hourly and some salaried) are allocated to a particular project; when an employee is assigned to a slot, his pay is determined by the slot he occupies. In this problem context, an employee is not hourly or salaried; instead, HourlyEmployee "has an" employee who will be paid for doing the work. In the context of the specific problem to be solved, the bias of programmers to create a designs that represent how they view the problem must be overcome to produce a design that actually represents the problem to be solved.

This shows why the "is-a" and "has-a" rule must be carefully applied. A designer needs to understand the problem to be solved, and any preconceived notions about how the world works must be set aside. If "is-a" does apply to the relationship, then the next step is to make sure that no other constraints suggest using composition rather than classification.

11.4.2 Be Sure the Object Type Is Not Mutable

Once a relationship is determined to be an "is-a" relationship, the next check is to be sure that the object type is not mutable. A mutable object type occurs when an object can change type, such as when Hourly-Employee is promoted to a SalariedEmployee position. To understand why this is a problem, remember that inheritance objects that instantiate the subclass actually represent a single object (allocation of memory), not a collection of objects. Therefore, when changing the object type from HourlyEmployee to SalariedEmployee, the employee part of the object must be copied to create a new SalariedEmployee object, and the HourlyEmployee object is then discarded. Depending on how the problem is defined and how the application is implemented, this might or might not be a problem, but it should be considered.

A classification solution to the employee program was given in Exhibit 3 (Program11.2); a composition solution is given here in Exhibit 6 (Program11.5). Note that the simplicity of the classification solution is somewhat lost, as the composition solution requires a static constructor to be used in an abstract factory pattern to produce the object of the correct type. Thus, some of the elegance of the object models is lost, but now the type of employee can be changed without losing the reference to the original employee object.

Exhibit 6: Program11.5: Composition Solution to Modeling Employees

start example

 /**  *  This class shows how to create an employee when the  *  employee is a basic "abstract" type. In this case, the  *  constructor for the employee is private, so the employee  *  can only be created by calling one of the static methods to  *  create a specific employee type. This use of a static method  *  to create a specific type of employee is sometimes called a  *  "factory pattern."  *  *  Note that because we use a composition design, it is easy  *  to change the object from an hourly to a salaried employee  *  without losing track of the employee object.  */ class Employee extends Person{   private PayType payType;   private Employee(String name) {     super(name);   }   public static Employee createHourlyEmployee(String name,     float ratePerHour, float hoursWorked) {     Employee employee = new Employee(name);     employee.payType = new HourlyPayType(ratePerHour,         hoursWorked);     return employee;   }   public static Employee createSalariedEmployee(String name,       float weeklySalary) {     Employee employee = new Employee(name);     employee.payType = new SalariedPayType(weeklySalary);     return employee;   }   public void changeToSalaried(float weeklySalary) {     payType = new SalariedPayType(weeklySalary);   }   public void changeToHourly(float ratePerHour, float       hoursWorked) {     payType = new HourlyPayType(ratePerHour, hoursWorked);   }   public float calculatePay() {     return payType.calculatePay();   }   public static void main(String args[]) {     Employee employee = createHourlyEmployee("Cindy," 12.23f,         40.00f);     System.out.println(employee.calculatePay());     // Here the employee is changed to a SalariedEmployee.     // Note that no new object is created.     employee.changeToSalaried(2075.00f);     System.out.println(employee.calculatePay());   } } /**  * The base class for an employee  */ class Person {   private String name;   public Person(String name) {     this.name = name;   }   public Person(Person person) {     this.name = person.name;   }   public void setName(String name) {     this.name = name;   }   public String getName(String name) {     return name;   } } /**  *  An interface that sets the employee to the correct type  *  and allows the employee's pay to be calculated  */ interface PayType {   public float calculatePay(); } /**  *  A class that specifies an hourly employee  */ class HourlyPayType implements PayType {   private float ratePerHour;   private float hoursWorked;   public HourlyPayType(float ratePerHour, float hoursWorked) {     this.ratePerHour = ratePerHour;     this.hoursWorked = hoursWorked;   }   public float calculatePay() {     return ratePerHour * hoursWorked;   } } /**  *  A class that specifies a salaried employee  */ class SalariedPayType implements PayType {   private float weeklySalary;   public SalariedPayType(float weeklySalary) {     this.weeklySalary = weeklySalary;   }   public float calculatePay() {     return weeklySalary;   } } 

end example

Changing an object's reference, as is done in the classification solution, produces two problems. The first occurs in the case of many references to the object. For example, an Employee object could be stored in a Hashtable to be looked up in a human resources system and as a vector of dependents for another employee. When the object reference is stored in multiple places in a program, whenever it is changed it must be updated everywhere it is stored; for the employee, in this case, both the human resource's Hashtable and the dependent's vector must have the object reference changed. This type of problem results in many knock-on effects. If not done correctly it can result in multiple copies of the "same" object existing in the program. Options that worked in one release of the program may not work in the next release, and it quickly becomes nearly impossible to track down the errors or to ensure that all of the relevant structures are updated. Because composition leaves the reference to the employee object unchanged, these problems are avoided.

A less significant but still important concern regarding creating new objects when the type changes is performance. Allocating and deallocating objects can be expensive operations in a computer, and problems could occur if the object is changed frequently in the program. Generally speaking, unless the use of the object is limited and well defined, it is best to avoid these problems with mutable object types and to use composition when an object can change type.

The third consideration is that objects that are mutable are often used to store the history of the object. For example, if the problem definition calls for storing a history of the pay types for an employee, there is no easy way in a classification design for an employee to keep track of the former object type. The pay type is part of the object and thus cannot be separated from the object. If a history of the type changes is needed, it is almost always necessary to use composition in a design. This does not mean that if objects can change their types that classification should not be used; however, care should be taken in considering the design because of the problems that could arise. All things being equal, if an object type can be changed, a design using composition should be seriously considered.

11.4.3 Check If the Type Simply Represents a Role

This check is very similar to the one used in Section 11.4.3. The problems that result from mutable object types are often the result of inheritance being used to represent a role for the object in a program. For example, the pay type of an employee (hourly or salaried) might not be an intrinsic quality of an employee, but simply a state that the employee is in while the program is running. It is the fact that the state is changeable (hence, the role that the object plays in the program is changeable) that caused problems with the employee type in Section 11.4.2. When a type is simply used to represent a role for an object, it is normally best to represent it not with a type but with a state variable. In the case of the employee in Section 11.4.2, a better design for the program is provided in Exhibit 6 (Program11.5). Here, composition is used to implement the payType state using an interface to call the correct calculatePay method. This question of role can also be used to explain some cases where a design uses multiple inheritance and to suggest alternative designs. Multiple inheritance occurs when a type extends more than one base class. For example, consider Exhibit 7 (Program11.6), which shows the design of an AmphibiousVehicle as inheriting from a LandVehicle and a WaterVehicle (note that this example does not compile in Java, as Java does not allow multiple inheritance). Some programmers would argue that an amphibious vehicle is a combination of vehicles that can run on land and ones that can run on water, so this is a natural design. However, note that LandVehicle and WaterVehicle both have a maxSpeed method. In the case of the AmphibiousVehicle, which method should be used? The answer obviously depends on whether the AmphibiousVehicle is on land or in water, which is a state of the vehicle. Noting this fact leads to the compositional design with an explicit state shown in Exhibit 8 (Program11.7).

Exhibit 7: Program11.6: Amphibious Vehicle Designed Using Multiple Inheritance

start example

 abstract class Vehicle {   abstract public float getMaxSpeed(); } class LandVehicle extends Vehicle {   float maxSpeed;   public float getMaxSpeed() {     return maxSpeed;   } } class WaterVehicle extends Vehicle {   float maxSpeed;   public float getMaxSpeed() {     return maxSpeed;   } } public class AmphibiousVehicle extends LandVehicle, WaterVe- hicle   { } 

end example

Exhibit 8: Program11.7: Amphibious Vehicle Designed Using Composition and Explicit State

start example

 abstract class Vehicle {   abstract public float getMaxSpeed(); } class LandVehicle extends Vehicle {   float maxSpeed;   public float getMaxSpeed() {     return maxSpeed;   } } class WaterVehicle extends Vehicle {   float maxSpeed;   public float getMaxSpeed() {     return maxSpeed;   } } public class AmphibiousVehicle {   public static final int ON_WATER = 0;   public static final int ON_LAND = 1;   private int myState = ON_LAND;   WaterVehicle waterVehicle = new WaterVehicle();   LandVehicle landVehicle = new LandVehicle();   public float getMaxSpeed() {     if (myState = = ON_WATER)       return waterVehicle.getMaxSpeed();     else       return landVehicle.getMaxSpeed();   } } 

end example

11.4.4 Check for Subclasses with Multiple Roles

As was stated in Section 11.4.2 and 11.4.3, if an object can change type or represents a role in a program that can change, it should be designed using composition. This type of behavior can also cause problems when an object can take on multiple roles. To illustrate this, an example of the jobs people have in a town is used. In this town, people are doctors, lawyers, mayors, and programmers. To make the problem easier, assume that, once an object of a given type is created, it will never change. Also assume that a person can have multiple roles. For example, the mayor might also be a doctor. A new type, Doctor-Mayor, would have to be created to represent objects of this type. But, instead, what if the mayor was also a lawyer? The type Lawyer-Mayor would have to be created. And, if the lawyer was a doctor and a mayor, a Lawyer-Mayor-Doctor class would be needed. In fact, for this simple example, 16 different classes would have to be created. In general this type of problem requires 2n different classes and is not supportable. The point is that, even when the classes do not represent roles and the object types are not mutable, if the types can be combined it is possible for the system to require a large number of classes. When this happens, it is best to rewrite the design using composition.

11.4.5 Check for Compatibility with the Program Implementation

The final consideration in choosing a design is to make sure that someone will actually be able to implement it. This might sound inane, as what designer would ever design a system that could not be implemented? It should take most programmers only a few years of experience to realize that often the design is isolated from the implementation, and that designers often make decisions based on what they feel would make a nice system. This leads to designs with serious flaws when it actually comes time to implement them.

To see how implementation details can be impacted negatively by a design, the design of the simple expression tree program from Chapter 5, Exhibit 14 (Program5.5), is again considered; however, in this example, the program simply takes operators and operands and performs a calculation on them. The main focus here is the design of the operator object. Because the operator can occur many times in a program, a single instance of each type of operator is created, greatly reducing the number of objects the program needs to create. Each time an instance of that operator type is needed, that single static instance is returned. [1] This can be accomplished using either composition or classification, and the designs are largely equivalent. Examples of both designs are provided in Exhibit 9 (Program11.8) and Exhibit 10 (Program11.9).

Exhibit 9: Program11.8: Operator

start example

 import java.util.Hashtable; /**  * The operator class. Other than defining the static methods to  * retrieve the operator type, this method does nothing except    define  * the abstract method "calculate" which all operators must    define.  */ abstract public class Operator {      private static Hashtable operators;      static {           operators = new Hashtable();           operators.put("+", new AddOperator());           operators.put("-", new SubtractOperator());      }     public static Operator getOperator(String operatorSymbol) {            return((Operator) operators.get(operatorSymbol));      }      public static boolean isOperator(String operatorSymbol) {           return operators.containsKey(operatorSymbol);      }      abstract public double calculate(double d1, double d2);      // Unit Test Application      public static void main(String args[]) {           // Calculate 3+5-2           System.out.println( getOperator("-").calculate(                               getOperator("+").calculate(3.0,                               5.0), 2.0));      }      /**       *  class to define an operator that does addition.       */      static private class AddOperator extends Operator {           public double calculate(double d1, double d2) {               return (d1 + d2);           }           public String toString() {                return "+";           }      }      /**       *    class to define an operator that does subtraction       */      static private class SubtractOperator extends Operator {           public double calculate(double d1, double d2) {                return (d1 - d2);           }           public String toString() {                return "-";           }      } } 

end example

Exhibit 10: Program11.9: Add and Subtract Operator Classes Using Composition

start example

 import java.util.Hashtable; /**  * This class defines the operator.  Note that unlike program    11.8,  * delegation is used to do the calculation.   An interface    is created  * which defines how an operation works, and that interface    is called  * from the operators calculate method.  */ public class Operator {      private static Hashtable operators;      private String operatorSymbol;      private Evaluator evaluator;      static {           operators = new Hashtable();           operators.put ("+", new Operator                         ("+", new AddEvaluator()));           operators.put ("-", new Operator                         ("-", new SubtractEvaluator()));      }    private Operator(String operatorSymbol, Evaluator evaluator) {           this.operatorSymbol = operatorSymbol;           this.evaluator = evaluator;      }      public static boolean isOperator(String operatorSymbol) {           return operators.containsKey(operatorSymbol);      }    public static Operator getOperator(String operatorSymbol) {           return((Operator) operators.get(operatorSymbol));      }      public double calculate(double d1, double d2) {           return evaluator.calculate(d1, d2);      }      public String toString() {           return (operatorSymbol);      }      // Unit Test Application      public static void main(String args[]) {           // Calculate 3+5-2           System.out.println( getOperator("-").calculate(                               getOperator("+").calculate                                   (3.0, 5.0), 2.0));      }      /**       *  interface for define how to do a calculation.       */      private interface Evaluator {           public double calculate(double d1, double d2);      } /**        *  Class to do an add calculation        */      static private class AddEvaluator implements Evaluator {           public double calculate(double d1, double d2) {                return (d1 + d2);           }       }      /**       *  Class to do a subtract calculation       */    static private class SubtractEvaluator implements Evaluator {           public double calculate(double d1, double d2) {                return (d1 - d2);           }      } } 

end example

Using this single instance of each operator type can result in a problem if at a later time it is decided that what is actually needed is to be able to use this operator class with an expression tree, as implemented in Program 5.5. If the designer specifies a composition relationship (an OperatorNode has an operator and two child nodes), the design is easy to implement [2] and is shown in Program11.10 (Exhibit 11 and Exhibit 12). Many designers, though, will get to the point of asking the "is-a/has-a" question, will determine that an OperatorNode "is-an" operator, and will feel that settles the issue; however, a much larger issue than if the OperatorNode "is-an" or "has-an" operator must be acknowledged. The Operator part of the object only has one instance in the entire program for each operator type, but OperatorNode exists for each occurrence of an operator in a tree. Therefore, the relationship between Operator and OperatorNode is one to many. This leads to a logical contradiction when classification is used to create an object. There is only one instance of the object that contains all the data and methods for the object, both from the super class and from the child class, but the Operator requires one instance of that object, and the OperatorNode requires many instances of that same object. It cannot be both one and many, so classification cannot be used to implement the OperatorNode class.

Exhibit 11: Program11.10a: Add and Subtract Operator Classes Using Classification

start example

 import java.util.Hashtable; /**  *  This class defines the operator. Note that, unlike Program11.8,  *  delegation is used to do the calculation. An interface  *  is created that defines how an operation works, and that  *  interface is called from the operator's calculate method.  */ public class Operator {   private static Hashtable operators;   private String operatorSymbol;   private Evaluator evaluator;   static {     operators = new Hashtable();     operators.put("+," new Operator("+," new AddEvaluator()));     operators.put("-," new Operator("-," new SubtractEvaluator         ()));   }   private Operator(String operatorSymbol, Evaluator evaluator) {     this.operatorSymbol = operatorSymbol;     this.evaluator = evaluator;   }   public static boolean isOperator(String operatorSymbol) {     return operators.containsKey(operatorSymbol);   }   public static Operator getOperator(String operatorSymbol) {     return((Operator) operators.get(operatorSymbol));   }   public double calculate(double d1, double d2) {     return evaluator.calculate(d1, d2);   }   public String toString() {     return (operatorSymbol);   }   // Unit Test Application   public static void main(String args[]) {     // Calculate 3 + 5 - 2     System.out.println(getOperator("-").calculate(                          getOperator("+").calculate(3.0, 5.0),                                                       2.0));   } } /**  *  Interface for defining how to do a calculation  */ interface Evaluator {   public double calculate(double d1, double d2); } /**  *  A class to do an add calculation  */ class AddEvaluator implements Evaluator {   public double calculate(double d1, double d2) {     return (d1 + d2);   } } /**  *  A class to do a subtract calculation  */ class SubtractEvaluator implements Evaluator {   public double calculate(double d1, double d2) {     return (d1 - d2);   } } 

end example

Exhibit 12: Program11.10b: Implementing an Operator Node Using Classification

start example

 import java.util.Hashtable; interface Node {   public void printTree();   public double evaluate(); } public class OperatorNode implements Node {   Operator operator;   Node left, right;   public OperatorNode(String operatorSymbol,        Node left, Node right) {     this.operator = Operator.getOperator(operatorSymbol);     this.left = left;     this.right = right;   }   public void printTree() {     System.out.print(" (");     left.printTree();     System.out.print(" " + operator.toString());     right.printTree();     System.out.print(")");   }   public double evaluate() {   return operator.calculate(left.evaluate(), right.evaluate       ());   }   public static void main(String args[]) {     Node root;     // ((5 - 3) + 1)     root = new OperatorNode("+",       new OperatorNode("-",         new OperandNode(5), new OperandNode(3)),       new OperandNode(1));     root.printTree();     System.out.println(" = "+ root.evaluate());     // (5-(3+1))     root = new OperatorNode("-",        new OperandNode(5),        new OperatorNode("+",          new OperandNode(3), new OperandNode(1)));     root.printTree();     System.out.println(" = "+ root.evaluate());   } } class OperandNode implements Node {   double value;   public OperandNode(float value) {     this.value = value;   }   public void printTree() {     System.out.print(" " + value);   }   public double evaluate() {     return value;   } } 

end example

As this example shows, anytime a class must be extended composition is nearly always a safer choice than classification. As shown in Program11.10 (Exhibit 11 and Exhibit 12), composition could be used to create the OperatorNode from either Exhibit 9 (Program11.8) or Exhibit 10 (Program11.9). (Note that Program11.10 [Exhibits 11 and 12] will work with either Exhibit 9 [Program11.8] or Exhibit 10 [Program11.9] without any changes, but classification could not.) If the final use of a class is not known in a design, it is almost always better to use composition, as it will allow the most latitude in later modifications of the design.

[1]This design pattern is referred to as a factory pattern. When used with the classification classes, the objects are called singletons, as there is only one instance of the class. The purpose of this book is not to define design patterns or to comment on their use; however, because these patterns occur here, they are identified for the benefit of readers doing further research into this area.

[2]This design is an example of a flyweight pattern.



 < 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