B. Service Contract Design

The first part of this book addressed the system architecture: How to decompose the system into its components and services and how to compose the required behavior out of the services. This is not the end of the design and you must continue the process by designing the details of each service.

Detailed design is a vast topic, worthy of its own book. This appendix limits its discussion of detailed design to the most important aspect of the design of a service: the public contract that the service presents to its clients. Only after you have settled on the service contract can you fill in internal design details such as class hierarchies and related design patterns. These internal design details as well as data contracts and operation parameters are domain-specific and, therefore, outside the scope of this appendix. However, in the abstract, the same design principles outlined here for the service contract as a whole apply even at the data contract and parameter levels.

This appendix shows that even with a task as specific to your system as the design of contracts for your services, certain design guidelines and metrics transcend service technology, industry domains, or teams. While the ideas in this appendix are simple, they have profound implications for the way you go about developing your services and structuring the construction work.

Is This a Good Design?

To understand how to design the services, you must first recognize the attributes of a good or a bad design. Consider the system architecture in Figure B-1. Is this a good design for your system? The system design in Figure B-1 uses a single large component to implement all the requirements of the system. In theory, you could build any system this way, by putting all the code in one monstrous function, with hundreds of arguments and millions of nested lines of conditional code. Yet no one in their right mind would suggest that a single large thing is a good design. It is literally the canonical example of what not to do. According to Chapter 4, you also cannot validate such a design.

Figure B-1 Monolithic system design

Next, consider the design in Figure B-2. Is this a good design for your system? The system design in Figure B-2 uses a huge number of small components or services to implement the system (to reduce the visual clutter, the figure does not show cross-service interaction lines). In theory, you could build any system this way by placing every requirement in separate service. That, too, is not just a bad design, but another canonical example of what not to do. As with the previous case, you also cannot validate such a design.

Figure B-2 Super-granular system design

Finally, examine the system design in Figure B-3. Is this a good design for your system? While you cannot state that Figure B-3 is a good design for your system, you could say that it is certainly a better design than a single large component or an explosion of small components.

Figure B-3 Modular system design

Modularity and Cost

The ability to determine that the system design of Figure B-3 is better than the previous two is surprising. After all, you do not know anything about the nature of the system, the domain, the developers, or the technology—yet you intuitively know it is better. Whenever you evaluate a modular design, you are using a mental model described by Figure B-4.

Figure B-4 Size and quantity effect on cost [Image adopted and modified from Juval Lowy, Programming .NET Components, 2nd ed. (O’Reilly Media, 2003); Juval Lowy, Programming WCF Services, 1st ed. (O’Reilly Media, 2007); and Edward Yourdon and Larry Constantine, Structured Design (Prentice-Hall, 1979).]

When you build a system out of smaller building blocks such as services, you have to pay for two elements of cost: the cost of building the services and the cost of putting it all together. You can build a system at any point on the spectrum between one large service and countless little services, and Figure B-4 captures the effect of that decomposition decision on the cost of building the system.

Cost per Service

The implementation cost per service (the blue line in Figure B-4) represents some nonlinear behavior. As the number of services decreases, their size increases (up to one large monolith on the far-left side of the curve). The problem is that as the size of a service increases, its complexity increases in a nonlinear way. A service 2 times as big as another may be 4 times more complex, and a service 4 times as big may be 20 or 100 times more complex. Increased complexity, in turn, induces a nonlinear increase in cost. As a result, cost is a compounded, nonlinear, monotonically increasing function of size. Consequently, as the number of services decreases, service size increases, and with each size increase, the cost explodes in a nonlinear way. In contrast, with a system design that has a multitude of services (the far right side of Figure B-4), the cost per service is miniscule, approaching zero.

Integration Cost

The integration cost of services increases in a nonlinear way with the number of services. This, too, is the result of complexity—in this case, the complexity of the possible interactions. More services imply more possible interactions, adding more complexity. As mentioned in Chapter 12, due to connectivity and ripple effects, as the number of services (n) increases, complexity grows in proportion to n2 but can even be on the order of nn. This interaction complexity directly affects the integration cost, which is why the integration cost (the red line in Figure B-4) is also a nonlinear curve. Consequently, at the far right side of Figure B-4, the integration cost shoots up ever higher as the number of services increases. In contrast, at the far left side of the curve where there is perhaps only single large service, the integration cost approaches zero since there is nothing to integrate.

