|« Alchemy: BitLists Mk2||Bikeshedding »|
Coupling and Cohesion are two properties that deserve your attention when you design software. These are important because they have a direct effect on how flexible and maintainable your software will be as your software continues to e developed. Will you be able to reuse it? Will you be able to adapt it? Will you need a shoe-horn to force that new feature in the future? These questions are much simpler to answer when you can properly gauge the level of Coupling and Cohesion of your components.
Coupling and Cohesion
The source of the terms Coupling and Cohesion originated from the 1968 National Symposium on Modular Programming by Larry Constantine. A more commonly referenced source is from a later published paper called, Structured Software Design (1974), Larry Constantine, Glenford Myers, and Wayne Stevens. This paper discusses characteristics of “good” programming practices that can reduce maintenance and modification costs for software.
Loose (Low) coupling and High (Tight) cohesion are the desirable properties for components in software. Low coupling allows components to be used independently from other components. High cohesion increases the probability that a component can be reused in more places, by limiting its capabilities to small well-defined tasks. For those of you keeping score at home, high cohesion is the realization of the S, Single Responsibility, in SOLID object-oriented design.
A measure of the degree to which a component depends upon other components of the system.
|Given two components X and Y:|
|If X changes||→||how much code in Y must be changed?|
Low coupling is desired because it allows components to be used and modified independently from one and other.
|The next two rows demonstrate coupling dependencies.|
Very generic coupling, the most compatible bricks.
A measure of how strongly related the various functions of a single software component are.
|Given a component W:|
|How focused is W at fulfilling a single responsibility?|
High cohesion is desired because:
- Leads to a simpler minimal class that embodies one concept
- A minimal class is easier to comprehend and use
- Therefore it is more likely that this class will be reused
These pieces demonstrate cohesion over a range of interfaces.
Reuse of Code
It’s important to set your expectations for what to consider reusable code. Without a clear set of expectations, how will you know when you have achieved your goals. More importantly, how can you formulate a design strategy or a reproducible process to consistently create reusable code.
Not all code is reusable
To further clarify this statement, not all code should be reused.
Because the majority of software that is written is designed to fulfill a very specific purpose. The challenge is to find the balance point between generic reusable building blocks, and a tool or application that meets a need. Generic software components are very valuable, however, they are almost useless by themselves.
What good is a linked-list without a mechanism to populate the data in the list, process the data to perform useful outcomes, and later inspect the data. Another example could be a button control for a GUI that handles the name, display and animations of the button. However, it is the actions that are triggered by the button that are important to the end user.
Good design and practices should be used for all software that you write. However, recognize and differentiate which portions of your application are generic and likely to be reused compared to the code that is very specialized. Both of these are of equal importance.
The value of reusable code
For every statement of code that you write or use, you should ask the question:
Does this add value or risk?
Reusable code only provides value If it:
- Is easy to learn and use
- Requires less work -> time -> money to develop your final target
- Has already been tested and proven to be robust
- Results with a project that is at least as robust as the code that it is built upon
If any one of these qualities is undetermined, you may be adding risk along with any perceived value for the reused code.
Increase the potential for code reuse
Here are some design guidelines that can help increase the potential for your code and components to be reusable:
Easy to use correctly, hard to use incorrectly
If you are fan of Scott Meyers’, Effective C++, you will probably recognize this from Item 18:
“Ideally, if an attempted use of an interface won’t do what the client expects, the code won’t compile; and of the code that does compile, it will do what the client wants”
Good interfaces are easy to use correctly and hard to use incorrectly. The example that is used to demonstrate this concept in Effective C++ uses a calendar date class to illustrate the point. Start with a constructor such as this:
This is a reasonable declaration if you live in the United States, which uses the format month/day/year. However, many other countries prefer the format day/month/year. Other possibilities exist such as the ISO 8601 date order with four-digit years: YYYY-MM-DD (introduced in ISO 2014), which is specifically chosen to be unambiguous.
Another problem with this interface is there are three integer input parameters. Without refering to some form of documentation, it may be impossible to deduce the order of the parameters. These are some of the possibilities for how the interface could be misused:
One possible approach to solving all of the issues above is to create new types to improve clarity and enforce compile-time correctness.
Declaring the only constructor privately forces the user to use the static member functions to create new instances of a Month object. Similar definitions could be created for the day and year parameters. The improved usage would look like this:
To encourage correct use, design interfaces that are consistent and behaviorally compatible.
Use these techniques to help prevent errors:
- Create new types
- Restrict operations on those types
- Constrain object values
- Eliminate client resource management
Create interfaces that are minimal and complete
Strive to create the minimal number of operations in your interface to access the complete behavior of the object. Minimal objects embody one concept at the right level of granularity. Also, minimal interfaces are easier to comprehend, and more likely to be used and reused.
Monolithic classes will generally dilute encapsulation. Instead of doing one thing well, they end up providing many things that are mediocre. It is more difficult to make these objects correct and error-safe because they tackle multiple responsibilities. These types of objects usually result from an attempt to deliver a complete solution for a problem.
Encapsulate - Hide/Protect Information
Encapsulation is one of the most valuable aspects of object-oriented programming. Encapsulation is necessary to create a proper abstract design. The purpose of abstraction is to simplify concepts by hiding the details of the abstraction.
A proper abstraction will handle the dirty details required to achieve a correct and error-free implementation. The hidden details verify the invariants of your object each command and when internal state or data is changed. Abstractions become even more effective when a number of internal variables can be managed, and a reduced set of values are presented through the public interface.
3 made up facts… Eh, but probably true
- Programmers like to get the job done
- Programmers are inventive
- If a programmer finds a way to complete a task, even if it requires them to blatantly ignore the comment below, they will still write to the data; Because it will get the job done (refer to factioid 1)
Declare all data members private. Private data is teh best means a class can use to protect its invariants now as well as in the future as it adapts to possible changes.
Public data members move the responsibility to maintain and guarantee the invariants from the object to all of the code that accesses the abstraction. Freely providing access to an objects data members effectively negates most of the benefits gained from creating the abstraction.
Metaphorical Thought Experiment
WARNING: Visualize this thought experiment at your own risk.
Next time you are considering creating a public data member, think of the human body as an abstraction of a much more complicated biological organism. One where the designer declared all of the data members public rather than private.
- Your internal organs would now all be external organs ripe for the picking of any black-market human organ peddeler.
- All figures of speech related to the body must be interpretted literally: “What’s on your mind?”
- There will be many more Do-it-yourself doctors.
- Negates the benefits of being declared a
- Couples do not need to worry about tight coupling, however, they will need to worry about becoming tangled.
- Humans in space?! Forget it!
The inferiority of non-
private data members
private data members are almost always inferior to even simple pass-through getter/setter methods. These methods provide the flexibility in the future to adapt to change as well as these benefits:
- Verify Invariants
- Lazy Evaluation / Data Caching
- Decouple for Future Modifications
Protected data members suffer from the same disadvantages as
public data members. The responsibility to maintain the invariants is shared with the unbounded set of objects which includes:
- Every class currently derived from this class
- Every class that will be derived from this class in the future
The one exception is the C-style struct or plain-old data (POD), which is used as a behaviorless aggregate and contains no member functions.
Use the weakest coupling relationship possible
Let’s examine the types of relationships that are possible in C++. The list below is ordered by a measure of coupling, tightest to loosest:
- Nonpublic inheritance
- Composition (member variable)
Choose the coupling relationship that provides the weakest level of coupling possible. This will provide more flexibility when something needs to be changed in the future.
Prefer writing non-member non-friend functions
This will improve encapsulation by minimizing dependencies, therefore, reduces coupling. The function implementation cannot be modified to depend on the private member data of the class. It can only be implemented based on what is publically accessible.
This will also break apart monolithic classes. The separable functionality can then be further encapsulated by composition.
Example: std::string contains 103
public member functions, 71 could be non-member non-
Use inheritance to achieve substitutability
Inheritance is a wonderful tool available with object-oriented programming. However, it should not be selected as a way to reuse code. Instead, use inheritance as a way to facilitate polymorphism.(For those of you keeping score at home, this concept is the L, Liskov substitutability principle, in SOLID object-oriented design.)
When you consider inheritance, seriously contemplate the is-a relationship between your objects. Think of is-a as “Can be substituted with…".
Public inheritance facilitates polymorphism, which implies substitutability:
|Original Object||… Can be substituted with:||… Not with:|
Working with your objects will feel cumbersome if you inherit without substitutability.
Don’t inherit to reuse; inherit to be reused.
From Item 37 in C++ Coding Standards, Herb Sutter and Andrei Alexandrescu
Both composition and
private inheritance means is-implemented-in-terms-of.
So which one do you use?
Use composition whenever possible. Choose
private inheritance when necessary. It becomes necessary when
protected access to information is required.
Remember, use the solution that creates the weakest coupling relationship:
Don’t underestimate the power of composition
Robust composition is easier to achieve compared to inheritance, especially with cohesive interfaces.
Minding the concepts of coupling and cohesion can lead to more robust software that avoids fragility and is more maintainable.
- Simple definitions and implementations are easier to comprehend and use
- Simple objects can be reused to create more useful objects with composition
- Only use public inheritance when derived objects can truly be substituted for the base object
- Every solution in a computer program is built upon a collection of much simpler solutions
Legos images, trademarks and copyrights belong to the LEGO Group.