Показаны сообщения с ярлыком RSDN Magazine. Показать все сообщения
Показаны сообщения с ярлыком RSDN Magazine. Показать все сообщения

среда, 14 июля 2010 г.

Строгие перечисления на C++

Эта статья опубликована в журнале RSDN Magazine 4, 2009 в соавторстве с Дмитрием Вьюковым (a.k.a. remark).

Проблема

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

Основные проблемы перечислений в этих языках следующие:

Во-первых, имена своих констант перечисление вносит в окружающее пространство имен, поэтому внутри одного и того же класса, структуры или пространства имен нельзя определить два перечисления с одинаковыми значениями литеральных констант. Но даже если конфликтов имен не происходит, то перечисление все равно «загрязняет» внешнее пространство имен, что негативно сказывается на работе с кодом, в частности на работе таких инструментов как IntelliSence.

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

В-третьих, отсутствует возможность указать нижележащий тип для перечисления. В настоящий момент размер (sizeof) перечисления определяется следующим образом [Страуструп2004]:

«Размер (sizeof) перечисления является размером некоторого интегрального типа, который в состоянии содержать весь диапазон значений перечисления. Результат не больше, чем sizeof(int) при условии, что элементы перечисления представимы в виде int или unsigned int. Например, sizeof(e1) может равняться 1 или 4, но не 8 на машине, где sizeof(int) == 4

ПРИМЕЧАНИЕ
На самом деле, sizeof(e1) может равняться 1, 2 или 4 (а не только 1 или 4, как об этом пишет Страуструп) но не 8

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

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

Подобного рода проблемы давно известны и часто обсуждаются компьютерным сообществом в различных блогах, форумах или конференциях. Первым, кто обратил внимание на проблему с перечислениями, был ни кто иной, как Бьярн Страуструп в своей книге «Дизайн и эволюция С++» [Страуструп2006]:

«В С концепция перечислений выглядит незаконченной. В первоначальном варианте языка их не было. Перечисления ввели без всякой охоты в качестве уступки тем, кто настойчиво требовал более основательных символических констант, чем препроцессорные макросы без параметров. Поэтому в С значение перечислителя имеет тип int, равно как и значение переменной, объявленной как имеющая тип перечисления. Значение типа int можно присваивать переменной типа перечисления. … Для тех стилей программирования, которые должен был поддерживать С++, не было нужды в перечислениях, поэтому правила С перешли в С++ без изменения.»

Помимо Страуструпа, на эту проблему обратил внимание Герб Саттер в своей статье “Enumerations” [Саттер2004]. И хотя пример Герба немного утрирован, он хорошо демонстрирует основные проблемы перечислений, а также показывает основное направление решения. Также благодаря Гербу Саттеру, возможно, в следующей редакции стандарта языка С++ мы увидим статически-типизированные перечисления, которые решат большую часть поднятых здесь вопросов (подробности можно почитать в [Саттер2007]). Но до тех пор нам не остается ничего другого, кроме как либо игнорировать эту проблему, либо попытаться собственными силами найти приемлемое решение.

Возможные решения

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

Решение 1. Соглашения по именованию

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

enum Color
{
   CLR_Red,
   CLR_Green,
   CLR_Black,
   //...
};

либо

enum Color
{
   Color_Red,
   Color_Green,
   Color_Black,
   //...
};

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

Решение 2. Вариант Герба Саттера

Второй вариант решения предложил Герб Саттер в своей статье [Саттер2004]. Его вариант решает проблему с именами и убирает возможность неявного преобразования значения перечисления к интегральному типу.

class Color {
  enum Color_ { Red_, Green_, Blue_, Purple_, Violet_ };
  Color_ value;
public:
  static const Color Red, Green, Blue, Purple, Violet;
  explicit Color( Color& other )    
    : value( other.value ) { }
  bool operator<( Color const& other )   
    { return value < other.value; }
  bool operator==( Color const& other )    
    { return value == other.value; }
    // etc.
  int ToInt() const     { return value; }
};
const Color Color::Red( Color::Red_ );
const Color Color::Green( Color::Green_ );
const Color Color::Blue( Color::Blue_ );
const Color Color::Purple( Color::Purple_ );
const Color Color::Violet( Color::Violet_ );
Решение 3. На основе структуры

Одно из самых простых решений проблемы внесения значений перечислений в окружающее пространство имен, предложил Дмитрий Вьюков (aka remark) на rsdn.ru [Вьюков2007_1]. Исходный вариант решения устраняет проблему вынесения констант перечисления в окружающее пространство имен, а также частично решает проблему типизации (т.к. не позволяет неявное преобразование интегральных констант к перечислимому типу, но в то же время позволяет сравнивать типы различных перечислений). Немного исправленный вариант исходного решения позволит повысит статическую типизацию перечислений, при этом оставаясь достаточно простым в реализации и сопровождении..

ПРИМЕЧАНИЕ
Существует альтернативное решение, когда вместо структуры используется пространство имен (namespace)

