IT Notes

Паттерн Посетитель на C++

Рекомендую сначала ознакомиться с двумя предыдущими частями:

  1. Паттерн Компоновщик на C++
  2. Паттерн Абстрактная фабрика на C++

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

Коротко о паттерне Посетитель

Посетитель - объект, предназначенный для выполнения обхода коллекций структур. Тип и способ хранения коллекции особого значения не имеет.

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

Результатом обхода Посетителя по коллекции становится новая структура. Она может представлять собой преобразование исходной коллекции в особый формат или содержать результаты анализа данных коллекции (например, статистический).

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

Возвращаемся к примеру простого компилятора

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

Мы предусмотрели возможность полиморфной генерации кода на C++ и Java с помощью Фабрик. Но каждый раз процесс генерации начинается с нуля. Это крайне неэффективно.

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

Работать с деревом синтаксического разбора будут конкретные Посетители. В нашем случае можно использовать CppVisitor и JavaVisitor. Нам понадобится всего по одному классу, вместо Фабрик и горы вспомогательных классов-элементов.

Рефакторинг части Компоновщика

Базовый класс Unit принимает следующий вид:

class ClassUnit;
class MethodUnit;
class PrintOperatorUnit;

class Unit {
public:
    using Flags = unsigned int;

    class Visitor {
    public:
        virtual ~Visitor() = default;

        virtual void enter( const ClassUnit& classUnit, unsigned int level ) = 0;
        virtual void leave( const ClassUnit& classUnit, unsigned int level ) = 0;

        virtual void enter( const MethodUnit& methodUnit, unsigned int level ) = 0;
        virtual void leave( const MethodUnit& methodUnit, unsigned int level ) = 0;

        virtual void visit( const PrintOperatorUnit& printUnit, unsigned int level ) = 0;

        virtual std::string getResult() const = 0;

    protected:
        virtual std::string generateShift( unsigned int level ) const {
            static const auto DEFAULT_SHIFT = "  ";
            std::string result;
            for( unsigned int i = 0; i < level; ++i ) {
                result += DEFAULT_SHIFT;
            }
            return result;
        }
    };

public:
    virtual ~Unit() = default;

    virtual void add( const std::shared_ptr< Unit >& /* unit */ ) {
        throw std::runtime_error( "Not supported" );
    }
    virtual void visit( Visitor* visitor, unsigned int level = 0 ) = 0;
    virtual Flags getFlags() const { return 0; }
};

Интерфейс Unit не сильно изменился по сравнению с тем, что было. Исчезли функции compile() и generateShift(), но добавилась функция visit(). С убранными функциями все понятно. Теперь Unit предназначен исключительно для компоновки и хранения структуры, но не для ее обработки и преобразований.

Добавленная функция visit() принимает указатель на Посетителя Visitor и уровень level, которому соответствует вызов. В этой функции и будет происходить обслуживание Посетителя.

Сам класс Visitor определен, как вложенный в Unit. Обратите внимание на его интерфейс. Для элементов Класса и Метода предусмотрены пары функций-членов enter() и leave(). Это нужно для отслеживания начала и конца этих синтаксических структур, которые могут иметь вложенные элементы. Оператор вывода PrintOperatorUnit вложенных элементов иметь не может, поэтому для него такие ухищрения излишни.

Кстати, generateShift() переехал в Visitor. Теперь вопросы выравнивания целиком и полностью его проблемы.

Элемент Класс

Вот новая версия ClassUnit:

class ClassUnit : public Unit {
public:
    enum AccessModifier {
        PUBLIC,
        PROTECTED,
        PRIVATE
    };

    static const std::vector< std::string > ACCESS_MODIFIERS;

public:
    explicit ClassUnit( const std::string& name ) : m_name( name ) { }

    void add( const std::shared_ptr< Unit >& unit ) {
        m_fields.push_back( unit );
    }

    const std::string& getName() const { return m_name; }

    void visit( Visitor* visitor, unsigned int level ) {
        visitor->enter( *this, level );

        for( const auto& f : m_fields ) {
            f->visit( visitor, level + 1 );
        }

        visitor->leave( *this, level );
    }

private:
    std::string m_name;
    std::vector< std::shared_ptr< Unit > > m_fields;

};
const std::vector< std::string > ClassUnit::ACCESS_MODIFIERS = { "public", "protected", "private" };

Изменений довольно много:

  1. Класс ClassUnit больше не абстрактный;
  2. Упрощена внутренняя структура хранения вложенных полей. Все добавляется в единый контейнер без разделения по уровням доступа;
  3. Появилась новая открытая функция-член getName(). Через нее Посетитель сможет узнать имя Класса.

Наибольший интерес для нас представляет функция ClassUnit::visit(). Сначала она сообщает посетителю о начале обхода путем вызова функции-члена Посетителя enter().

