I am designing a toolkit that has several modules. I'm trying to make the modules as independent as possible, so they can even be compiled independently (e.g. as a library).
One of the modules is logging
and another one is geometry
. Right now a base class in geometry
receives a pointer to a logging
object and then uses it to log data:
#include "../logging/logger.h"
class GeometryBase {
public:
//...
void do_something() { if (logger) logger->debug("doing something"); }
void setLogger(Logger* logger) {//...};
private:
Logger* logger = nullptr;
};
So for this I need to include ../logging/logger.h
, which means compiling this module requires logging
headers. Is there a way to get around this, so even if the logging
headers don't exist, this would still compile?
Right now I can think of using macros to make all the parts related to logging conditional during preprocessing. Like:
#ifdef USE_LOGGING
#include "../logging/logger.h"
#endif
class GerometryBase {
//...
void do_something() { if (logger) _log("doing something"); }
#ifdef USE_LOGGING
void _log(const std::string& s) {//...}
Logger* logger = nullptr;
#else
void _log(const std::string& s) {// do nothing}
void* logger = nullptr;
#endif
}; // class
Are there better/cleaner ways to do this? Are there recommended guidelines or best practices for such design?
==================================================================================
Here is an example implementation using function pointers (based on rioki's idea) that does help decoupling the objects:
obj.h
#ifndef MYOBJ_H_
#define MYOBJ_H_
#include <iostream>
class MyObj {
public:
MyObj() { std::cout << "constructing MyObj" << std::endl; }
void setLogger( void (*p)(const char*, int) ) {
logger = p;
}
void do_somthing() {
if (logger) {
logger("this is a debug message", 1);
}
}
private:
void (*logger)(const char*, int ) = nullptr;
};
#endif
logger.h
#ifndef LOGGER_H
#define LOGGER_H
void logger(const char* , int);
#endif
logger.cpp
#include <iostream>
#include "logger.h"
void logger(const char* str, int lvl) {
std::cout << "level " << lvl << " " << str << std::endl;
}
main.cpp
#include "logger.h"
#include "obj.h"
int main() {
MyObj obj;
obj.setLogger(logger);
obj.do_somthing();
return 0;
}
output:
constructing MyObj
level 1 this is a debug message
Do you really need a logger in your geometry module? Always ask "do I need really A in B?" to determinate if the coupling of two modules is reasonable.
There are multiple ways to remove the dependencies between your two modules.
Does the geometry class really need a logger? No, it only logs fatal error.
Then throw an exception in case you have a fatal error, catch it and log it in the higher level code. This makes the geometry fully independent to the logger or any other module.
Does the geometry class really need a logger? Maybe, I write a bunch of diagnostic information.
How about you define a fully virtual interface (abstract base class) for the logger. This will only introduce a dependency to the header. You need only the header of the interface but not the entire module. If the pointer to the logger is NULL, just don't log anything.
How about you define any function writing diagnostic information taking a ostream
. Like this you can catch all information and log it in a higher level. This allows you to pass a stringstream or cout and increases your flexibility. The only dependency in one you already have, the C++ standard library.
How about you define the setLogger, not as taking an object, but a std::function
. For example:
class GerometryBase
{
public:
void setLogger(std::function<void (const std::string&)> value)
{
logger = value;
}
private:
std::function<void (const std::string&)> logger;
void log(const std::string& msg)
{
if (logger)
{
logger(msg);
}
}
}
To bind the logger to the geometry classes:
Logger logger;
Box box;
box.setLogger([&] (const std::string& msg) {
logger.log(msg);
});
There are many ways you can reduce coupling between modules. You just have to think about it for a while. Going over the standard library is my favorite way, it is standard for a good reason. Since C++11 introduced lambdas, coupling in my modules has significantly decreased.