IT Notes

Паттерн Абстрактная фабрика на C++

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

Абстрактная фабрика (Abstract factroy) является одним из самых известных представителей порождающих паттернов. Классы-фабрики, реализующие этот паттерн, решают единственную задачу - создание взаимосвязанных объектов.

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

std::string generateProgram() {
    ClassUnit myClass( "MyClass" );
    myClass.add(
        std::make_shared< MethodUnit >( "testFunc1", "void", 0 ),
        ClassUnit::PUBLIC
    );
    // …
}

Имеется явная зависимость от типов ClassUnit и MethodUnit. А эти классы предназначены для генерации кода на C++. Если нам понадобится код на Java (с сохранением поддержки C++), то мы ничего не сможем сделать.

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

template< class ClassType, class MethodType, class PrintOperatorType >
std::string generateProgram() {
    ClassType myClass( "MyClass" );
    myClass.add(
        std::make_shared< MethodType >( "testFunc1", "void", 0 ),
        ClassUnit::PUBLIC
    );
    myClass.add(
        std::make_shared< MethodType >( "testFunc2", "void", MethodUnit::STATIC ),
        ClassUnit::PRIVATE
    );
    myClass.add(
        std::make_shared< MethodType >( "testFunc3", "void", MethodUnit::VIRTUAL | MethodUnit::CONST ),
        ClassUnit::PUBLIC
    );

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

    return myClass.compile();
}

int main() {
    std::cout << generateProgram< ClassUnit, MethodUnit, PrintOperatorUnit >() << std::endl;
}

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

Вот мы и подошли вплотную к Абстрактной фабрике. Определим соответствующий базовый класс:

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

    virtual std::unique_ptr< ClassUnit > createClass( const std::string& name ) = 0;

    virtual std::unique_ptr< MethodUnit > createMethod(
        const std::string& name,
        const std::string& returnType,
        Unit::Flags flags
    ) = 0;

    virtual std::unique_ptr< PrintOperatorUnit > createPrintOperator( const std::string& text ) = 0;
};

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

Классы ClassUnit, MethodUnit и PrintOperatorUnit теперь тоже становятся абстрактными. Уберем из них реализации функции compile(), поскольку они становятся специфическими. При этом реализация add() сохраняется. Также нам приходится реализовывать функции доступа к закрытым переменным:

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

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

    using Fields = std::vector< std::shared_ptr< Unit > >;

public:
    explicit ClassUnit( const std::string& name ) : m_name( name ) {
        m_fields.resize( ACCESS_MODIFIERS.size() );
    }

    void add( const std::shared_ptr< Unit >& unit, Flags flags ) {
        int accessModifier = PRIVATE;
        if( flags < ACCESS_MODIFIERS.size() ) {
            accessModifier = flags;
        }

        m_fields[ accessModifier ].push_back( unit );
    }

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

    const Fields& getFields( unsigned int accessGroup ) const {
        if( ACCESS_MODIFIERS.size() <= accessGroup ) {
            throw std::out_of_range( "Invalid access group index" );
        }
        return m_fields[ accessGroup ];
    }

private:
    std::string m_name;
    std::vector< Fields > m_fields;

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

// ================================================================================
class MethodUnit : public Unit {
public:
    enum Modifier {
        STATIC      = 1,
        CONST       = 1 << 1,
        VIRTUAL     = 1 << 2
    };

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, Flags /* flags */ = 0 ) {
        m_body.push_back( unit );
    }

protected:
    const std::string& getName() const { return m_name; }
    const std::string& getReturnType() const { return m_returnType; }
    Flags getFlags() const { return m_flags; }
    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;
};

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

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

private:
    std::string m_text;
};

На этом этапе мы уже можем перейти к функции-генератору на основе Абстрактной фабрики:

std::string generateProgram( const std::shared_ptr< LangFactory >& factory ) {
    auto myClass = factory->createClass( "MyClass" );
    myClass->add(
        factory->createMethod( "testFunc1", "void" ),
        ClassUnit::PUBLIC
    );
    myClass->add(
        factory->createMethod( "testFunc2", "void" ),
        ClassUnit::PRIVATE
    );
    myClass->add(
        factory->createMethod( "testFunc3", "void", MethodUnit::VIRTUAL | MethodUnit::CONST ),
        ClassUnit::PUBLIC
    );

    std::shared_ptr< MethodUnit > method = factory->createMethod( "testFunc4", "void", MethodUnit::STATIC );
    method->add( factory->createPrintOperator( R"(Hello, world!\n)" ) );
    myClass->add( method, ClassUnit::PROTECTED );

    return myClass->compile();
}

Теперь все классы, представляющие синтаксические структуры, создаются только с помощью вызова соответствующих функций-членов Фабрики. Зависимости устранены.

Осталось только предоставить конкретные реализации классов. Начнем с реализации тех, что обеспечат поддержку синтаксиса C++:

// ================================================================================
class CppClass : public ClassUnit {
public:
    CppClass( const std::string& name ) : ClassUnit( name ) { }

    std::string compile( unsigned int level = 0 ) const {
        std::string result = generateShift( level ) + "class " + getName() + " {\n";
        for( size_t i = 0; i < ACCESS_MODIFIERS.size(); ++i ) {
            if( getFields( i ).empty() ) {
                continue;
            }

            result += ACCESS_MODIFIERS[ i ] + ":\n";
            for( const auto& f : getFields( i ) ) {
                result += f->compile( level + 1 );
            }

            result += "\n";
        }
        result += generateShift( level ) + "};\n";

        return result;
    }
};

// ================================================================================
class CppMethod : public MethodUnit {
public:
    CppMethod( const std::string& name, const std::string& returnType, Flags flags ) :
        MethodUnit( name, returnType, flags ) { }

    std::string compile( unsigned int level = 0 ) const {
        std::string result = generateShift( level );
        if( getFlags() & STATIC ) {
            result += "static ";
        } else if( getFlags() & VIRTUAL ) {
            result += "virtual ";
        }

        result += getReturnType() + " ";
        result += getName() + "()";

        if( getFlags() & CONST ) {
            result += " const";
        }

        result += " {\n";

        for( const auto& b : getBody() ) {
            result += b->compile( level + 1 );
        }

        result += generateShift( level ) + "}\n";

        return result;
    }
};

// ================================================================================
class CppPrintOperator : public PrintOperatorUnit {
public:
    CppPrintOperator( const std::string& text ) : PrintOperatorUnit( text ) { }

    std::string compile( unsigned int level = 0 ) const {
        return generateShift( level ) + "printf( \"" + getText() + "\" );\n";
    }
};

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

А вот и соответствующая фабрика в качестве заключительного штриха нашего рефакторинга:

class CppFactory : public LangFactory {
public:
    std::unique_ptr< ClassUnit > createClass( const std::string& name ) {
        return std::unique_ptr< ClassUnit >( new CppClass( name ) );
    }

    std::unique_ptr< MethodUnit > createMethod(
        const std::string& name,
        const std::string& returnType,
        Unit::Flags flags
    ) {
        return std::unique_ptr< MethodUnit >( new CppMethod( name, returnType, flags ) );
    }

    std::unique_ptr< PrintOperatorUnit > createPrintOperator( const std::string& text ) {
        return std::unique_ptr< PrintOperatorUnit >( new CppPrintOperator( text ) );
    }
};

Пробный запуск:

int main() {
    std::cout << generateProgram( std::make_shared< CppFactory >() ) << std::endl;

    return 0;
}

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

// ================================================================================
class JavaClass : public ClassUnit {
public:
    JavaClass( const std::string& name ) : ClassUnit( name ) { }

    std::string compile( unsigned int level = 0 ) const {
        std::string result = generateShift( level ) + "class " + getName() + " {\n";
        for( size_t i = 0; i < ACCESS_MODIFIERS.size(); ++i ) {
            for( const auto& f : getFields( i ) ) {
                result += f->compile( level + 1 );
                result += "\n";
            }
        }
        result += generateShift( level ) + "}\n";

        return result;
    }
};