Затем в цикле происходит посещение каждого поля Класса. Для них запускается функция visit() с текущим посетителем в качестве первого аргумента.

В самом конце функции мы прощаемся с Посетителем. Говорим ему с помощью leave(), что обход окончен.

Элемент Метод

Следующим по порядку идет MethodUnit:

class MethodUnit : public Unit {
public:
    enum Modifier {
        PUBLIC      = 1,
        PROTECTED   = 1 << 1,
        PRIVATE     = 1 << 2,
        STATIC      = 1 << 3,
        CONST       = 1 << 4,
        VIRTUAL     = 1 << 5
    };

public:
    MethodUnit( const std::string& name, const std::string& returnType, Flags flags ) :
        m_name( name ), m_returnType( returnType ), m_flags( flags ) { }

    void add( const std::shared_ptr< Unit >& unit ) {
        m_body.push_back( unit );
    }

    void visit( Visitor* visitor, unsigned int level = 0 ) {
        visitor->enter( *this, level );

        for( const auto& b : m_body ) {
            b->visit( visitor, level + 1 );
        }

        visitor->leave( *this, level );
    }

    const std::string& getName() const { return m_name; }
    const std::string& getReturnType() const { return m_returnType; }
    Flags getFlags() const { return m_flags; }

    ClassUnit::AccessModifier getModifier() const {
        if( getFlags() & MethodUnit::PUBLIC ) {
            return ClassUnit::PUBLIC;
        } else if( getFlags() & MethodUnit::PROTECTED ) {
            return ClassUnit::PROTECTED;
        }

        return ClassUnit::PRIVATE;
    }

protected:
    const std::vector< std::shared_ptr< Unit > >& getBody() const {
        return m_body;
    }

private:
    std::string m_name;
    std::string m_returnType;
    Flags m_flags;

    std::vector< std::shared_ptr< Unit > > m_body;
};

Для него мы добавили функции-геттеры getName() и getReturnType(). А также создали вспомогательную функцию getModifier(), которая на основе значения m_flags определяет область видимости.

Функция обхода visit() дословно повторяет то, что мы видели для ClassUnit. Уже можно задуматься над созданием вспомогательного базового класса или другим способом устранения дублирования кода. Но чтобы не увеличивать размер статьи, я этого делать не буду.

Элемент Оператор печати

Остался последний элемент:

class PrintOperatorUnit : public Unit {
public:
    explicit PrintOperatorUnit( const std::string& text ) : m_text( text ) { }

    void visit( Visitor* visitor, unsigned int level ) {
        visitor->visit( *this, level );
    }

    const std::string& getText() const { return m_text; }

private:
    std::string m_text;
};

Думаю, этот код не нуждается в пояснениях.

Использование нового Компоновщика

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

std::unique_ptr< Unit > generateProgram() {
    auto myClass = std::unique_ptr< ClassUnit >( new ClassUnit( "MyClass" ) );

    myClass->add(
        std::make_shared< MethodUnit >( "testFunc1", "void", MethodUnit::PUBLIC )
    );

    myClass->add(
        std::make_shared< MethodUnit >( "testFunc2", "void", MethodUnit::STATIC )
    );

    myClass->add(
        std::make_shared< MethodUnit >(
            "testFunc3",
            "void",
            MethodUnit::VIRTUAL | MethodUnit::CONST | MethodUnit::PUBLIC
        )
    );

    auto method = std::make_shared< MethodUnit >(
        "testFunc4",
        "void",
        MethodUnit::STATIC | MethodUnit::PROTECTED
    );
    method->add( std::make_shared< PrintOperatorUnit >( R"(Hello, world!\n)" ) );
    myClass->add( method );

    return myClass;
}

int main() {
    auto program = generateProgram();

    return 0;
}

Отлично. Just as planned! Осталось только реализовать Посетителей.

Java-Посетитель

В этот раз начнем с генератора кода на Java. Вот как он может выглядеть:

class JavaVisitor : public Unit::Visitor {
public:
    void enter( const ClassUnit& classUnit, unsigned int level ) {
        m_result += generateShift( level ) + "class " + classUnit.getName() + " {\n";
    }

    void leave( const ClassUnit& /* classUnit */, unsigned int level ) {
        m_result += generateShift( level ) + "}\n";
    }

    void enter( const MethodUnit& methodUnit, unsigned int level ) {
        m_result += generateShift( level );

        auto mod = methodUnit.getModifier();
        m_result += ClassUnit::ACCESS_MODIFIERS[ mod ] + " ";

        if( methodUnit.getFlags() & MethodUnit::STATIC ) {
            m_result += "static ";
        } else if( !( methodUnit.getFlags() & MethodUnit::VIRTUAL ) ) {
            m_result += "final ";
        }

        m_result += methodUnit.getReturnType() + " ";
        m_result += methodUnit.getName() + "()";

        m_result += " {\n";
    }

