문제

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?

==================================================================================

Update

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
도움이 되었습니까?

해결책 2

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.

다른 팁

For the "so they can even be compiled independently" you can just declare the class as a class,

class Logger;

Then you can use it at will for formal result and argument types, but since the compiler doesn't know its size or it members you can't do stuff with it, such as in function implementations.

But there's a big difference between including a header in another header, and just including it in an implementation file: the latter contributes once to the total build time, whereas the former potentially contributes many times, once for each translation unit.

If, on the other hand, you're doing header-only modules, then there's no way around including all the relevant code.

You could declare interfaces in common header files and resolve concrete dependencies at runtime. In your example the geometry module includes #include "common/logger.hpp" which defines an abstract class Logger. The user of the geometry lib can decide if he uses the Logger implementation from you logger lib or implements his own one.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top