Area of Minimum Cost

With any given system you will always have to pay for both elements of cost (implementation cost and integration cost). The dashed green line in Figure B-4 represents the sum of these two cost elements, or the total system cost. As you can see, for any system there is an area of minimum cost, where the services are not too big and not too small, not too many and not too few. Whenever you design a system, you must bring it to the area of minimum cost (and keep it there). Note that you do not necessarily wish to be in the very minimum of the total cost curve, but merely in the area of minimum cost where the total system cost is relatively flat. Once the curve begins to level, the cost of finding the absolute minimum will exceed any savings in system cost. As mentioned in Chapter 4, every design effort always has a point of diminishing return where it is simply good enough.

What you must avoid are the edges of the chart, because these edges are nonlinearly worse and become many multiples (even dozens of times) more expensive. The challenge with building a nonlinearly more expensive system is that the tools all organizations have at their disposal are fundamentally linear tools. The organization can give you another developer and then another developer, or another month and then another month. But if the nature of the underlying problem is nonlinear, you will never catch up. Systems designed outside the area of minimum cost have already failed before anyone has written the first line of code.

As explained in Chapter 4, a good volatility-based decomposition provides the smallest set of building blocks that you can put together to satisfy all requirements—known and unknown, present and future. Such a decomposition yields a service count in the area of minimum cost, but it says nothing about their shape. Even when the decomposition follows The Method guidelines, keeping the services in the area of minimum cost requires you to design each service contract correctly.

Services and Contracts

Each service in the system exposes a contract to its clients. The contract is merely a set of operations that the clients can call. As such, the contract is the public interface that the service presents to the world. Many programming languages even use the interface keyword to define the service contract. While the service contract is an interface, not all interfaces are service contracts. Service contracts are a formal interface that the service commits to support, unchanged.

To use an analogy from the human world, life is full of both formal and informal contracts. An employment contract defines (often using legal jargon) the obligations of both the employer and the employee to each other. A commercial contract between two companies defines their interactions as a service provider and a service consumer. These are formal forms of interfacing, and the parties to the contract often face severe implications if they violate the contract or change its terms. In contrast, when you hail a taxi, there is an implied informal contract: The driver will take you safely to your destination, and you will pay for this service. Neither of you signed a formal contract describing the nature of that interaction.

Contracts as Facets

A contract goes beyond being just a formal interface: It represents a facet of the supporting entity to the outside world. For example, a person can sign an employment contract representing that person as an employee. That person could have other facets, but the employer only sees and cares about that particular facet. A person can sign additional contracts such as a land lease contract, a marriage contract, a mortgage contract, and so on. Each one of these contracts is a facet of the person: as an employee, as a landlord, as a spouse, or as a homeowner. Similarly, a service can support more than one contract.

From Service Design to Contract Design

Well-designed services are in the area of minimum cost of Figure B-4. Unfortunately, it is difficult to answer the fundamental question of what makes a good service in this area. What you can do is go through a series of reasonable reductions until you find a question that you can answer. The first reduction assumes a one-to-one ratio between services and their contracts. Given this assumption, you could relabel Figure B-4, replacing the word “Service” with the word “Contract,” and the behavior of the chart will remain unchanged.

In reality, a single service can support multiple contracts, and multiple services can support a specific contract. In these cases, the curves in Figure B-4 shift left to right or up and down, but their behavior remains the same.

Attributes of Good Contracts

Under the assumption that services and contracts are mapped one-to-one, you have transformed the question “What is a good service?” into the question “What is a good contract?” Good contracts are logically consistent, cohesive, and independent facets of the service. These attributes are best explained using analogies from daily life.

Would you sign an employment contract that states you can only work at the company so long as you live at a specific address? You would reject such a contract because it is logically inconsistent to condition your employment status on your address. After all, if you do the agreed-upon work to the expected standard, where you live is irrelevant. Good contracts are always logically consistent.