    void leave( const MethodUnit& /* methodUnit */, unsigned int level ) {
        m_result += generateShift( level ) + "}\n\n";
    }

    void visit( const PrintOperatorUnit& printUnit, unsigned int level ) {
        m_result += generateShift( level ) + "System.out.print( \"" + printUnit.getText() + "\" );\n";
    }

    std::string getResult() const { return m_result; }

private:
    std::string m_result;
};

По сути, я просто перенес код из функций Unit::compile(), которые у нас получились в прошлый раз. Правда, теперь необходимые значения (имя класса, имя метода и т.д.) извлекаются из переданных элементов.

Также обратите внимание, что результат добавляется к переменной m_result. Извлечь его можно с помощью предусмотренной функции-геттера getResult().

Небольшой проблемой получившегося Посетителя является то, что он "одноразовый". Если мы запустим его второй раз, то старый результат останется на месте. А новый к нему "приклеится". Но устранить этот недостаток довольно легко. Достаточно обзавестись функцией Visitor::clear().

Воспользуемся нашим Java-Посетителем:

int main() {
    auto program = generateProgram();

    JavaVisitor javaVisitor;
    program->visit( &javaVisitor );
    std::cout << javaVisitor.getResult();

    return 0;
}

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

class MyClass {
  public final void testFunc1() {
  }

  private static void testFunc2() {
  }

  public void testFunc3() {
  }

  protected static void testFunc4() {
    System.out.print( "Hello, world!\n" );
  }

}

Все правильно. Можно двигаться дальше.

Cpp-Посетитель

Реализация этого Посетителя получилась чуть сложнее:

class CppVisitor : public Unit::Visitor {
public:
    void enter( const ClassUnit& classUnit, unsigned int level ) {
        m_result += generateShift( level ) + "class " + classUnit.getName() + " {\n";
    }

    void leave( const ClassUnit& /* classUnit */, unsigned int level ) {
        m_result += generateShift( level ) + "};\n";
    }

    void enter( const MethodUnit& methodUnit, unsigned int level ) {
        auto mod = methodUnit.getModifier();
        if( m_modifiers.find( level ) == m_modifiers.end() || m_modifiers[ level ] != mod ) {
            m_result += ClassUnit::ACCESS_MODIFIERS[ mod ] + ":\n";
            m_modifiers[ level ] = mod;
        }

        m_result += generateShift( level );
        if( methodUnit.getFlags() & MethodUnit::STATIC ) {
            m_result += "static ";
        } else if( methodUnit.getFlags() & MethodUnit::VIRTUAL ) {
            m_result += "virtual ";
        }

        m_result += methodUnit.getReturnType() + " ";
        m_result += methodUnit.getName() + "()";

        if( methodUnit.getFlags() & MethodUnit::CONST ) {
            m_result += " const";
        }

        m_result += " {\n";
    }

    void leave( const MethodUnit& /* methodUnit */, unsigned int level ) {
        m_result += generateShift( level ) + "}\n\n";
    }

    void visit( const PrintOperatorUnit& printUnit, unsigned int level ) {
        m_result += generateShift( level ) + "printf( \"" + printUnit.getText() + "\" );\n";
    }

    std::string getResult() const {
        return m_result;
    }

private:
    std::string m_result;
    std::unordered_map< unsigned int, ClassUnit::AccessModifier > m_modifiers;

};

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

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

Остальная часть кода, как и для Java-Посетителя, дословно повторяет то, что раньше было в функциях compile(). Все готово к пробному запуску:

int main() {
    auto program = generateProgram();

    CppVisitor cppVisitor;
    program->visit( &cppVisitor );
    std::cout << cppVisitor.getResult();

    return 0;
}

На консоль будет выведено:

class MyClass {
public:
  void testFunc1() {
  }

private:
  static void testFunc2() {
  }

public:
  virtual void testFunc3() const {
  }

protected:
  static void testFunc4() {
    printf( "Hello, world!\n" );
  }

};

Выводы

Пример получился довольно длинным. Материал пришлось разбить на три части. В них мы посмотрели паттерны Компоновщик, Абстрактная фабрика и Посетитель. Все они полезны по своему. Имеют слабые и сильные стороны.

Для нашего конкретного случая более предпочтительным выглядит решение из комбинации Компоновщик+Посетитель. Оно дает существенное преимущество по сравнению с версией на основе Абстрактной фабрики. Это преимущество заключается в четком разделении логики формирования структуры дерева и его обработки.

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

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

Похожие публикации

Комментарии

Удобно

НОРМ ТАК ТО

Отличные статьи, очень познавательно. Спасибо!