Модульный Дизайн 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 ответа:
Вам действительно нужен регистратор в вашем модуле геометрии? Всегда спрашивайте: "действительно ли мне нужно а в Б?"определить, является ли соединение двух модулей разумным.
Существует несколько способов удаления зависимостей между вашими двумя модулями.
Действительно ли классу геометрии нужен регистратор? Нет, он только регистрирует фатальную ошибку.
Затем бросьте исключение в случае, если у вас есть фатальная ошибка, поймать его и зарегистрировать его в коде более высокого уровня. Это делает геометрию полностью независимо от регистратора или любого другого модуля.
Действительно ли классу геометрии нужен регистратор? Может быть, я пишу кучу диагностической информации.
Как насчет определения полностью виртуального интерфейса (абстрактного базового класса) для регистратора. Это только введет зависимость в заголовок. Вам нужен только заголовок интерфейса, но не весь модуль. Если указатель на регистратор равен нулю,просто не регистрируйте ничего.
Как насчет определения любой функции записи диагностическая информация принимая 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); } } }
Для привязки регистратора к классам геометрии:
Есть много способов уменьшить сцепление между модулями. Тебе просто нужно подумать об этом какое-то время. Просмотр стандартной библиотеки - мой любимый способ, он стандартен по уважительной причине. С тех пор как C++11 ввел лямбды, сцепление в моих модулях значительно уменьшилось.Logger logger; Box box; box.setLogger([&] (const std::string& msg) { logger.log(msg); });
Для "так что они могут быть даже скомпилированы независимо" вы можете просто объявить класс как класс,
class Logger;
Тогда вы можете использовать его по желанию для формальных типов результатов и аргументов, но поскольку компилятор не знает его размер или его члены, вы не можете делать с ним что-либо, например, в реализациях функций.
Но есть большая разница между включением заголовка в другой заголовок и просто включением его в файл реализации: последний вносит один раз в общее время сборки, в то время как первый потенциально вносит свой вклад много раз, по одному разу для каждой единицы перевода.
Если, с другой стороны, вы делаете только заголовочные модули, то нет никакого способа обойти включение всего соответствующего кода.
Можно объявлять интерфейсы в общих заголовочных файлах и разрешать конкретные зависимости во время выполнения. В вашем примере модуль geometry включает в себя
#include "common/logger.hpp"
, который определяет абстрактный классLogger
. Пользователь библиотеки geometry lib может решить, использует ли он реализацию Logger из вашей библиотеки logger lib или реализует свою собственную.