In this exercise we will see how the concepts learned until now can be used to help us design software that adheres to the SOLID principles.
As a example we use an Calculator which has the following two operations:
- sum: calculates the sum of two numbers.
- multiply: calculates the product of two numbers.
In addition to doing the calculations the calculator must also log these operations and their results, as illustrated in the figure above. We will now see how this can be done without hard coding the logging functionality inside the calculator itself.
The IntCalculatorBad.cpp
source file shows how logging can be added to the methods of the class.
int IntCalculatorBad::sum(int a, int b)
{
int res = a + b;
std::cout << "taking the sum of: " << a << " and " << b << " which is " << res << std::endl;
return res;
}
Note that in C++ it is common to use std::cout
and the left-shift
operator <<
to print output, rather than printf
.
Using std::cout
is argueably less ergonomic than printf
but it has the benefit that it can print different types without having to specify a format string manually, which will especially useful when we use templates later.
A nasty thing about this piece of code is that it is difficult to:
- test the logging behavior
- disable the logging
- reuse the logging functionality
These are the symptoms of bad design. Ask yourself is this code violating any of the SOLID principles?
A fundamental technique in software development is dependency injection
.
In this case we wish to inject the a pointer to a logger object into the Calculator.
Implement the IntCalculator class declared in int int_calculator.hpp
.
The constructor should take a pointer to a Logger class as a parameter.
Next implement the sum and multiply methods. In addition to doing the computations the function should write their results to the log in the format:
Addition:
taking the sum of: 'a' and 'b' which is 'a+b'
Multiplication:
taking the product of: 'a' and 'b' which is 'a*b'
Use the test defined in test_int_calculator.cpp
to check that the class works as expected.
Now we wish to change our logging mechanism such that it writes to a file instead of the console. We wish to extend the program, but we do not wish to modify any of the existing code (Open-closed principle).
Implement the FileLogger class. The class should work in a similar way to the PrintLogger, only that it instead writes to a file specified when the logger was constructed. For inspiration on how to write to files see cplusplus.
To test the code we could replace the logger in the test case and compare the resulting log file with some very long string.
TEST_CASE("IntCalculator")
{
DummyLogger logger; // <- replace with FileLogger
IntCalculator calc(&logger);
...
taking the sum of: 0 and 0 which is 0
taking the sum of: 0 and 1 which is 1
taking the sum of: 0 and 2 which is 2
...
taking the sum of: 10 and 10 which is 10
However, a test based on comparing string is prone to break as soon as the formatting changes the slightest.
Since the logging no longer depends on the calculator it can be tested seperately as shown in test_logger.cpp
.
Run the test defined in test_logger.cpp
to verify that the logging functionality is correct.
By delegating the responsibility of how to log to the concrete implementation of the logger we have implemented the strategy
design pattern.
In this case we are selecting the behaviour when the calculator object is constructed, but it would also be possible to switch it out at runtime by implementing a method for setting the logger like set_logger(Logger* log)
.
One weakness of the IntCalculator
is that it can only do arithmetic with integers.
We now wish to implement the template class TemplateCalculator, which performs arithmetic on any type for which addition and multiplication is defined. Implement TemplateCalculator as a template class.
It should work in a similar way to the IntCalculator class, only that its type can be specified at compile time.
Rerun the test defined in test_template_calculator.cpp
to verify the implmentation.