Would you sign an employment contract that does not specify how much you are paid? Again, you would reject it. Good contracts are always cohesive and contain all the aspects required to describe the interaction—no more, no less.

Would you make your marriage contract dependent on your employment contract? You would reject this contract because the independence of the contract is just as important. Each contract or facet should stand alone and operate independently of the other contracts.

These attributes also guide the process of obtaining the contract. Would you pay a real estate lawyer to craft a contract just for you to rent your apartment? Or would you search the web for an apartment rental contract, print the first search result, fill in the blanks with the address and the rent, and be done with it? If an online contract is good enough for millions of other rentals without being specific to any apartment (which would truly be a nontrivial achievement), would it not be good enough for you? The contract must have evolved to include all the cohesive details such as rent and to avoid the inconsistent things like where the renters work. It must also be independent of other contracts—that is, a true stand-alone facet.

Note that you are not searching for a better contract than anyone else is using. You simply want to reuse the very same contract that everyone else is using. It is precisely because it is so reusable that it is a good contract. The final observation is that logically consistent, cohesive, and independent contracts are reusable contracts.

Note that reusability is not a binary trait of a contract. Every contract lies somewhere on the spectrum of reusability. The more reusable the contract, the more it is logically consistent, cohesive, and independent. Imagine the contract in front of the service in Figure B-1. That contract is massive, and it is extremely specific for that particular service. It is certainly logically inconsistent because it is a bloated dumping ground for everything that the system does. The likelihood that anyone else in the world will ever reuse that service contract is basically zero.

Next, imagine the contract on one of the tiny services in Figure B-2. That contract is miniscule and extremely specialized for its context. Something so small cannot possibly be cohesive. Again, the likelihood that anyone else will ever reuse that contract is zero.

The services in Figure B-3 offer at least some hope. Perhaps the contracts on the services in Figure B-3 have evolved to include everything pertaining to their interactions—no more, no less. The small number of interactions also indicates independent facets. The contracts could very well be reusable.

Contracts as Elements of Reuse

An important observation is that the basic element of reuse is the contract, not the service itself. For example, the computer mouse I use to write this book is unlike any other mouse. Each part of it is not reusable. The case of the mouse was designed for this particular mouse model, and I cannot mount it on any other mouse (except another instance of the same model) without costly modification. However, the interface “mouse–hand” is reusable; I can operate that mouse, and so can you. Your mouse supports exactly the same interface; put differently, it reuses the interface. Many thousands of different mouse models exist, yet it is precisely the fact that across all models each reuses the same interface which is the ultimate indication of a good interface. In fact, the interface “mouse–hand” should be called “tool–hand” (see Figure B-5).

Figure B-5 Reusing interfaces [Figure inspired by Matt Ridley, The Rational Optimist: How Prosperity Evolves (HarperCollins, 2010). Images: Mountainpix/Shutterstock; New Africa/Shutterstock.]

Our species has been reusing the “tool–hand” interface since prehistoric times. While no grain of stone from the stone axe is reusable in the mouse, and no piece of electronics from the mouse is useful in the stone axe, both reuse the same interface. Good interfaces are reusable, while the underlying services never are.

Factoring Contracts

When designing the contracts for your services, you must always think in terms of elements of reuse. That is the only way to assure that even after architecture and decomposition, your services will remain in the area of minimum cost. Note that the obligation to design reusable contracts has nothing to do with whether someone will actually end up reusing the contracts. The degree of actual reuse or demand for the contract by other parties is completely immaterial. You must design the contracts as if they will be reused countless times in perpetuity, across multiple systems including your current one and those of your competitors. A simple example will go a long way to demonstrate this point.

Design Example

Suppose you need to implement a software system for running a point-of-sale register. The requirements for the system likely have use cases for looking up an item’s price, integrating with inventory, accepting payment, and tracking loyalty programs, among others. All of this can easily be done using The Method and the appropriate Managers, Engines, and so on. For illustration purposes, suppose the system needs to connect to a barcode scanner and read an item’s identifier with it. The barcode scanner device is nothing more than a Resource to the system, so you need to design the service contract for the corresponding ResourceAccess service. The requirements for the barcode scanner access service are that it should be able to scan an item’s code, adjust the width of the scanning beam, and manage the communication port to the scanner by opening and closing the port. You could define the IScannerAccess service contract like so:

interface IScannerAccess
{
   long ScanCode();
   void AdjustBeam();
   void OpenPort();
   void ClosePort();
}

The IScannerAccess service contract supports the required features of a scanner. This easily enables different types of service providers, such as the BarcodeService and the QRCodeService to implement the IScannerAccess contract:

class BarcodeScanner : IScannerAccess
{...}
class QRCodeScanner : IScannerAccess
{...}

You may feel content because you have reused the IScannerAccess service contract across multiple services.

Factoring Down

Sometime later, the retailer contacts you with the following issue: In some cases it is better to use other devices, such as a numerical keypad, to enter item code. However, the IScannerAccess contract assumes the underlying device uses some kind of an optical scanner. As such, it is unable to manage non-optical devices such as numerical keypads or radio frequency identification (RFID) readers. From a reuse perspective, it is better to abstract the actual reading mechanism and rename the scanning operation to a reading operation. After all, which mechanism the hardware device uses to read the item code should be irrelevant to the system. You should also rename the contract to IReaderAccess and ensure there is nothing in the contract’s design that precludes all types of code readers from reusing the contract. For example, the AdjustBeam() operation is meaningless for a keypad. It is better to break up the original the IScannerAccess into two contracts, and factor down the offending operation:

interface IReaderAccess
{
   long ReadCode();
   void OpenPort();
   void ClosePort();
}
interface IScannerAccess : IReaderAccess
{
   void AdjustBeam();
}

This enables now proper reuse of IReaderAccess:

class BarcodeScanner : IScannerAccess
{...}
class QRCodeScanner : IScannerAccess
{...}
class KeypadReader : IReaderAccess
{...}
class RFIDReader : IReaderAccess
{...}

Factoring Sideways

With that change done, more time passes, and the retailer decides to have the software also control the conveyer belt attached to the point-of-sale workstation. This requires the software to start and stop the belt, as well as manage its communication port. While the conveyer belt uses the same kind of communication port as the reading devices, the belt cannot reuse IReaderAccess because the contract does not support a conveyer belt, and the belt cannot read codes. Furthermore, there is a long list of such peripheral devices, each with its own functionality, and the introduction of every one of them will duplicate parts of the other contracts.

Observe that every change in the business domain leads to a reflected change in the system’s domain. This is the hallmark of a bad design. A good system design should be resilient to changes in the business domain.

The root problem is that IReaderAccess is a poorly designed contract. Even though all the operations are things a reader should support, ReadCode() is not logically related to OpenPort() and ClosePort(). The reading operation involves one facet of the device, as a provider of codes, something that is essential to the business of the retailer (it is an atomic business operation), while the port management involves a different facet relating to the entity as communication device. In this regard, IReaderAccess is not logically consistent: It is a mere grab-bag of every requirement for the service. IReaderAccess is more like the design in Figure B-1 than anything else.

A better approach is to factor sideways the OpenPort() and ClosePort() operations to a separate contract called ICommunicationDevice:

interface ICommunicationDevice
{
   void OpenPort();
   void ClosePort();
}
interface IReaderAccess
{
   long ReadCode();
}

The implementing services will have to support both contracts:

class BarcodeScanner : IScannerAccess,ICommunicationDevice
{...}

Note that the sum of work inside BarcodeScanner is exactly the same as with the original IScannerAccess. However, because the communication facet is independent of the reading facet, other entities (such as belts) can reuse the ICommunicationDevice service contract and support it:

interface IBeltAccess
{
   void Start();
   void Stop();
}
class ConveyerBelt : IBeltAccess,ICommunicationDevice
{...}

This design allows you to decouple the communication–management aspect of the devices from the actual device type (be it barcode readers or conveyer belts).

The real issue with the point-of-sale system was not the specifics of the reading devices, but rather the volatility of the type of devices connected to the system. Your architecture should rely on volatility-based decomposition. As this simple example shows, the principle extends to the contract design of individual services as well.

Factoring Up

Factoring operations into separate contracts (like ICommunicationDevice out of IReaderAccess) is usually called for whenever there is a weak logical relation between the operations in the contract.