struct Color
{
    enum Type
    {
        Red, Green, Black
    };
    Type t_;
    Color(Type t) : t_(t) {}
    operator Type () const {return t_;}
private:
  // Предотвращает неявное преобразование значений перечисления
// к любым типам, кроме типа type, что препятствует сравнению
// значений перечислений с интегральными типами или со значениями
// других перечислений
template<typename T>
operator T () const;
};

Пример использования:

Color c = Color::Red;
switch(c)
{
   case Color::Red:
     //некоторый код
   break;
}
Color2 c2 = Color2::Green;
c2 = c; // Ошибка компиляции!
c2 = 3; // Ошибка компиляции!
if (c2 == Color::Red ) {} // Ошибка компиляции!
if (c2) {} //Ошибка компиляции!

Для того чтобы как-то облегчить и унифицировать использование подобной конструкции можно создать несколько макросов:

#define DEFINE_SIMPLE_ENUM(EnumName, seq) \
struct EnumName {\
   enum type \
   { \
      BOOST_PP_SEQ_FOR_EACH_I(DEFINE_SIMPLE_ENUM_VAL, EnumName, seq)\
   }; \
   type v; \
   EnumName(type v) : v(v) {} \
   operator type() const {return v;} \
private:
template<typename T>
operator T () const;};\

#define DEFINE_SIMPLE_ENUM_VAL(r, data, i, record) \
BOOST_PP_TUPLE_ELEM(2, 0, record) = BOOST_PP_TUPLE_ELEM(2, 1, record),

Пример определения перечисления будет выглядеть таким образом:

DEFINE_SIMPLE_ENUM(Color,
  ((Red, 1))
  ((Green, 3))
  )

Способ использование созданного перечисления при этом никак не изменится.

Вариант 4. Строгое перечисление

Четвертый вариант – это немного модифицированный вариант решения, предложенный Дмитрием Вьюковым (aka remark) на форуме rsdn.ru [Вьюков2007_2].

Это наиболее функциональный, но и наиболее сложный в реализации вариант, который позволяет следующее:

1.    Задавать тип, лежащий в основе перечисления. По-умолчанию, как и у встроенных перечислений применяется тип int, но в случае необходимости пользователь может указать другой интегральный тип при определении перечисления.

2.    Определить (или задать явно) внутреннее имя (InternalName) перечисления. Если имя не задано явно, то в этом случае внутреннее имя определяется автоматически на основе символьного имени константы. В самом простом случае, пользователь получает описание перечисления, не делая никаких дополнительных действий и только в том случае, если внутреннее символьное имя должно быть отличным от литеральных констант, он может задать его явно.

3.    Задать внешнее имя (ExternalName). Например, для вывода пользователю.

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

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

6.    Получить все значения перечисления (EnumType::GetEntities). По-сути это получение «метаданных» перечислимого типа, аналогичное тому как это делается во многих других языках программирования с помощью рефлексии (reflection). С помощью этого можно решать самые различные задачи, начиная от привязки перечисления к выпадающим спискам (combo box) пользовательского интерфейса, заканчивая явным перебором всех возможных значений перечисления для каких-либо бизнес-целей.

7.    Связать значения перечисления с объектами произвольных классов или примитивными типами. Хотя эта функциональность может показаться излишней, возможность связывания значения перечисления с произвольным значением другого типа может быть весьма полезной. Таким способом можно связать значение одного перечисления с другим, получив категорию перечислений, а связав значение перечисления с некоторым типом можно удобным способом реализовать шаблон проектирования «Метод шаблона». Это тоже является добавлением своего рода метаинформации и может помочь в решении множества других задач.

Начнем с простого варианта использования.

Определение перечисления.

DEFINE_STRICT_ENUM(Color, int, 
((Red,      1))
((Black,    3))
((Green,    5))
)

Пример использования

Color c = Color::Red;
std::cout<<"Value: "<<c.GetValue()
        <<", Internal name: "<<c.InternalName()<<std::endl;

