Proponents of Service Oriented Architectures (SOA) oftentimes argue that one of its primary benefits is loose coupling. Some contend that services are inherently loosely coupled, and therefore make it easier than ever before to build complex applications by simply integrating their functions into a cohesive whole in order to achieve some larger objective. The skeptic in me recalls similar promises made in the past, so I have to ask will the mere adoption of such an architectural approach guarantee loose coupling? This article will explore the concept of coupling within the context of services by referring to an older definition of the term.
The use of the word coupling poses a semantics problem since there are several connotations for the word when we discuss services. Many who speak of SOA consider systems that use this approach to be loosely coupled because sometimes it is possible to change service behavior and the consumer might only need to rebind to the service without having to recompile. Such modifications might involve alterations of programmatic logic behind the service facade, or ones brought about through the application of policies. Others consider services to be loosely coupled because, through the use of open standards, they promise vendor, transport, and protocol neutrality. These are only a few of the forms of coupling, yet as software designers and developers we should consider the word coupling from yet another perspective. As was true with object-oriented designs, the coupling produced through the design of the software, especially where interfaces and signatures on operations are concerned, will continue to get us into trouble if we're not careful.
Coupling is a computer science term that is used to describe the strength of a connection or the nature of the dependencies that exist between one thing and another. Another way to put it is that coupling describes the degree to which one component depends upon another. Traditionally we have used coupling to describe the relationship that exists between routines (i.e. functions, procedures, etc.), but we can also extend the concept to include web pages, windows, classes, logical layers within an application, or the relationship that might exist between consumers and services.
When the strength of a connection between routines is high (i.e. one routine is highly dependent upon another), then we say that the routines are Tightly Coupled (a.k.a. High Coupling). Conversely, routines are considered to be Loosely Coupled (a.k.a. Low Coupling) when the connections are “relatively weak”.
Generally, we can judge how tightly coupled routines are by applying a simple litmus test. If we are able to make changes to the internal workings of one routine without having to make any changes to the calling routine, and we can do so without breaking the functionality of the dependent routine, then we have achieved loose coupling. If, however, the changes made to the called routine force us to alter the dependent routine, then we have a higher degree of coupling between the two. You can apply similar logic to analyze the level of coupling that exists between web pages, windows, classes, logical layers, and so on.
The implication made above is that loose coupling can generally be achieved when the thing (i.e. routine, class, service operation, etc.) upon which some other item is dependent is self contained and autonomous. Therefore, we can also say that encapsulation and information hiding tends to promote loose coupling as well. These concepts are especially important to keep in mind when designing services for consumers.
With respect to services, we can judge the degree of coupling between a consumer and a service by evaluating how easy it is to “link” them together so that they are able to collaborate to achieve some end, or to “unlink” them and replace the service upon which a consumer is dependent with some other service. The easier it is to link and unlink consumers and services, the more loosely coupled is the relationship between the two. It logically follows that we should seek to achieve loose coupling between consumers and services in order to facilitate maintenance and adaptability to change.
In the end, we will never be able to completely decouple the constituent parts of a system, nor do we want to if our applications are to do anything useful. Indeed, the efforts to achieve loose coupling can be taken to extremes resulting in overly complex designs that can be as hard to maintain as more tightly coupled approaches, so we really need to be pragmatic and consider the unique needs our systems.
Services can be thought of as being a set of operations that are usually implemented as methods on a class. Since methods or operations are essentially routines, then we can use the classic definitions for coupling which follow.
Simple-Data coupling occurs when data is passed to a routine using “simple” or primitive data types as parameters. For example ...
public bool SetCustomerData(int customerId, string firstName, string lastName)
Routines that use this style generally yield the loosest coupling according to the "classic definition" of the word coupling; this is less true with service-oriented designs. Keep in mind that as the number of parameters increase, ease of maintenance tends to decrease, and the level of coupling increases as well.
Some people use the following approach to both decrease coupling and allow for greater flexibility for the data passed to a service ...
public bool SetCustomerData(int customerId, string customerData)
This approach for passing the customerData parameter is known as XML String and is quite common in practice. The idea here is that the consumer will build an XML document and pass it as a string into the service. The service would then load the string into an XML document whose schema it will hopefully understand. While it is true this approach decreases coupling, I would recommend against it because an explicit contract that governs the type and structure of data passed does not exist. If the schema used by the consumer does not match the one expected by the service, things can go awry pretty quickly. Furthermore, it leaves open the possibility that the consumer might build XML that is not well formed. Note that passing data back to consumers in a similar fashion should probably be avoided as well.
Data-Structure coupling occurs when more complex data types are listed as the parameters in the operation definition, for example ...
public bool SetCustomerData(Customer customerData)
According to the classic definition of coupling, this results in a higher degree of coupling than Simple-Data coupling because now both the consumer and service must share a common and explicit definition of a non-standard data type. With service-oriented designs, this is actually the preferred approach.
The XSD for Customer data type might look like this (partial listing shown)...
<s:complexType name="Customer"> <s:attribute name="Id" type="s:int" use="required" /> <s:attribute name="FName" type="s:string" /> <s:attribute name="LName" type="s:string" /> </s:complexType>
Fortunately we can use C# and XML Serialization Attributes to not only define a class that we can work with programmatically behind the service facade, but also produce the schema that you see above. The C# to do this appears below ...
[XmlType("Customer")] public class Customer { public Customer() {;} [XmlAttribute("Id")] public int Id; [XmlAttribute("FName")] public string FirstName; [XmlAttribute("LName")] public string LastName; }
The fact that the consumer must have access to this type is really not a problem, rather, it is beneficial because the exact structure and type of data is made clear to all sides. Consider for a moment the advantage of using this approach versus the ambiguity that is inherent in the XML String approach. All sides have a contract that clearly defines what data the service can work with and how that data should be passed.
It should be noted that it is generally considered a bad practice to use Data-Structure coupling when only a small portion of the data in the complex type is used by the service. In such a case, you might consider refactoring the service operation to only use the parameters it really needs.
An evil style of Data-Structure coupling that should be avoided at all costs looks like this ...
public bool SetCustomerData(object customerData)
Unfortunately, the WSDL produced for this operation is lacking a meaningful type definition for the customerData parameter (partial listing shown)...
<wsdl:types> <s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org"> <s:element name="SetCustomerData"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="customerData" /> </s:sequence> </s:complexType> </s:element> <s:element name="SetCustomerDataResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="SetCustomerDataResult" type="s:boolean" /> </s:sequence> </s:complexType> </s:element> </s:schema> </wsdl:types>
When a parameter to a service uses object as its type, the consumer is allowed to pass any type of object, which means that this approach is quite flexible and loosely coupled. Regardless, I believe that this approach is even more problematic than the XML String approach because the service must properly cast the object, and we all know that consumers sometimes don’t do the right thing. In this case the consumer may pass some type of object that the service has no clue how to handle.
Finally we have another style of Data-Structure coupling that is an improvement upon the evil approach discussed above ...
public bool SetCustomerData(XmlElement customerData)
The WSDL produced for this operation is shown below (partial listing shown)......
<wsdl:types> <s:schema elementFormDefault="qualified" targetNamespace="http://tempuri.org"> <s:element name="SetCustomerData"> <s:complexType> <s:sequence> <s:element minOccurs="0" maxOccurs="1" name="customerData"> <s:complexType> <s:sequence> <s:any /> </s:sequence> </s:complexType> </s:element> </s:sequence> </s:complexType> </s:element> <s:element name="SetCustomerDataResponse"> <s:complexType> <s:sequence> <s:element minOccurs="1" maxOccurs="1" name="SetCustomerDataResult" type="s:boolean" /> </s:sequence> </s:complexType> </s:element> </s:schema> </wsdl:types>
In this style the consumer must at least build a well-formed document, and the service can easily validate it against known schemas. This yields an extremely flexible, open, and loosely coupled service operation definition. Still, I would recommend using the form of Data-Structure coupling where the Customer type appears in the operation definition because an explicit contract is provided to all consumers.
The use of datasets in services also results in Data-Structure coupling, unfortunately, now we’re creating a coupling that creates a dependency on the .Net framework, which in turn does not promote interoperability with non .Net platforms.
Note: The Windows Communication Foundation (a.k.a. Indigo) uses Data Contracts to define complex data types that can be passed to and from service operations. Data contracts are quite similar to the class with XML serialization attributes shown above. Whenever you employ Data Contracts on a service operation in WCF, you will be choosing Data-Structure Coupling.
With control coupling, the consumer passes instructions(e.g. a command, a flag, etc.) to the service in order to direct its internal behavior. The following code snippet illustrates this style of coupling ...
public string SetCustomerData(int transactionType, Customer customer)
The transactionType parameter listed above provides the consumer the means to tell the service whether it should add, update, or delete the data in the customerData parameter. The problem here is that the consumer has to know something about the internal operations of the service (in this case, the proper value of transactionType to pass) in order to work with the service. Since this would violate the idea that web services should be opaque (i.e. the details of their implementation should be hidden and encapsulated), this is a less than favorable approach. It would be better to provide explicit operations that don’t rely on the consumer to provide such control commands, for example ...
public bool AddCustomer(Customer customerData) ... public bool UpdateCustomer(Customer customerData) ... public bool DeleteCustomer(Customer customerData) ...
Global Data Coupling occurs when the two parties share some set of "global data". For example, if two objects were to operate upon some in-memory data structure, we would have this type of coupling. The potential risk here is that one party doesn’t know what the other one is doing, and it would be possible for one party to make certain changes to the common data that could adversely affect the other. Therefore, Global Data coupling creates a kind of dependency that is not readily apparent. Traditionally, this had been viewed as only being acceptable when one party used that shared data in a read-only fashion.
I can think of at least a couple of scenarios where such coupling might occur. The first case is stateful services, for example, one service operation might persist information in session state so that multiple calls to that operation or some other operation within the service can use that data without having to resubmit a whole message payload to the service. This might be ok depending upon how autonomous you believe service operations should really be.
The other is when a consumer and a service exist on the same machine and both are able to update some common set of data. In this case Global Data Coupling absolutely should be avoided. One of the dangers is that the service might be moved to another machine, in which case when the service tries to access the shared data, an exception would likely occur. Additionally, if services are to be autonomous with explicit boundaries, then all of the information they require to do their work should really be provided explicitly as messages.
Content Coupling happens when a caller is able to alter the internal data within a called routine. While this is possible in object-oriented programming scenarios when, for example, one class accesses a reference variable on another local or remote class, this shouldn’t be a problem with Service-Oriented Architectures because communication occurs via discrete XML messages that are serialized.
Now that we’ve refreshed our memories on the classic definitions for coupling from a developer's perspective, I would also like to address another issue that can lead to tight coupling with services. Some believe that the problem of chatty communications (i.e. a scenario when a client needs to call many operations on a server component in order to get some unit of work done) to be the unique province of distributed object systems. This tenet is utterly false! It is still possible to build chatty systems when we use SOA, which in turn leads to high coupling and typically poor performance. In order to reduce coupling we should refactor our designs to get more done with fewer calls. To explain what I mean, let’s start with a code snippet that illustrates a chatty style of communications between a consumer and a service …
Customer customer = WebProxy.GetCustomer( customerId); Order order = WebProxy.GetOrder( customer.LastOrderId ); OrderDetails orderDetails = WebProxy.GetOrderDetails( order ); OrderDetails backOrders = WebProxy.GetBackOrders( orderDetails );
This approach is similar to the Transaction Script Design Pattern, and it really isn’t appropriate for use in services due to the high cost of remote calls. To improve on this design, we might do something like this to not only reduce network latency, but to also achieve looser coupling ...
OrderDetails backOrders = WebProxy.GetBackOrders( customerId );
In this case the GetBackOrders operation would act as a façade and a mediator that would encapsulate all of the logic that was previously the responsibility of the consumer in the prior code snippet.
While Service-Oriented Architectures promote non-proprietary transports, protocols, vendor implementations, and so forth, and to a large degree allow us to change the service behavior without having to recompile the consumers, we still need to be aware of how service interfaces and the styles of communication we define can increase coupling, which in turn can lead to greater difficulties maintaining our systems.