Предварительно рекомендую ознакомиться с предыдущей статьей, в которой мы обсуждали паттерн Компоновщик. Ведь сейчас мы займемся устранением недостатка, на котором остановились в прошлый раз. Сделаем код расширяемым, чтобы обеспечить генерацию программ и на других языках программирования.
Абстрактная фабрика (Abstract factroy) является одним из самых известных представителей порождающих паттернов. Классы-фабрики, реализующие этот паттерн, решают единственную задачу — создание взаимосвязанных объектов.
Основная идея Абстрактной фабрики заключается в том, что мы избавляемся от зависимостей при создании конкретных экземпляров классов. Например, в функции generateProgram() из прошлой статьи нам встречались следующие фрагменты кода:
1 2 3 4 5 6 7 8 9 |
[crayon-67597225a2848016949680 inline="true" class="lang-cpp"]std::string generateProgram() { ClassUnit myClass( "MyClass" ); myClass.add( std::make_shared< MethodUnit >( "testFunc1", "void", 0 ), ClassUnit::PUBLIC ); // ... } |
[/crayon]
Имеется явная зависимость от типов
ClassUnit и
MethodUnit. А эти классы предназначены для генерации кода на C++. Если нам понадобится код на Java (с сохранением поддержки C++), то мы ничего не сможем сделать.
Указанную проблему можно решить с помощью шаблонов. Для рассмотренного примера его даже можно считать вполне приемлемой альтернативой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
[crayon-67597225a284c398977757 inline="true" class="lang-cpp"]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; } |
[/crayon]
Однако с ростом количества поддерживаемых синтаксических структур список параметров шаблона может разрастись до огромных размеров. Поэтому хоть такое решение и имеет право на жизнь, нам оно не подходит.
Вот мы и подошли вплотную к Абстрактной фабрике. Определим соответствующий базовый класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[crayon-67597225a284f244175847 inline="true" class="lang-cpp"]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; }; |
[/crayon]
Назначение всех функций-членов Фабрики вполне понятно из их названий. В качестве аргументов они принимают параметры, которые будут переданы конструкторам создаваемых элементов.
Классы ClassUnit, MethodUnit и PrintOperatorUnit теперь тоже становятся абстрактными. Уберем из них реализации функции compile(), поскольку они становятся специфическими. При этом реализация add() сохраняется. Также нам приходится реализовывать функции доступа к закрытым переменным:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
[crayon-67597225a2856680618835 inline="true" class="lang-cpp"]// ================================================================================ 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; }; |
[/crayon]
На этом этапе мы уже можем перейти к функции-генератору на основе Абстрактной фабрики:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
[crayon-67597225a2859866471638 inline="true" class="lang-cpp"]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(); } |
[/crayon]
Теперь все классы, представляющие синтаксические структуры, создаются только с помощью вызова соответствующих функций-членов Фабрики. Зависимости устранены.
Осталось только предоставить конкретные реализации классов. Начнем с реализации тех, что обеспечат поддержку синтаксиса C++:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
[crayon-67597225a285b969937758 inline="true" class="lang-cpp"]// ================================================================================ 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"; } }; |
[/crayon]
Представленный код нам уже знаком. Мы практически дословно скопировали его из прошлой статьи. Единственное отличие заключается в том, что мы вынуждены использовать специальные функции доступа к закрытым переменным базовых классов.
А вот и соответствующая фабрика в качестве заключительного штриха нашего рефакторинга:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[crayon-67597225a285d939952899 inline="true" class="lang-cpp"]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 ) ); } }; |
[/crayon]
Пробный запуск:
1 2 3 4 5 6 |
[crayon-67597225a285f036401419 inline="true" class="lang-cpp"]int main() { std::cout << generateProgram( std::make_shared< CppFactory >() ) << std::endl; return 0; } |
[/crayon]
Все работает. Прекрасно. Но мы на этом не заканчиваем. Все затевалось для того, чтобы добавить поддержку генерации на еще одном языке программирования. Создадим Java-фабрику со всеми вспомогательными классами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
[crayon-67597225a2861970344290 inline="true" class="lang-cpp"]// ================================================================================ 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 ) ); } }; |
[/crayon]
Получившийся код более или менее работоспособен. В нем принимается в расчет специфика Java. Например, не учитывается модификатор метода
const, который в Java не предусмотрен. А отсутствие модификатора
virtual равносильно
final-методу.
Запустим generateProgram() с этой Фабрикой:
1 2 3 4 5 6 |
[crayon-67597225a2869740562347 inline="true" class="lang-cpp"]int main() { std::cout << generateProgram( std::make_shared< JavaFactory >() ) << std::endl; return 0; } |
[/crayon]
На консоль будет выведено следующее:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[crayon-67597225a286b881217795 inline="true" class="lang-java"]class MyClass { final void testFunc1() { } void testFunc3() { } static void testFunc4() { System.out.print( "Hello, world!\n" ); } static void testFunc2() { } } |
[/crayon]
Основная проблема заключается в отсутствии модификаторов доступа для методов. В Java модификатор должен указываться для каждого метода отдельно. Да и просто вполне логично, что он является свойством самой операции в любом языке программирования.
В качестве первого приближения расширим перечисление Modifier в классе MethodUnit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[crayon-67597225a286f202735065 inline="true" class="lang-cpp"]class MethodUnit : public Unit { public: enum Modifier { PUBLIC = 1, PROTECTED = 1 << 1, PRIVATE = 1 << 2, STATIC = 1 << 3, CONST = 1 << 4, VIRTUAL = 1 << 5 }; // Остальное без изменений // ... }; |
[/crayon]
Теперь добавим соответствующую интерпретацию модификатора для
JavaMethod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
[crayon-67597225a2872309019954 inline="true" class="lang-cpp"]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; } }; |
[/crayon]
Чтобы все корректно работало, нужно добавить передачу флага в функции-генераторе:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[crayon-67597225a2874908929928 inline="true" class="lang-cpp"]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(); } |
[/crayon]
Теперь код на Java генерируется корректно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[crayon-67597225a2876444715532 inline="true" class="lang-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() { } } |
[/crayon]
Для C++ тоже все работает. Но работу заканчивать все еще рано. У нас осталась одна маленькая, но очень неприятная особенность — дублирование кода. Мы передаем модификатор доступа и при создании
MethodUnit, и при добавлении элемента в
ClassUnit.
Мы уже остановились на том, что модификатор обязательно должен находиться в MethodUnit. Значит, нам нужно, чтобы ClassUnit умел его читать. Для этого добавим в класс Unit виртуальную функцию-член getFlags():
1 2 3 4 5 6 7 8 9 |
[crayon-67597225a287e311348309 inline="true" class="lang-cpp"]class Unit { public: // ... virtual Flags getFlags() const { return 0; } // ... }; |
[/crayon]
В
MethodUnit уже есть реализация этой функции. Она нас вполне устроит. Теперь проведем рефакторинг функции
add() в классе
ClassUnit:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
[crayon-67597225a2883639355056 inline="true" class="lang-cpp"]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 ); } // ... }; |
[/crayon]
Обратите внимание, что объявление перечисления
MethodUnit::Modifier должно быть видно в
ClassUnit. Для этого нужно либо поменять порядок определения классов, либо писать код, как положено, — разделив на заголовочный файл и исходник с реализацией.
Кроме того, заметим, что теперь параметр flags во всех реализациях функции-члена add() не учитывается. Можем спокойно его убрать:
1 2 3 4 5 6 7 8 9 10 11 |
[crayon-67597225a2889660457855 inline="true" class="lang-cpp"]class Unit { public: virtual void add( const std::shared_ptr< Unit >& /* unit */ ) { throw std::runtime_error( "Not supported" ); } // ... }; // Во всех наследниках Unit со своей реализацией add() делаем соответствующие изменения. |
[/crayon]
Окончательно функция-генератор принимает вид:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
[crayon-67597225a288b605981471 inline="true" class="lang-cpp"]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(); } |
[/crayon]
Теперь все работает. Но реализация вышла довольно многословной. Получилось много классов. Есть дублирование логики обхода дерева синтаксического разбора. Конечно, часть из этих проблем мы можем решить.
В частности, можно вернуться к идее шаблонов, но применить ее относительно самой Фабрики. Для каждого варианта шаблона, заполненного параметрами, можно предусмотреть псевдоним (определив его с помощью typdef или using). Тогда нам не придется писать реализации Фабрик, как под копирку.
Дублирование алгоритма обхода мы можем устранить с помощью еще одного паттерна — Шаблонный метод. Но и это не решит все проблемы. В полноценной системе при большом числе поддерживаемых языков программирования и синтаксических структур мы получим астрономическое число классов.
Однако в большом количестве классов есть и свои преимущества. Код получился очень гранулированным. Все классы решают одну очень узкую задачу, а это снижает количество потенциальных ошибок. Поэтому не так уж все и плохо.
Тем не менее, в следующий раз мы подойдем к решению рассмотренный задачи с другого конца. Для этого мы используем более монолитный паттерн — Посетитель…