Color::Entries entries = Color::GetEntries();
for(size_t i = 0; i < entries.size(); ++i)
{
    std::cout<<entries[i].Value()<< "\t"
        <<entries[i].InternalName()
        <<"\t"<<entries[i].ExternalName()<<std::endl;
}
switch(c.GetValue())
{
    //Использовать выражение Color::Red().GetValue() в блоках
    //case нельзя, т.к. это выражение не является константой
    //времени компиляции.
    //Специально для этой цели внутри каждого класса
    //определяется внутреннее перечисление, которое содержит
    //все значения констант с завершающим символом “_”
    case Color::Red_:
      std::cout<<”Hello, Red color!”<<std::endl;
      break;

Результат выполнения:

Value: 1 Internal Name: Red
1    Red
3    Black
5    Green

Более сложный пример использования подразумевают явное указание внутреннего имени перечисления (т.к. в первом примере внутреннее имя вычислялось автоматически на основании имени константы), или указание дополнительное «внешнего» имени, предназначенное для конечного пользования.

DEFINE_STRICT_ENUM_WITH_NAME( Color, int, true,
((Red,   1, "Red"))
((Black, 3, "Black"))
((Green, 5, "Green"))
)

DEFINE_STRICT_ENUM_WITH_DESC( Color, int, false,
((Red,   1, "Red color",   "Красный"))
((Black, 3, "Black color", "Черный"))
((Green, 5, "Green color", "Зеленый"))
)
Color c = Color::Red;
std::cout<<"Value: "<<c.GetValue()
        <<", Internal name: "<<c.InternalName()
        <<", External name: "<<c.ExternalName()<<std::endl;

Color::Entries entries = Color::GetEntries();
for(size_t i = 0; i < entries.size(); ++i)
{
    std::cout<<entries[i].Value()<< "\t"
        <<entries[i].InternalName()
        <<"\t"<<entries[i].ExternalName()<<std::endl;
}

Результат выполнения:

Value: 1, Internal Name: Red,
1    Red color      Красный
3    Black color    Черный
5    Green color    Зеленый

Третий вариант использования показывает привязку значений перечислений с объектами произвольных классов (в данном случае производится привязка одного перечисления со значениями другого перечисления):

DEFINE_STRICT_ENUM(ColorCategory, int,
                   ((Bright, 1))
                   ((Dark, 2))
                   )

DEFINE_STRICT_ENUM_TAG(Color, int, enum_strict, ColorCategory,
                   ((Red,      1, (ColorCategory::Bright) ))
                   ((Black,    3, (ColorCategory::Dark)   ))
                   ((Green,    5, (ColorCategory::Bright) )) )

Пример использования.

Color c = Color::Red;
std::cout<<"Value: "<<c.GetValue()
        <<", Internal name: "<<c.InternalName()
        <<", Category: "<<c.Tag().InternalName()<<std::endl;

Результат выполнения

Value: 1, Internal Name: Red, Category: Bright

ПРИМЕЧАНИЕ
Полный исходный код реализации приведен в конце статьи

Несмотря на богатую функциональность, у этого решения также имеются недостатки:

1.    Каждое значение перечисления обязательно нужно задавать явно. Нельзя сказать, что это ограничение присуще каждому сценарию использования. Бывают случаи, когда без явного задания перечисления все равно не обойтись (например, при реализации коммуникационных протоколов, либо при строгом соответствии значений перечисления каким-либо известным константам).

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

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

4.    Усложняется просмотр значений переменных перечисления в отладчике

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

ПРИМЕЧАНИЕ
Приведенная реализация строгих перечислений имеет ряд ограничений. Во-первых, реализация не предусматривает использование перечислений из потоков, выполнение которых начинается до выполнения функции main. Во-вторых, реализация не является безопасной с точки зрения многопоточности: если первое обращение к перечислению произойдет из нескольких потоков одновременно, это приведет к созданию двух экземпляров «метаданных» перечисления. В-третьих, реализация не является переносимой (работает только на VC++2005/2008 и, скорее всего VC++2010), поскольку используются нестандартные расширения языка, а именно __declspec(selectany). 

Визуализация строгих перечислений

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

По-умолчанию, переменные перечислений в отладчике выглядят следующим образом.
Figure1

Рисунок 1 – Отображение строгого перечисления по-умолчанию

Для тех, кого такой вариант отображения не устраивает, разработчики отладчика Visual Studio предусмотрели вариант изменения способа отображения объектов произвольных классов с помощью файла autoexp.dat, который расположен в папке Microsoft Visual Studio 8\Common7\Packages\Debugger\.

Для того чтобы объекты произвольного класса отображались нужным вам способом необходимо всего лишь добавить в этот файл несколько строк кода.

StrictEnumBase<*>{
  preview(
    #(<str>"Name "</str>, $e.valueName_, <str>", Value "</str>, $e.value_)
  )
}

Color{
  preview(
    #(<str>"Name "</str>, $e.valueName_, <str>", Value "</str>, $e.value_)
  )
}

Отладчик подхватит все изменения в файле autoexp.dat при следующем запуске. В результате внесенных изменений строгие перечисления будут отображаться следующим образом.

Figure2

Рисунок 2 – Визуализация строгого перечисления в отладчике

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

ПРИМЕЧАНИЕ
В текущем разделе показаны примеры решения проблемы визуализации для Microsoft Visual C++ 2005 и выше, но аналогичные решения можно найти и для других сред разработки, в частности GDB также поддерживает аналогичную функциональность.

Вместо заключения

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

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

Литература

1.    Бьярн Страуструп. Язык программирования С++. Бином. 2004
2.    Бьярн Страуструп. Дизайн и эволюция С++. Питер. 2007
3.    Herb Sutter, Jum Hyslop, C/C++ Users Journal, 22(5), May 2004
4.    Herb Sutter, David E. Miller, Bjarne Stroustrup Strongly Typed Enums (revision 3), July 2007
5.    Дмитрий Вьюков. [Trick] Делаем правильные enum'ы. http://rsdn.ru/forum/cpp/2647727.aspx. Сентябрь 2007
6.    Дмитрий Вьюков. Enum с тэгом. http://rsdn.ru/forum/cpp/2655200.aspx. Сентябрь 2007

Исходный код строгого перечисления
#pragma once
#ifndef __STRICT_ENUM__
#define __STRICT_ENUM__

#include <iostream>
#include <sstream>
#include <string>
#include <vector>
#include <map>
#include <boost\preprocessor.hpp>

namespace detail
{
    /**   Пустой тэг для связывания со значением enum'а */
    struct EmptyTag
    {
    };
}

/**   Для задания признака то, является перечисление строгим
*   (при этом генерируется исключение при попытке установить
*    значение перечислению отличное от заданного) или нет*/
enum Strictness
{
enum_strict,    //!< перечисление является строгим
enum_nonstrict, //!< перечисление не является строгим (по-умолчанию)
};

/**   Базовый класс для типобезопасных перечислений
*
*    Пример использования смотри в конце файла
*/
template<typename Derived, typename Type = int,
Strictness strict = enum_nonstrict,
typename TagType = detail::EmptyTag>
class StrictEnumBase
{
    /**   Класс с описанием значения перечисления */
    class EntryDescription
    {
    public:
        /**   Конструктор */
        EntryDescription(Type value, const std::string& internalName,
const std::string& externalName)
            : value_(value)
            , internalName_(internalName)
            , externalName_(externalName)
        {}

/**   Значение */
        Type Value() const
        {
            return value_;
        }

/**   Внутреннее имя */
        std::string InternalName() const
        {
            return internalName_;
        }

/**   Внешнее имя */
        std::string ExternalName() const
        {
            return externalName_;
        }

    private:
/*const*/ Type value_; //!< значение
        /*const*/ std::string internalName_; //!< внутреннее имя
        /*const*/ std::string externalName_; //!< внешнее имя
    };
   
/**   Информация, сопоставляемая с каждым элементом -
      описание и пользовательский тэг */
    class EntryInfo
    {
    public:
        /**   Конструктор
         */
        EntryInfo(const EntryDescription& desc, const TagType& tag)
            : desc_(desc)
            , tag_(tag)
        {}
       
        /**   Получить описание
         */
        const EntryDescription& Description() const
        {
            return desc_;
        }

        /**   Получить тэг
         */
        const TagType& Tag() const
        {
            return tag_;
        }

    private:
        /*const*/ EntryDescription desc_;  //!< описание
        /*const*/ TagType tag_;            //!< тэг
    };


    /**   Внутренний тип для хранения информации об элементах */
    typedef std::map<Type, EntryInfo> IntEntries;

public: //Comparison operators
    bool operator < (const StrictEnumBase& other) const
    {
        return value_ < other.value_;
    }

    bool operator > (const StrictEnumBase& other) const
    {
        return value_ > other.value_;
    }

    bool operator <= (const StrictEnumBase& other) const
    {
        return value_ <= other.value_;
    }

    bool operator >= (const StrictEnumBase& other) const
    {
        return value_ >= other.value_;
    }

    bool operator == (const StrictEnumBase& other) const
    {
        return value_ == other.value_;
    }

    bool operator != (const StrictEnumBase& other) const
    {
        return value_ != other.value_;
    }
public: //Public interface
/**   Получить значение перечисления */
    const Type& Value() const
    {
        return value_;
    }

/**   Получить описание значения.
*    Можно получать и для неизвестных значений
*/
    EntryDescription Description() const
    {
        IntEntries::iterator iter = GetIntEntries().find(value_);
        if (iter == GetIntEntries().end())
            return MakeEmptyDescription(value_);
        return iter->second.Description();
    }

/**   Получить тэг, связанный со значением
    *    Можно получать только для известных значений
    *    @return Тэг
    *    @throw std::out_of_range Если вызывается для
*        неизвестного значения
    */
    const TagType& Tag() const // throw (std::out_of_range)
    {
        IntEntries::iterator iter = GetIntEntries().find(value_);
        if (iter == GetIntEntries().end())
            throw std::out_of_range("StrictEnum: out of range");
        return iter->second.Tag();
    }


/**   Внутреннее имя значения перечисления */
std::string InternalName() const
{
return Description().InternalName();
}

/**   Внешнее имя значения перечисления */
std::string ExternalName() const
{
return Description().ExternalName();
}

/**   Проверить, является ли значение известным */
    bool IsKnown() const
    {
        return IsExist(value_);
    }
public: //Static functions
/**   Тип для хранения информации об элементах */
typedef std::vector<EntryDescription> Entries;

/**   Проверка существования заданного значения в перечислении */
    static bool IsExist(Type value)
    {
return GetIntEntries().find(value) != GetIntEntries().end();
    }

/**  Создать объект из значения
*   @throw std::out_of_range Если устанавливается неправильное
*   значение (только в случае strict == enum_strict).
*/
    static Derived FromValue(Type value)
    {
AssertExist(value);
        return Derived(value);
    }

/**   Создать объект из значения. Если значение неправильное,
*    то возвращает значение по-умолчанию
*    @param defaultValue Значение по-умолчанию
*/
    static Derived FromValue(Type value, Derived defaultValue)
    {
        if (IsExist(value))
            return Derived(value);
        else
            return defaultValue;
    }

/**   Получить описания всех известных значений перечисления */
static Entries GetEntries()
    {
        // Копируем описания элементов из внутреннего
// представления во внешнее
        IntEntries intEntries = GetIntEntries();
        Entries entries;
        entries.reserve(intEntries.size());
        for (IntEntries::const_iterator iter = intEntries.begin();
iter != intEntries.end(); ++iter)
            entries.push_back(iter->second.Description());
        return entries;
    }

protected:
/**   Тип значения */
    typedef Type ValueType;

/**   Конструктор для известных значений */
    StrictEnumBase(Type value, const std::string& internalName,
const std::string& externalName, const TagType& tag = TagType())
        : value_(value)
, valueName_(internalName)
    {
        GetIntEntries().insert(std::make_pair(value,
EntryInfo(EntryDescription(value, internalName, externalName),
tag)));
    }

    /**   Конструктор для неизвестных значений */
    explicit StrictEnumBase(Type value)
        : value_(value)
    {
    }

private:
/**   Непосредственно значение */
    Type value_;
/**
*    Строковое представление значения
*  (нужно только для визуализации, в противном случае можно удалить)
*/
std::string valueName_;

    /**   Получить множество значений, которые могут содержаться
*    в данном перечислении
    */
    static IntEntries& GetIntEntries()
    {
        static IntEntries entries;
        return entries;
    }

  /**    Проверить, что значение является известным
     *    @throw std::out_of_range Если неизвестное значение
     */
    static void AssertExist(Type value)
    {
        if (strict == enum_strict && !IsExist(value))
            throw std::out_of_range("StrictEnum: out of range");
    }

    /**   Создать описание для неизвестного значения
    */
    static EntryDescription MakeEmptyDescription(Type value)
    {
        std::ostringstream stream;
        stream << "<" << value << ">";
        return EntryDescription(value, stream.str(), "");
    }
};

#define DEFINE_STRICT_ENUM(EnumName, Type, seq) \
class EnumName : public tools::common::StrictEnumBase<EnumName, Type> { \
friend class tools::common::StrictEnumBase<EnumName, Type>; \
EnumName(ValueType value, const std::string& sIntName, const std::string& sExtName) \
: tools::common::StrictEnumBase<EnumName, Type>(value, sIntName, sExtName) \
{} \
EnumName(ValueType value) \
: tools::common::StrictEnumBase<EnumName, Type>(value) \
{} \
public: \
enum { \
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_VAL, EnumName, seq) \
    }; \
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_DEF, EnumName, seq);\
};\
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_DECL, EnumName, seq);

