25.4. Protected Variations
Problem How to design objects, subsystems, and systems so that the variations or instability in these elements does not have an undesirable impact on other elements?Solution Identify points of predicted variation or instability; assign responsibilities to create a stable interface around them.Note: The term "interface" is used in the broadest sense of an access view; it does not literally only mean something like a Java interface.Example For example, the prior external tax calculator problem and its solution with Polymorphism illustrate Protected Variations (Figure 25.1). The point of instability or variation is the different interfaces or APIs of external tax calculators. The POS system needs to be able to integrate with many existing tax calculator systems, and also with future third-party calculators not yet in existence.By adding a level of indirection, an interface, and using polymorphism with various ITaxCalculatorAdapter implementations, protection within the system from variations in external APIs is achieved. Internal objects collaborate with a stable interface; the various adapter implementations hide the variations to the external systems.Discussion This is a very important, fundamental principle of software design! Almost every software or architectural design trick in bookdata encapsulation, polymorphism, data-driven designs, interfaces, virtual machines, configuration files, operating systems, and much moreis a specialization of Protected Variations.Protected Variations (PV) was first published as a named pattern by Cockburn in [VCK96], although this very fundamental design principle has been around for decades under various terms, such as the term information hiding [Parnas72].
Mechanisms Motivated by Protected Variations
PV is a root principle motivating most of the mechanisms and patterns in programming and design to provide flexibility and protection from variationsvariations in data, behavior, hardware, software components, operating systems, and more.At one level, the maturation of a developer or architect can be seen in their growing knowledge of ever-wider mechanisms to achieve PV, to pick the appropriate PV battles worth fighting, and their ability to choose a suitable PV solution. In the early stages, one learns about data encapsulation, interfaces, and polymorphismall core mechanisms to achieve PV. Later, one learns techniques such as rule-based languages, rule interpreters, reflective and metadata designs, virtual machines, and so forthall of which can be applied to protect against some variation.For example:
Core Protected Variations Mechanisms
Data encapsulation, interfaces, polymorphism, indirection, and standards are motivated by PV. Note that components such as virtual machines and operating systems are complex examples of indirection to achieve PV.
Data-Driven Designs
Data-driven designs cover a broad family of techniques including reading codes, values, class file paths, class names, and so forth, from an external source in order to change the behavior of, or "parameterize" a system in some way at run-time. Other variants include style sheets, metadata for object-relational mapping, property files, reading in window layouts, and much more. The system is protected from the impact of data, metadata, or declarative variations by externalizing the variant, reading it in, and reasoning with it.
Service Lookup
Service lookup includes techniques such as using naming services (for example, Java's JNDI) or traders to obtain a service (for example, Java's Jini, or UDDI for Web services). Clients are protected from variations in the location of services, using the stable interface of the lookup service. It is a special case of data-driven design.
Interpreter-Driven Designs
Interpreter-driven designs include rule interpreters that execute rules read from an external source, script or language interpreters that read and run programs, virtual machines, neural network engines that execute nets, constraint logic engines that read and reason with constraint sets, and so forth. This approach allows changing or parameterizing the behavior of a system via external logic expressions. The system is protected from the impact of logic variations by externalizing the logic, reading it in, and using an interpreter.
Reflective or Meta-Level Designs
An example of this approach is using the java.beans.Introspector to obtain a BeanInfo object, asking for the getter Method object for bean property X, and calling Method.invoke . The system is protected from the impact of logic or external code variations by reflective algorithms that use introspection and meta-language services. It may be considered a special case of data-driven designs.
Uniform Access
Some languages, such as Ada, Eiffel, and C#, support a syntactic construct so that both a method and field access are expressed the same way. For example, aCircle.radius may invoke a radius():float method or directly refer to a public field, depending on the definition of the class. We can change from public fields to access methods, without changing the client code.
Standard Languages
Official language standards such as SQL provide protection against a proliferation of varying languages.
The Liskov Substitution Principle (LSP)
LSP [Liskov88] formalizes the principle of protection against variations in different implementations of an interface, or subclass extensions of a superclass.Liskov88].
Informally, software (methods, classes, …) that refers to a type T (some interface or abstract superclass) should work properly or as expected with any substituted implementation or subclass of T call it S . For example:
For this method addTaxes , no matter what implementation of ITaxCalculatorAdapter is passed in as an actual parameter, the method should continue to work "as expected." LSP is a simple idea, intuitive to most object developers, that formalizes this intuition.Structure-Hiding Designs In the first edition of this book, an important, classic object design principle called Don't Talk to Strangers or the Law of Demeter [Lieberherr88] was expressed as one of the nine GRASP patterns. Briefly, it means to avoid creating designs that traverse long object structure paths and send messages (or talk) to distant, indirect (stranger) objects. Such designs are fragile with respect to changes in the object structuresa common point of instability. But in the second edition the more general PV replaced Don't Talk to Strangers, because the latter is a special case of the former. That is, a mechanism to achieve protection from structure changes is to apply the Don't Talk to Strangers rules.Don't Talk to Strangers places constraints on what objects you should send messages to within a method. It states that within a method, messages should only be sent to the following objects:
public void addTaxes( ITaxCalculatorAdapter calculator, Sale sale )
{
List taxLineItems = calculator.getTaxes( sale );
// ...
}
- The this object (or self ).
- A parameter of the method.
- An attribute of this .
- An element of a collection which is an attribute of this .
- An object created within the method.
This code traverses structural connections from a familiar object (the Sale ) to a stranger object (the Payment ), and then sends it a message. It is very slightly fragile, as it depends on the fact that Sale objects are connected to Payment objects. Realistically, this is unlikely to be a problem.But, consider this next fragment, which traverses farther along the structural path:
class Register
{
private Sale sale;
public void slightlyFragileMethod()
{
// sale.getPayment() sends a message to a "familiar" (passes #3)
// but in sale.getPayment().getTenderedAmount()
// the getTenderedAmount() message is to a "stranger" Payment
Money amount = sale.getPayment().getTenderedAmount();
// ...
}
// ...
}
Or more generally:
public void moreFragileMethod()
{
AccountHolder holder =
sale.getPayment().getAccount().getAccountHolder();
// …
}
The example is contrived, but you see the pattern: Traversing farther along a path of object connections in order to send a message to a distant, indirect objecttalking to a distant stranger. The design is coupled to a particular structure of how objects are connected. The farther along a path the program traverses, the more fragile it is . Why? Because the object structure (the connections) may change. This is especially true in young applications or early iterations.Karl Lieberherr and his colleagues have done research into good object design principles, under the umbrella of the Demeter project. This Law of Demeter (Don't Talk to Strangers) was identified because of the frequency with which they saw change and instability in object structure, and thus frequent breakage in code that was coupled to knowledge of object connections.Yet, as will be examined in the following "Speculative PV and Picking your Battles" section, it is not always necessary to protect against this; it depends on the instability of the object structure. In standard libraries (such as the Java libraries) the structural connections between classes of objects are relatively stable. In mature systems, the structure is more stable. In new systems in early iteration, it isn't stable.In general, the farther along a path one traverses, the more fragile it is, and thus it is more useful to conform to Don't Talk to Strangers.Strictly obeying this lawprotection against structural variationsrequires adding new public operations to the "familiars" of an object; these operations provide the ultimately desired information, and hide how it was obtained. For example, to support Don't Talk to Strangers for the previous two cases:
public void doX()
{
F someF =
foo.getA().getB().getC().getD().getE().getF();
// …
}
Contraindications
// case 1
Money amount = sale.getTenderedAmountOfPayment();
// case 2
AccountHolder holder = sale.getAccountHolderOfPayment();
Caution: Speculative PV and Picking Your Battles
First, two points of change are worth defining:
- variation point Variations in the existing, current system or requirements, such as the multiple tax calculator interfaces that must be supported.
- evolution point Speculative points of variation that may arise in the future, but which are not present in the existing requirements.[4]
[4] In the UP, evolution points can be formally documented in Change Cases ; each describes relevant aspects of an evolution point for the benefit of a future architect.
PV is applied to both variation and evolution points.
A caution: Sometimes the cost of speculative "future-proofing" at evolution points outweighs the cost incurred by a simple, more "brittle" design that is reworked as necessary in response to the true change pressures. That is, the cost of engineering protection at evolution points can be higher than reworking a simple design.For example, I recall a pager message-handling system where the architect added a scripting language and interpreter to support flexibility and protected variation at an evolution point. However, during rework in an incremental release, the complex (and inefficient) scripting was removedit simply wasn't needed. And when I started OO programming (in the early 1980s) I suffered the disease of "generalize-itis" in which I tended to spend many hours creating superclasses of the classes I really needed to write. I would make everything very general and flexible (and protected against variations), for that future situation when it would really pay offwhich never came. I was a poor judge of when it was worth the effort.The point is not to advocate rework and brittle designs. If the need for flexibility and protection from change is realistic, then applying PV is motivated. But if it is for speculative future-proofing or speculative "reuse" with very uncertain probabilities, then restraint and critical thinking is called for.Novice developers tend toward brittle designs, intermediate developers tend toward overly fancy and flexible, generalized ones (in ways that never get used). Expert designers choose with insight; perhaps a simple and brittle design whose cost of change is balanced against its likelihood.Benefits
- Extensions required for new variations are easy to add.
- New implementations can be introduced without affecting clients.
- Coupling is lowered.
- The impact or cost of changes can be lowered.
Related Patterns and Principles
- Most design principles and patterns are mechanisms for protected variation, including polymorphism, interfaces, indirection, data encapsulation, most of the GoF design patterns, and so on.
- In [Pree95] variation and evolution points are called "hot spots."
Also Known As; Similar To PV is essentially the same as the information hiding and open-closed principles, which are older terms. As an "official" pattern in the pattern community, it was named "Protected Variations" in 1996 by Cockburn in [VCK96].
Information Hiding
David Parnas's famous paper On the Criteria To Be Used in Decomposing Systems Into Modules [Parnas72] is an example of classics often cited but seldom read. In it, Parnas introduces the concept of information hiding . Perhaps because the term sounds like the idea of data encapsulation, it has been misinterpreted as data encapsulation, and some books erroneously define the concepts as synonyms. Rather, Parnas intended information hiding to mean hide information about the design from other modules, at the points of difficulty or likely change . To quote his discussion of information hiding as a guiding design principle:
We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.
That is, Parnas's information hiding is the same principle expressed in PV, and not simply data encapsulationwhich is but one of many techniques to hide information about the design. However, the term has been so widely reinterpreted as a synonym for data encapsulation that it is no longer possible to use it in its original sense without misunderstanding.
Open-Closed Principle
The Open-Closed Principle (OCP), described by Bertrand Meyer in [Meyer88], is essentially equivalent to the PV pattern and to information hiding. A definition of OCP is:
Modules should be both open (for extension; adaptable) and closed (the module is closed to modification in ways that affect clients).
OCP and PV are essentially two expressions of the same principle, with different emphasis: protection at variation and evolution points. In OCP, "module" includes all discrete software elements, including methods, classes, subsystems, applications, and so forth.In the context of OCP, the phrase "closed with respect to X" means that clients are not affected if X changes. For example, "the class is closed with respect to instance field definitions" through the mechanism of data encapsulation with private fields and public accessing methods. At the same time, they are open to modifying the definitions of the private data, because outside clients are not directly coupled to the private data.As another example, "the tax calculator adapters are closed with respect to their public interface" through implementing the stable ITaxCalculatorAdapter interface. However, the adapters are open to extension by being privately modified in response to changes in the APIs of the external tax calculators, in ways that do not break their clients.