c : класс logger без глобальных или одиночных символов или передача его каждому методу

#c #logging #dependency-injection #singleton

#c #ведение журнала #внедрение зависимостей #синглтон

Вопрос:

Кто-нибудь знает, возможно ли иметь класс, подобный logger, без :

  • использование одноэлементного или глобального (a la std::cout)

  • передача экземпляра / указателя / ссылки на каждый метод, которому это необходимо

Я беру пример класса logger, но у меня есть несколько классов в моем приложении, которые выиграли бы от этого (например, диспетчер отмены).

С каждым решением связано несколько проблем :

  • использование синглтона проблематично для тестирования (наряду со многими причинами, по которым его вообще не рекомендуется использовать). То же самое с глобальным. Кроме того, ничто не гарантирует, что в приложении будет только ОДИН экземпляр, и это даже не требование (почему бы, например, не иметь 2 регистратора?)

  • передача его каждому объектному конструктору (внедрение зависимостей) приводит к большому количеству шаблонного кода и может быть подвержена ошибкам, потому что вам приходится копировать / вставлять один и тот же код много раз. Можно ли серьезно рассматривать возможность наличия указателя на Logger в конструкторе каждого отдельного класса???????

Итак, мне было интересно, есть ли третья альтернатива в C , о которой я никогда не слышал? Для меня это звучит так, как будто для этого потребуется какая-то черная магия под капотом, но я был приятно удивлен некоторыми методами, которые я изучил в stack overflow, которые я не смог найти в Google, поэтому я знаю, что здесь есть настоящие гуру 😉

Удивительно, но я нашел много дискуссий о том, как создавать одиночные символы или почему не следует использовать одиночные символы, но я не смог найти сообщение, посвященное моей проблеме…

Комментарии:

1. Я видел класс logging, который просто открывал файл журнала каждый раз, когда в журнал записывалась инструкция, а имя журнала было жестко запрограммировано (и необязательно передавалось, если вы хотели записать в другой файл). Никаких глобальных значений, одиночных символов или передачи объектов каждому методу, но это не совсем то решение black magic, которое вы ищете. На самом деле я все еще работаю над удалением этого класса log из продукта 🙂

2. если класс logger существует, то можно ли вам пойти и отредактировать каждый метод? Если да, то я могу кое-что предложить.

3. @iammilind : Вы имеете в виду каждый метод класса logger? Да, это возможно, мне не терпится услышать ваше предложение 🙂 @DXM : :))))

4. извините, я, возможно, неправильно понял ваш вопрос. Я думал, вы хотите регистрировать все функции, которые вызываются по всему вашему кодовому потоку. После просмотра приведенного ниже ответа кажется, что вы хотите чего-то другого. Или, может быть, я все еще не совсем понял, в чем именно заключается ваш вопрос. 🙂

5. Взгляните на Hypodermic . Выглядит не очень красиво, но я думаю, что это может быть решением.

Ответ №1:

Я бы предположил, что вы могли бы сделать что-то похожее на то, что делается в Java с пакетом Log4j (и, вероятно, выполняется с его версией Log4c):

Есть статический метод, который может возвращать несколько экземпляров logger:

 Logger someLogger = Logger.getLogger("logger.name");
  

getLogger() Метод не возвращает одноэлементный объект. Он возвращает именованный регистратор (создавая его при необходимости). Logger это просто интерфейс (в C это может быть полностью абстрактный класс) — вызывающему не нужно знать фактические типы создаваемых Logger объектов.

Вы могли бы продолжать имитировать Log4j и иметь перегрузку getLogger() , которая также принимает объект factory:

 Logger someLogger = Logger.getLogger("logger.name", factory);
  

Этот вызов будет использоваться factory для создания экземпляра logger, предоставляя вам больше контроля над тем, какие базовые Logger объекты создавались, вероятно, помогая вашему издевательству.

Таким образом, нет необходимости передавать что-либо в конструкторы, методы и т.д. Вашего собственного кода. Вы просто берете нужное имя Logger , когда оно вам нужно, и регистрируетесь в нем. В зависимости от потокобезопасности кода ведения журнала, который вы пишете, то, что getLogger() возвращает, может быть статическим членом вашего класса, поэтому вам нужно будет вызывать getLogger() только один раз для каждого класса.

Комментарии:

1. Звучит разумно, однако, если вы используете factory, вам все равно придется передавать factory конструктору вашего класса, не так ли?

2. Нет, вы этого не делаете. Если метод сборки фабрики таков, что factory может быть функтором , вы можете просто создать экземпляр объекта factory «на лету» в вызывающем коде getLogger() .

3. Я действительно не вижу преимущества этого над singleton. Если все они должны обернуть некоторый дескриптор файла журнала ошибок, то этот дескриптор файла уже является синглтоном. Обертывание его единственной структурой с одним полем-членом или простым одноэлементным классом с одним элементом-указателем файла и без виртуальных методов в любом случае приравнивается только к объекту-дескриптору одноэлементного файла. Мне тоже не нравятся одиночные символы, но это звучит не выгодно, как способ обойти их. По сути, это getSingletonSuitableFor(someParam).

4. пользователь1024732: В многопоточном программировании запись в файл журнала требует синхронизации. Создание нескольких файлов журнала (по одному файлу на поток операционной системы) разрешено в одних проектах и запрещено в других. При обработке нескольких параллельных транзакций каждое сообщение журнала должно иметь префикс с идентификатором транзакции, поэтому должны быть некоторые промежуточные вспомогательные объекты ведения журнала, которым сообщается, какой идентификатор текущей транзакции он обрабатывает, и соответствующим образом изменяет строку сообщения журнала. В целом, чем больше программное обеспечение, тем меньше оно может вызвать проблем у новичков.

Ответ №2:

Как насчет класса с некоторыми статическими методами?

 class Logger
{
public:
  static void record(string message)
  {
    static ofstream fout("log");
    fout << message << endl;;
  }
  ...
};

...

void someOtherFunctionSomewhere()
{
  Logger::record("some string");
  ...
}
  

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

Комментарии:

1. Это здорово, это хорошо работает с регистратором, но как насчет классов, которым нужно состояние? Например, диспетчер отмены хранит историю действий пользователя и, следовательно, должен отслеживать, что происходит.

2. @Dinaiz: Нет проблем. Создайте record шаблон, определите operator<< для своего класса состояния, и все готово.

Ответ №3:

Я не думаю, что в C есть хорошая альтернатива, однако в Emacs Lisp есть одна, о которой, возможно, стоит подумать: динамическое связывание переменных. Основная концепция заключается в следующем: когда вы ссылаетесь на переменную, вы получаете доступ не обязательно к глобальной переменной по имени, но к той, которая последней определена в пути выполнения с тем же именем. В псевдо-C коде это выглядело бы следующим образом:

 // Pseudo-C   with dynamic binding
Logger logger = Logger("GlobalLogger");

void foo() { logger.log("message"); }

int main() 
{
   foo(); // first
   {
      Logger logger = Logger("MyLogger");
      foo(); // second
   }
   foo(); // third
}
  

В этом примере псевдо-C при первом вызове foo() будет использоваться GlobalLogger, при втором вызове вместо него будет вызван MyLogger, поскольку MyLogger будет переопределять глобальную переменную до тех пор, пока она находится в области видимости, даже внутри foo() . Третий вызов возвращался к GlobalLogger, поскольку MyLogger выпадал из области видимости. Это позволяет переопределить глобальный регистратор пользовательским для выбранных фрагментов кода без необходимости пропускать объект logger через весь код и без необходимости устанавливать глобальную переменную.

Реальный C не имеет динамической привязки, но должна быть возможность реплицировать его аспекты:

 std::stack<Logger> logger = { Logger("GlobalLogger") };

void foo() { logger.top().log("message"); }

int main()
{
    foo();
    {
      logger.push(Logger("MyLogger"));
      foo();
      logger.pop();
    }
    foo();
}
  

Для дальнейшей очистки стек должен быть скрыт внутри DynamicBinding класса, ручные операции .push () /.pop () могут быть скрыты за защитой области видимости, и возникнут проблемы с многопоточностью, о которых нужно будет позаботиться. Но базовая концепция может сработать и обеспечить большую гибкость, чем простая одноэлементная или глобальная переменная.