#define DEFINE_STRICT_ENUM_DEF(r, aux, i, record) \
static const aux BOOST_PP_TUPLE_ELEM(2, 0, record);

#define DEFINE_STRICT_ENUM_VAL(r, aux, i, record) \
BOOST_PP_TUPLE_ELEM(2, 0, record)_ = BOOST_PP_TUPLE_ELEM(2, 1, record),


#define DEFINE_STRICT_ENUM_DECL(r, aux, i, record) \
__declspec(selectany) const aux aux::BOOST_PP_TUPLE_ELEM(2, 0, record)(BOOST_PP_TUPLE_ELEM(2, 1, record), BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(2, 0, record)), "");

#define DEFINE_STRICT_ENUM_TAG(EnumName, Type, strict_f, tag_t, seq) \
class EnumName : public tools::common::StrictEnumBase<EnumName, Type, strict_f, tag_t> { \
friend class tools::common::StrictEnumBase<EnumName, Type, strict_f, tag_t>; \
EnumName(ValueType value, const std::string& sIntName, const std::string& sExtName, tag_t tag) \
: tools::common::StrictEnumBase<EnumName, Type, strict_f, tag_t>(value, sIntName, sExtName, tag) \
{} \
EnumName(ValueType value) \
: tools::common::StrictEnumBase<EnumName, Type, strict_f, tag_t>(value) \
{} \
public: \
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_TAG_DEF, EnumName, seq);\
};\
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_TAG_DECL, EnumName, seq);

