April 26th, 2006
As I mentioned in the last article, coupling describes how dependant one object is on another object (that it uses).
- OOAD 101 Lesson 1: What is an object and what is object-oriented?
- OOAD 101 Lesson 2: Coupling and cohesion trump reuse and inheritance
- OOAD 101 Lesson 3: Understanding Coupling
- OOAD 101 Lesson 4: Understanding cohesion
To look at different levels of coupling, lets use our card game example. Say we have two classes: Deck and Card. The following shows the potential coupling from tight to loose:
- Deck uses the internals of Card : Say Card had a field called “card_name” and Deck referred to that field directly. Well later, if we change the name of the “card_name” field we will break Deck. Furthermore, if we wanted to use another object to store all the card properties, we would have to go to all the places where Deck referred to card_name and change it to use this new object.
- Deck only uses the methods of Card, but creates Card directly : in this case, Deck uses a getCardName() method to access the card_name. Now, when we change card_name, Deck is no longer affected. The problem here is we may need to introduce a new base class for Card. Lets say for example that we created Card to use XML to store its data. But we get a new requirement where some of the card types need to store the data in SQL. Well because Deck refers to Card directly, we are forced to create a new base class that both XMLCard and SQLCard sub-class from AND we need to update Deck.
- Deck only sees Card via an interface (IGenericCard) that Card implements, but Deck still creates Card directly : Another solution to the above situation is to create an IGenericCard interface that XMLCard and SQLCard implement . XMLCard and SQLCard are free to be two completely different classes. The problem here is that ultimately Deck is still creating the cards directly. So every time I add a new card type, I will need to update Deck so that it can create that card type. And if I have 600 card types…
- Deck only sees IGenericCard and uses a factory to create Card : The way to solve the above coupling problem is to have deck use some other object (typically called a factory) create the card. The only downside to this is that in the end, you still have a direct reference to the IGenericCard object. So, if for example, you wanted the card to perform its functions asynchronously, you couldn’t. Or, if you needed Card to be implemented in c++, but Deck in Java, you would be stuck as well.
- Deck communicates with Card via a message : If you needed the Deck object on a Windows client, and the IGenericCard object implemented on a server, you would need the two objects to be so loosely coupled that they interacted with each other via messages. In this case you could completely change the implementation, and event the programming language of IGenericCard and not ever impact Deck.
Formally, I have experienced the following levels of coupling:
Field level coupling: This level maps to “Deck uses the internal of Card”. Namely, given two objects A and B, A modifies or references B’s fields directly.
Method level coupling: This level maps to “Deck only uses the methods of Card, but…”. To move to this level, we create getters and setters for B’s fields and A uses those rather than accessing the fields directly. A still creates B directly using one of B’s constructors, and all references to instances of B are of type class B – this is know as having a reference to a concrete class.
Constructor level coupling: This level maps to “Deck only sees Card via an interface…”. To move to this level, we create an interface I that B implements. After creating an instance of class B, A references that instance via the interface I. Class A however is still coupled to Class B via its constructor.
API level coupling: This level maps to “Deck only sees IGenericCard and uses…”. To move to this level, we refactor the system using one or more creational patterns. A then uses the creational objects, such as factory or builder, to create an instance of B. A is now only coupled to B via the method signatures of interface I.
Message level coupling: This level maps to “Deck communicates with Card via a message” (note: the last example I gave is a bit broad and covers the last three levels in the formal model). To move to this level, we refactor the system using the message oriented behavioral patterns such as adapter, command, mediator, an interpreter. At this level of coupling A has no direct or even inferred knowledge of B. A and B are only coupled via the messages A invokes and the messages B services. At this level of coupling, however, the message invocation system still ties A and B to the same language and processor by using a shared data structure to represent the message.
Protocol level coupling: To move to this level, the messaging objects are refactored to introduce the concept of marshalling – that is where the message can be streamed out as protocol standard message. At this level, Object A can exist on a separate process implemented in different language than B. A and B are only coupled via the messaging protocol the messaging objects marshal to.
There are additional levels of coupling beyond this one, but hopefully for now you get the idea
Notice how the more de-coupled the objects become, the more formal the overall system that the objects live in needs to become. For example, to move from constructor level coupling to language level coupling, you need to re-factor your system using one or more creational patterns (more on these in the next article) as a guide.
When deciding your coupling levels, you need to weigh the trade-offs. Rather than focusing on the benefit of going to the next loosest coupling level, focus instead on the consequence of staying at the coupling level you are at. If you are currently at Constructor level coupling, before you move to API level coupling ask yourself how many new types of objects will you be creating in this area of the system in the next 12 months? If the answer is a low number (in the 1-5) range, then you may never get the payback in that time period for incurring the cost of refactoring. However, as soon as you encounter a project that puts you over that ROI threshold, you should add the refactoring task into your plans.
Entry Filed under: Software Development