|< Free Open Study >|
4.7 Semantic Constraints Between Classes
It is very common for containment hierarchies to have semantic constraints imposed upon them. In the case of the meal class, we wanted the appetizer to come first, the entree second, and the dessert third. We captured this information in the definition of the class. This is the preferable method for capturing semantic constraints because it makes it impossible for a user of the class to build objects that violate the semantic constraint. However, it is not always possible to implement constraints in this way. Consider another semantic constraint of the meal class. We expand the definition of trimmings to state that a trimming object is a potato and two vegetables. Let us assume that there are four possible vegetables: peas, corn, squash, and asparagus. The semantic constraint is that peas and corn is a disallowed combination of vegetables. If we follow our previous implementation of semantics constraints, we will attempt to capture the information in the class definition. We create the Squash-SquashMeal, the SquashPeasMeal, the SquashAsparagusMeal, the Squash-CornMeal, the AsparagusAsparagusMeal, the … fourteen different combinations of vegetables, each encapsulated as a separate meal class. We leave out definitions for the PeasCornMeal and the CornPeasMeal. This is an obvious proliferation of classes. Adding another choice of vegetable leads to 16 more classes, the next gives us 32 classes, etc. We end up with 2n - 2 classes for n vegetables.
Implementors will avoid this proliferation of classes by implementing the constraint in the behavior of the class. The typical place to implement the constraint is in the constructor of a class, that is, the operation of a class that builds its objects. In this way the class refuses to build objects that violate the semantic constraint. Whose constructor should check the constraint in this example? It depends on the domain of the application. Are all trimmings with peas and corn illegal, or is it just when they are used in a steak platter, or just when used in a meal? The class demanding the constraint is the one whose constructor tests the constraint. In general, designers try to place the constraint as low in the containment hierarchy as possible.
An alternative to capturing the constraint in the constructor of the class is to allow for the creation of illegal objects with respect to the constraint, but then check the state of the object in each method. For example, we may state that the relationship between a car class and an engine class is that of containment, namely, the car contains an engine. Does this imply that any car can contain any engine? We know that this is not possible. We first try to capture the constraint within the class definition by defining the ChevyChevelle454, the ChevyChevelle318, etc. We find a proliferation of classes, so we attempt to implement the constraint in the behavior of the class. Do we want to model the constraint in the constructor disallowing the creation of illegal objects, or do we want to allow for the creation of illegal objects but have the methods of the car class perform state tests on the object? It depends on our domain. Do we want it to be impossible for a mechanic to put the wrong engine in a car, or do we want to allow for that, leaving a car that will not work completely? If we want it to be impossible for a mechanic to put the wrong engine in a car, then we will test the semantic constraint in the constructor of the car class. If we want the latter design, then the constructor allows illegal cars to be built. The illegal cars might execute the start_engine method with no problems, but when sent the drive() message, they exhibit no behavior, due to the bad internal state of the car.
Consider the semantic constraint itself. The car needs to know which car/engine combinations are legal. How does the car get this information? It could be maintained in each type of car; for example, the ChevyChevelle class contains a list of allowed engines. This is a distributed design with respect to semantic information. People who have changed their own oil and needed to find the appropriate oil filter know of this solution. They pick up each type of oil filter and look for their car's make and model on the back of the filter. Another solution is to place a table of car and engine types in the car class. The cars method(s) can check for legal combinations in this way. Others feel that this clutters the car class a bit too much, so in order to distribute the system intelligence, they push the table into a third-party catalog class. A catalog object is consulted by the appropriate method(s) of the car class. Both of these solutions are equivalent to our do-it-yourselfer looking for his or her oil filter in a table or catalog that describes the allowed oil filters for each make and model of car. Either a distributed or a centralized solution to the implementation of the semantic constraint information may be more applicable than the other in a given domain. In general, a centralized solution is better in domains where existing constraint information is volatile, while stable constraint information allows for a more distributed model.
|< Free Open Study >|