#define DEFINE_STRICT_ENUM_TAG_DEF(r, aux, i, record) \
static const aux BOOST_PP_TUPLE_ELEM(3, 0, record);

#define DEFINE_STRICT_ENUM_TAG_DECL(r, aux, i, record) \
__declspec(selectany) const aux aux::BOOST_PP_TUPLE_ELEM(3, 0, record)(BOOST_PP_TUPLE_ELEM(3, 1, record), BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(3, 0, record)), "", BOOST_PP_TUPLE_ELEM(3, 2, record));



#define DEFINE_STRICT_ENUM_WITH_DESC(EnumName, Type, seq) \
class EnumName : public tools::common::StrictEnumBase<EnumName, Type> { \
    friend class tools::common::StrictEnumBase<EnumName, Type>; \
    EnumName(ValueType value, const std::string& internalName, const std::string& externalName) \
    : tools::common::StrictEnumBase<EnumName, Type>(value, internalName, externalName) \
        {} \
    EnumName(ValueType value) \
        : tools::common::StrictEnumBase<EnumName, Type>(value) \
    {} \
public: \
    BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_WITH_DESC_DEF, EnumName, seq);\
};\
    BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_WITH_DESC_DECL, EnumName, seq);

#define DEFINE_STRICT_ENUM_WITH_DESC_DEF(r, aux, i, record) \
    static const aux BOOST_PP_TUPLE_ELEM(4, 0, record);

