Модульный Дизайн C++


Я разрабатываю инструментарий, который имеет несколько модулей. Я пытаюсь сделать модули максимально независимыми, чтобы их можно было даже компилировать независимо (например, как библиотеку).

Один из модулей - logging, а другой - geometry. Прямо сейчас базовый класс в geometry получает указатель на Объект logging и затем использует его для регистрации данных:
#include "../logging/logger.h"
class GeometryBase {
    public:
      //...
      void do_something() { if (logger) logger->debug("doing something"); }
      void setLogger(Logger* logger) {//...};
    private:
      Logger* logger = nullptr;
};

Поэтому для этого мне нужно включить ../logging/logger.h, что означает, что компиляция этого модуля требует заголовков logging. Есть ли способ обойти его? это, так что даже если заголовки logging не существуют, это все равно будет компилироваться?

Прямо сейчас я могу подумать об использовании макросов, чтобы сделать все части, связанные с журналированием, условными во время предварительной обработки. Например:

#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

Есть ли лучшие / более чистые способы сделать это? Существуют ли рекомендуемые руководящие принципы или лучшие практики для такого дизайна?

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

Обновить

Вот пример: реализация с использованием указателей функций (основанных на идее риоки), которая действительно помогает развязать объекты:

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 

Лесоруб.h

#ifndef LOGGER_H
#define LOGGER_H

void logger(const char* , int);

#endif

Лесоруб.cpp

#include <iostream>
#include "logger.h"

void logger(const char* str, int lvl) {

    std::cout << "level " << lvl << " " << str << std::endl;

}

Главная.cpp

#include "logger.h"
#include "obj.h"

int main() {
    MyObj obj;

    obj.setLogger(logger);
    obj.do_somthing();


    return 0;

}

Вывод:

constructing MyObj
level 1 this is a debug message
3 2
c++

3 ответа:

Вам действительно нужен регистратор в вашем модуле геометрии? Всегда спрашивайте: "действительно ли мне нужно а в Б?"определить, является ли соединение двух модулей разумным.

Существует несколько способов удаления зависимостей между вашими двумя модулями.

Действительно ли классу геометрии нужен регистратор? Нет, он только регистрирует фатальную ошибку.

Затем бросьте исключение в случае, если у вас есть фатальная ошибка, поймать его и зарегистрировать его в коде более высокого уровня. Это делает геометрию полностью независимо от регистратора или любого другого модуля.

Действительно ли классу геометрии нужен регистратор? Может быть, я пишу кучу диагностической информации.

Как насчет определения полностью виртуального интерфейса (абстрактного базового класса) для регистратора. Это только введет зависимость в заголовок. Вам нужен только заголовок интерфейса, но не весь модуль. Если указатель на регистратор равен нулю,просто не регистрируйте ничего.

Как насчет определения любой функции записи диагностическая информация принимая a ostream. Таким образом, вы можете поймать всю информацию и зарегистрировать ее на более высоком уровне. Это позволяет вам передавать stringstream или cout и увеличивает вашу гибкость. Единственная зависимость в той, которая у вас уже есть, - это стандартная библиотека C++.

Как насчет определения setLogger, не как взятие объекта, а как std::function. Например:

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);
        }
    }
}

Для привязки регистратора к классам геометрии:

Logger logger;
Box box;

box.setLogger([&] (const std::string& msg) {
    logger.log(msg);
});
Есть много способов уменьшить сцепление между модулями. Тебе просто нужно подумать об этом какое-то время. Просмотр стандартной библиотеки - мой любимый способ, он стандартен по уважительной причине. С тех пор как C++11 ввел лямбды, сцепление в моих модулях значительно уменьшилось.

Для "так что они могут быть даже скомпилированы независимо" вы можете просто объявить класс как класс,

class Logger;

Тогда вы можете использовать его по желанию для формальных типов результатов и аргументов, но поскольку компилятор не знает его размер или его члены, вы не можете делать с ним что-либо, например, в реализациях функций.

Но есть большая разница между включением заголовка в другой заголовок и просто включением его в файл реализации: последний вносит один раз в общее время сборки, в то время как первый потенциально вносит свой вклад много раз, по одному разу для каждой единицы перевода.

Если, с другой стороны, вы делаете только заголовочные модули, то нет никакого способа обойти включение всего соответствующего кода.

Можно объявлять интерфейсы в общих заголовочных файлах и разрешать конкретные зависимости во время выполнения. В вашем примере модуль geometry включает в себя #include "common/logger.hpp", который определяет абстрактный класс Logger. Пользователь библиотеки geometry lib может решить, использует ли он реализацию Logger из вашей библиотеки logger lib или реализует свою собственную.