Sometimes identical operations are found in several unrelated contracts, and these operations are logically related to their respective contracts. Not including them would make the contract less cohesive. For example, suppose that for safety reasons, the system must immediately abort all devices. In addition, all devices must support some kind of diagnostics that assures they operate within safe limits. Logically, aborting is just as much a scanner operation as reading, and just as much a belt operation as starting or stopping.

In such cases, you can factor the service contracts up, into a hierarchy of contracts instead of separate contracts:

interface IDeviceControl
{
   void Abort();
   long RunDiagnostics();
}
interface IReaderAccess : IDeviceControl
{...}
interface IBeltAccess : IDeviceControl
{...}

Contract Design Metrics

The three contract design techniques (factoring down to a derived contract, factoring sideways to a new contract, or factoring up to a base contract) result in fine-tuned, smaller, and more reusable contracts. Having more reusable contracts is certainly a benefit, and the smaller contracts are necessary when starting with bloated contracts. But too much of a good thing is a bad thing. The risk is that you keep doing this until eventually you end up with contracts that are too granular and fragmented, as in Figure B-2. You therefore need to balance the two opposing forces: the cost of implementing the service contracts versus the cost of putting them together. The way to strike the balance is to use design metrics.

Measuring Contracts

It is possible to measure contracts and rank them from worst to best. For example, you could measure the cyclomatic complexly of the code. You are unlikely to have a simple implementation of a large complex contract, and the complexity of overly granular contracts would be horrendous. You could measure the defects associated with the underlying services: Low-quality services are likely the result of the complexity of poor contracts. You could measure how many times each contract is reused in the system, and how many times a contract was checked out and changed: Clearly a contract that is reused everywhere and has never changed is a good contract. You could assign weights to these measurements and rank the results. I have conducted such measurements for years across different technology stacks, systems, industries, and teams. Regardless of this diversity, some uniform metrics have emerged that are valuable in gauging the quality of contracts.

Size Metrics

Service contracts with just one operation are possible, but you should avoid them. A service contract is a facet of an entity, and that facet must be pretty dull if you can express it with just one operation. Examine that single operation and ask yourself some questions about it. Does it use too many parameters? Is it too coarse, so that you should factor the single operation into several operations? Should you factor this operation into an existing service contract? Is it something that should best reside in the next subsystem to be built? I cannot tell you which corrective action to take, but I can tell you that a contract with just one operation is a red flag, and you must investigate it further.

The optimal number of service contract operations is between 3 and 5. If you design a service contract with more operations, perhaps 6 to 9, you are still doing relatively well, but you have started to drift away from the area of minimum cost in Figure B-4. Take a look at the operations and determine whether any can be collapsed into other operations, since it is quite possible to over-factor operations. If the service contract has 12 or more operations, it is very likely a poor design. You should look for ways to factor the operations into either separate service contracts or a hierarchy of contracts. You must immediately reject contracts with 20 operations or more, as there are no possible circumstances where such contracts are benign. Such a contract is certain to plaster over some grave design mistake. You must have little tolerance for large contracts because of their nonlinear effects on development and maintenance costs.

Interestingly, in the human world you always use contract size metrics to assess the quality of a contract. For example, would you sign an employment contract that has just one sentence? You would decline this contract because there is no way that a single sentence (or even a single paragraph) could capture all the aspects of you as an employee. Such a contract is certain to leaves out crucial details such as liability or termination and may incorporate other contracts with which you are unfamiliar. On the other extreme, would you sign an employment contract containing 2000 pages? You would not even bother to read it, regardless of what it promises. Even a 20-page contract is cause for concern: If the nature of the employment requires so many pages, the contract is likely taxing and complex. But if the contract has 3–5 pages, you may not sign it, but you will read it carefully. From a reuse perspective, note that the employer will likely furnish you with the same contract as all other employees have. Anything other than total reuse would be alarming.

Avoid Properties

Many service development stacks deliberately do not have property semantics in contract definitions, but you can easily circumvent those by creating property-like operations, such as the following:

string GetName();
string SetName();