#define DEFINE_STRICT_ENUM_WITH_DESC_DECL(r, aux, i, record) \
    __declspec(selectany) const aux aux::BOOST_PP_TUPLE_ELEM(4, 0, record)(BOOST_PP_TUPLE_ELEM(4, 1, record), BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(4, 2, record)), BOOST_PP_TUPLE_ELEM(4, 3, record));

#define DEFINE_STRICT_ENUM_WITH_NAME(EnumName, Type, seq) \
class EnumName : public tools::common::StrictEnumBase<EnumName, Type> { \
friend class tools::common::StrictEnumBase<EnumName, Type>; \
EnumName(ValueType value, const std::string& internalName, const std::string& externalName) \
: tools::common::StrictEnumBase<EnumName, Type>(value, internalName, externalName) \
{} \
EnumName(ValueType value) \
: tools::common::StrictEnumBase<EnumName, Type>(value) \
{} \
public: \
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_WITH_NAME_DEF, EnumName, seq);\
};\
BOOST_PP_SEQ_FOR_EACH_I(DEFINE_STRICT_ENUM_WITH_NAME_DECL, EnumName, seq);

#define DEFINE_STRICT_ENUM_WITH_NAME_DEF(r, aux, i, record) \
static const aux BOOST_PP_TUPLE_ELEM(3, 0, record);

#define DEFINE_STRICT_ENUM_WITH_NAME_DECL(r, aux, i, record) \
__declspec(selectany) const aux aux::BOOST_PP_TUPLE_ELEM(3, 0, record) \
(BOOST_PP_TUPLE_ELEM(3, 1, record), BOOST_PP_STRINGIZE(BOOST_PP_TUPLE_ELEM(3, 0, record)), "");


/*
Примеры использования строгих перечислений:

DEFINE_STRICT_ENUM(Color, int,
((Red,      1))
((Black,    3))
((Green,    5))
)
 DEFINE_STRICT_ENUM_TAG(Color, int, enum_strict, ColorCategory,
((Red,      1, (ColorCategory::Bright) ))
((Black,    3, (ColorCategory::Dark)   ))
((Green,    5, (ColorCategory::Bright) ))
)
*/
#endif //__STRICT_ENUM__

четверг, 14 января 2010 г.

Шаблоны проектирования. История успеха

Эта статья опубликована в 3-м номере журнала RSDN Magazine за 2009 год.

Введение

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

История зарождения

Труды архитектора и философа Кристофера Александера (Christopher Alexander), такие как “The Pattern Language” (1977), “The Timeless Way of Building” (1979), “Notes On The Synthesis Of Form” (1964) упоминаются в компьютерной литературе не реже, чем труды Дейкстры, Хоара или Кнута. Практически в каждой книге о шаблонах проектирования говорится о серьезном влиянии трудов Александера на область разработки программного обеспечения и в частности на идею создания шаблонов проектирования. Однако шаблоны проектирования – это не единственная область, на которую оказали влияние труды этого замечательного человека. Еще в 1979 году в своей книге “Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design” Эд Йордон (Ed Yourdon) и Ларри Константайн (Larry L. Constantine),одни из первых определили фундаментальные понятия модульности ПО, такие как сцепление (cohesion) и связность (coupling), основываясь на понятиях, приведенных в книге Алексендера “Notes On The Synthesis Of Form”.

По мнению Александера основной задачей при декомпозиции системы является осуществление следующих двух условий:

  • максимизация связей внутри компонентов (высокое сцепление, high cohesion) и
  • минимизация связей между компонентами (низкая связанность, low coupling).

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

Следующим упоминанием трудов Александера, на этот раз книг “The Timeless Way of Building” и “The Pattern Language” стали Том ДеМарко (Tom DeMarco) и Тим Листер (Tim Lister) в своей знаменитой книге “Peopleware: Productive Projects and Teams”, вышедшей в свет в 1987 году. Авторы считают, что окружение является существенным фактором, влияющим на продуктивность человека, занятого интеллектуальным трудом, и в попытке определить идеальное рабочее место ДеМарко и Листер обратили свои взоры на труды знаменитого архитектора. Анализируя удачные рабочие места различных организаций, авторы создали четыре шаблона, которые, по их мнению, существенно повышают комфорт сотрудника и позволяют выполнять свою работу максимально продуктивно.

