Я разрабатываю инструментарий, который имеет несколько модулей. Я пытаюсь сделать модули настолько независимыми, насколько это возможно, чтобы они даже могли быть скомпилированы независимо (например, в виде библиотеки).
Один из модулей 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
Есть ли лучшие / более чистые способы сделать это? Есть ли рекомендуемые рекомендации или лучшие практики для такого дизайна?
================================================== ================================
Вот пример реализации, использующей указатели на функции (основанные на идее rioki), которые помогают отделить объекты:
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;
}
выход:
constructing MyObj
level 1 this is a debug message
Вам действительно нужен регистратор в вашем геометрическом модуле? Всегда спрашивайте «мне действительно нужно А в Б?» определить, разумно ли соединение двух модулей.
Есть несколько способов удалить зависимости между двумя вашими модулями.
Класс геометрии действительно нуждается в регистраторе? Нет, регистрируется только фатальная ошибка.
Затем создайте исключение в случае фатальной ошибки, перехватите ее и зарегистрируйте в коде более высокого уровня. Это делает геометрию полностью независимой от регистратора или любого другого модуля.
Класс геометрии действительно нуждается в регистраторе? Может быть, я пишу кучу диагностической информации.
Как насчет того, чтобы определить полностью виртуальный интерфейс (абстрактный базовый класс) для регистратора. Это только введет зависимость от заголовка. Вам нужен только заголовок интерфейса, но не весь модуль. Если указатель на регистратор равен NULL, просто ничего не регистрируйте.
Как насчет того, чтобы определить какую-либо функцию записи диагностической информации, принимая 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;
Затем вы можете использовать его по своему желанию для формальных типов результатов и аргументов, но поскольку компилятор не знает его размера или членов, вы не можете ничего с ним делать, например, в реализациях функций.
Но есть большая разница между включением заголовка в другой заголовок и просто включением его в файл реализации: последний вносит один раз в общее время сборки, тогда как первый потенциально вносит вклад много раз, один раз для каждой единицы перевода.
Если, с другой стороны, вы делаете модули только для заголовков, то нет никакого способа обойти весь соответствующий код.
Вы можете объявить интерфейсы в общих заголовочных файлах и разрешить конкретные зависимости во время выполнения. В вашем примере модуль геометрии включает в себя #include "common/logger.hpp"
который определяет абстрактный класс Logger
, Пользователь геометрии lib может решить, использует ли он реализацию Logger из вашей библиотеки logger или реализует свою собственную.