(by Ian Robinson)
This article discusses some of the challenges in evolving a community of service providers and consumers. It describes some of the coupling issues that arise when service providers change parts of their contract, particularly document schemas, and identifies two well-understood strategies - adding schema extension points and performing "just enough" validation of received messages - for mitigating such issues. Both strategies help protect consumers from changes to a provider contract, but neither of them gives the provider any insight into the ways it is being used and the obligations it must maintain as it evolves. Drawing on the assertion-based language of one of these mitigation strategies - the "just enough" validation strategy - the article then describes the "Consumer-Driven Contract" pattern, which imbues providers with insight into their consumer obligations, and focuses service evolution around the delivery of the key business functionality demanded by consumers.
Last significant update: 12 Jun 06
Since the original publication, there are some updates to this article. There is an updated version in the ThoughtWorks Anthology and occasional blogs.
Contents
To illustrate some of the problems we encounter while evolving services, consider a simple ProductSearch service, which allows consumer applications to search our product catalogue. A search result has the following structure:
An example search result document looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Products xmlns="urn:example.com:productsearch:products">
<Product>
<CatalogueID>101</CatalogueID>
<Name>Widget</Name>
<Price>10.99</Price>
<Manufacturer>Company A</Manufacturer>
<InStock>Yes</InStock>
</Product>
<Product>
<CatalogueID>300</CatalogueID>
<Name>Fooble</Name>
<Price>2.00</Price>
<Manufacturer>Company B</Manufacturer>
<InStock>No</InStock>
</Product>
</Products>
The ProductSearch service is currently consumed by two applications: an internal marketing application and an external reseller's Web application. Both consumers use XSD validation to validate received documents prior to processing them. The internal application uses the CatalogueID, Name, Price and Manufacturer fields; the external application the CatalogueID, Name and Price fields. Neither uses the InStock field: though considered for the marketing application, it was dropped early in the development lifecycle.
One of the most common ways in which we might evolve a service is to add an additional field to a document on behalf of one or more consumers. Depending on how the provider and consumers have been implemented, even a simple change like this can have costly implications for the business and its partners.
In our example, after the ProductSearch service has been in production for some time, a second reseller considers using it, but asks that a Description field be added to each product. Because of the way the consumers have been built, the change has significant and costly implications both for the provider and the existing consumers, the cost to each varying based on how we implement the change. There are at least two ways in which we can distribute the cost of change between the members of the service community. First, we could modify our original schema and require each consumer to update its copy of the schema in order correctly to validate search results; the cost of changing the system is here distributed between the provider - who, faced with a change request like this, will always have to make some kind of change - and the consumers, who have no interest in the updated functionality. Alternatively, we could choose to add a second operation and schema to the service provider on behalf of the new consumer, and maintain the original operation and schema on behalf of the existing consumers. The cost of change is now constrained to the provider, but at the expense of making the service more complex and more costly to maintain.
Chief among the benefits of service-enabling an enterprise's application landscape are increased organizational agility and reduced overall cost of implementing change. An SOA increases organizational agility by placing high-value business functions in discrete, reusable services, and then connecting and orchestrating these services to satisfy core business processes. It reduces the cost of change by reducing the dependencies between services, allowing them to be rapidly recomposed and tuned in response to change or unplanned events.
A business can only fully realise these benefits, however, if its SOA enables services to evolve independently of one another. To increase service independence, we build services that share contracts, not types. Even so, we often end up having to evolve consumers at the same rate as the service provider, chiefly because we've made the consumers depend on a particular version of the provider's contract. In the end, service providers find themselves adopting a cautious approach to changing any element of the contract they offer their consumers; this, in part, because they cannot anticipate or gain insight into the ways in which consumers realise this contract. At worst, service consumers realise a provider contract and couple themselves to the provider by naively expressing the whole of a document schema within their internal logic.
Contracts enable service independence; paradoxically, they can also couple service providers and consumers in undesirable ways. Without introspecting the function and role of the contracts we implement in our SOA, we subject our services to a form of "hidden" coupling that we are rarely equipped to address in any systematic fashion. The absence of any programmatic insights into the ways in which a community of services has adopted a contract, and the lack of constraints on the implementation choices made by service providers and consumers, combine to undermine the purported benefits of SOA-enabling the enterprise. In short, the enterprise becomes burdened with services.
We can begin our investigations into the contract and coupling problems that bedevil our ProductSearch service by looking at the issue of schema versioning. The WC3 Technical Architecture Group (TAG) has described a number of versioning strategies that might help us evolve our service's message schemas in ways that mitigate our coupling problems. These strategies range from the excessively liberal none, which mandates that services must not distinguish between different versions of a schema, and must therefore tolerate all changes, to the exceedingly conservative big bang, which requires services to abort if they receive an unexpected version of a message.
Both extremes bring with them problems that inhibit the delivery of business value and exacerbate the total cost of ownership of the system. Explicit and implicit "no versioning" strategies result in systems that are alike in being unpredictable in their interactions, fragile, and costly to change downstream. Big bang strategies, on the other hand, give rise to tightly coupled service landscapes where schema changes ripple through providers and consumers, disrupting uptime, retarding evolution and reducing revenue generating opportunities.
Our example service community effectively implements a big bang strategy. Given the costs associated with enhancing the business value of the system, it is clear that the providers and consumers would benefit from a more flexible versioning strategy - what the TAG finding calls a compatible strategy - which provides for backwards- and forwards-compatible schemas. In the context of evolving services, backwards-compatible schemas enable consumers of newer schemas to accept instances of an older schema: a service provider built to handle new versions of a backwards-compatible request, say, could nonetheless still accept a request formatted according to an old schema. Forwards-compatible schemas, on the other hand, enable consumers of older schemas to process an instance of a newer schema. This is the sticking point for the existing ProductSearch consumers: if the search result schema had been made forwards-compatible when first put into production, the consumers would be able to handle instances of the new version of the search result without breaking or requiring modification.
Making schemas both backwards- and forwards-compatible is a well-understood design task, best expressed by the Must Ignore pattern of extensibility (see gthe papers by David Orchard and Dare Obasanjo). The Must Ignore pattern recommends that schemas incorporate extensibility points, which allow extension elements to be added to types and additional attributes to each element. The pattern also recommends that XML languages define a processing model that specifies how consumers process extensions. The simplest model requires consumers to ignore elements that they do not recognize - hence the name of the pattern. The model may also require consumers to process elements that have a "Must Understand" flag, or abort if they cannot understand them.
This is the schema on which we originally based our search results documents:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns="urn:example.com:productsearch:products"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="urn:example.com:productsearch:products"
id="Products">
<xs:element name="Products" type="Products" />
<xs:complexType name="Products">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Product">
<xs:sequence>
<xs:element name="CatalogueID" type="xs:int" />
<xs:element name="Name" type="xs:string" />
<xs:element name="Price" type="xs:double" />
<xs:element name="Manufacturer" type="xs:string" />
<xs:element name="InStock" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:schema>
Let's now roll back time and, from the outset of our service's lifetime, specify a forwards-compatible, extensible schema:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns="urn:example.com:productsearch:products"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="urn:example.com:productsearch:products"
id="Products">
<xs:element name="Products" type="Products" />
<xs:complexType name="Products">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Product">
<xs:sequence>
<xs:element name="CatalogueID" type="xs:int" />
<xs:element name="Name" type="xs:string" />
<xs:element name="Price" type="xs:double" />
<xs:element name="Manufacturer" type="xs:string" />
<xs:element name="InStock" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" name="Extension" type="Extension" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Extension">
<xs:sequence>
<xs:any minOccurs="1" maxOccurs="unbounded" namespace="##targetNamespace" processContents="lax" />
</xs:sequence>
</xs:complexType>
</xs:schema>
This schema includes an optional Extension element at the foot of each product. The extension element itself can contain one or more elements from the target namespace:
Now when we receive a change request to add a description to each product, we can publish a new schema with an additional Description element that the provider inserts into the extension container. This allows the ProductSearch service to return results that include product descriptions, and consumers using the new schema to validate the entire document. Consumers using the old schema will not break, though they will not process the description. The new results documents look like this:
<?xml version="1.0" encoding="utf-8"?>
<Products xmlns="urn:example.com:productsearch:products">
<Product>
<CatalogueID>101</CatalogueID>
<Name>Widget</Name>
<Price>10.99</Price>
<Manufacturer>Company A</Manufacturer>
<InStock>Yes</InStock>
<Extension>
<Description>Our top of the range widget</Description>
</Extension>
</Product>
<Product>
<CatalogueID>300</CatalogueID>
<Name>Fooble</Name>
<Price>2.00</Price>
<Manufacturer>Company B</Manufacturer>
<InStock>No</InStock>
<Extension>
<Description>Our bargain fooble</Description>
</Extension>
</Product>
</Products>
The revised schema looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns="urn:example.com:productsearch:products"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="urn:example.com:productsearch:products"
id="Products">
<xs:element name="Products" type="Products" />
<xs:complexType name="Products">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Product">
<xs:sequence>
<xs:element name="CatalogueID" type="xs:int" />
<xs:element name="Name" type="xs:string" />
<xs:element name="Price" type="xs:double" />
<xs:element name="Manufacturer" type="xs:string" />
<xs:element name="InStock" type="xs:string" />
<xs:element minOccurs="0" maxOccurs="1" name="Extension" type="Extension" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Extension">
<xs:sequence>
<xs:any minOccurs="1" maxOccurs="unbounded" namespace="##targetNamespace" processContents="lax" />
</xs:sequence>
</xs:complexType>
<xs:element name="Description" type="xs:string" />
</xs:schema>
Note that the first version of the extensible schema is forwards-compatible with the second, and that the second is backwards-compatible with the first. This flexibility, however, comes at the expense of increased complexity. Extensible schemas allow us to make unforeseen changes to an XML language, but by the same token, they provide for requirements that may very well never arise; in so doing, they obscure the expressive power that comes from a simple design, and frustrate the meaningful representation of business information by introducing meta-informational container elements into the domain language.
We'll not discuss schema extensibility further here. Suffice to say, extension points allow us to make backwards- and forwards-compatible changes to schemas and documents without breaking service providers and consumers. Schema extensions do not, however, help us manage the evolution of a system when we need to make what is ostensibly a breaking change to a contract.
As a value-add, our ProductSearch service includes in the search results a field indicating whether or not the product is currently in stock. The service populates this field using an expensive call into a legacy inventory system - a dependency which is costly to maintain. The service provider would like to remove this dependency, clean up the design, and improve the overall performance of the system - preferably without imposing any of the cost of change on the consumers. In speaking to the consumers' owners, the provider team discovers that none of the consumer applications actually do anything with this value; though expensive, it is redundant.
Unfortunately, with our existing setup, if we remove a required component - in this case, the InStock field - from our extensible schema, we will break existing consumers. To fix the provider, we have to fix the entire system: when we remove the functionality from the provider and publish a new contract, each consumer application will have to be redeployed with the new schema, and the interactions between services thoroughly tested. The ProductSearch service in this respect cannot evolve independently of its consumers: provider and consumers must all jump at the same time.
Our service community is frustrated in its evolution because each consumer implements a form of "hidden" coupling that naively reflects the entirety of the provider contract in the consumer's internal logic. The consumers, through their use of XSD validation, and to a lesser extent, static language bindings derived from a document schema, implicitly accept the whole of the provider contract, irrespective of their appetite for processing the component parts.
David Orchard provides some clues as to how we might have avoided this issue when he alludes to the Internet Protocol's Robustness Principle: "In general, an implementation must be conservative in its sending behaviour and liberal in its receiving behaviour". We can augment this principle in the context of service evolution by saying that message receivers should implement "just enough" validation: that is, they should only process data that contributes to the business functions they implement, and should only perform explicitly bounded or targeted validation of the data they receive - as opposed to the implicitly unbounded, "all-or-nothing" validation inherent in XSD processing.
One way we can target or bound consumer-side validation is to assert pattern expressions along the received message's document tree axes, perhaps using a structural tree pattern validation language like Schematron. Using Schematron, each consumer of the ProductSearch service can programmatically assert what it expects to find in the search results:
<?xml version="1.0" encoding="utf-8" ?>
<schema xmlns="http://www.ascc.net/xml/schematron">
<title>ProductSearch</title>
<ns uri="urn:example.com:productsearch:products" prefix="p"/>
<pattern name="Validate search results">
<rule context="*//p:Product">
<assert test="p:CatalogueID">Must contain CatalogueID node</assert>
<assert test="p:Name">Must contain Name node</assert>
<assert test="p:Price">Must contain Price node</assert>
</rule>
</pattern>
</schema>
Schematron implementations typically transform a Schematron schema such as this into an XSLT transformation that the message receiver can apply to a document to determine its validity.
Notice that this sample Schematron schema makes no assertions about elements in the underlying document for which the consuming application has no appetite. In this way, the validation language explicitly targets a bounded set of required elements. Changes to the underlying document's schema will not be picked up by the validation process unless they disturb the explicit expectations described in the Schematron schema, even if those changes extend to deprecating or removing formerly mandatory elements.
Here then is a relatively lightweight solution to our contract and coupling problems, and one that doesn't require us to add obscure meta-informational elements to a document. So let's roll back time once again, and reinstate the simple schema described at the outset of the article. But this time round, we'll also insist that consumers are liberal in their receiving behaviour, and only validate and process information that supports the business functions they implement (using Schematron schemas rather than XSD to validate received messages). Now when the provider is asked to add a description to each product, the service can publish a revised schema without disturbing existing consumers. Similarly, on discovering that the InStock field is not validated or processed by any of the consumers, the service can revise the search results schema - again without disturbing the rate of evolution of each of the consumers.
At the end of this process, the ProductSearch results schema looks like this:
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns="urn:example.com:productsearch:products"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
elementFormDefault="qualified"
targetNamespace="urn:example.com:productsearch:products"
id="Products">
<xs:element name="Products" type="Products" />
<xs:complexType name="Products">
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" name="Product" type="Product" />
</xs:sequence>
</xs:complexType>
<xs:complexType name="Product">
<xs:sequence>
<xs:element name="CatalogueID" type="xs:int" />
<xs:element name="Name" type="xs:string" />
<xs:element name="Price" type="xs:double" />
<xs:element name="Manufacturer" type="xs:string" />
<xs:element name="Description" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:schema>
The use of Schematron in the above example leads to some interesting observations about contracts between providers and consumers, with implications beyond document validation. In this section we draw out and generalize some of these insights and express them in terms of a pattern we call Consumer-Driven Contract.
The first thing to note is that document schemas are only a portion of what a service provider has to offer consumers to enable them to exploit its functionality. We call the sum total of these externalized exploitation points the provider contract.
A provider contract expresses a service provider's business function capabilities in terms of the set of exportable elements necessary to support that functionality. From a service evolution point of view, a contract is a container for a set of exportable business function elements. A non-normative list of these elements includes:
The definition of contract here is a little broader than the one we might usually offer when talking about services, but from a service evolution perspective it usefully abstracts the significant forces that impact our problem domain. That said, the definition is not meant to be exhaustive in terms of the kinds of elements a provider contract might contain: it refers simply to a logical set of exportable business function elements that are candidates for including in a service evolution strategy. From a logical point of view, this set of candidate elements is open, but in practice internal or external factors, such as interoperability requirements or platform limitations, may constrain the type of elements a contract can contain. For example, a contract belonging to a service that conforms to the WS-Basic profile will likely not contain policy elements.
Notwithstanding any such constraints, the scope of a contract is determined simply by the cohesion of its member elements. A contract can contain many elements and be broad in scope, or focus narrowly on only a few, just so long as it expresses some business function capability.
How do we decide whether to include a candidate contractual element in our provider contract? We do so by asking ourselves whether any of our consumers might reasonably express one or more expectations that the business function capability encapsulated by the element continue to be satisfied throughout the service's lifetime. We've already seen how consumers of our example service can express an interest in parts of the document schema exported by the service, and how they might assert that their expectations regarding this contractual element continue to be met. Thus, our document schema is part of our provider contract.
Provider contracts have the following characteristics:
If we decide to account for consumer expectations regarding the schemas we expose when evolving our service - and consider it worth our provider knowing about them - then we need to import those consumer expectations into the provider. The Schematron assertions in our example look very much like the kinds of tests that, if implemented by the provider, might help ensure the provider continues to meet its commitments to its clients. By implementing these tests, the provider gains a better understanding of how it can evolve the structure of the messages it produces without breaking existing functionality in the service community. And where a proposed change would in fact break one or more consumers, the provider will have immediate insight into the issue and be better able to address it with the parties concerned, accommodating their requirements or providing incentives for them to change as business factors dictate.
In our example, we can say that the set of assertions generated by all consumers expresses the mandatory structure of the messages to be exchanged during the period in which the assertions remain valid for their parent applications. If the provider were possessed of this set of assertions, it would be able to ensure that every message it sends is valid for every consumer insofar as the set of assertions is valid and complete.
Generalizing this structure, we can distinguish what we have already called the provider contract from the individual contractual obligations that obtain in instances of provider-consumer relationships, which we will now call consumer contracts. When a provider accepts and adopts the reasonable expectations expressed by a consumer, it enters into a consumer contract.
Consumer contracts have the following characteristics:
Consumer contracts allow us to reflect on the business value being exploited at any point in a provider's lifetime. By expressing and asserting expectations of a provider contract, consumer contracts effectively define which parts of that provider contract currently support the business value realized by the system, and which do not. This leads us to suggest that service communities might benefit from being specified in the first instance in terms of consumer contracts. In this view, provider contracts emerge to meet consumer expectations and demands. To reflect the derived nature of this new contractual arrangement, we call such provider contracts consumer-driven contracts or derived contracts.
The derivative nature of consumer-driven provider contracts adds a heteronomous aspect to the relationship between service provider and consumer. That is, providers are subject to an obligation that originates from outside their boundaries. This in no way impacts on the fundamentally autonomous nature of their implementations; it simply makes explicit the fact that services depend for success on their being consumed.
Consumer-driven contracts have the following characteristics:
The following table summarizes the characteristics of the three types of contract described in this article:
| Contract | Open | Complete | Number | Authority | Bounded |
|---|---|---|---|---|---|
| Provider | Closed | Complete | Single | Authoritative | Space/time |
| Consumer | Open | Incomplete | Multiple | Non-authoritative | Space/time |
| Consumer-Driven | Closed | Complete | Single | Non-authoritative | Consumers |
The Consumer-Driven Contract pattern recommends building service communities using consumer and consumer-driven contracts. The pattern does not however specify the form or structure consumer and consumer-driven contracts should adopt, nor does it determine how consumer expectations are communicated to the provider and asserted during the provider's lifetime.
Contracts may be expressed and structured in several ways. In their simplest form, consumer expectations can be captured in a spreadsheet or similar document and implemented during the design, development and testing phases of a provider application. By going a little further and introducing unit tests that assert each expectation, we can ensure that contracts are described and enforced in a repeatable, automated fashion with each build. In more sophisticated implementations, expectations can be expressed as Schematron- or WS-Policy-like assertions that are evaluated at runtime in the input and output pipelines of a service endpoint.
As with the structure of contracts, we have several options when it comes to communicating expectations between providers and consumers. Since the Consumer-Driven Contract pattern is implementation agnostic we could, given the appropriate organizational setup, transmit expectations simply by talking to other teams, or using email. Where the number of expectations and/or consumers grows too large to manage in this manner, we may consider introducing a contract service interface and implementation into the connected systems' infrastructure. Whatever the mechanism, it is likely communications will be conducted out-of-band and prior to any conversations that exercise the business functionality of the system.
Consumer-driven contracts offer two significant benefits when it comes to evolving services. First, they focus the specification and delivery of service functionality around key business value drivers. A service is of value to the business only to the extent it is consumed. Consumer-driven contracts tie service evolution to business value by asserting the value of exportable service community elements - the things that consumers require of providers to do their job. As a result, providers expose a lean contract that is clearly aligned with the business goals that underpin their consumers. Change - service evolution - only emerges where consumers express a clear need.
Of course, our ability to start with a minimal set of lean requirements and evolve our service as and when consumers demand presupposes that we are in a position to evolve, deploy and operate the service in a controlled and efficient manner. This is where the Consumer-Driven Contract pattern provides a second key benefit. Consumer-driven provider contracts give us the fine-grained insight and rapid feedback we require to plan changes and assess their impact on applications currently in production. In practice, this allows us to target individual consumers and provide incentives for them to relinquish an expectation that is stopping us from making a change that is not currently backwards- and/or forwards-compatible. By deriving our service providers from consumer contracts, we imbue them with a repository of knowledge and a feedback mechanism that we can draw on during the operations part of the system lifecycle.
In this article we've identified the motivation for introducing consumer-driven contracts into the service landscape, and thereafter described how the Consumer-Driven Contract pattern addresses the forces that determine service evolution. We will end by discussing the scope of the pattern's applicability, together with some of the issues that may arise while implementing consumer and consumer-driven contracts.
The Consumer-Driven Contract pattern is applicable in the context of either a single enterprise or a closed community of well-know services: more specifically, an environment in which providers can exert some influence over how consumers establish contracts with them. No matter how lightweight the mechanisms for communicating and representing expectations and obligations, providers and consumers must know about, accept and adopt an agreed upon set of channels and conventions. This inevitably adds a layer of complexity and protocol dependence to an already complex service infrastructure.
We've suggested that systems built around consumer-driven contracts are better able to manage breaking changes to contracts. But we don't mean to suggest that the pattern is a cure-all for the problem of breaking changes: when all's said and done, a breaking change is still a breaking change. We do believe, however, that the pattern provides many insights into what actually constitutes a breaking change, and as such may serve as the foundation for a service versioning strategy. Moreover, as we've already discussed, service communities that implement the pattern are better placed to anticipate the effects of service evolution. Provider development and operations teams in particular can more effectively plan their evolutionary strategies - perhaps by deprecating contractual elements for a specific period and simultaneously targeting recalcitrant consumers with incentives to move up to new versions of a contract.
Consumer-driven contracts do not necessarily reduce the coupling between services. Loosely-coupled services are relatively independent of one another, but remain coupled nonetheless. What the pattern does do, however, is excavate and put on display some of those residual, "hidden" couplings, so that providers and consumers can better negotiate and manage them.
We've discussed ways in which consumer and consumer-driven contracts express business value. But we should make clear that we do not regard such contracts as an index or measure of business value - they are not a business metric = and despite some superficial resemblances to specifications such as WS-Agreement and WSLA, they are not intended to express Service Level Agreements. The underlying assumption here is that services, by themselves, are of no value to the business; their value is in being consumed. By specifying services closer to where they are being used - by consumers - we aim to exploit business value in a lean, just-in-time fashion.
Finally, we should point out that there is a risk that allowing consumer contracts to drive the specification of a service provider may undermine the conceptual integrity of that provider. Services encapsulate discrete, identifiable, reusable business functions whose integrity should not be compromised by unreasonable demands falling outside their mandate.
You can listen to Ron Jacobs of Microsoft interviewing myself and Martin Fowler at the Microsoft Architect Insight Conference in March 2006. We discuss Consumer Driven Contracts in the context of changing architectures.
Ian Cartwright, Duncan Cragg, Martin Fowler, Robin Shorrock, Joe Walnes
|
|
|
© Copyright Martin Fowler, all rights reserved