Идея шаблонов проектирования в области разработки ПО появилась в умах сторонников объектно-ориентированного программирования во второй половине 80-х годов прошлого века. Как и многие другие хорошие идеи, идея повторного использования не только кода, но и архитектурных и проектных решений, пришла в голову одновременно разным экспертам в области разработки программного обеспечения.

Одним из таких экспертов был Кент Бек. Вот что он пишет:

«Впервые я услышал о шаблонах будучи студентом Университета Орегона. Многие студенты, с которыми я жил в общежитии на первом курсе, были из Школы Архитектуры. И поскольку я рисовал планы домов с шести или семи лет, они указали мне на труды Кристофера Александера. Я прочитал его книгу “The Timeless Way of Building” от корки до корки в течение семи месяцев.

Я работал на Tektronix в течение полутора лет, когда я снова столкнулся с трудами Александера. Я нашел потертую старую книгу “Notes on the Synthesis of Form”. Объяснение методологии Александера во вступлении ко второму изданию нашло отражение с моей точкой зрения, что снова меня привело к книге “The Timeless Way of Building”. Мне показалось, что все, что он не любит в архитекторах, я не люблю в разработчиках ПО. Я убедил Варда Каннингема, что мы нашли что-то важное».

В 1987 году Вард Каннингем (Ward Cunningham) и Кент Бек (Kent Beck) поделились своим первым опытом применения языка шаблонов на практике на конференции OOPSLA-87. В то время они оба работали над одним проектом и столкнулись со сложностью проектирования пользовательского интерфейса. Тогда они решили воспользоваться идеей языка шаблонов Кристофера Александера. Александер считал, что наиболее оптимальным является проектирования жилища теми, кто будет в них проживать, т.к. именно они наиболее точно осознают собственные потребности. Кент Бек и Вард Каннингем посчитали эту идею весьма заманчивой и разработали язык шаблонов для проектирования пользовательских интерфейсов пользователями.

«Мы предлагаем радикальное изменение проектирования и реализации, используя концепцию адаптированную из работы Кристофера Александера, архитектора и основателя Центра по изучению структуры окружающей среды (Center for Environmental Structure). Александер предлагает, чтобы дома и офисы проектировались и строились их жителями. Он считает, что эти люди лучше всего знают свои потребности в определенной структуре. Мы согласны с этим мнением и считаем, что тоже самое относится и к компьютерным программам. Пользователь сам должен писать свои программы. Идея выглядит глупой, если взглянуть на размер и сложность зданий и программ, а также множество лет обучения профессии проектировщика. Однако Александер предлагает убедительный сценарий, который основывается на концепции «языка шаблонов».

Язык шаблонов предоставляет проектировщику работающие решения всех известных проблем в области проектирования. Это последовательность кусочков знаний, которые изложены и организованы в таком виде, что позволяют проектировщику задавать правильные вопросы и получать ответы в нужные моменты времени. Александер организовывал эти кусочки знаний в письменные шаблоны, в определенной однотипной структуре. Каждый шаблон имел выражение, описывающее проблему, условия, которые привели к этой проблеме и, самое главное, решение, которое работает в этих условиях. Язык шаблонов содержит шаблоны целого строения, например, жилого здания, или интерактивной компьютерной программы. В языке шаблонов, шаблоны связаны с другими шаблонами и решения применяются на основании этой взаимосвязи. Письменные шаблоны включают эти взаимосвязи в качестве пролога и эпилога».

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

В это же время, начиная с 1988 года Эриx Гамма (Erich Gamma), Андре Веинанд (Andre Weinand) и Рудольф Марти (Rudolf Marty) начали работу над объектно-ориентированной библиотекой на С++ под названием “ET++”. В этом же году на конференции OOPSLA’88 они выступают с докладом об этой библиотеке. Эрих также задумался о важности повторного использования проектных решений (или шаблонов) своей библиотеки. В 1991 году Эриху приходит идея написании диссертации на тему шаблонов проектирования и он начинает сотрудничать с другими членами «Банды четырех» для дополнительного изучения этой темы. Именно в 1991 году перед конференцией European Conference of Object-Oriented Programming (ECOOP) Ерих Гамма и Ричард Хелм собрались вместе для создания первого каталога шаблонов проектирования, которые, в конечном счете, стали основой знаменитой книгой “Design Patterns”. Они определили множество шаблонов, включая следующие:

  1. Composite
  2. Decider
  3. Observer
  4. Constrainer

Многие из этих шаблонов попали в книгу “Design Patterns”, в то время, как многие другие так и остались неизвестными.

В конце 1988 года Джеймс Коплиен (James Coplien) начал каталогизировать специфичные для С++ низкоуровневые шаблоны, которые он называл идиомами. Ранние черновики этой работы использовались для обучения объектно-ориентированному программированию и С++ в AT&T с начала 1989 года. В 1991 года вышла книга Джеймса Комплиена “Advanced C++ Programming Styles and Idioms”, которую можно считать первой книгой по С++ для продолжающих программистов и первой книгой по шаблонам проектирования.

Питер Код (Peter Coad) также параллельно исследовал вопрос шаблонов проектирования, в результате чего на свет появилась статья в Communications of the ACM в 1992 году.

