Пространства имен в С++ и паттерн "Адаптер"

Тема в разделе "C и С++ FAQ", создана пользователем rrrFer, 13 сен 2016.

?

Используете ли вы пространства имен (namespace) в своих проектах?

  1. Да

    100,0%
  2. Нет

    0 голосов
    0,0%
  3. Узнать результаты

    0 голосов
    0,0%
  1. rrrFer

    rrrFer Well-Known Member
    Команда форума C\C++ Team

    Регистрация:
    6 сен 2011
    Сообщения:
    1.324
    Симпатии:
    36
    Известен модульный подход к программированию, согласно которому программу нужно рассматривать как совокупность модулей, каждый из которых представляет собой логически завершенный фрагмент программы. Механизмом логического группирования в С++ являются пространства имен, который часто применяется совместно с раздельной компиляцией [1].
    Для задания пространств имен в С++ применяется ключевое слово namespace, однако классы и структуры также являются пространствами имен [2].

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

    Последний пункт тесно связан с понятием инкапсуляции и подразумевает, что модуль должен предоставлять интерфейс, через который с ним должны взаимодействовать пользователи. За этим интерфейсом должны скрываться детали реализации. Важно, что раздельная компиляция в С++ сама по себе не предоставляет механизмов инкапсуляции (пользователь вынужден заглядывать в заголовочные файлы чтобы узнать как использовать функции и эти же файлы часто уже содержат детали - например, описания структур и множество вспомогательных функций, которые хотелось бы скрыть от пользователя), однако для этого в языке есть пространства имен.

    2 Модуль, класс, пространство имен
    Понятие модуля тесно связано с понятием пространства имен и класса. На самом деле, правильно спроектированный класс предоставляет пользователю удобный интерфейс и скрывает реализацию, а также решает только одну задачу (см. принципы объектно-ориентированного программирования SOLID - Open/Closed principle и Single Responsibility Principle) [3], поэтому класс можно рассматривать как модуль, однако модуль является более широким понятием.
    Пространство имен позволяет сгруппировать элементы программы в логически связанные части, а также выделить интерфейс (и даже несколько интерфейсов для разных групп пользователей), через который следует работать с этими элементами. Таким образом, пространства имен позволяют реализовать модули в С++, однако группировать в них можно не только элементы одного модуля - так Страуструп рассматривает вариант использования пространств имен для каждого программиста [2], т.е. пространства имен являются более общим понятием.
    Кстати, класс в С++ тоже задаем пространство имен, за счет этого имена членов класса не засоряют глобальное пространство имен программы, хотя мы и можем обратиться к ним через оператор ::. В связи с этим иногда может появляться ощущение, что вместо пространств имен всегда можно использовать классы, однако это не так - представьте, что вы разрабатываете математический модуль, функции которого работают с дробями, комплексными числами и тому подобными математическими объектами. Целесообразно ввести класс дробей, комплексных чисел, ..., однако чтобы показать их принадлежность одному модулю (пусть с именем math), их нужно поместить в пространство имен, а использование класса для этой цели недопустимо и скорее всего будет нарушить принципы SOLID.

    3 Использование пространств имен. Пример
    В С++ имеется возможность добавления операторов, которые при этом часто не являются членами класса:
    Код (C):
    Complex operator*(const Complex& a, const Complex& b) {
      Complex result(a.real*b.real-a.imagine*b.imagine, a.real*b.imagine + b.real*a.imagine);
      return result;
    }
    Такие функции очень часто сильно связаны с классом и нередко являются дружественными (friend) - так в этом примере функция должна быть дружественной чтобы иметь доступ к данным класса. Соответственно, функция должна находиться внутри модуля работы с комплексными числами, но не являться членом класса - для этого ее вместо с классом целесообразно поместить в пространство имен:

    Код (C):
    namespace complex {
      class Complex {
      double real, imagine;
      public:
      Complex(double real_, double imagine_);
      };

      Complex operator*(const Complex& a, const Complex& b);
    }
    Реализация объявления функций (классов, структур, ...) пространства имен может находиться в разных файлах - в одном файле можно указать, что в пространстве math есть класс Complex, а в другом - Fraction. Это очень важная особенность, которой не обладают классы в С++.

    В С++ пространства могут иерархически вкладываться друг в друга, для этого совсем не обязательно одно пространство описывать непосредственно внутри другого (как класс Complex из примера выше расположен внутри пространства complex) - есть директива using. Директива using позволяет импортировать имена из пространства, при этом можно выполнить это как для всех имен, так и только для конкретных:

    Код (C):
    namespace math {
      using namespace complex; // импортирует все имена из пространства complex
      using fraction::Fraction; // позволяет использовать имя Fraction из пространства fraction
      using equations::gauss; // импорт имени gauss из пространства equations
    }
    При этом не различаются имена классов и функций - так, например, gauss может быть и именем класса, и именем функции. При импорте вы можете переименовать часть имен, например, если они не соответствуют вашим соглашениям о кодировании - если в вашем коде имена классов начинаются всегда с большой буквы, а в пространстве equations (которое писал другой программист) имя класса gauss начинается с маленькой:
    Код (C):

    namespace math {
      using namespace complex;
      using fraction::Fraction;
      using equations::gauss;

      typedef equations::gauss Gauss; // переименование
    }
    Все эти возможности используются при описании интерфейса модуля.

    4 Интерфейсы пространств имен. Паттерн и Адаптер. Пример

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

    Во-первых модуль matrix должен быть вложен в math:

    Код (C):
    namespace math {
      namespace matrix {
      using other_matrix_nsp::mul;
      // ...
      }
    }
    Тут показано, что модуль math включает в себя пространство имен matrix и, возможно много всего другого, описанного в других файлах. Пространство matrix при этом может использовать модуль работы с матрицами, разработанный другими программистами (можно отобрать из него нужные функции и классы, выполнить переименование и т.д. При таком подходе пространство matrix выполняет роль адаптера (см. паттерн Адаптер [4]) - другие компоненты модуля math при этом должны использовать именно matrix чтобы ослабить зависимости от чужого кода (расположенного в пространстве other_matrix). За счет этого при каких либо изменениях в other_matrix (которые от вас не зависят, т.к. модуль разрабатывает другими программистами) вам будет достаточно изменить лишь реализацию методов matrix (вместо того, чтобы по всему коду править ошибки).

    До сих пор, при использовании имен из других пространств мы выполняли импорт всех или части имен и, максимум, - переименование. Однако, более гибким подходом является создание нового пространства (с новыми именами), лишь при реализации которого используется код из других пространств. Мы могли также применить это для реализации math::matrix, для этого в нашем пространстве имен надо было объявить интерфейс, а в .cpp-файле реализовать его с помощью элементов other_matrix_nsp:
    Код (C):

    // math_matrix.h
    namespace math {
      namespace matrix {
      class Matrix {
      // ...
      public:
      Matrix mul(const Matrix& a, const Matrix &b);
      };
      }
    }
     
    Код (C):

    // math_matrix.cpp
    #include "math_matix.h"
    #include "other_matrix.h" // подключение стороннего модуля работы с матрицами
    using other_matrix_nsp; // импорт имен стороннего модуля в реализацию (имена не попадут в интерфейс нашего модуля)
    using math::matrix; // импорт имен из нашего модуля

    Matrix Matrix::mul(const Matrix& a, const Matrix &b) {
      // реализация умножения матриц с использованием модуля other_matrix_nsp
    }
     
    Таким образом, с помощью пространств имен мы изолировали сторонний код реализовав паттерн Адаптер. Теперь займемся модулем трансформации фигур:

    Код (C):
    // transform.h
    namespace transform {
      // объявление функций модуля
      void rotate(vector<Point> &points, double angle);
      void move(vector<Point> &points, Point distance);
      void set_view_point(Point view);
      // объявление (не определение) переменной, отвечающей за позицию наблюдателя за сценой
      extern Point view;
    }
    Код (C):
    // transform.сpp
    using namespace transform // импорт имен из пространства

    Point view; // определение переменной (в этой единице трансляции она будет располагаться физически, будет выделена память)

    // реализация функий модуля:
    void rotate(vector<Point> &points, double angle){
    // ...
    }
    void move(vector<Point> &points, Point distance) {
      // ...
    }
    void set_view_point(Point view) {
    // ...
    }
    Мы схематично реализовали модуль, однако было бы очень нежелательно чтобы им кто-то пользовался кроме автора кода, т.к. очень легко все сломать и использовать неправильно. Например, изменять вручную переменную view (без вызова соответствующего метода, который возможно выполняет какие-то проверки). Для этого нужно выделить интерфейс для клиента:

    Код (C):
    namespace transform_interface {
      using transform::rotate;
      using transform::move;
      using transform set_view_point;
    }

    namespace simple_transform_interface {
      using transform::rotate;
    }
    Таким образом, клиент получает интерфейс и не знает деталей реализации вращения фигур. При использовании простого интерфейса, он даже не узнает, что там где-то есть позиция наблюдателя. В этом примере вместо использования оператора using могла быть реализация, подобная той, что мы использовали для умножения матриц - мы могли объявить полностью новые интерфейсы и реализовать их с помощью модуля transform - в этом случае клиент не узнал бы даже о существовании transform, т.к. такого пространства не было бы в переданных ему заголовочных файлах.

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

    Литература по теме:
    1. Раздельная компиляция в С++.- URL: http://iforum.pro/blogs/rrrfer-1394/razdel-naya-kompilyaciya-v-s-707/
    2. Страуструп Б. Язык программирования C++. 3-е издание. – М.: Бином, 1999. – 991 с.
    3. SOLID принципы. Рефакторинг.- URL: http://pro-prof.com/archives/1914
    4. Шаблон проектирования Адаптер на примерах. -URL: http://pro-prof.com/archives/1372
     
    WebWare Team нравится это.
Загрузка...

Поделиться этой страницей