// ================================================================================
class JavaMethod : public MethodUnit {
public:
    JavaMethod( const std::string& name, const std::string& returnType, Flags flags ) :
        MethodUnit( name, returnType, flags ) { }

    std::string compile( unsigned int level = 0 ) const {
        std::string result = generateShift( level );

        // Нужно что-то придумать с модификатором доступа для метода

        if( getFlags() & STATIC ) {
            result += "static ";
        } else if( !( getFlags() & VIRTUAL ) ) {
            result += "final ";
        }

        result += getReturnType() + " ";
        result += getName() + "()";

        result += " {\n";

        for( const auto& b : getBody() ) {
            result += b->compile( level + 1 );
        }

        result += generateShift( level ) + "}\n";

        return result;
    }
};

// ================================================================================
class JavaPrintOperator : public PrintOperatorUnit {
public:
    JavaPrintOperator( const std::string& text ) : PrintOperatorUnit( text ) { }

    std::string compile( unsigned int level = 0 ) const {
        return generateShift( level ) + "System.out.print( \"" + getText() + "\" );\n";
    }
};

// ================================================================================
class JavaFactory : public LangFactory {
public:
    std::unique_ptr< ClassUnit > createClass( const std::string& name ) {
        return std::unique_ptr< ClassUnit >( new JavaClass( name ) );
    }

    std::unique_ptr< MethodUnit > createMethod(
        const std::string& name,
        const std::string& returnType,
        Unit::Flags flags
    ) {
        return std::unique_ptr< MethodUnit >( new JavaMethod( name, returnType, flags ) );
    }

    std::unique_ptr< PrintOperatorUnit > createPrintOperator( const std::string& text ) {
        return std::unique_ptr< PrintOperatorUnit >( new JavaPrintOperator( text ) );
    }
};

Получившийся код более или менее работоспособен. В нем принимается в расчет специфика Java. Например, не учитывается модификатор метода const, который в Java не предусмотрен. А отсутствие модификатора virtual равносильно final-методу.

Запустим generateProgram() с этой Фабрикой:

int main() {
    std::cout << generateProgram( std::make_shared< JavaFactory >() ) << std::endl;

    return 0;
}

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

class MyClass {
    final void testFunc1() {
    }

    void testFunc3() {
    }

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

    static void testFunc2() {
    }

}

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

В качестве первого приближения расширим перечисление Modifier в классе 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
    };

    // Остальное без изменений
    // …
};

Теперь добавим соответствующую интерпретацию модификатора для JavaMethod:

class JavaMethod : public MethodUnit {
public:
    JavaMethod( const std::string& name, const std::string& returnType, Flags flags ) :
        MethodUnit( name, returnType, flags ) { }

    std::string compile( unsigned int level = 0 ) const {
        std::string result = generateShift( level );

        // Учитываем флаг модификатора доступа
        if( getFlags() & PUBLIC ) {
            result += "public ";
        } else if( getFlags() & PROTECTED ) {
            result += "protected ";
        } else {
            result += "private ";
        }

        if( getFlags() & STATIC ) {
            result += "static ";
        } else if( !( getFlags() & VIRTUAL ) ) {
            result += "final ";
        }

        result += getReturnType() + " ";
        result += getName() + "()";

        result += " {\n";

        for( const auto& b : getBody() ) {
            result += b->compile( level + 1 );
        }

        result += generateShift( level ) + "}\n";

        return result;
    }
};

Чтобы все корректно работало, нужно добавить передачу флага в функции-генераторе:

std::string generateProgram( const std::shared_ptr< LangFactory >& factory ) {
    auto myClass = factory->createClass( "MyClass" );
    myClass->add(
        factory->createMethod( "testFunc1", "void", MethodUnit::PUBLIC ),
        ClassUnit::PUBLIC
    );
    myClass->add(
        factory->createMethod( "testFunc2", "void", MethodUnit::STATIC ),
        ClassUnit::PRIVATE
    );
    myClass->add(
        factory->createMethod(
            "testFunc3",
            "void",
            MethodUnit::VIRTUAL | MethodUnit::CONST | MethodUnit::PUBLIC
        ),
        ClassUnit::PUBLIC
    );

    std::shared_ptr< MethodUnit > method = factory->createMethod(
        "testFunc4", "void", MethodUnit::STATIC | MethodUnit::PROTECTED
    );
    method->add( factory->createPrintOperator( R"(Hello, world!\n)" ) );
    myClass->add( method, ClassUnit::PROTECTED );

    return myClass->compile();
}

Теперь код на Java генерируется корректно:

class MyClass {
    public final void testFunc1() {
    }

    public final void testFunc3() {
    }

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

    private static void testFunc2() {
    }

}

Для C++ тоже все работает. Но работу заканчивать все еще рано. У нас осталась одна маленькая, но очень неприятная особенность - дублирование кода. Мы передаем модификатор доступа и при создании MethodUnit, и при добавлении элемента в ClassUnit.

Мы уже остановились на том, что модификатор обязательно должен находиться в MethodUnit. Значит, нам нужно, чтобы ClassUnit умел его читать. Для этого добавим в класс Unit виртуальную функцию-член getFlags():

class Unit {
public:
    // …

    virtual Flags getFlags() const { return 0; }

    // …
};

В MethodUnit уже есть реализация этой функции. Она нас вполне устроит. Теперь проведем рефакторинг функции add() в классе ClassUnit:

class ClassUnit : public Unit {
public:
    // …

    void add( const std::shared_ptr< Unit >& unit, Flags /* flags - больше этот параметр не нужен */ = 0 ) {
        int accessModifier = PRIVATE;
        if( unit->getFlags() & MethodUnit::PUBLIC ) {
            accessModifier = PUBLIC;
        } else if( unit->getFlags() & MethodUnit::PROTECTED ) {
            accessModifier = PROTECTED;
        }

        m_fields[ accessModifier ].push_back( unit );
    }

    // …
};

Обратите внимание, что объявление перечисления MethodUnit::Modifier должно быть видно в ClassUnit. Для этого нужно либо поменять порядок определения классов, либо писать код, как положено, - разделив на заголовочный файл и исходник с реализацией.

Кроме того, заметим, что теперь параметр flags во всех реализациях функции-члена add() не учитывается. Можем спокойно его убрать:

class Unit {
public:
    virtual void add( const std::shared_ptr< Unit >& /* unit */ ) {
        throw std::runtime_error( "Not supported" );
    }

    // …
};

// Во всех наследниках Unit со своей реализацией add() делаем соответствующие изменения.

Окончательно функция-генератор принимает вид:

std::string generateProgram( const std::shared_ptr< LangFactory >& factory ) {
    auto myClass = factory->createClass( "MyClass" );
    myClass->add( factory->createMethod( "testFunc1", "void", MethodUnit::PUBLIC ) );
    myClass->add( factory->createMethod( "testFunc2", "void", MethodUnit::STATIC ) );
    myClass->add(
        factory->createMethod(
              "testFunc3",
              "void",
              MethodUnit::VIRTUAL | MethodUnit::CONST | MethodUnit::PUBLIC
        )
    );

    std::shared_ptr< MethodUnit > method = factory->createMethod(
        "testFunc4", "void", MethodUnit::STATIC | MethodUnit::PROTECTED
    );
    method->add( factory->createPrintOperator( R"(Hello, world!\n)" ) );
    myClass->add( method );

    return myClass->compile();
}

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

В частности, можно вернуться к идее шаблонов, но применить ее относительно самой Фабрики. Для каждого варианта шаблона, заполненного параметрами, можно предусмотреть псевдоним (определив его с помощью typdef или using). Тогда нам не придется писать реализации Фабрик, как под копирку.

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

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

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

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

Комментарии

норм

Очень интересно

Спасибо за отзывы :)

Очень интересно написано, Познавательно!!!

Статья интересная .ссупер

Интересно!