К этому времени (начало 1990-х годов) интерес к шаблонам проектирования вырос достаточно, чтобы ключевые специалисты в этом вопросе взялись за объединение опыта каждого из них. В 1993 году проводится множество конференций и семинаров, на которых основное внимание уделяется шаблонам проектирования. Поворотным событием в истории шаблонов проектирования становится знаменитая книга Банды четырех, которая впервые была представлена на конференции OOPSLA’94, где было продано 750 копий этой книги, что более чем в семь раз превосходит количество любых технических книг, когда-либо проданных на этой конференции.

История успеха

Книга Design Patterns является одной из самых популярных технических книг за всю историю, о ней вот уже пятнадцать лет пишут статьи, ссылаются в других книгах, обсуждают, критикуют, хвалят. По некоторым сведениям эта книга является самой продаваемой компьютерной книгой за всю историю (на начало 2009 года продано более 350 000 экземпляров и ежемесячно продается более 1000).

После выхода книги банды четырех тема шаблонов проектирования стала доступна не только в академических кругах, но и стала активно обсуждаться и применяться простыми разработчиками. Тема шаблонов проектирования активно затрагивается на различных коференциях и семинарах, включая OOPSLA (Object-Oriented Programming, Systems, Languages & Applications) и PLoP (Pattern Languages of Programs). О шаблонах проектирования вышло множество книг. Часть из них продолжают исследование классических шаблонов проектирования, поднятых бандой четырех в своей книге, но помимо этого, шаблоны стали заполнять все большее и большее количество смежных областей. Сегодня шаблоны практически повсюду: существуют шаблоны кодирования, шаблоны рефакторинга, шаблоны реализации корпоративных приложений, шаблоны работы с базами данных, шаблоны распределенных приложений, шаблоны работы с многопоточностью и множество других.

Шаблоны проектирования получили весьма широкое распространение, но несмотря на это они в течение длительного времени оставались прерогативой архитектора и разработчика, а не менеджера. Но за последние несколько лет эта картина начала меняться. Вслед за книгой  Джеймса Коплиена и Нила Харрисона “Organizational Patterns of Agile Software Development” вышла книга Тома ДеМарко, Тима Листера и др. “Adrenaline Junkies and Template Zombies: Understanding Patterns of Project Behavior”, посвященная шаблонам поведения программных проектов.

Рассматривая историю возникновения шаблонов проектирования и их непосредственную близость к языку шаблонов Кристофера Александера нельзя не отметить существенные различия. Несмотря на распространения идеи шаблонов, язык шаблонов в понимании Александера так и не был создан. Существуют шаблоны, предназначенные для решения различных задач, шаблоны для различных уровней абстракции и различных уровней иерархии системы. Эти шаблоны каким-то образом связаны с другими шаблонами более высокого и более низкого уровней, но при этом эта связь не является жесткой и формальной, в результате даже опытный разработчик не может описать всю систему с помощью какого-либо языка шаблонов. О шаблонах проектирования знают многие разработчики, проектировщики, архитекторы, но об этом явлении совершенно ничего не известно пользователям, хотя именно эта идея лежит в основе языка шаблонов Александера. Александер считал, что пользователь лучше всего знает свои нужды и если им дать формальный инструмент, то с его помощью они сами смогут построить лучшие прототипы, которые впоследствии будут реализованы командой разработчиков. Эту идею пытались развить Кент Бек и Вард Каннингем в самом начале своей работы с шаблонами, и даже получили положительные результаты, но идея шаблонов пошла по другому пути, который привел к повторному использованию идей командой разработчиков, но без участия в этом процессе конечного пользователя. На сегодняшний день основная роль шаблонов – это повторное использование опыта в различных областях разработки ПО, устранение коммуникационного барьера внутри команды разработчиков и между ними, повышение качества создаваемого продукта, за счет использования проверенных годами решений. Шаблоны не стали «серебряной пулей», но они сделали достаточно для компьютерного сообщества, чтобы к этому явлению относились с уважением и знали не только, что из себя представляют шаблоны проектирования, но и знали историю этого феномена.

Литература
  1. Christopher Alexander, Notes On The Synthesis Of Form, 1964
  2. Christopher Alexander, The Pattern Language, 1977
  3. Christopher Alexander, The Timeless Way of Building, 1979
  4. Edward Yourdon, Larry L. Constantine, Structured Design: Fundamentals of a Discipline of Computer Program and Systems Design, 1979
  5. Tom DeMarco, Timothy Lister, Peopleware: Productive Projects and Teams, 1999
  6. Kent Beck, Ward Cunningham, Using Pattern Languages for Object-Oriented Programs, OOPSLA-87
  7. James Coplien, Software Patterns, 1996
  8. Jonathan Erickson, Dr. Dobb's Journal, March 1998
  9. James Coplien, Advanced C++ Programming Styles and Idioms, 1991
  10. Erich Gamma et al. Design Patterns: Elements of Reusable Object-Oriented Software, 1994