Comments 35
Господи, как в 90е… используйте тег source lang=«cpp»
А всё написаное обычный процесс переработки подробно описаный многими людьми, например, неплохо описано это, у Роберта Мартина в его книге «Чистый код».
А всё написаное обычный процесс переработки подробно описаный многими людьми, например, неплохо описано это, у Роберта Мартина в его книге «Чистый код».
+3
Когда прочел первый код сразу задался вопросом почему не сразу решили задачу как в последнем коде? Ведь это очевидное решение, которое описано во всех книгах по С++? Хотя для новичков такое решение не очевидно, они чаще всего не понимают для чего нужны виртуальные функции, особенно чистые виртуальные. В этом плане статья хорошо показывает как нужно правильно писать такие интерфейсы, хотя и сильно растянута. В своей практике иногда встречаю код подобный первому в статье, и делаю замечание разработчикам, в ответ получаю, «что ты прав, но мне сейчас некогда, потом переделаю» и т. д. В результате все остается как было и что либо расширить уже довольно сложно.
+1
Статья такая растянутая потому, что хотелось ответить на много вопросов. Это своеобразная статья-ответ. Я вообще хотел её в личный блог засунуть, но перечитал, посмотрел… как-то складно вроде получилось, можно и людям показать :-) Наверняка кто-то что-то да почерпнёт для себя.
-1
тем кто переходит с процедурных языков такое решение не очень очевидно, но по моему для обучения этому есть ВУЗы…
0
В вашем последнем коде в классах потомках в описаниях функций вы не указали virtual, хотя это и не обязательно, но мне кажется что нужно ставить, чтобы было ясно что они виртуальные и тогда не нужно лезть в описание предка и смотреть какие они на самом деле.
+4
UFO just landed and posted this here
Вы серьёзно думаете, что все, кто прочитал этот пост тут же пошлют резюме в Яндекс?
Во-первых, я не гарантирую, что мои ответы правильные :-) Я так же не гарантирую, что мои ответы, — это то, что хотят услышать в Яндексе. Ведь, как вы правильно заметили, «моё решение — далеко не единственное, имеющее свои плюсы и минусы». Во-вторых, думаю, что «совсем некомпетентных людей» на хабре нет.
Просто я бы мог написать «мне по работе попалась вот такая задача и я её решил так». Но «вот такую задачу» долго рассказывать, а тут есть уже готовые вопросы на которые можно просто дать ссылку.
Во-первых, я не гарантирую, что мои ответы правильные :-) Я так же не гарантирую, что мои ответы, — это то, что хотят услышать в Яндексе. Ведь, как вы правильно заметили, «моё решение — далеко не единственное, имеющее свои плюсы и минусы». Во-вторых, думаю, что «совсем некомпетентных людей» на хабре нет.
Просто я бы мог написать «мне по работе попалась вот такая задача и я её решил так». Но «вот такую задачу» долго рассказывать, а тут есть уже готовые вопросы на которые можно просто дать ссылку.
0
UFO just landed and posted this here
Я всё же настаиваю на том, что это не решения. Это мои собственные мысли. Я не являюсь сотрудником Яндекса, я не располагаю инсайдерской информацией, я не знаю правильных решений… и судя по размерам полей ввода, Яндекс ждёт не таких ответов.
0
Я хочу пройти собеседование в Яндексе, но не хочу слать резюме…
+1
Я бы сказал, что в конкретном варианте даже полезен compile time полиморфизм, для этого есть совсем неплохая либа boost::mpl, есть попроще Loki, да и самому здесь тоже писать очень мало, зато все отсеится на этапе компиляции и не будет лишнего кода. Когда-то клепал примерчик:
#include <iostream>
class Foo
{
public:
int GetValue()const{return 1;}
};
class Bar
{
public:
int GetValue()const{return 2;}
};
template<int v>
struct Int2Type
{
enum {value = v};
};
template<typename T>
struct TypeTrait
{
enum {IsValid = 0};
};
template<>
struct TypeTrait<Foo>
{
enum {IsValid = 1};
};
template<typename T>
inline void _DoWork(const T& t, Int2Type<1>)
{
std::cout << "value: " << t.GetValue() << std::endl;
}
// ошибка, тип не поодерживается
// либо вообще убрать данную функцию, в этом случае будет ошибка компиляции на DoWork(bar);
template<typename T>
inline void _DoWork(const T& t, Int2Type<0>)
{
// в идеале здесь требуется статическая проверка этапа компиляции
std::cout << "error" << std::endl;
}
template<typename T>
inline void DoWork(const T& t)
{
_DoWork(t, Int2Type<TypeTrait<T>::IsValid>());
}
int main(int argc, char *argv[])
{
Foo foo;
Bar bar;
DoWork(foo);
DoWork(bar);
}
К сожалению почему-то подсветка синтаксиса в комментариях не заводится или её нет в предпросмотре.
#include <iostream>
class Foo
{
public:
int GetValue()const{return 1;}
};
class Bar
{
public:
int GetValue()const{return 2;}
};
template<int v>
struct Int2Type
{
enum {value = v};
};
template<typename T>
struct TypeTrait
{
enum {IsValid = 0};
};
template<>
struct TypeTrait<Foo>
{
enum {IsValid = 1};
};
template<typename T>
inline void _DoWork(const T& t, Int2Type<1>)
{
std::cout << "value: " << t.GetValue() << std::endl;
}
// ошибка, тип не поодерживается
// либо вообще убрать данную функцию, в этом случае будет ошибка компиляции на DoWork(bar);
template<typename T>
inline void _DoWork(const T& t, Int2Type<0>)
{
// в идеале здесь требуется статическая проверка этапа компиляции
std::cout << "error" << std::endl;
}
template<typename T>
inline void DoWork(const T& t)
{
_DoWork(t, Int2Type<TypeTrait<T>::IsValid>());
}
int main(int argc, char *argv[])
{
Foo foo;
Bar bar;
DoWork(foo);
DoWork(bar);
}
К сожалению почему-то подсветка синтаксиса в комментариях не заводится или её нет в предпросмотре.
0
Я считаю, что сначала требуется знать шаблоны проектирования. Программист знающий шаблоны проектирования сразу определится с реализацией и в итоге не будет кучи промежуточных вариантов, которые опять же требуют время.
В конечном итоге меня очень сильно смущает смешивание сущностей. Введение объект CodeGeneratorBadProcessor, в итоге мы получаем некоторую путаницу. Да и судя по вашему примеру если этого класса не будет, то никто его и не создаст, я бы еще понял, если созданием объектов занималась фабрика. Тут все же лучше использовать исключения, а не некий объект, который выделяется из общего списка.
Опять же очень плохой момент в абстрактном классе невиртуальный деструктор, очень долго можно веселиться с ним.
Опять же путаница в стилях именования. Классы все буквы с большой, методы первые буквы с маленькой. Хорошо, что листинги еще короткие, а вот в больших листингах и реальной работе будет колоссальная путаница в итоге.
Исходный вариант тоже довольно веселый, если из enum убрать PHP, можно убрать и кидание исключения, компилятор просто не даст скомпилится коду с неправильным типом.
В конечном итоге меня очень сильно смущает смешивание сущностей. Введение объект CodeGeneratorBadProcessor, в итоге мы получаем некоторую путаницу. Да и судя по вашему примеру если этого класса не будет, то никто его и не создаст, я бы еще понял, если созданием объектов занималась фабрика. Тут все же лучше использовать исключения, а не некий объект, который выделяется из общего списка.
Опять же очень плохой момент в абстрактном классе невиртуальный деструктор, очень долго можно веселиться с ним.
Опять же путаница в стилях именования. Классы все буквы с большой, методы первые буквы с маленькой. Хорошо, что листинги еще короткие, а вот в больших листингах и реальной работе будет колоссальная путаница в итоге.
Исходный вариант тоже довольно веселый, если из enum убрать PHP, можно убрать и кидание исключения, компилятор просто не даст скомпилится коду с неправильным типом.
0
Про природу промежуточных вариантов я уже пояснил.
От CodeGeneratorBadProcessor мы в конце концов счастливо отказываемся. Он ни чем не хуже и не лучше изначального _language.
Про деструктор: долго веселиться можно с публичным деструктором. Попробуйте повеселиться с защищённым ,-) А у меня именно такой.
Что касается фабрик, статического полиморфизма и прочего… всё это хорошо и правильно, но на чём-то надо было остановиться :-) Что-то я стал подумывать, а не написать ли про статический полиморфизм?
От CodeGeneratorBadProcessor мы в конце концов счастливо отказываемся. Он ни чем не хуже и не лучше изначального _language.
Про деструктор: долго веселиться можно с публичным деструктором. Попробуйте повеселиться с защищённым ,-) А у меня именно такой.
Что касается фабрик, статического полиморфизма и прочего… всё это хорошо и правильно, но на чём-то надо было остановиться :-) Что-то я стал подумывать, а не написать ли про статический полиморфизм?
-1
Вопрос не относящийся к топику, это только у меня некоторые комментарии раздваиваются? Или у кого-то тоже?
-1
UFO just landed and posted this here
1) в топике не рассмотрен вопрос, на который написан топик:
как выбрать динамически тип в «безIFовом программировании»?
и почему пользователь должен знать о всех типах генераторов, если ему достаточно знать номер генератора?
2) тут без фабричного метода не обойтись.
3) с отсутствием виртуального деструктора хорошо справляется boost::shared_ptr www.boost.org/doc/libs/1_45_0/libs/smart_ptr/sp_techniques.html#abstract
как выбрать динамически тип в «безIFовом программировании»?
и почему пользователь должен знать о всех типах генераторов, если ему достаточно знать номер генератора?
2) тут без фабричного метода не обойтись.
3) с отсутствием виртуального деструктора хорошо справляется boost::shared_ptr www.boost.org/doc/libs/1_45_0/libs/smart_ptr/sp_techniques.html#abstract
0
UFO just landed and posted this here
generateCode
и его напарник параметров не принимает, в чём смысл городить интерфейсы?Код ниже туп, прост, при этом легко изменяется. Понадобятся параметры, сделаем
tuple<...> makejava(int param) { return make_tuple(...); }
. Затем можно и интерфейс выделить.java = make_tuple("Java...", "http...");
php = make_tuple("PHP...", "http...");
void gen(tuple<std::string, std::string> const & lang)
{
// ...
}
int main()
{
gen(java);
}
+3
Если нету подгрузки нужных кодогенераторов в рантайме, то имхо лучше привести всё к такому виду
CodeGenerator cg;
//или же
CodeGenerator cg;
gc.generate();
+1
Парсер лох(((
CodeGenerator<Java> cg;
//или же
CodeGenerator cg;
gc.generate<Java>();
* This source code was highlighted with Source Code Highlighter.
+2
Вообще тут 4 основных варианта в итоге, по нарастанию сложности подсистемы:
1) Простая иерархия наследования с базовым классом, когда вызывающий код создаёт класс конкретной реализации (как в топике)
2) Подход с дженериками, который по сути является мостом
3) Подход с адаптерами
4) Подход с фабрикой (из топика cd-riper на ЖЖ)
И часто все эти подходы сваливаются в одну кучу, т.к. одну и ту же задачу можно решить каждым из них.
Попробуем разобраться с конца:
Подход с фабрикой нужен для наиболее сложной подсистемы, когда мы имеем независимые действия и классы.
Если более кратко — запрашивающий объект код знает только о необходимом действии, но не знает о требующемся ему классе.
Это позволяет инкапсулировать логику создания объекта в зависимости от параметров действия и настроек среды, используется когда такая логика нетривиальна.
Берем фабрику и накладываем условие связи каждого запрошенного действия с конкретным классом, получаем адаптер, теперь запрашивающий код уже знает конкретный тип адаптера и создаёт его.
Накладываем ещё одно ограничение, теперь код всех наших адаптеров перестаёт зависить от адаптируемой реализации: выносим весь этот код в 1 класс и получаем мост
Если накладываем ещё одно ограничение, когда классы интерфейса моста становятся тривиальными и просто перебрасывают вызовы — мы выкидываем этот класс и получаем простую иерархию наследования.
Если в заданном примере требуется создать генератор кода исходя из конфигурационного объекта, в котором содержится описание типа: «требователен к производительности, архитектура x86_64, операционная система: xxx», тогда получаем фабрику в чистом виде.
Если подобных требований нет, и у нас есть классы генераторов для разных языков, но при этом все эти классы имеют разный интерфейс, определяемый различным пакетом настроек компилятора например, тогда у нас появляется архитектура, основанная на адаптерах.
Если упростить всё до одинакового интерфейса кодогенерирующих классов (как в примере — генерируются только заранее известные куски кода) — получаем реализацию с мостом.
Если ещё пряглядеться к задаче, то можно увидеть, что запрашиваемая вызывающим кодом задача четко соответствует интерфейсу классов — генераторов, так что и мост нам не нужен.
Ну и не стоит забывать, что создание объектов, в зависимости от ситуации, можно упаковать в IoC контейнер.
Поэтому первым вопросом, который надо задать при написании подобного кода будет вопрос о перспективах развития подсистемы, исходя из этого уже и выбираем паттерн реализации.
В описанном примере я лично считаю подход из топика достаточно уместным, а подход из ЖЖ cd-riper'a соответственно неоправданным монструозным усложнением системы.
1) Простая иерархия наследования с базовым классом, когда вызывающий код создаёт класс конкретной реализации (как в топике)
2) Подход с дженериками, который по сути является мостом
3) Подход с адаптерами
4) Подход с фабрикой (из топика cd-riper на ЖЖ)
И часто все эти подходы сваливаются в одну кучу, т.к. одну и ту же задачу можно решить каждым из них.
Попробуем разобраться с конца:
Подход с фабрикой нужен для наиболее сложной подсистемы, когда мы имеем независимые действия и классы.
Если более кратко — запрашивающий объект код знает только о необходимом действии, но не знает о требующемся ему классе.
Это позволяет инкапсулировать логику создания объекта в зависимости от параметров действия и настроек среды, используется когда такая логика нетривиальна.
Берем фабрику и накладываем условие связи каждого запрошенного действия с конкретным классом, получаем адаптер, теперь запрашивающий код уже знает конкретный тип адаптера и создаёт его.
Накладываем ещё одно ограничение, теперь код всех наших адаптеров перестаёт зависить от адаптируемой реализации: выносим весь этот код в 1 класс и получаем мост
Если накладываем ещё одно ограничение, когда классы интерфейса моста становятся тривиальными и просто перебрасывают вызовы — мы выкидываем этот класс и получаем простую иерархию наследования.
Если в заданном примере требуется создать генератор кода исходя из конфигурационного объекта, в котором содержится описание типа: «требователен к производительности, архитектура x86_64, операционная система: xxx», тогда получаем фабрику в чистом виде.
Если подобных требований нет, и у нас есть классы генераторов для разных языков, но при этом все эти классы имеют разный интерфейс, определяемый различным пакетом настроек компилятора например, тогда у нас появляется архитектура, основанная на адаптерах.
Если упростить всё до одинакового интерфейса кодогенерирующих классов (как в примере — генерируются только заранее известные куски кода) — получаем реализацию с мостом.
Если ещё пряглядеться к задаче, то можно увидеть, что запрашиваемая вызывающим кодом задача четко соответствует интерфейсу классов — генераторов, так что и мост нам не нужен.
Ну и не стоит забывать, что создание объектов, в зависимости от ситуации, можно упаковать в IoC контейнер.
Поэтому первым вопросом, который надо задать при написании подобного кода будет вопрос о перспективах развития подсистемы, исходя из этого уже и выбираем паттерн реализации.
В описанном примере я лично считаю подход из топика достаточно уместным, а подход из ЖЖ cd-riper'a соответственно неоправданным монструозным усложнением системы.
+1
а чем плох такой метод
// Lang.h
class Lang {
public:
virtual std::string generate(void) = 0;
bool isRegistred;
}
// LangCPP.h
class LangCPP: public Lang
{
public:
std::string generate(void){ if(isRegistred) return «i am cpp»; else return «exception»; }
}
//LangCPP.h
isRegistred = Generator::Instance().register(«cpp», LangCPP);
// LangJava.h
class LangJava: public Lang
{
public:
std::string generate(void){ if(isRegistred) return «i am java»; else return «exception»; }
}
//LangCPP.cpp
isRegistred = Generator::Instance().register(«java», LangJava);
// Generator.h
class Generator
{
typedef std::map<std::string,Lang> Langs;
Langs langs;
public:
static Generator& Instance();
bool register(char* name, Lang &lang)
{
std::string strName = std::string(name);
return langs.instert(Langs::value_type(strName, lang)).second;
}
Lang* generate(const char* type)
{
Langs::iterator i = langs.find(std::string(type));
if(i != langs.end())
{
Lang l = (*i).second;
return l();
}
return NULL;
}
}
// main.cpp
std::cout << Generator::Instance().generate(«cpp»).generate();
* This source code was highlighted with Source Code Highlighter.
// Lang.h
class Lang {
public:
virtual std::string generate(void) = 0;
bool isRegistred;
}
// LangCPP.h
class LangCPP: public Lang
{
public:
std::string generate(void){ if(isRegistred) return «i am cpp»; else return «exception»; }
}
//LangCPP.h
isRegistred = Generator::Instance().register(«cpp», LangCPP);
// LangJava.h
class LangJava: public Lang
{
public:
std::string generate(void){ if(isRegistred) return «i am java»; else return «exception»; }
}
//LangCPP.cpp
isRegistred = Generator::Instance().register(«java», LangJava);
// Generator.h
class Generator
{
typedef std::map<std::string,Lang> Langs;
Langs langs;
public:
static Generator& Instance();
bool register(char* name, Lang &lang)
{
std::string strName = std::string(name);
return langs.instert(Langs::value_type(strName, lang)).second;
}
Lang* generate(const char* type)
{
Langs::iterator i = langs.find(std::string(type));
if(i != langs.end())
{
Lang l = (*i).second;
return l();
}
return NULL;
}
}
// main.cpp
std::cout << Generator::Instance().generate(«cpp»).generate();
* This source code was highlighted with Source Code Highlighter.
+1
обычно когда вспоминают о без ифовом программировании, в качестве альтернативы указывают статическую типизацию на шаблонах… и последний вариант на нее отличненько ложится, так как все известно в компайл тайме…
0
Весь пост ждал реализации фабрики… безуспешно. =(
0
if не являются «вселенским злом», в конце концов, совсем без них программу не напишешь (если только не самую тривиальную). Но то, что они имеют тенденцию разрастаться как снежный ком и усложнять логику программы — это аксиома. Поэтому необходимо самым пристальным образом следить за ними. Самый простой способ (скажем так, первый шаг к победе) — это разбиение вложенных if, поскольку вложенные if уже начинают чуток попахивать вселенским злом.
0
Sign up to leave a comment.
Ветвления. Что с ними можно сделать