Design Patterns:Elements of Rousable Object-Oriented Software Implementation dependencies can cause problems when you're trving to reuse a subclass.Should any aspect of the inherited implementation not be appropriate for new prob ns, he p replaced byso more appropriate.This dependency limits flexibility and ultimately reusability One cure for this is to inherit only from abstract classes,since they usually provide little or no implementation. Obiect composition is defined dynamically at run-time through obiects acquiring eferences to other objects.Composition requires objects to inter es,which turn requires carefully designed interfaces 9t0 you from using one object with many others.But there is a payoff.Because objects are accessed solely through their interfaces,we don't break encapsulation.Any object can be replaced at run-time by another as long as it has the same type. Moreover.because an obiect's implementation will be written in terms of obiect interfaces there are substantially fewer implementation dependencies Object composition has another effect on system design.Favoring object composition over class inheritance helps you keep each class encapsulated and focused on one task.Your classes and class hierarchies will remain small and will be less likely to grow into unmanageable monsters.On the other hand,a design based on object composition will have more objects (if fewer classes),and the systea's vior will d dep end on their int onshing inst in one class That leads us to our second principle of object-oriented design: Favor object composition over class inheritance Ideally,you shouldn't have to create new components to achieve reuse.You should be able to get all the functionality you need just by assembling existing mponents through object composition.But this is rarely the case ,because th e set of available components is never quite rich enough in practice.Reuse by inheritance makes it easier to make new components that can be composed with old ones. Inheritance and object composition thus work together Nevertheless,our experience is that designers overuse inheritance as a reuse technique,and designs are often made more reusable (and simpler)by depending more on object composition.You'11 see object composition applied again and again in the design patterns Delegation Delegation is a way of making composition as powerful for reuse as inheritance [Lie86.J2911.In delegation. two obiects are involved in handling a reguest: 32
Design Patterns: Elements of Reusable Object-Oriented Software 32 Implementation dependencies can cause problems when you're trying to reuse a subclass. Should any aspect of the inherited implementation not be appropriate for new problem domains, the parent class must be rewritten or replaced by something more appropriate. This dependency limits flexibility and ultimately reusability. One cure for this is to inherit only from abstract classes, since they usually provide little or no implementation. Object composition is defined dynamically at run-time through objects acquiring references to other objects. Composition requires objects to respect each others' interfaces, which in turn requires carefully designed interfaces that don't stop you from using one object with many others. But there is a payoff. Because objects are accessed solely through their interfaces, we don't break encapsulation. Any object can be replaced at run-time by another as long as it has the same type. Moreover, because an object's implementation will be written in terms of object interfaces, there are substantially fewer implementation dependencies. Object composition has another effect on system design. Favoring object composition over class inheritance helps you keep each class encapsulated and focused on one task. Your classes and class hierarchies will remain small and will be less likely to grow into unmanageable monsters. On the other hand, a design based on object composition will have more objects (if fewer classes), and the system's behavior will depend on their interrelationships instead of being defined in one class. That leads us to our second principle of object-oriented design: Favor object composition over class inheritance. Ideally, you shouldn't have to create new components to achieve reuse. You should be able to get all the functionality you need just by assembling existing components through object composition. But this is rarely the case, because the set of available components is never quite rich enough in practice. Reuse by inheritance makes it easier to make new components that can be composed with old ones. Inheritance and object composition thus work together. Nevertheless, our experience is that designers overuse inheritance as a reuse technique, and designs are often made more reusable (and simpler) by depending more on object composition. You'll see object composition applied again and again in the design patterns. Delegation Delegation is a way of making composition as powerful for reuse as inheritance [Lie86, JZ91]. In delegation, two objects are involved in handling a request:
Design Patterns:Elements of Rousable Object-Oriented Software a receiving object delegates operations to its delegate.This is analogous to subclasses deferring requests to parent classes.But with inheritance, an inherited operation can always refer to the receiving object through the this member variable in C++and self in Smalltalk.To achieve the same effect with delegation,the receiver passes itself to the delegate to let the delegated operation refer to the receiver. For example,instead of making class window a subclass of Rectangle (because windows happen to be rectangular),the Window class might reuse the behavior of Rectangle by keeping a Rectangle instance variable and delegating Rectangle-specific behavior to it.In other words,instead of a Window being a Rectangle,it would have a Rectangle.window must now forward requests to its Rectangle instance explicitly.whereas before it would have inherited those operations The following diagram depicts the window class delegating its Area operation to a Rectangle instance Window Rectangle rea(9 heigh retum rectangle->Area() A plain arrowhead line indicates that a class keeps a reference to an instance of another class.The reference has an optional name,"rectangle"in this case. The main advantage of delegation is that it makes it easy to compose behaviors at run-time and to change the way they're composed.Our window can become circular at run-time aimly by replacing ita Rectangle instance with a circle instance assuming Rectangle and circle have the same type. Delegation has a disadvantage it shares with other techniques that make software more flexible through object composition:Dynamic,highly parameterized software is harder to understand than more static software.There are also run-time inetticiencies,but the human inetticiencies are more important in the long run. Delegation is a good design choice only when it simplifies more than it complicates It isn't easy to give rules that tell you exactly when to use delegation,because how effective it will be depends on the context and on how much experience you 33
Design Patterns: Elements of Reusable Object-Oriented Software 33 a receiving object delegates operations to its delegate. This is analogous to subclasses deferring requests to parent classes. But with inheritance, an inherited operation can always refer to the receiving object through the this member variable in C++ and self in Smalltalk. To achieve the same effect with delegation, the receiver passes itself to the delegate to let the delegated operation refer to the receiver. For example, instead of making class Window a subclass of Rectangle (because windows happen to be rectangular), the Window class might reuse the behavior of Rectangle by keeping a Rectangle instance variable and delegating Rectangle-specific behavior to it. In other words, instead of a Window being a Rectangle, it would have a Rectangle. Window must now forward requests to its Rectangle instance explicitly, whereas before it would have inherited those operations. The following diagram depicts the Window class delegating its Area operation to a Rectangle instance. A plain arrowhead line indicates that a class keeps a reference to an instance of another class. The reference has an optional name, "rectangle" in this case. The main advantage of delegation is that it makes it easy to compose behaviors at run-time and to change the way they're composed. Our window can become circular at run-time simply by replacing its Rectangle instance with a Circle instance, assuming Rectangle and Circle have the same type. Delegation has a disadvantage it shares with other techniques that make software more flexible through object composition: Dynamic, highly parameterized software is harder to understand than more static software. There are also run-time inefficiencies, but the human inefficiencies are more important in the long run. Delegation is a good design choice only when it simplifies more than it complicates. It isn't easy to give rules that tell you exactly when to use delegation, because how effective it will be depends on the context and on how much experience you
Design Patterns:glemente of Rousable Object-Oriented Software have with it.Delegation works best when it's used in highly stylized ways-that is,in standard patterns. several design patterns use delegation.The State (338),Strategy (349),and visitor (366)patterns depend on it.In the state pattern,an object delegates requests to a State object that represents its current state.In the Strategy pattern,an object delegates a specific request to an object that represents a strategy for carrying out the request.An obiect will only have one state,but it can have many strategies for different requests.The purpose of both patterns is to chang of an object by changing the objects to which it delegates requests.In visitor,the operation that gets performed on each element of an object structure is always delegated to the visitor object. other patterns use delegation less heavily.Mediator (305)introduces an obiect to mediate communication between other obiects.Sometimes the Mediator obiect implements operations simply by forwarding them to the other objects:other times it passes along a reference to itself and thus uses true delegation Chain of Responsibility(251)handles requests by forwarding them from one object to another along a chain of obiects.Sometimes this request carries with it a reference to the original object receiving the request,in which case the pattern is using delegation.Bridge (171)decouples an abstraction from its implementation.If the ract tor re hed y ma then the abstraction may simply delegate operations to that implementation Delegation is an extreme example of object composition.It shows that you can always replace inheritance with object composition as a mechanism for code reuse. Inheritance versus Parameterized Types Another (not strictly object-oriented)technique for reusing functionality is hrough param eterized types,also known as generics (Ada,Eiffel)and te plate (C++).This technique lets you define a type without specifying all the other types it uses.The unspecified types are supplied as parameters at the point of use.For example,a List class can be parameterized by the type of elements it To declare a list of integers,you supply the type "integer"as a pa to the List parameterized type. ist of string objects,you supply the "String"type as a parameter.The language implementation will create a customized version of the List class template for each type of element Parameterized types give us a third way (in addition to class inheritance and obiect composition)to compose behavior in obiect-oriented systems.Many designs can be implemented using any of these three technigues.To parameterize a sorting routine by the operation it re elements,we could make the omparison 34
Design Patterns: Elements of Reusable Object-Oriented Software 34 have with it. Delegation works best when it's used in highly stylized ways—that is, in standard patterns. Several design patterns use delegation. The State (338), Strategy (349), and Visitor (366) patterns depend on it. In the State pattern, an object delegates requests to a State object that represents its current state. In the Strategy pattern, an object delegates a specific request to an object that represents a strategy for carrying out the request. An object will only have one state, but it can have many strategies for different requests. The purpose of both patterns is to change the behavior of an object by changing the objects to which it delegates requests. In Visitor, the operation that gets performed on each element of an object structure is always delegated to the Visitor object. Other patterns use delegation less heavily. Mediator (305) introduces an object to mediate communication between other objects. Sometimes the Mediator object implements operations simply by forwarding them to the other objects; other times it passes along a reference to itself and thus uses true delegation. Chain of Responsibility (251) handles requests by forwarding them from one object to another along a chain of objects. Sometimes this request carries with it a reference to the original object receiving the request, in which case the pattern is using delegation. Bridge (171) decouples an abstraction from its implementation. If the abstraction and a particular implementation are closely matched, then the abstraction may simply delegate operations to that implementation. Delegation is an extreme example of object composition. It shows that you can always replace inheritance with object composition as a mechanism for code reuse. Inheritance versus Parameterized Types Another (not strictly object-oriented) technique for reusing functionality is through parameterized types, also known as generics (Ada, Eiffel) and templates (C++). This technique lets you define a type without specifying all the other types it uses. The unspecified types are supplied as parameters at the point of use. For example, a List class can be parameterized by the type of elements it contains. To declare a list of integers, you supply the type "integer" as a parameter to the List parameterized type. To declare a list of String objects, you supply the "String" type as a parameter. The language implementation will create a customized version of the List class template for each type of element. Parameterized types give us a third way (in addition to class inheritance and object composition) to compose behavior in object-oriented systems. Many designs can be implemented using any of these three techniques. To parameterize a sorting routine by the operation it uses to compare elements, we could make the comparison
Design Patterns:Elements of Rousable Object-Oriented Software 1.an operation implemented by subclasses (an application of Template Nethod (360) 2.the responsibility of an object that's passed to the sorting routine (Strategy (349),or 3.an argument of a C++template or Ada generic that specifies the name of the function to call to compare the elements. There are important differences between these techniques.Object you change the behavior being composed at run-time,but it also requires indirection and can be less efficient.Inheritance lets you provide default implementations for operations and lets subclasses override them.Parameterized types let you change the types that a class can use.But neither inheritance nor parameterized types can change at run-time.Which approach is best depends on your design and mentation constraints. None of the patterns in this book concerns parameterized types,though we use them on occasion to customize a pattern's C++implementation.Parameterized types aren't needed at all in a language like Smalltalk that doesn't have compile-time type checking. Relating Run-Time and Compile-Time structures -oriented program's run-time structure often bears little resemblance to its code structure.The code structure is frozen at compile-time;it consists of classes in fixed inheritance relationships.A program's run-time structure consists of rapidly changing networks of communicating objects.In fact,the two structures are largely independent.Trying to understand one from the other is like trying to unders tand the dyne is of living ecosystems from the static taxonomy of plants and animals,and vice versa. Consider the distinction between object aggregation and acquaintance and how differently they manifest themselves at compile-and run-times.Aggregation implies that one object owns or is responsible for another object.Generally we speak of an object having or being part or another object.Aggregation implies that an aggregate object and its owner have identical lifetimes Acquaintance implies that an object merely knows of another object.Sometimes acquaintance is called "association"or the "using"relationship.Acquainted oblects may request operations of each other,but they aren't responsible for each other.Acquaintance is a weaker relationship than aggregation and suggests much looser coupling between objects. Tn our dlagrams,a plain arrowhead line denotes acquaintance.An arrowhead line with a diamond at its base denotes aggregation 35
Design Patterns: Elements of Reusable Object-Oriented Software 35 1. an operation implemented by subclasses (an application of Template Method (360), 2. the responsibility of an object that's passed to the sorting routine (Strategy (349), or 3. an argument of a C++ template or Ada generic that specifies the name of the function to call to compare the elements. There are important differences between these techniques. Object composition lets you change the behavior being composed at run-time, but it also requires indirection and can be less efficient. Inheritance lets you provide default implementations for operations and lets subclasses override them. Parameterized types let you change the types that a class can use. But neither inheritance nor parameterized types can change at run-time. Which approach is best depends on your design and implementation constraints. None of the patterns in this book concerns parameterized types, though we use them on occasion to customize a pattern's C++ implementation. Parameterized types aren't needed at all in a language like Smalltalk that doesn't have compile-time type checking. Relating Run-Time and Compile-Time Structures An object-oriented program's run-time structure often bears little resemblance to its code structure. The code structure is frozen at compile-time; it consists of classes in fixed inheritance relationships. A program's run-time structure consists of rapidly changing networks of communicating objects. In fact, the two structures are largely independent. Trying to understand one from the other is like trying to understand the dynamism of living ecosystems from the static taxonomy of plants and animals, and vice versa. Consider the distinction between object aggregation and acquaintance and how differently they manifest themselves at compile- and run-times. Aggregation implies that one object owns or is responsible for another object. Generally we speak of an object having or being part of another object. Aggregation implies that an aggregate object and its owner have identical lifetimes. Acquaintance implies that an object merely knows of another object. Sometimes acquaintance is called "association" or the "using" relationship. Acquainted objects may request operations of each other, but they aren't responsible for each other. Acquaintance is a weaker relationship than aggregation and suggests much looser coupling between objects. In our diagrams, a plain arrowhead line denotes acquaintance. An arrowhead line with a diamond at its base denotes aggregation:
Design Patterns:Elements of Reusable Object-Oriented Software Aggregatee It's easy to confuse aggregation and acquaintance,because they are often implemented in the same way.In Smalltalk,all variables are references to other objects.There's no distinction in the programming language between aggregation and acquaintance.In C++,aggregation can be implemented by defining member variables that are real instances,but it's more common to define them as pointera nces.Acquaintance is implemented with pointers and references as well Ultimately,acquaintance and aggregation are determined more by intent than by explicit language mechanisms.The distinction may be hard to see in the compile-time structure,but it's signiticant.Aggregation relationships tend to be fewer and more permanent than acquaintance.Acquaintance es,in contrast,are made and remade more frequently,sometimes existing only for the duration of an operation.Acquaintances are more dynamic as well,making them more difficult to discern in the source code. With such disparity between a program's run-time and compile-time structures, it's clear that code won't reveal evervthing about how a system will work.The system's run-time atructure must be imposed more by the desig ner than the language The relationships between objects and their types must be designed with great care,because they determine how good or bad the run-time structure is Many design patterns (in particular those that have obiect scope)capture the distinction between compile-time and run-time structures explicitly.Composite (183)and Decorator (196)are especially useful for building complex run-time stru (3261 often hard to understand unless you know the pattern.Chain of Responsibility(251)also results in communication patterns that inheritance doesn't reveal.In general,the run-time structures aren't clear from the code until you understand the patterns. Designing for Change The key to maximizing reuse lies in anticipating new requirements and changes to existing requirements,and in designing your systems so that they can evolve accordingly. To design the system so that it's robust to such changes,you must consider how the system might need to change over its lifetime.A design that doesn't take change into ac ount risks major redesign in the future.Those changes might involve redefinition and mentation,client modification, retesting. 36
Design Patterns: Elements of Reusable Object-Oriented Software 36 It's easy to confuse aggregation and acquaintance, because they are often implemented in the same way. In Smalltalk, all variables are references to other objects. There's no distinction in the programming language between aggregation and acquaintance. In C++, aggregation can be implemented by defining member variables that are real instances, but it's more common to define them as pointers or references to instances. Acquaintance is implemented with pointers and references as well. Ultimately, acquaintance and aggregation are determined more by intent than by explicit language mechanisms. The distinction may be hard to see in the compile-time structure, but it's significant. Aggregation relationships tend to be fewer and more permanent than acquaintance. Acquaintances, in contrast, are made and remade more frequently, sometimes existing only for the duration of an operation. Acquaintances are more dynamic as well, making them more difficult to discern in the source code. With such disparity between a program's run-time and compile-time structures, it's clear that code won't reveal everything about how a system will work. The system's run-time structure must be imposed more by the designer than the language. The relationships between objects and their types must be designed with great care, because they determine how good or bad the run-time structure is. Many design patterns (in particular those that have object scope) capture the distinction between compile-time and run-time structures explicitly. Composite (183) and Decorator (196) are especially useful for building complex run-time structures. Observer (326) involves run-time structures that are often hard to understand unless you know the pattern. Chain of Responsibility (251) also results in communication patterns that inheritance doesn't reveal. In general, the run-time structures aren't clear from the code until you understand the patterns. Designing for Change The key to maximizing reuse lies in anticipating new requirements and changes to existing requirements, and in designing your systems so that they can evolve accordingly. To design the system so that it's robust to such changes, you must consider how the system might need to change over its lifetime. A design that doesn't take change into account risks major redesign in the future. Those changes might involve class redefinition and reimplementation, client modification, and retesting