Предварительно рекомендую ознакомиться с предыдущей статьей, в которой мы обсуждали паттерн Компоновщик. Ведь сейчас мы займемся устранением недостатка, на котором остановились в прошлый раз. Сделаем код расширяемым, чтобы обеспечить генерацию программ и на других языках программирования.
Абстрактная фабрика (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
). Тогда нам не придется писать реализации Фабрик, как под копирку.
Дублирование алгоритма обхода мы можем устранить с помощью еще одного паттерна - Шаблонный метод. Но и это не решит все проблемы. В полноценной системе при большом числе поддерживаемых языков программирования и синтаксических структур мы получим астрономическое число классов.
Однако в большом количестве классов есть и свои преимущества. Код получился очень гранулированным. Все классы решают одну очень узкую задачу, а это снижает количество потенциальных ошибок. Поэтому не так уж все и плохо.
Тем не менее, в следующий раз мы подойдем к решению рассмотренный задачи с другого конца. Для этого мы используем более монолитный паттерн - Посетитель…
Очень интересно
Спасибо за отзывы :)
Очень интересно написано, Познавательно!!!
Статья интересная .ссупер
Интересно!
Anonymous
норм