In the context of service contracts, avoid properties and property-like operations. Properties imply state and implementation details. When the service exposes properties, the client knows about such details, and when the service changes the client (or clients) would change along with it. You should not bother clients with the use of properties or even the knowledge of them. Good service contracts allow clients to invoke abstract operations without caring about the actual implementation. The clients simply invoke operations and let the service worry about how to manage its state.

A good interaction between a service provider and a service consumer is always behavioral. That interaction should be phrased in terms of DoSomething(), such as Abort(). How the service goes about doing that should be of no concern to the client. This, too, mimics real life: It is always better to tell than to ask.

Avoiding properties is also a good practice in any distributed system. It is always preferable to keep the data where the data is, and only invoke operations on it.

Limit the Number of Contracts

A service should not support more than one or two contracts. Since contracts are independent facets of the service, if the service supports three or more independent business aspects, it suggests the service may be too big.

Interestingly, you can derive the number of contracts per service using the estimation techniques of Chapter 7. Using only orders of magnitude, should the number of contracts per service be 1, 10, 100, or 1000? Clearly, 100 or 1000 contracts is a poor design, and even 10 contracts seems very large. So, in order of magnitude, the number of contracts per service is 1. Using the “factor of 2” technique, you can narrow the range further: Is the number of contracts more like 1, 2, or 4? It cannot be 8 because that is almost 10, which is already ruled out. So the number of contracts per service is between 1 and 4. This is still a wide range. To reduce the uncertainty, you can use the PERT technique, with 1 as the lowest estimation, 4 as the highest, and 2 as the likely number. The PERT calculation yields 2.2 as the number of contracts per service:

2.2=1+4*2+46

In practice, in well-designed systems, the majority of services I have examined had only one or two contracts, with a single contract as the more common case. Of the services with two or more contracts, the additional contracts were almost always non-business-related contracts that captured aspects such as security, safety, persistence, or instrumentation, and those contracts were reused across other services.

Using Metrics

The service contract design metrics are evaluation tools, not validation tools. Complying with the metrics does not mean you have a good design—but violating the metric implies you have a bad design. As an example, consider the first version of IScannerAccess. That service contract has 4 operations, right in the middle of the range of the 3 to 5 operations metric, yet the contract was logically inconsistent.

Avoid trying to design to meet the metrics. Like any design task, service contract design is iterative in nature. Spend the time necessary to identify the reusable contract your service should expose, and do not worry about the metrics. If you violate the metrics, keep working until you have decent contracts. Keep examining the evolving contracts to see if they are reusable across systems and projects. Ask yourself if the contracts are logically consistent, cohesive, and independent facets. Once you have devised such contracts, you will see that they match the metrics.

The Contract Design Challenge

The ideas and techniques discussed in this appendix are straightforward, self-evident, and simple. Designing contracts is an acquired skill, and practice goes a long way toward getting it done quickly and correctly. However, there is a big difference between “simple” and “simplistic.” While the ideas in this appendix are simple, they are far from simplistic. Indeed, life is full of ideas that are simple but not simplistic. For example, you may wish to be healthy. That is a simple idea that may involve changes to your diet, lifestyle, daily routine, and even work—none of which is simplistic.

Coming up with reusable service contracts is a time-consuming, highly contemplative task. It is absolutely paramount to get the contracts right, or you will face a nonlinearly worse problem (see Figure B-4). The real challenge is not designing the contracts (which is simple enough), but rather getting management’s support to do so. Most managers are unaware of the consequences of incorrect contract design. By rushing to implementation, they will cause the project to fail. This is especially the case with a junior hand-off (see Chapter 14).

Even senior developers may require mentorship to be able to design contracts correctly, and you, as the architect, can guide and train them. This will enable you to make the contract design part of each service life cycle. With a junior team, you cannot trust the developers to come up with correct reusable contracts; most likely, they will come up with service contracts resembling Figure B-1 or Figure B-2. You must use the approach of Chapter 14 to either carve up the time to design the contracts before work begins or, preferably, use a few senior skilled developers to design the contracts of the next set of services in parallel to the construction activities for the current set of services (see Figure 14-6). You should use the concepts of this appendix and Figure B-4 to educate your manager on what it really takes to ship well-designed services.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset