Сергей Деревяго

Интерфейсы и Сообщения


1 Введение
1.1 Оформление исходного кода
1.2 Класс sh_ptr
1.3 Исключения и класс CException
2 Интерфейсы
2.1 Что такое интерфейс
2.2 Фабрики объектов
3 Сообщения
3.1 Что такое сообщения
3.2 Классы Publisher и Subscriber
4 Пример: Поиск текста в файле
4.1 Постановка задачи
4.2 Интерфейс FindText
4.3 Реализация FindText
4.4 Интерфейс пользователя
5 Заключение
Документация
Исходный код

1. Введение

Данный материал представляет собой краткое описание сути и возможностей интерфейсов и сообщений, а также содержит реальный пример их использования. Его целью является практически полезное введение программиста в круг тех действительно полезных вещей, о которых так много говорят, но так мало используют. И дело тут в том, что в представлениях рядовых разработчиков "теоретические изыскания" объектной ориентации и "суровая правда жизни" разделены непроходимой пропастью неумения "посвященных" просто и понятно изложить реальную полезность "абстрактных изысканий". Надеюсь, что простой и понятный законченный пример окажется способен изменить существующее положение дел.

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

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

Буду рад всем содержательным комментариям и исправлениям, которыми вы захотите со мной поделиться.

С уважением, Сергей Деревяго.


1.1. Оформление исходного кода

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

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

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

  1. Здравый смысл. Зачастую, это первая жертва неукоснительного следовании любому набору правил. Если вы чувствуете, что исполнение какого-либо правила в данном конкретном месте противоречит здравому смыслу -- победить должен здравый смысл.
  2. Символы табуляции. Использование символов табуляции категорически запрещается. Дело в том, что разные редакторы, принтеры и проч. всегда имеют собственное мнение по поводу значения символа табуляции, поэтому его появление в коде -- гарантия неудобочитаемости. Тем не менее, программист может набирать текст так, как считает нужным, просто он должен обеспечить отсутствие символов табуляции перед передачей исходного кода в CVS, что достаточно просто автоматизируется.
  3. Длина строки -- не более 80 символов. Длинные строки вызывают множество ненужных проблем при работе с консолью.
  4. Каждая "глобальная сущность" в программе должна иметь doxygen комментарий (/** */), текст которого попадет впоследствии в online документацию. Достаточно хорошей практикой является самостоятельное получение документации по собственным файлам для проверки удобочитаемости.
  5. Стиль расстановки скобок, отступов и проч. -- на усмотрение программиста. Главное -- это последовательное и аккуратное его использование. Программа не должна содержать закомментированные фрагменты кода, т.к. для этого существуют системы версионного контроля (CVS) и средства для визуального сравнения (Araxis Merge).
  6. Включаемые файлы не должны содержать директиву using namespace std, т.к. ее действие уже невозможно отменить, и включивший ваш код программист получит множество ненужных проблем.
  7. Все исключения, возбуждаемые нашим кодом, должны быть классами, унаследованными от CException. Внешние исключения, возбуждаемые чужим кодом, должны инкапсулироваться в соответствующие классы, унаследованные от ExternalCException.
  8. Прикладной код не должен включать системно и/или реализационно зависимые хедеры (типа windows.h или unistd.h), т.к. это создает труднопреодолимые проблемы при портировании. Все, что можно использовать в прикладном коде -- это определенные в стандарте C++ средства плюс ваши собственные интерфейсы, использующиеся для сокрытия деталей реализации, зависящих от платформы.

1.2. Класс sh_ptr

Класс sh_ptr -- это наиболее важный представитель семейства умных указателей, без которых современное программирование на C++ просто немыслимо:
/**
 * Данный класс является разделяемым "умным указателем" с подсчетом ссылок на
 * объект типа T. В его задачу входит автоматическое уничтожение переданного
 * объекта, когда на него не останется больше ссылок.
 * Если необходимо "забрать" владение переданным указателем у всех копий sh_ptr,
 * то нужно воспользоваться вызовом set(0), в результате чего деструктор sh_ptr
 * ничего не уничтожит, т.к. delete 0 является пустой операцией.
 */
template <class T>
class sh_ptr {
      /** разделяемое представление */
      struct Rep {
             /** указатель на разделяемый объект */
             T* ptr;
             /** количество ссылок */
             size_t refs;

             /** создает представление с единственной ссылкой */
             Rep(T* ptr_) : ptr(ptr_), refs(1) {}

             /** удаляет разделяемый объект */
             ~Rep() { delete ptr; }

             /** для ускорения работы */
             void* operator new(size_t)
             {
              return fixed_alloc<Rep>::alloc();
             }

             /** для ускорения работы */
             void operator delete(void* ptr, size_t)
             {
              fixed_alloc<Rep>::free(ptr);
             }
      };

      /** указатель на разделяемое представление */
      Rep* rep;
      
 public:
      /**
       * Создает объект, ответственный за уничтожение переданного указателя.
       * Указатель будет корректно удален даже в случае возникновения исключения
       * в конструкторе sh_ptr, т.о. вызов sh_ptr<T>(new T) не приведет к утечке
       * ресурсов. Тем не менее, не стоит забывать, что вызов вроде
       * f(sh_ptr<T1>(new T1), sh_ptr<T2>(new T2)) уже чреват потерей объектов,
       * т.к. порядок вычисления подвыражений не определен и новые объекты T1 и
       * T2 могут быть созданы до соответствующих sh_ptr, отвечающих за
       * освобождение ресурсов.
       */
      explicit sh_ptr(T* ptr=0)
      {
       try { rep=new Rep(ptr); }
       catch (...) {
             delete ptr;
             throw;
       }
      }

      /**
       * Копирует переданный объект просто увеличивая количество ссылок на
       * разделяемый указатель.
       */
      sh_ptr(const sh_ptr& shp)
      {
       rep=shp.rep;
       rep->refs++;
      }

      /**
       * Уменьшает количество ссылок на разделяемый указатель и удаляет его,
       * если на него никто больше не ссылается.
       */
      ~sh_ptr() { if (--rep->refs==0) delete rep; }

      /**
       * Присваивает новое значение. Количество ссылок на старый разделяемый
       * указатель уменьшается (что может привести к его удалению), а на новый
       * -- увеличивается.
       */
      sh_ptr& operator=(const sh_ptr& shp)
      {
       shp.rep->refs++;
       if (--rep->refs==0) delete rep;
       rep=shp.rep;
       
       return *this;
      }

      /**
       * Обменивает содержимое объектов.
       */
      void swap(sh_ptr& shp)
      {
       Rep* tmp=rep;
       rep=shp.rep;
       shp.rep=tmp;
      }

      /**
       * Возвращает ссылку на разделяемый объект при применении к sh_ptr
       * оператора разыменования.
       */
      T& operator*() const { return *rep->ptr; }

      /**
       * Возвращает указатель на разделяемый объект при применении к sh_ptr
       * оператора ->.
       */
      T* operator->() const { return rep->ptr; }

      /**
       * Возвращает значение разделяемого указателя.
       */
      T* get() const { return rep->ptr; }

      /**
       * Устанавливает значение разделяемого указателя, т.е. все копии текущего
       * sh_ptr станут указывать на новый объект.
       */
      void set(T* ptr) { rep->ptr=ptr; }

      /**
       * Возвращает количество ссылок.
       */
      size_t refs() const { return rep->refs; }
};
Рассмотрим маленький пример, иллюстрирующий основные возможности sh_ptr:
void f()
{
 // создаем себе явную "реализацию строки с подсчетом ссылок", т.е. при
 // копировании указателя p1 сам объект string("просто строка") копироваться не
 // будет, что может привести к существенному росту производительности
 sh_ptr<string> p1(new string("просто строка"));

 // копируем p1, теперь p1 и p2 указывают на один и тот же объект
 sh_ptr<string> p2=p1;

 // естественно, и у p1 и у p2 количество ссылок равно 2
 assert(p1.refs()==2);
 assert(p2.refs()==2);

 // получаем размер строки, данный синтаксис возможен благодаря перегрузке
 // operator->()
 int sz=p1->size();

 // присваиваем строке новое значение посредством перегруженного operator*()
 // естественно, и p1 и p2 теперь будут указывать на новое значение
 *p2="новое значение";

 // а сейчас, p1 будет указывать на старый объект со значением "новое значение",
 // в то время как p2 -- уже на новый, но с тем же значением
 p2=sh_ptr<string>(new string("новое значение"));

 // т.е. будут равны значения объектов
 assert(*p1==*p2);
 // но не их адреса
 assert(p1.get()!=p2.get());

 // явно получаем значение указателя для собственных нужд
 string* ptr=p1.get();

 // а вот это уже серьезная ошибка: p3 ничего не знает о том, что значением
 // указателя ptr владеет p1 (чей деструктор применит к нему delete) и,
 // следовательно, тоже будет пытаться удалить ptr в деструкторе
 sh_ptr<string> p3(ptr);

 // исправляем ошибку: p3 теперь владеет нулевым указателем и его деструктор
 // никаких разрушительных действий не произведет, т.к. delete 0 суть пустая
 // операция
 p3.set(0);

 // а по выходу из блока деструкторы всех sh_ptr корректно уничтожат
 // принадлежащие им объекты
}
Хоть это, в принципе, и очевидно, но еще раз напомню, что не стоит создавать сложные объекты, ведущие внутри себя подсчет ссылок для ускорения копирования по значению. Правильный подход -- это понимание того факта, что пользователь и сам прекрасно может получить необходимую функциональность путем использования указателя sh_ptr на ваш объект. И еще, в отличие от уродца-std::auto_ptr, класс sh_ptr может и должен применяться со страндартными контейнерами, т.е. использование std::vector<sh_ptr<ClassName> > -- это довольно удачная идея, гарантирующая автоматическое удаление объектов в деструкторе вектора.

1.3. Исключения и класс CException

Данный раздел описывает ключевые моменты, связанные с возбуждением и обработкой исключений. Тема эта чрезвычайно важна и сама по себе, но гораздо более важным является тот факт, что для абсолютного большинства программистов принципы обработки исключений, изложенные далее, окажутся совершенно неожиданными.
  1. Все типы исключений, возбуждаемых в проекте -- это CException и производные от него классы.
  2. Объект-исключение (аргумент выражения throw) всегда один и тот же -- это sh_ptr<CException>.
Сам класс CException определен следующим образом:
/**
 * Общий базовый класс для всех исключений проекта. Все исключения должны
 * возбуждаться в виде sh_ptr<CException>, который содержит указатель на
 * собственно исключение, производное от CException. В принципе, отдельный
 * производный класс исключений стоит создавать только в том случае, когда у
 * пользователя должна быть возможность отличить его от "просто CException", а
 * в остальный случаях вполне подойдет и сам CException, означающий: "обнаружена
 * общего вида ошибка со следующим описанием".
 */
class CException {
 public:
      /**
       * Структура для инкапсуляции информации о месте возникновения исключения.
       */
      struct FileLine {
             /** имя файла */
             const char* fname;
             /** номер строки */
             int line;

             /**
              * Создает объект по переданным имени файла и строке.
              */
             FileLine(const char* f, int l) : fname(f), line(l) {}
      };

#ifdef DERS_RETHROW_BUG
      /** указывает на последний созданный и пока не уничтоженный объект */
      static sh_ptr<CException> current;
#endif

 private:
      /** запрещаем копирование */
      CException(const CException&);
      /** запрещаем присваивание */
      CException& operator=(const CException&);

 protected:
      /**
       * Защищенный конструктор. Для создания объектов необходимо использовать
       * функцию newCException().
       */
      CException(const FileLine& loc, const std::string& msg,
        sh_ptr<CException> nest);

 public:
      /** место возникновения исключения */
      const FileLine location;
      /** сообщение */
      const std::string message;
      /** указатель на вложенное исключение, причину текущего, или 0 */
      const sh_ptr<CException> nested;

      /**
       * Предназначена для создания объектов вместо конструктора. Создает объект
       * в свободной памяти по переданным месту исключения, сообщению и,
       * возможно, вложенному исключению. Для передачи объекта loc удобно
       * использовать макрос _FLINE_.
       */
      friend sh_ptr<CException> newCException(const FileLine& loc,
        const std::string& msg, sh_ptr<CException> nest=sh_ptr<CException>());

      /**
       * Виртуальный деструктор.
       */
      virtual ~CException();

      /**
       * Возвращает имя класса-исключения. Должна обязательно переопределяться в
       * производных классах.
       */
      virtual std::string getClassName() const;

      /**
       * Возвращает "полное описание" исключения, включающее имя класса и место
       * возникновения. Должна переопределяться производными классами, имеющими
       * дополнительные данные.
       */
      virtual std::string toString() const;

      /**
       * Возвращает "полное описание" текущего исключения и всех исключений,
       * вложенных в него. Фактически, результаты применения toString() к
       * каждому из исключений объединяются в одну строку.
       */
      std::string toStringAll() const;

      /**
       * Удобная функция для проверки является ли текущее исключение исключением
       * класса E (или производным от него). Возвращает ненулевой указатель
       * запрашиваемого типа в случае положительного ответа.
       */
      template<class E>
      E* is() { return dynamic_cast<E*>(this); }

      /**
       * Удобная функция для проверки является ли текущее константное исключение
       * исключением класса E (или производным от него). Возвращает ненулевой
       * указатель запрашиваемого типа в случае положительного ответа.
       */
      template<class E>
      const E* is() const { return dynamic_cast<const E*>(this); }

      /**
       * Переопределенный для ускорения работы распределитель памяти. Корректно
       * работает и с производными классами.
       */
      void* operator new(size_t size)
      {
       return sized_alloc::alloc(size);
      }

      /**
       * Переопределенный для ускорения работы распределитель памяти. Корректно
       * работает и с производными классами.
       */
      void operator delete(void* ptr, size_t size)
      {
       sized_alloc::free(ptr, size);
      }
};
Остановимся подробно на каждом из пунктов.
  1. Требование использовать свою собственную иерархию классов исключений имеет вполне понятные причины:
    1. Возбуждение исключений посредством любых подвернувшихся под руку типов (вроде int или string), очевидно, не самая удачная идея. Необходимо использовать иерархию специализированных классов.
    2. Использование "чужих" иерархий (например, std::exception) также нежелательно в силу того, что они имеют не вполне подходящую нам функциональность и в чужом коде за ними уже закреплен вполне определенный смысл.
  2. Но гораздо более интересным, естественно, является вопрос: Почему sh_ptr<CException>? Дело в том, что использование sh_ptr с исключениями имеет целый ряд преимуществ:
    1. Непосредственно в момент возбуждения исключения, а также по ходу его обработки, объект-исключение несколько раз копируется, что, во-первых, довольно накладно, т.к. объекты-исключения могут содержать достаточно большое количество информации, а, во-вторых, исключение, возбужденное конструктором копирования в процессе обработки начального исключения, сразу же приведет к аварийному завершению приложения посредством вызова std::terminate(). В то время как при использовании sh_ptr<CException> объект CException все время обработки продолжает жить в одном и том же месте выделенной посредством new памяти, а копируются только объекты sh_ptr, чей конструктор копирования работает быстро и никаких исключений не возбуждает.
    2. Объекты класса CException содержат указатель sh_ptr<CException> nested на исключение, являющееся причиной текущего. Такой подход позволяет сохранить точную информацию о всех возникших исключениях, а также получить некий аналог stack trace, пройдя по цепочке объектов-исключений (в принципе, строка, возвращаемая функцией CException::toStringAll(), вполне подходит для данной цели).
    3. Имея на руках функцию toCException() для перехвата исключений можно всегда использовать один-единственный catch (...) блок, что очень удобно.
Рассмотрим следующий пример:

ceexample.cpp
/** @file
 * Отдельный файл-пример, иллюстрирующий вопросы, связанные с обработкой
 * исключений. К проекту ftext он не относится.
 */

#include <stdio.h>
#include <vector>
#include "cexception.hpp"

using namespace std;

/** исключение-пример использования CException в качестве базового класса */
class ExampleCException : public CException {
 protected:
      ExampleCException(const FileLine& loc, const std::string& msg,
        sh_ptr<CException> nest) : CException(loc, msg, nest) {}

 public:
      friend sh_ptr<CException> newExampleCException(const FileLine& loc,
        const std::string& msg, sh_ptr<CException> nest=sh_ptr<CException>());

      virtual std::string getClassName() const { return "ExampleCException"; }
};

inline sh_ptr<CException> newExampleCException(const CException::FileLine& loc,
  const std::string& msg, sh_ptr<CException> nest)
{
#ifndef DERS_RETHROW_BUG
 // обычная версия тела функции
 return sh_ptr<CException>(new ExampleCException(loc, msg, nest));
#else
 // версия для компиляторов, некорректно обрабатывающих перевозбуждение
 // исключений
 // в этом случае возбуждаемое исключение следует сохранять в переменной
 // CException::current, чтобы функция toCException() смогла впоследствии
 // получить (последнее возбужденное) исключение не перевозбуждая его
 return CException::current=sh_ptr<CException>(new ExampleCException(loc, msg,
   nest));
#endif 
}

int i;

void g()
{
 // использование собственного блока try вокруг тела "сложной" функции является
 // хорошо зарекомендовавшей себя практикой, а если в спецификации функии
 // написано, что она возбуждает только CException, то без этого просто не
 // обойтись
 try {
     switch (i) {
 // возбуждаем наше ExampleCException с помощью функции newExampleCException()
 // макрос _FLINE_ заменяет громоздкое CException::FileLine(__FILE__, __LINE__)
            case 1: throw newExampleCException(_FLINE_, "Hello from g()");
 // неявно возбуждаем std::out_of_range путем доступа за пределы массива
            case 2: {
                 vector<int> v;
                 v.at(0);
            }
 // явно возбуждаем исключение типа int
            case 3: throw 3;
     }
 }
 // единственный блок catch, перехватывающий все исключения
 // т.к. используется формат (...), тип перехваченного исключения теряется, тем
 // не менее, функция toCException() перевозбуждает исключение, определяет его
 // тип и возвращает нам в виде sh_ptr<CException>
 // далее это восстановленное исключение передается в качестве nested новому
 // исключению, содержащему сообщение о проблеме в функции g()
 catch (...) {
       throw newCException(_FLINE_, "Problems in g()", toCException(_FLINE_));
 }
}

void f()
{
 try { g(); }
 // аналогично, перехватываем все исключения и сообщаем о проблеме в функции f()
 catch (...) {
       throw newCException(_FLINE_, "Problems in f()", toCException(_FLINE_));
 }
}

int main()
{
 for (i=1; i<=4; i++) {
     try {
         if (i<4) f();
 // возбуждаем "неизвестное исключение" прямо из этого блока, чтобы показать
 // зачем функция toCException() также принимает параметр CException::FileLine
         else throw 4;
     }
 // перехватываем возникшие исключения и используем toStringAll() для вывода
 // полной информации о всей цепочке исключений
     catch (...) {
           printf("\tException #%d:\n%s", i,
             toCException(_FLINE_)->toStringAll().c_str());
     }
 }
}

После запуска на исполнение он выводит следующую информацию:

	Exception #1:
CException [ceexample.cpp:90]: Problems in f()
CException [ceexample.cpp:81]: Problems in g()
ExampleCException [ceexample.cpp:64]: Hello from g()
	Exception #2:
CException [ceexample.cpp:90]: Problems in f()
CException [ceexample.cpp:81]: Problems in g()
STDExternalCException [ceexample.cpp:81], typeName="St12out_of_range", message="vector"
	Exception #3:
CException [ceexample.cpp:90]: Problems in f()
CException [ceexample.cpp:81]: Problems in g()
UnknownExternalCException [ceexample.cpp:81], typeName="unknown", message="Unknown exception"
	Exception #4:
UnknownExternalCException [ceexample.cpp:107], typeName="unknown", message="Unknown exception"
Как можно видеть, цепочки исключений позволяют получить достаточно подробную информацию о происшедшем, а т.к. реальный код вместо загадочного сообщения вроде "Problems in f()" предоставляет гораздо более содержательную диагностику, то отслеживание причин ошибочной ситуации еще более упрощается.

Хорошо, с тем, как перехватывать сразу все исключения мы уже разобрались. Но что делать в случае необходимости реализации дифференцированного подхода? Ведь пока не появился sh_ptr можно было использовать несколько блоков catch:

void f()
{
 try { g(); }
 catch (const ExampleCException&) {}  // игнорируем ExampleCException
 catch (...) { throw; }  // а все остальные перевозбуждаем
}
Нет проблем. Нам никто не запрещает использовать несколько блоков catch и сейчас, просто они становятся гораздо менее полезны. А предыдущий пример может быть переписан следующим образом:
void f()
{
 try { g(); }
 catch (...) {
       sh_ptr<CException> ce=toCException(_FLINE_);

       if (ce->is<ExampleCException>()) {}  // игнорируем ExampleCException
       else { throw; }  // а все остальные перевозбуждаем
 }
}
Обратите внимание, что функция toCException() не превращает текущее исключение (например, просто int) в sh_ptr<CException> -- она просто возвращает объект sh_ptr<CException>, содержащий информацию о текущем, все еще не обработанном исключении (т.е. else { throw; } перевозбудит все тот же int).

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


2. Интерфейсы

2.1. Что такое интерфейс

Интерфейс -- это абстрактный базовый класс, определяющий некоторое количество "абстрактных операций". Я называю данные операции абстрактными, т.к. они предоставляют пользователю только сигнатуры операций (параметры, возбуждаемые исключения и т.п.) и концептуальное описание сути действий (обычно, в виде комментариев), которые будут произведены реализацией в результате вызова пользователем соответствующей операции. Интерфейс, как и меню, показывает нам что может быть сделано с реализующим его объектом, позволяя, при этом, абстрагироваться от того, как именно это произойдет. "Абстракция -- это избирательное невежество", дающее нам возможность разбить задачу на части, которые могут быть восприняты мозгом, в то время как сложность всей проблемы в целом, как правило, остается за рамками человеческих возможностей.

Рассмотрим следующий пример. Допустим, что по ходу реализации проекта у нас возникает необходимость сопоставления имен файлов с некоторой маской. В данном случае:

  1. Имеется некоторая нетривиальная операция, которую мы бы хотели осуществить, но нет стандартного (т.е. определенного в стандарте C++) способа это сделать.
  2. Существуют несколько возможных реализаций данной операции, которые, в частности, зависят и от способа задания маски. Так, например, возникает вопрос: "Можно ли будет использовать маски вида *.[ch]pp?", ответ на который для пользователей Windows будет далеко не очевиден.
Исходя из вышеперечисленного, в нашем проекте появляется следующий интерфейс:
/**
 * Данный интерфейс предназначен для сопоставления имен файлов с маской.
 * Предполагается, что маска задается в момент создания объекта, что позволяет
 * произвести всю необходимую инициализацию только один раз.
 */
class NameMatcher {
 public:
      /**
       * Выполняет сопоставление переданного имени файла с маской, заданной в
       * момент создания объекта. Возвращает true, если имя файла удовлетворяет
       * маске. Возбуждает CException  при обнаружении ошибок.
       */
      virtual bool match(const std::string& fileName)=0;

      /**
       * Обязательный виртуальный деструктор.
       */
      virtual ~NameMatcher() {}

 private:
      /** запрещаем присваивание */
      NameMatcher& operator=(const NameMatcher&);
};
Начиная с этого момента, у нас возникает прекрасная возможность распараллелить ход работ, назначив задание по созданию промышленной реализации NameMatcher второму разработчику, в то время как первый продолжит свою работу над прикладным кодом, создав для себя тривиальнейшую тестовую реализацию вроде:
/** временная тестовая реализация */
class NameMatcherTest : public NameMatcher {
      /** все файлы подходят */
      virtual bool match(const std::string&) { return true; }
};
Думаю, самое время предостеречь начинающих, что создание тестовой реализации NameMatcherTest отнють не означает, что прикладной код будет напрямую создавать объекты данного класса
void f()
{
 NameMatcherTest nm;

 for (;;) {
     string name;
     // ...
     if (nm.match(name)) {
        // ...
     }
     // ...
 }
}
а потом всюду заменит имя NameMatcherTest на что-то вроде NameMatcherProfessional, созданное вторым разработчиком. Это означает, что прикладной код будет работать только через интерфейс
void f(sh_ptr<NameMatcher> nm)
{
 for (;;) {
     string name;
     // ...
     if (nm->match(name)) {
        // ...
     }
     // ...
 }
}
и никаких массовых замен щедро рассыпанного по коду упоминания NameMatcherTest никогда не понадобится: для этого и нужно промежуточное звено -- абстрактный базовый класс NameMatcher.

"Ну да, все просто замечательно!" -- воскликнет проницательный читатель, -- "Уж не хотите ли вы сказать, что в коде вообще не будет упоминания NameMatcherTest?" Нет, дорогой читатель! Не хочу и не скажу, т.к. одно упоминание NameMatcherTest нам наверняка понадобится, но не более того. И речь об этом пойдет в следующем разделе.


2.2. Фабрики объектов

Техника использования интерфейсов основана на том, что прикладной код ссылается только на стандартные функции/классы (стандартные в смысле стандарта C++ и стандартного инструментария проекта) и создаваемые разработчиками интерфейсы. И никаких #include чисто системных файлов вроде unistd.h или windows.h! Малейшее их упоминание в прикладном коде -- и перенос приложения на другую платформу, или даже на другой компилятор для той же самой платформы, превратится в сущий кошмар! Именно тот кошмар, который и происходит в 95, а то и более, процентах компаний по разработке программного обеспечения. Стандарты нужно уважать! Они, как и наставления по технике безопасности, кровью писаны, и множество судеб уже было и, что более печально, еще будет искалечено путем игнорирования "досадных инструкций", которые "никому не нужны".

Ну ладно, хватит крови, поговорим о более приятных вещах. Прикладной код -- это та основа приложения (процентов 80% всех усилий), которая реализует собственно идею приложения, тем самым отличая его от других. Если пристально вглядеться, то этот код работает со множеством различных источников и стоков данных платформно-независимым способом, и этой его независимостью обязательно нужно воспользоваться. Даже если вы не планируете портирование приложения (ха!) на другие платформы, четкое разделение того, что делается, от того как это делается будет чрезвычайно полезно для проекта.

Хорошо, а что же такое не прикладной код? Это код, отвечающий за "как делается" и, вообще говоря, зависящий от платформы. Именно он знает имена классов, реализующих наши интерфейсы.

Итак, прикладной код работает через ссылки и (умные) указатели на интерфейсы (надеюсь вы помните, что по счастливой случайности в C++ нельзя создавать объекты абстрактных базовых классов?), а зависящий код предоставляет ему объекты, их реализующие. И создаются эти объекты в фабриках объектов, а прикладной код получает указатели на них, и с этих самых пор отвечает за их корректное удаление. К счастью, ответственность за удаление практически всегда можно поручить умным указателям (с подсчетом ссылок).

Определим, например, пространство имен Factory и разместим в нем функции для создания необходимых нам объектов. Можно, конечно, использовать и класс Factory с набором статических функций, но пространство имен имеет то неоспоримое преимущество, что может объявляться по частям, т.е. объявление

/**
 * Фабрика объектов.
 */
namespace Factory {

 /**
  * Создает объект по переданной маске, реализующий интерфейс NameMatcher. В
  * случае возникновения ошибок возбуждает CException.
  */
 sh_ptr<NameMatcher> newNameMatcher(const std::string& mask);

}
будет находиться в том же заголовочном файле, что и интерфейс NameMatcher, а определение -- вместе с реализацией интерфейса:

namematcherimpl.cpp
/** @file
 * Реализация интерфейса NameMatcher.
 */

#include "namematcher.hpp"
#include <ctype.h>

using namespace std;

namespace {

 /**
  * Класс-реализация интерфейса NameMatcher, выполняющая простейшее
  * сопоставление маски с концом файла.
  */
 class NameMatcherImpl : public NameMatcher {
       /** суффикс, на который должно заканчиваться имя файла */
       string suff;
       /** индекс последнего символа суффикса */
       int send;

       /**
        * Проверяет соответствие имени файла маске. Данная простейшая реализация
        * допускает только маски вида "*ext" (т.е. имя файла должно
        * заканчиваться на "ext") и игнорирует регистр букв. Звездочка "*" в
        * начале маски не обязательна, т.е. маска "ext" эквивалентна "*ext".
        */
       virtual bool match(const std::string& fileName);

  public:
       /**
        * Создает объект по переданной маске.
        */
       NameMatcherImpl(const std::string& mask);
 };

 NameMatcherImpl::NameMatcherImpl(const std::string& mask)
 {
  int beg= (mask[0]=='*') ? 1 : 0;
  suff.reserve(mask.size()-beg);

  for (int i=beg; i<mask.size(); i++) suff.push_back(tolower(mask[i]));
  send=suff.size()-1;
 }

 bool NameMatcherImpl::match(const std::string& fileName)
 {
  if (fileName.size()<suff.size()) return 0;

  for (int fend=fileName.size()-1, i=0; i<suff.size(); i++)
      if (tolower(fileName[fend-i])!=suff[send-i]) return 0;

  return 1;
 }

}

sh_ptr<NameMatcher> Factory::newNameMatcher(const std::string& mask)
{
 try { return sh_ptr<NameMatcher>(new NameMatcherImpl(mask)); }
 catch (...) {
       throw newCException(_FLINE_, "Can't create NameMatcherImpl object",
         toCException(_FLINE_));
 }
}

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

Обратите внимание на использование умного указателя sh_ptr -- важную деталь дизайна фабрики объектов. Фабрика создает объект по требованию пользователя, но не знает, когда необходимо его уничтожить. В то время как пользователь получает указатель на созданный объект и должен (очевидно) не забыть его корректно уничтожить даже в случае возникновения исключений (что тоже должно быть очевидно, но зачастую ускользает от внимания "рядовых разработчиков").


3. Сообщения

3.1. Что такое сообщения

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

В данном разделе будет описана модель передачи сообщений вида Publisher/Subscriber, т.е. Издатель/Подписчик. Ее отличительной чертой является пересылка сообщения сразу нескольким подписчикам (возможно, ни одному). Издатель публикует сообщения о соответствующих изменениях своего состояния или наступлении определенных событий, а подписчики получают возможность внести изменения в собственное состояние.

Рассмотрим следующий пример. Пусть у нас есть визуальный компонент СтрокаВвода, публикующий сообщения НажатаКлавиша. По ходу работы на его сообщения могут независимо друг от друга подписываться объекты пользователя ПроверкаФормата и ЗаписьМакроса. Преимущества использования рассылки сообщений очевидны:

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

3.2. Классы Publisher и Subscriber

Рассмотрим следующие классы, позволяюшие реализовать модель Издатель/Подписчик.
/**
 * Данный класс является издателем, передающим сообщения типа M всем подписанным
 * подписчикам (класс Subscriber). Кроме сообщений, передаваемых из функции
 * send(), Publisher самостоятельно передает сообщения о подписке и прекращении
 * подписки. Им гарантируется, что подписавшийся подписчик всегда получит
 * subscribedMsg() и unsubscribedMsg() сообщения (которые могут служить своего
 * рода аналогами конструктора и деструктора), а все сообщения, передаваемые
 * посредством send(), будут заключены между ними. Деструктор класса Publisher
 * корректно информирует всех подписавшихся о прекращении подписки.
 */
template<class M>
class Publisher {
      friend class Subscriber<M>;

      /** часть реализации, не зависящая от параметра M */
      PubSub_private::PubImpl impl;

      /** запрещаем копирование */
      Publisher(const Publisher&);
      /** запрещаем присваивание */
      Publisher& operator=(const Publisher&);

 public:
      /**
       * Создает объект.
       */
      Publisher() {}

      /**
       * Проходит по всем подписчикам и удаляет себя из их списков издателей. В
       * процессе удаления подписчики получают сообщение о прекращении подписки,
       * т.е. вызывается их функция unsubscribedMsg().
       */
      virtual ~Publisher();

      /**
       * Подписывает переданного подписчика (если он еще не был подписан) и
       * передает ему сообщение о подписке, т.е. вызывается его функция
       * subscribedMsg(). Возвращает false, если он уже был подписан.
       */
      bool subscribe(Subscriber<M>& sub);

      /**
       * Отменяет подписку переданного подписчика (если он был подписан) и
       * передает ему сообщение о прекращении подписки, т.е. вызывается его
       * функция unsubscribedMsg(). Возвращает false, если подписчик не был
       * подписан.
       */
      bool unsubscribe(Subscriber<M>& sub);

      /**
       * Возвращает true, если переданный подписчик в настоящее время подписан.
       */
      bool isSubscribed(Subscriber<M>& sub) const
      {
       return impl.isSubscribed(&sub);
      }

      /**
       * Возвращает количество подписанных подписчиков.
       */
      int subCount() const { return impl.subCount(); }

      /**
       * Отправляет переданное сообщение всем подписанным подписчикам, т.е.
       * вызывается функция regularMsg() каждого из них. Если regularMsg()
       * вернет false, то подписчик будет сразу же отписан -- именно поэтому
       * send() не является const функцией. Возвращает количество подписчиков,
       * получивших сообщение.
       */
      int send(const M& msg);
};

/**
 * Данный класс является подписчиком сообщений типа M, передаваемых издателем
 * (класс Publisher). Пользователь должен создать производный класс и
 * переопределить виртуальные функции, соответствующие тем сообщениям, которые
 * он хочет получать. Класс Subscriber автоматически ведет список всех
 * издателей, на сообщения которых он подписан, для корректной отписки в
 * деструкторе.
 */
template<class M>
class Subscriber {
      friend class Publisher<M>;

      /** определение типа для краткости */
      typedef typename std::list<Publisher<M>*> list_type;
      /** список издателей, на сообщения которых подписан данный подписчик */
      list_type pubs;

      /** запрещаем копирование */
      Subscriber(const Subscriber&);
      /** запрещаем присваивание */
      Subscriber& operator=(const Subscriber&);

 public:
      /**
       * Создает объект.
       */
      Subscriber() {}

      /**
       * Проходит по списку издателей и отписывается от каждого из них.
       * Переопределенная пользователем Функция unsubscribedMsg() при этом не
       * вызывается.
       */
      virtual ~Subscriber();

      /**
       * Данная функция может быть переопределена пользователем для обработки
       * сообщений издателя, посылаемых им из send(). Если она вернет false, то
       * подписчик будет сразу же отписан (с вызовом unsubscribedMsg()).
       * Объект-издатель передается по константной ссылке pub для предотвращения
       * его изменения в процессе передачи сообщения подписчикам, т.к. это может
       * привести к ошибкам. Все исключения, возбуженные regularMsg(), будут
       * проигнорированы.
       */
      virtual bool regularMsg(const Publisher<M>& pub, const M& msg);

      /**
       * Данная функция может быть переопределена пользователем для получения
       * сообщений о подписке. Объект-издатель передается по константной ссылке
       * pub для предотвращения его изменения в процессе передачи сообщения
       * подписчикам, т.к. это может привести к ошибкам. Все исключения,
       * возбуженные subscribedMsg(), будут проигнорированы.
       */
      virtual void subscribedMsg(const Publisher<M>& pub);

      /**
       * Данная функция может быть переопределена пользователем для получения
       * сообщений о прекращении подписки. Объект-издатель передается по
       * константной ссылке pub для предотвращения его изменения в процессе
       * передачи сообщения подписчикам, т.к. это может привести к ошибкам. Все
       * исключения, возбуженные unsubscribedMsg(), будут проигнорированы.
       */
      virtual void unsubscribedMsg(const Publisher<M>& pub);
};
Нетривиальной их частью является необходимость следить за временем жизни издателей и подписчиков, связанных между собой. Для иллюстрации эффектов, возникающих при безвременной кончине издателей/подписчиков, рассмотрим следующий пример:

psexample.cpp
/** @file
 * Отдельный файл-пример, иллюстрирующий вопросы, связанные с временем жизни
 * классов Publisher и Subscriber. К проекту ftext он не относится.
 */

#include <stdio.h>
#include <string>
#include "pubsub.hpp"

using namespace std;

/** для вывода информации о создании/уничтожении объекта */
struct Named {
       string name;
       
       Named(const string& n) : name(n)
       {
        printf("constructed: %s\n", name.c_str());
       }
       
       ~Named() { printf("destructed: %s\n", name.c_str()); }
};

/** издатель */
struct Pub : Named, Publisher<string> {
       Pub(const string& name) : Named(name) {}
};

/** подписчик */
struct Sub : Named, Subscriber<string> {
       Sub(const string& name) : Named(name) {}

       virtual bool regularMsg(const Publisher<string>& pub, const string& msg)
       {
        printf("%s received %s from %s\n", name.c_str(), msg.c_str(),
          static_cast<const Pub&>(pub).name.c_str());
        return 1;
       }

       virtual void subscribedMsg(const Publisher<string>& pub)
       {
        printf("%s received subscribedMsg from %s\n", name.c_str(), 
          static_cast<const Pub&>(pub).name.c_str());
       }

       virtual void unsubscribedMsg(const Publisher<string>& pub)
       {
        printf("%s received unsubscribedMsg from %s\n", name.c_str(), 
          static_cast<const Pub&>(pub).name.c_str());
       }
};

int main()
{
 Pub p1("p1");
 {
   Sub s1("s1");
   {
     Pub p2("p2");
     {
       Sub s2("s2");

       p1.subscribe(s1);
       p1.subscribe(s2);
       p2.subscribe(s1);
       p2.subscribe(s2);

       p1.send("message1");
       p2.send("message1");
     }

     p1.send("message2");
     p2.send("message2");
    }

   p1.send("message3");
 }

 p1.send("message4");
}

Результатом его работы является следующий вывод:

constructed: p1
constructed: s1
constructed: p2
constructed: s2
s1 received subscribedMsg from p1
s2 received subscribedMsg from p1
s1 received subscribedMsg from p2
s2 received subscribedMsg from p2
s1 received message1 from p1
s2 received message1 from p1
s1 received message1 from p2
s2 received message1 from p2
destructed: s2
s1 received message2 from p1
s1 received message2 from p2
s1 received unsubscribedMsg from p2
destructed: p2
s1 received message3 from p1
destructed: s1
destructed: p1
Как можно видеть:
  1. После уничтожения подписчика s2 издатели не передают ему сообщение message2, т.е. никаких подвисших ссылок на удаленный объект не возникает. В этом случае, он также не получает и сообщение unsubscribedMsg.
  2. В процессе уничтожения издателя p2 подписчик s1 получает сообщение unsubscribedMsg. Если подписчик знает, что никто не мог его отписать с помощью вызова функции unsubscribe() соответствующего издателя, то получение unsubscribedMsg свидетельствует о том, что издатель просто прекратил свое существование.
  3. К моменту передачи сообщения message4 все подписчики уже прекратили свое существование, поэтому данное сообщение не получает никто.
Следует отметить, что пункт 2 описывает достаточно интересный побочный эффект. Дело в том, что на практике довольно часто встречается задача по необходимости отслеживания времени жизни взаимосвязанных объектов. Так, например, класс Connection (соединение с БД) может порождать объекты класса Statement (SQL-выражение). При этом, мы должны гарантировать, что попытка использования Statement после того, как породивший его Connection уже прекратил свое существование будет корректно обработана. И здесь нам на помощь приходит сладкая парочка Publisher/Subscriber, которая используется только из-за сообщений unsubscribedMsg, автоматически генерируемых деструктором издателя:
/** соединение с БД */
class Connection {
      /**
       * Издатель, используемый для автоматического информирования объектов
       * Statement о закрытии соединения.
       */
      Publisher<int> pub;

 public:
      /** создает объект Statement по текущему соединению */
      sh_ptr<Statement> newStatement()
      {
       try {
           sh_ptr<Statement> ret(new Statement(this)); // создаем объект
           pub.subscribe(*ret);  // и сразу же его подписываем

           return ret;
       }
       catch (...) {
             throw newDBException(_FLINE_, "Can't create Statement object",
               toCException(_FLINE_));
       }
      }
};

/** SQL-выражение, порожденное по Connection */
class Statement : private Subscriber<int> {
      friend class Connection;

      /**
       * Указатель на соединение, породившее данный объект. Если объект
       * Connection прекратит свое существование раньше Statement, то указатель
       * будет обнулен, поэтому все функции его использующие должны обязательно
       * вызывать checkConn() для проверки.
       */
      Connection* pConn;

      /**
       * Данная функция должна вызываться для проверки того, что породившее
       * Statement соединение все еще существует. В случае обнаружения
       * несуществующего соединения будет возбуждено DBException.
       */
      void checkConn()
      {
       if (!pConn) throw newDBException(_FLINE_, "Connection closed");
      }

      /**
       * Функция-обработчик сообщения от породившего объекта Connection. Она
       * будет автоматически вызвана в процессе уничтожения соединения и
       * обнулит указатель pConn.
       */
      virtual void unsubscribedMsg(const Publisher<int>&)
      {
       pConn=0;
      }

      /** создает новый объект по соединению */
      Statement(Connection* pc) : pConn(pc)
      {
       // ...
      }

 public:
      /** исполняет SQL-выражение в рамках породившего соединения */
      int execDirect()
      {
       checkConn();  // проверяем соединение

       // ...
      }
};
Естественно, это не более чем грубый набросок для иллюстрации применения описанной выше техники. В реальном коде и Connection и Statement являются интерфейсами и живут в соответствующем пространстве имен.

4. Пример: Поиск текста в файле

4.1. Постановка задачи

А сейчас перейдем к практическому примеру, иллюстрирующему использование интерфейсов и сообщений в реальном коде. Кстати сказать, внимательное изучение исходного кода поможет овладеть и другими техническими приемами, рекомендуемыми к использованию при разработке на C++.

Итак, разрабатываемое нами приложение (упрощенный аналог утилиты grep) должно:

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

4.2. Интерфейс FindText

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

В идеале, интерфейс пользователя (ИП) и код ядра приложения должны полностью избежать ненужного зацепления, когда код ИП и код ядра прямо ссылаются на фрагменты друг друга. К счастью, использование интерфейса, четко разделяющего эти две главные части приложения, позволяет нам без проблем достигнуть данного идеала. Думаю понятно, что именно ядро приложения должно скрываться от ИП за интерфейсом, т.к. оно является пассивной стороной, реагирующей на действия пользователя. А по сему, определим интерфейс FindText, предоставляющий функцию search():

class FindText {
 public:
      virtual void search(const std::string& dir, bool recur,
        const std::string& mask, const std::string& text)=0;

      // ...
};
Ну вот: ИП с помощью фабрики объектов создает себе объект, реализующий интерфейс FindText, вызывает функцию search() и... Что "и"?! Дело ясное, что реализация search() прекрасно выполнит работу по поиску текста, заблокировав по ходу дела ИП на заметное (т.е. более 0.1 секунды) время, но где же результат работы? А еще неплохо бы информировать пользователя о достигнутом прогрессе. Да дать ему возможность прервать обработку...

Короче, как бы это реализации search() сообщать вызвавшей стороне о ходе работ? Не удивлюсь, если вы скажете: "Посредством сообщений!" Правильное решение! Осталось только утрясти номенклатурку.

Классы Publisher и Subscriber позволяют нам передавать сообщения некоторого фиксированного типа M (являющегося их параметром шаблона) или производного от него. Нетрудно видеть, что существует два полярных подхода к определению типов сообщений:

  1. Определим один базовый класс для всех типов сообщений, генерируемых FindText. Это позволит нам использовать единственную специализацию шаблонов Publisher/Subscriber. С другой стороны, пользователя крайне редко интересуют все возможные сообщения сразу, и на их пересылку в издателе и фильтрацию в подписчике будут впустую расходоваться ресурсы.
  2. Определим множество классов сообщений, по одному на каждый тип. Это позволит подписчику максимально точно выбирать сообщения для обработки, но (в сколь-нибудь реальном коде) приведет к взрывному росту количества различных специализаций Publisher/Subscriber.
И как всегда в нашей жизни, истина лежит где-то посередине. Как показывает практика, золотой серединой является следующая классификация сообщений:
  1. Сообщения о промежуточных результатах работы. В нашем случае, это сообщения об обнаружении очередной строки файла, содержащей заданный текст. Относительное время их появления невозможно предугадать, фактически, их может не быть вообще.
  2. Информационные сообщения о ходе работ. Они, как правило, используются для отображения прогресса работы и должны генерироваться с достаточной частотой. В процессе их обработки ИП может реагировать на действия пользователя, например, досрочно прервать обработку.
  3. Сообщения о нефатальных ошибках обработки (фатальные, очевидно, сразу же приведут к завершению работы search()). Также как и сообщения о результатах, они поступают неравномерно, если и поступают вообще.
В результате чего, мы приходим к следующим типам сообщений (обратите внимание, что MsgBase инкапсулирует общую информацию, а не служит для приведения всех сообщений к одному базовому классу в смысле единой специализации шаблонов Publisher/Subscriber):
/**
 * Данная структура инкапсулирует ссылку на пославший сообщение интерфейс и
 * является общим базовым классом всех сообщений. Ссылка может быть использована
 * для прекращения поиска (т.е. для прерывания работы search()) посредством
 * вызова функции stopSearch().
 */
struct MsgBase {
       /** ссылка на пославший сообщение интерфейс */
       FindText& ft;

       /**
        * Создает объект по переданным данным.
        */
       MsgBase(FindText& ft_) : ft(ft_) {}
};

/**
 * Данная структура представляет собой сообщение, которое передает интерфейс
 * FindText своим подписчикам в случае обнаружения искомой строки файла.
 */
struct FoundMsg : MsgBase {
       /** имя файла */
       const std::string fileName;
       /** номер строки (с 1) */
       const int lineNum;
       /** текст строки */
       const std::string lineText;

       /**
        * Создает объект по переданным данным.
        */
       FoundMsg(FindText& ft, const std::string& file, int num,
         const std::string& txt) : MsgBase(ft), fileName(file), lineNum(num),
         lineText(txt) {}
};

/**
 * Данная структура представляет собой информационное сообщение о ходе работы,
 * передаваемое интерфейсом FindText своим подписчикам.
 */
struct InfoMsg : MsgBase {
       /** типы информационных сообщений: файл открыт, файл закрыт */
       enum Type { opened, closed };

       /** тип сообщения */
       const Type type;
       /** имя файла */
       const std::string fileName;

       /**
        * Создает объект по переданным данным.
        */
       InfoMsg(FindText& ft, Type t, const std::string& fn) : MsgBase(ft),
         type(t), fileName(fn) {}
};

/**
 * Данная структура представляет собой сообщение о нефатальной ошибке (например,
 * недостаточно прав доступа для открытия файла) не останавливаюшей обработку,
 * которое передает интерфейс FindText своим подписчикам.
 */
struct ErrorMsg : MsgBase {
       /** сообщение об ошибке */
       const std::string errMsg;

       /**
        * Создает объект по переданным данным.
        */
       ErrorMsg(FindText& ft, const std::string& err) : MsgBase(ft), errMsg(err)
         {}
};
Ну а пополненный интерфейс FindText выглядит следующим образом:
/**
 * Данный интерфейс предназначен для выполнения поиска подстроки в файлах по
 * маске. Реализующие его классы осуществляют поиск и публикуют сообщения,
 * информирующие пользователя о ходе работы и позволяющие корректным образом ее
 * прервать.
 */
class FindText {
 public:
      /**
       * Выполняет поиск подстроки text в файлах, удовлетворяющих маске mask.
       * Поиск производится в директории dir и, если задан флаг recur, во всех
       * ее поддиректориях. В процессе работы происходит рассылка сообщений, на
       * которые можно подписаться с помощью соответствующих издателей.
       * Возбуждает CException в случае обнаружения фатальных ошибок.
       */
      virtual void search(const std::string& dir, bool recur,
        const std::string& mask, const std::string& text)=0;

      /**
       * Данная функция предназначена для остановки работы функции search(). Она
       * может быть вызвана только в процессе обработки сообщения. При 
       * обнаружении ошибок возбуждает CException.
       */
      virtual void stopSearch()=0;

      /**
       * Издатель, публикующий сообщения о найденных строках. В процессе
       * обработки сообщения подписчик может вызвать функцию stopSearch() для
       * прекращения поиска.
       */
      virtual Publisher<FoundMsg>& foundPub()=0;

      /**
       * Издатель, публикующий информационные сообщения о ходе работы. В
       * процессе обработки сообщения подписчик может вызвать функцию
       * stopSearch() для прекращения поиска.
       */
      virtual Publisher<InfoMsg>& infoPub()=0;

      /**
       * Издатель, публикующий сообщения о нефатальных ошибках, встреченных по
       * ходу работы. В процессе обработки сообщения подписчик может вызвать
       * функцию stopSearch() для прекращения поиска.
       */
      virtual Publisher<ErrorMsg>& errorPub()=0;

      /**
       * Обязательный виртуальный деструктор.
       */
      virtual ~FindText() {}

 private:
      /** запрещаем присваивание */
      FindText& operator=(const FindText&);
};

4.3. Реализация FindText

Коль скоро интерфейс FindText благополучно определен -- самое время перейти к его реализации. Еще раз напомню, что начиная с этого момента работа по разработке приложения может вестись уже двумя группами (группа ИП и группа ядра) полностью параллельно, т.к. все, что их соединяет -- это интерфейс FindText, и он уже определен.

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

/**
 * Фабрика объектов.
 */
namespace Factory {

 /**
  * Создает объект, реализующий интерфейс FindText. В случае возникновения 
  * ошибок возбуждает CException.
  */
 sh_ptr<FindText> newFindText();

}
А продолжим уже непосредственно реализацией. Прежде всего отметим, что первое действие реализации функции search() -- это (рекурсивный) поиск фалов по маске. К сожалению, стандарт C++ не предоставляет нам средств для осуществления подобного поиска, но, к счастью, мы прекрасно понимаем, что проблема может быть решена путем введения еще одного интерфейса:

findfile.hpp
/** @file
 * Определение интерфейса FindFile.
 */

#ifndef __FINDFILE_HPP__
 #define __FINDFILE_HPP__

#include "cexception.hpp"

/**
 * Данный интерфейс предоставляет callback функцию для FindFile.
 */
class FindFileCallback {
 public:
      /**
       * Данная функция будет вызываться в процессе работы FindFile::find() для
       * каждого найденного файла. Если она вернет false, то FindFile::find()
       * завершит свою работу. Исключения, возбужденные found() приведут к
       * аварийному завершению работы FindFile::find(), которая в этом случае
       * также возбудит исключение.
       */
      virtual bool found(const std::string& fileName)=0;

      /**
       * Обязательный виртуальный деструктор.
       */
      virtual ~FindFileCallback() {}

 private:
      /** запрещаем присваивание */
      FindFileCallback& operator=(const FindFileCallback&);
};

/**
 * Данный интерфейс предназначен для поиска файлов по маске с возможностью
 * рекурсивного поиска в поддиректориях.
 */
class FindFile {
 public:
      /**
       * Выполняет поиск файлов по маске mask в директории dir и, если задан
       * флаг recur, во всех ее поддиректориях. Для каждого найденного файла
       * вызывается функция found() переданного объекта FindFileCallback. Если
       * она вернет false, то поиск прекратится и сама функция find() тоже
       * вернет false. Возбуждает CException при обнаружении ошибок.
       */
      virtual bool find(const std::string& dir, bool recur,
        const std::string& mask, FindFileCallback& cb)=0;

      /**
       * Обязательный виртуальный деструктор.
       */
      virtual ~FindFile() {}

 private:
      /** запрещаем присваивание */
      FindFile& operator=(const FindFile&);
};

/**
 * Фабрика объектов.
 */
namespace Factory {

 /**
  * Создает объект, реализующий интерфейс FindFile. В случае возникновения 
  * ошибок возбуждает CException.
  */
 sh_ptr<FindFile> newFindFile();

}

#endif

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

textfinder.hpp
/** @file
 * Определение интерфейса TextFinder.
 */

#ifndef __TEXTFINDER_HPP__
 #define __TEXTFINDER_HPP__

#include "cexception.hpp"

/**
 * Данный интерфейс предназначен для проверки содержит ли переданная строка
 * заданный текст. Предполагается, что текст задается в момент создания объекта,
 * что позволяет произвести всю необходимую инициализацию только один раз.
 */
class TextFinder {
 public:
      /**
       * Проверяет входит ли заданный текст в переданную строку. Возвращает
       * true, если найдено хотя бы одно вхождение. Возбуждает CException  при
       * обнаружении ошибок.
       */
      virtual bool findIn(const std::string& line)=0;

      /**
       * Обязательный виртуальный деструктор.
       */
      virtual ~TextFinder() {}

 private:
      /** запрещаем присваивание */
      TextFinder& operator=(const TextFinder&);
};

/**
 * Фабрика объектов.
 */
namespace Factory {

 /**
  * Создает объект по переданному тексту, реализующий интерфейс TextFinder. В
  * случае возникновения ошибок возбуждает CException.
  */
 sh_ptr<TextFinder> newTextFinder(const std::string& text);

}

#endif

Ну, вроде бы и все, пора приниматься за работу.

/** @file
 * Реализация интерфейса FindText.
 */

#include "findtext.hpp"
Включение заголовочного файла с объявлениями самым первым в файл с соответствующими реализациями является довольно полезной привычкой, т.к. позволяет отследить тот случай, когда данный хедер не является самодостаточным (например, не включает другие хедеры с необходимыми ему объявлениями).
#include <string.h>
#include <errno.h>
#include "findfile.hpp"
#include "textfinder.hpp"
#include "autofile.hpp"
#include "stringbuf.hpp"

using namespace std;
Импортируем все определения из пространства имен std. Надеюсь, вы заметили, что ни один из заголовочных файлов не содержал подобной директивы? Это не случайно, т.к. действие подобной директивы уже невозможно отменить, и если кто-то поместил ее в хедер, то включая его в свой файл вы автоматически получаете то, что вредит вашему коду, но уже ничего не поделаешь.
namespace {

 /** вспомогательная структура для автоматической очистки флага */
 struct AutoFlag {
        /** ссылка на флаг */
        bool& flag;

        /** создает объект, контролирующий переданный флаг */
        AutoFlag(bool& f) : flag(f) {}
        /** очищает флаг */
        ~AutoFlag() { flag=0; }
 };
Подобного рода вспомогательные структуры встречаются достаточно часто, но, как правило, не удостаиваются помещения в отдельный заголовочный файл ввиду своей излишней специфичности и упрощенной реализации.
 struct OpenCloseSender;

 /**
  * Класс-реализация интерфейса FindText.
  */
 class FindTextImpl : public FindText, FindFileCallback, Publisher<FoundMsg>,
   Publisher<InfoMsg>, Publisher<ErrorMsg> {
Обратите внимание, что здесь используется реализационное (т.е. private) наследование сразу от нескольких классов.
       friend struct OpenCloseSender;

       /** флаг, показывающий выполняется ли в данное время search() */
       bool running;
       /** флаг, показывающий, что search() должна остановиться */
       bool shouldStop;
       /** объект для поиска текста в строке */
       sh_ptr<TextFinder> txtFnd;

       virtual void search(const std::string& dir, bool recur,
         const std::string& mask, const std::string& text);

       virtual void stopSearch();

       virtual Publisher<FoundMsg>& foundPub();

       virtual Publisher<InfoMsg>& infoPub();

       virtual Publisher<ErrorMsg>& errorPub();

       /**
        * Данная функция вызывается для каждого найденного файла. Она читает
        * содержимое файла, ищет заданную подстроку и посылает соответствующие
        * сообщения.
        */
       virtual bool found(const std::string& fileName);

       /**
        * Читает строку текста из f в str. При обнаружении ошибки чтения
        * посылает сообщение ErrorMsg. Возвращает false по EOF или ошибке.
        */
       bool readLine(FILE* f, string& str, const std::string& file);

  public:
       /**
        * Создает объект, устанавливая начальные значения флагов.
        */
       FindTextImpl() : running(0), shouldStop(0) {}
 };

 /**
  * Вспомогательная структура для автоматической рассылки сообщений
  * InfoMsg::opened и InfoMsg::closed.
  */
 struct OpenCloseSender {
        /** ссылка на посылающий сообщения объект */
        FindTextImpl& fti;
        /** имя файла */
        const string& file;

        /** посылает сообщение InfoMsg::opened */
        OpenCloseSender(FindTextImpl& fti_, const string& file_) : fti(fti_),
          file(file_)
        {
         fti.Publisher<InfoMsg>::send(InfoMsg(fti, InfoMsg::opened, file));
        }

        /** посылает сообщение InfoMsg::closed */
        ~OpenCloseSender()
        {
         fti.Publisher<InfoMsg>::send(InfoMsg(fti, InfoMsg::closed, file));
        }
 };
Еще один пример вспомогательной структуры, корректно выполняющей свою работу даже при возникновении исключений.
 void FindTextImpl::search(const std::string& dir, bool recur,
         const std::string& mask, const std::string& text)
 {
  try {
      if (running)
         throw newCException(_FLINE_, "search() already running");
      running=1;
      
      AutoFlag autoRunning(running);
      AutoFlag autoShouldStop(shouldStop);

      txtFnd=Factory::newTextFinder(text);

      sh_ptr<FindFile> ff=Factory::newFindFile();
      ff->find(dir, recur, mask, *this);
  }
  catch (...) {
        throw newCException(_FLINE_, "Can't search", toCException(_FLINE_));
  }
Традиционный способ преобразования любого возникшего исключения в обещанное в спецификации CException (естественно, реальный тип возбуждаемого исключения -- это sh_ptr<CException>). Используется один-единственный блок catch (...) и уже внутри него исключение перевозбуждается, а полученная при этом информация возвращается функцией toCException() в виде sh_ptr<CException>. Данное исключение является причиной текущего, а по сему, оно передается ему в качестве nested.
 }
Обратите внимание, что с использованием всех необходимых интерфейсов реализация search() становится подкупающе простой и изящной.
 void FindTextImpl::stopSearch()
 {
  if (!running) throw newCException(_FLINE_, "search() isn't running");
  shouldStop=1;
 }

 Publisher<FoundMsg>& FindTextImpl::foundPub()
 {
  return *this;
 }

 Publisher<InfoMsg>& FindTextImpl::infoPub()
 {
  return *this;
 }

 Publisher<ErrorMsg>& FindTextImpl::errorPub()
 {
  return *this;
 }

 bool FindTextImpl::found(const std::string& fileName)
 {
  try {
      if (shouldStop) return 0;

      AutoFILE f(fopen(fileName.c_str(), "r"));
      if (!f.get()) {
         Publisher<ErrorMsg>::send(ErrorMsg(*this, StringBuf("Can't open \"")+
           fileName+"\": "+strerror(errno)));
         return 1;
      }

      OpenCloseSender ocs(*this, fileName);

      string str;
      str.reserve(1024);

      for (int lineNum=1; readLine(f.get(), str, fileName); lineNum++) {
          if (txtFnd->findIn(str))
             Publisher<FoundMsg>::send(FoundMsg(*this, fileName, lineNum, str));
      }

      return 1;
  }
  catch (...) {
        throw newCException(_FLINE_, "Can't process \""+fileName+"\"",
          toCException(_FLINE_));
  }
 }
Да и found() не намного сложнее.
 bool FindTextImpl::readLine(FILE* f, string& str, const std::string& file)
 {
  if (feof(f)) return 0;
  str.clear();
  
  for (int ch; ; ) {
      switch (ch=fgetc(f)) {
             case EOF: {
                  if (ferror(f)) {
                     Publisher<ErrorMsg>::send(ErrorMsg(*this,
                       StringBuf("Can't read from \"")+file+"\": "+
                       strerror(errno)));
                     return 0;
                  }

                  return (str.size()) ? 1 : 0;
             }
             case '\n': return 1;
             default: {
                      str.push_back(ch);
                      break;
             }
      }
  }
 }
Вот только функции ввода/вывода, как правило, достаточно сложны. Что делать -- за полный контроль за потоком приходится платить.
}

sh_ptr<FindText> Factory::newFindText()
{
 try { return sh_ptr<FindText>(new FindTextImpl); }
 catch (...) {
       throw newCException(_FLINE_, "Can't create FindTextImpl object",
         toCException(_FLINE_));
 }
}
Ну и в завершение, традиционная реализация соответствующей функции из фабрики объектов.

4.4. Интерфейс пользователя

А сейчас займемся интерфейсом пользователя. Он должен:
  1. Предоставлять возможность задания всех необходимых параметров поиска.
  2. Предоставлять возможность запуска поиска и его преждевременной остановки.
  3. Отображать результаты поиска.
  4. Отображать информационные сообщения о ходе работы и собирать статистическую информацию.
В силу того, что создание GUI приложения сопряжено со множеством неуместных для данного материала вопросов (да и любой пионер по нонешним временам запросто набросает вам компонентов на форму), остановим свой выбор на простейшем интерфейсе командной строки.

Создаваемый нами ИП будет разбирать параметры командной строки и выводить результаты поиска в stdout. Кроме того, у пользователя будет возможность задать файл для вывода информации обо всех сгенерированных сообщениях, что весьма полезно для отладки. Ну а для аварийной остановки поиска вполне подойдет проверенный временем ^C.

ftext.cpp
/** @file
 * Главный файл проекта ftext. Содержит функцию main() и все необходимые части
 * интерфейса пользователя.
 */

#include <stdio.h>
#include "findtext.hpp"
#include "stringbuf.hpp"

using namespace std;

/**
 * Данный класс предназначен для вывода на экран результатов поиска.
 */
class Finder : public Subscriber<FoundMsg>, public Subscriber<ErrorMsg> {
      /**
       * Выводит найденные строки.
       */
      virtual bool regularMsg(const Publisher<FoundMsg>& pub,
        const FoundMsg& msg);

      /**
       * Выводит сообщения об ошибках.
       */
      virtual bool regularMsg(const Publisher<ErrorMsg>& pub,
        const ErrorMsg& msg);
};

bool Finder::regularMsg(const Publisher<FoundMsg>&, const FoundMsg& msg)
{
 printf("%s:%d:%s\n", msg.fileName.c_str(), msg.lineNum, msg.lineText.c_str());
 return 1;
}

bool Finder::regularMsg(const Publisher<ErrorMsg>&, const ErrorMsg& msg)
{
 fprintf(stderr, "%s\n", msg.errMsg.c_str());
 return 1;
}

/**
 * Данный класс предназначен для записи в лог информации по всем публикуемым
 * сообщениям.
 */
class Logger : public Subscriber<FoundMsg>, public Subscriber<ErrorMsg>,
  public Subscriber<InfoMsg> {
      /** лог для записи */
      FILE* log;

      /**
       * Выводит найденные строки.
       */
      virtual bool regularMsg(const Publisher<FoundMsg>& pub,
        const FoundMsg& msg);

      /**
       * Выводит сообщения об ошибках.
       */
      virtual bool regularMsg(const Publisher<ErrorMsg>& pub,
        const ErrorMsg& msg);

      /**
       * Выводит информационные сообщения.
       */
      virtual bool regularMsg(const Publisher<InfoMsg>& pub,
        const InfoMsg& msg);

 public:
      /** количество файлов, открытых во время поиска */
      int filesOpened;
      /** количество файлов, закрытых во время поиска */
      int filesClosed;
      /** количество найденных строк */
      int linesFound;
      /** количество встреченных ошибок */
      int errorsEncountered;

      /**
       * Открывает переданный файл для дописывания в него информации.
       */
      Logger(const string& fname);

      /**
       * Дописывает в лог статистическую информацию и закрывает файл.
       */
      ~Logger();
};

Logger::Logger(const string& fname)
{
 log=fopen(fname.c_str(), "a");
 if (!log) {
    throw newCException(_FLINE_, StringBuf("Can't open \"")+fname+
      "\" file for logging");
 }

 filesOpened=filesClosed=linesFound=errorsEncountered=0;
}

Logger::~Logger()
{
 fputc('\n', log);
#define PR(var) fprintf(log, #var ": %d\n", var)
 PR(filesOpened);
 PR(filesClosed);
 PR(linesFound);
 PR(errorsEncountered);
#undef PR
 fputc('\n', log);

 fclose(log);
}

bool Logger::regularMsg(const Publisher<FoundMsg>& pub, const FoundMsg& msg)
{
 linesFound++;
 fprintf(log, "FoundMsg: Publisher(%p) FindText(%p) fileName(%s) lineNum(%d) "
   "lineText(%s)\n", &pub, &msg.ft, msg.fileName.c_str(), msg.lineNum,
   msg.lineText.c_str());

 return 1;
}

bool Logger::regularMsg(const Publisher<ErrorMsg>& pub, const ErrorMsg& msg)
{
 errorsEncountered++;
 fprintf(log, "ErrorMsg: Publisher(%p) FindText(%p) errMsg(%s)\n", &pub,
   &msg.ft, msg.errMsg.c_str());

 return 1;
}

bool Logger::regularMsg(const Publisher<InfoMsg>& pub, const InfoMsg& msg)
{
 if (msg.type==InfoMsg::opened) filesOpened++;
 else filesClosed++;

 fprintf(log, "InfoMsg : Publisher(%p) FindText(%p) type(%s) fileName(%s)\n",
   &pub, &msg.ft, (msg.type==InfoMsg::opened) ? "opened" : "closed",
   msg.fileName.c_str());

 return 1;
}

/**
 * Вспомогательная структура для разбора параметров коммандной строки.
 */
struct CmdLineParser {
       /** присутствует -l */
       bool isL;
       /** имя лог-файла, если установлен isL */
       string logName;
       /** присутствует -r */
       bool isR;
       /** имя директория для поиска файлов */
       string dirName;
       /** маска для поиска файлов */
       string mask;
       /** подстрока для поиска в файлах */
       string text;

       /**
        * Создает объект и инициализирует поля структуры.
        */
       CmdLineParser() : isL(0), isR(0) {}

       /**
        * Разбирает коммандную строку формата
        * "[-l logname] [-r] dirname mask text"
        * и заполняет поля структуры. В случае некорректного формата возвращает
        * false.
        */
       bool parse(int argc, char** argv);
};

bool CmdLineParser::parse(int argc, char** argv)
{
 if (argc<4 || argc>7) return 0;

 int curr=1;
 while (argv[curr][0]=='-') {
       string opt(argv[curr]);

       if (opt=="-l") {
          if (isL) return 0;

          isL=1;
          curr++;

          logName=argv[curr++];
       }
       else if (opt=="-r") {
          if (isR) return 0;

          isR=1;
          curr++;
       }
       else return 0;
 }

 if (curr+3!=argc) return 0;

 dirName=argv[curr++];
 mask=argv[curr++];
 text=argv[curr++];

 return 1;
}

/**
 * Выводит информацию о формате коммандной строки.
 */
void printUsage()
{
 fprintf(stderr,
"Usage:\n"
"  ftext [-l logname] [-r] dirname mask text\n"
"For example:\n"
"  ftext -l ftext.log -r . *pp return\n"
 );
}

int main(int argc, char** argv)
{
 try {
     CmdLineParser clp;
     if (!clp.parse(argc, argv)) {
        // Некоторые системы/реализации вместо "*pp" сразу передают все файлы,
        // заканчивающиеся на "pp". Данный цикл печатает все полученные
        // параметры для диагностики подобной ситуации.
        // На таких системах/реализациях вместо "*pp" лучше передавать просто
        // "pp", т.е. без начальной "*".
        printf("Invalid command line:\n");
        for (int i=0; i<argc; i++)
            printf("%s%s", argv[i], (i!=argc-1)? " " : "\n\n");

        printUsage();
        return 1;
     }

     sh_ptr<FindText> ft=Factory::newFindText();

     Finder f;
     ft->foundPub().subscribe(f);
     ft->errorPub().subscribe(f);

     sh_ptr<Logger> l;
     if (clp.isL) {
        l.set(new Logger(clp.logName));

        ft->foundPub().subscribe(*l);
        ft->errorPub().subscribe(*l);
        ft->infoPub().subscribe(*l);
     }
     
     ft->search(clp.dirName, clp.isR, clp.mask, clp.text);

     return 0;
 }
 catch (...) {
       fprintf(stderr, "Uncaught exception:\n%s",
         toCException(_FLINE_)->toStringAll().c_str());
       return 2;
 }
}

Обратите внимание, что использующиеся реализации интерфейсов NameMatcher (файл namematcherimpl.cpp) и TextFinder (файл textfinderimpl.cpp) налагают определенные ограничения на вид маски и формат текста для поиска. Дело в том, что разработка более мощных реализаций не дала бы ничего нового в плане изучения интерфейсов и сообщений. И даже более того: тот факт, что пользователь может с легкостью включить в проект свои собственные расширенные реализации только улучшает материал.


5. Заключение

Ну вот, собственно, и все. Осталось только резюмировать достоинства и недостатки применения интерфейсов и сообщений. Достоинства:
  1. Зацепление взаимодействующих частей приложения существенно снижается вплоть до необходимого и достаточного уровня.
  2. Посредством использования интерфейсов удается четко разбить задачу на подзадачи, чья реализация может вестись полностью параллельно.
  3. Благодаря четко выделенным интерфейсам реальное повторное использование кода существенно возрастает.
  4. Отсутствие неконтролируемой привязки прикладного кода к системно-зависимым API существенно упрощает портирование.
  5. Благодаря повсеместному применению интерфейсов, проблема безболезненного изменения поведения отдельных частей приложения находит эффективное решение: приложение представляет собой набор четко ограниченных кубиков, взаимодействующих между собой посредством формально заданных интерфейсов.
  6. Проектирование приложения посредством создания интерфейсов сразу на целевом языке полностью исключает весь пласт ошибок и чужеродных неэффективностей, возникающих при первоначальном проектировании в рамках "универсальных моделей" типа UML с последующим переводом.
Ну а недостатки в данном случае являются прямым продолжением достоинств:
  1. В силу повсеместного использования "дополнительного уровня косвенности" взаимодействие отдельных реализаций интерфейсов (ведь именно они, в конечном итоге, выполняют всю работу) становится достаточно трудно отследить. Простой, написанный "в лоб" код выглядит более очевидным, но эта его простота уже совершенно не подходит для действительно сложных систем, требующих постоянного внесения изменений.
  2. Опять же, из-за внесения множества косвенных (фактически, виртуальных) вызовов страдает производительность. Тем не менее, не следует считать, что производительность становится неприемлемой (и именно по этой причине), т.к. необходимость оптимизации "узких мест" свойственна любой сложной системе. Тем более, что дополнительная потребность в ресурсах сравнительно невелика и растет (даже менее чем) линейно относительно числа функций, ставших виртуальными. Как всегда, действительно существенный прирост производительности дает только замена самого алгоритма, а не технических деталей него реализации.

Copyright © С. Деревяго, 2003-2004

Никакая часть данного материала не может быть использована в коммерческих целях без письменного разрешения автора.