Coupling is usually contrasted with cohesion. Low coupling often correlates with high cohesion, and vice versa. Low coupling is often a sign of a well-structured computer system and a good design, and when combined with high cohesion, supports the general goals of high readability and maintainability. The goal of this case study is to show the benefits of low coupling and high cohesion, and how it can be implemented with C++. The case study consists of designing an application that accesses a file in order to get data, processes it, and prints the result to an output file.
Solution without design
For this first solution, the design is ignored and only one a class named
CDataProcessor is used to:
- Get data from a file.
- Process data.
- Print the result.
And the main method invokes the methods of this class.
Drawback of this solution
CDataProcessor class has many responsibilities, so we can’t easily reuse the algorithm in other applications.
- High coupling: the processing is high coupled with a console and also with a data provider.
To improve cohesion, each responsibility must be assigned to a different class, so we need three classes:
CFileProvider: To get data from a file.
CDataProcessing: To process data, and this class can use other classes to complete processing, but to simplify the design, we consider that's sufficient for our processing.
CResultReporting: To report result to a file.
So each class has its own responsibility. The advantages of this design are:
- Easy to understand classes.
- Easy to maintain.
- Easy to reuse classes in other applications.
What happens if data exists in a database rather than a file? In our last design, our application is high coupled with a file provider.
To resolve this problem, we need an interface that provide methods to get data from anywhere, and for the case of the file, we need a class that implement this interface.
For that, using NVI can be good solution. This pattern is more useful than using only abstract classes because it's possible to define a pre- and post-condition. It's a useful object-oriented programming technique, particularly at development time. Pre- and post-conditions ensure that invariants of a class hierarchy (and in general, an abstraction) are not violated at designated points during execution of a program.
In our case, we can add the
CFileProvider inherits from
IDataProvider to implement
GetDataFromImpl, and the same design can be used for
Here’s the new collaboration between classes after refactoring:
In the latest design, the creation of concrete instances of
IReportResult are created by the main method. A better approach is to assign this responsibility to a factory class, so a logic to instantiate a family of instances needed is isolated.
The orchestration between all classes is implemented in the main method. It's better to assign this responsibility to a Controller class, so we can use it in other applications.
The controller needs three classes to interact with them, so the question is how do we bind instances to the controller.
In C++, two solutions can be used:
- Add a method named
- Use a template so the controller will be instantiated like that:
The difference between the two solutions is that for the first one, each Data Provider must inherit from
IDataProvider, and for the second one,
CFileProvider only needs the method
GetData(Data&) even if it’s inherited from another class than
There are many discussions between C++ gurus about using OOP or templates. Here's a good article about this tension between the two approaches.
Here’s the new collaboration between the classes after refactoring:
Benefits of refactoring
After refactoring, this application became more flexible and we can use it for different scenarios:
- Can get data from file, database, XML file, CSV file, …
- Can process with many classes not just one.
- Can report to console, file, …