
И ещё: этот топик (как и прошлый) — не наезд ни на кого. Просто интересно порассуждать об интересных вопросах. Здесь нет подтекста, намёка, вызова. Параноиков и сторонников теорий заговоров попрошу расслабиться.
В этот раз хотел бы взглянуть на вопрос 4.
Вот его код. Немного причёсанный так, чтобы он собирался
#include <string>
#include <stdexcept>
#include <iostream>
class CodeGenerator
{
public:
enum Lang {JAVA, C_PLUS_PLUS, PHP};
CodeGenerator(Lang language) { _language=language; }
std::string generateCode()
{
switch(_language) {
case JAVA:
return std::string("Java: System.out.println(\"Hello world!\");");
case C_PLUS_PLUS:
return std::string("C++: std::cout << \"Hello world!\";");
}
throw new std::logic_error("Bad language");
}
std::string someCodeRelatedThing() // used in generateCode()
{
switch(_language) {
case JAVA:
return std::string("http://www.java.com/");
case C_PLUS_PLUS:
return std::string("http://www.cplusplus.com/doc/tutorial/");
}
throw new std::logic_error("Bad language");
}
private:
Lang _language;
};
int main() {
CodeGenerator cg(CodeGenerator::JAVA);
std::cout << cg.generateCode()
<< "\tMore info: "
<< cg.someCodeRelatedThing()
<< std::endl;
CodeGenerator ec(CodeGenerator::PHP);
try {
ec.generateCode();
} catch (std::logic_error * e) {
std::cout << "ERROR: " << e->what() << std::endl;
}
return 0;
}
Давайте наметим, от чего тут хочется избавиться:
- Хочется не сваливать все языки в одну кучу. Если их разнести, то их станет проще поддерживать, обозревать глазом, тестировать.
- Хочется избавиться от множественных switch-ей. Они загромождают код и за ними за всеми надо следить при добавлении новых языков. Это хлопотно. Кроме того, эти switch-и каждый раз выполняются и пользователь класса не может с этим ничего поделать.
- Хочется избавиться от бомбы замедленного действия в лице _language. Эта замечательная переменная устанавливается в одном месте, а используется (и вызывает исключения!) в другом (вернее в других). Это безусловно не облегчает отладку и поиск причин отказов.
- Хочется отделить интерфейс от реализации чтобы эти две вещи не сдерживали развитие друг друга.
- Хочется придти к такому дизайну, который будет поощрять программиста реализовывать раннюю диагностику ошибок, однократную «расшифровку» значения _language. В общем, действовать разумно. Существующее решение поощряет прямо противоположное поведение.
Разделяем и властвуем
Для начала, разнесём всё, что касается отдельных языков по отдельным классам. Ничего нового мы не сделали, но разрабатывать и тестировать стало чуть-чуть легче.
#include <string>
#include <stdexcept>
#include <iostream>
class CodeGeneratorJavaProcessor {
public:
std::string code() {
return std::string("Java: System.out.println(\"Hello world!\");");
}
std::string thing() {
return std::string("http://www.java.com/");
}
};
class CodeGeneratorCppProcessor {
public:
std::string code() {
return std::string("C++: std::cout << \"Hello world!\";");
}
std::string thing() {
return std::string("http://www.cplusplus.com/doc/tutorial/");
}
};
class CodeGenerator
{
public:
enum Lang {JAVA, C_PLUS_PLUS, PHP};
CodeGenerator(Lang language) { _language=language; }
std::string generateCode()
{
switch(_language) {
case JAVA:
return CodeGeneratorJavaProcessor().code();
case C_PLUS_PLUS:
return CodeGeneratorCppProcessor().code();
}
throw new std::logic_error("Bad language");
}
std::string someCodeRelatedThing()
{
switch(_language) {
case JAVA:
return CodeGeneratorJavaProcessor().thing();
case C_PLUS_PLUS:
return CodeGeneratorCppProcessor().thing();
}
throw new std::logic_error("Bad language");
}
private:
Lang _language;
};
int main() {
CodeGenerator cg(CodeGenerator::JAVA);
std::cout << cg.generateCode()
<< "\tMore info: "
<< cg.someCodeRelatedThing()
<< std::endl;
CodeGenerator ec(CodeGenerator::PHP);
try {
ec.generateCode();
} catch (std::logic_error * e) {
std::cout << "ERROR: " << e->what() << std::endl;
}
return 0;
}
Не будем долго задерживаться на обсуждении этого чисто механического улучшения.
Единый интерфейс и отказ о множественных switch-ей
Давайте создадим для всех языков единый интерфейс. Теперь мы можем хранить в CodeGenerator не код языка, а указатель на класс генератора этого языка. Преобразование кода языка в класс происходит один раз.
#include <string>
#include <stdexcept>
#include <iostream>
class CodeGeneratorAbstractProcessor {
public:
virtual std::string code() = 0;
virtual std::string thing() = 0;
virtual ~CodeGeneratorAbstractProcessor() {}
};
class CodeGeneratorJavaProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string code() {
return std::string("Java: System.out.println(\"Hello world!\");");
}
std::string thing() {
return std::string("http://www.java.com/");
}
};
class CodeGeneratorCppProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string code() {
return std::string("C++: std::cout << \"Hello world!\";");
}
std::string thing() {
return std::string("http://www.cplusplus.com/doc/tutorial/");
}
};
class CodeGeneratorBadProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string code() {
throw new std::logic_error("Bad language");
return std::string();
}
std::string thing() {
throw new std::logic_error("Bad language");
return std::string();
}
};
class CodeGenerator
{
public:
enum Lang {JAVA, C_PLUS_PLUS, PHP};
CodeGenerator(Lang language) : processor(0) {
switch(language) {
case JAVA:
processor = new CodeGeneratorJavaProcessor();
break;
case C_PLUS_PLUS:
processor = new CodeGeneratorCppProcessor();
break;
case PHP:
processor = new CodeGeneratorBadProcessor();
break;
}
}
~CodeGenerator() { delete processor; }
std::string generateCode() { return processor->code(); }
std::string someCodeRelatedThing() { return processor->thing(); }
private:
CodeGeneratorAbstractProcessor * processor;
};
int main() {
CodeGenerator cg(CodeGenerator::JAVA);
std::cout << cg.generateCode()
<< "\tMore info: "
<< cg.someCodeRelatedThing()
<< std::endl;
CodeGenerator ec(CodeGenerator::PHP);
try {
ec.generateCode();
} catch (std::logic_error * e) {
std::cout << "ERROR: " << e->what() << std::endl;
}
return 0;
}
Я так понимаю, что именно о таком дизайне говорилось в ответе на предыдущую мою заметку. Автор сомневался, что тут можно ещё хоть что-то улучшить. Что ж, давайте посмотрим.
А не поменять ли нам взгляд на вещи?
Давайте рассмотрим, что делает теперь CodeGenerator. Всего две вещи:
- Преобразует enum в объект в конструкторе
- Предоставляет обёртку над объектом, который и выполняет всю работу
void i_like_to_convert_enum_to_lang(CodeGenerator::Lang lang_code) {
CodeGenerator cg(lang_code);
std::cout << cg.generateCode()
<< "\tMore info: "
<< cg.someCodeRelatedThing()
<< std::endl;
}
То есть, предполагается, что мы по жизни тащим enum, и преобразуем его в алгоритм там, где нам вдруг это понадобится. Со всеми накладными расходами: создание/удаление объектов, проверка корректности нашего enum-a, обработка исключений… Может быть разумней было бы один раз преобразовать enum в объект и дальше жить с ним? Это решило бы множество проблем и дало бы множество преимуществ. Скажем, единый сквозной объект мог бы собирать статистку о своей работе, что-то кэшировать… Как вам такое решение:
void i_like_to_use_generators(CodeGeneratorAbstractProcessor * cg) {
std::cout << cg->code()
<< "\tMore info: "
<< cg->thing()
<< std::endl;
}
Здесь мы действительно отказались от CodeGenerator.
Конечно, такой решительный шаг не обошёлся без изменения интерфейса, но за-то сколько преимуществ мы получили!
Кстати (возвращаясь к обсуждению предыдущей заметки), обратите внимание, как только мы отказались от бомбы _language и похожей не её бомбы processor, операция создания и удаления генератора на столько приблизились друг к другу, что необходимость полиморфного удаления объектов пропала.
И сейчас самое время ввернуть лирическое отступление.
<Лирическое отступление>
Среди программистов, склонных ко всяким хаскелям, есть любопытные движения борцов против if и других ветвлений.
Проблема if-ов в том, что они множатся как зараза. Приведу пример, который можно очень часто встретить в реальной жизни. Допустим вы пишите программу с возможностью включения отладки.
Часто это делается так:
#include <iostream>
#include <unistd.h>
int main(int argc, char *argv[]) {
// анализируем параметры (первое ветвление)
bool debug(false);
int r;
while ((r = getopt(argc, argv, "d")) != -1) {
switch ( r ){
case 'd':
debug = true;
break;
}
}
// в основном теле каждый раз проверяем,
// не включена ли отладка
// (размножившиеся ветвления)
if (debug) {
std::cout << "Some debugging" << std::endl;
}
return 0;
}
Примеры такого кода можно найти в исходниках cdrtools, Python'a, git, cmake, m4 (переменная debug), firefox (debug_mode), mpalyer/mencoder (поля типа debug, b_debug в различных структурах), x11vnc (debug/crash_debug/debug_pointer)… посмотрите исходники вашей любимой программы и вы найдёте дополнительные иллюстрации.
Проблема в том, что первый if преобразует одно логическое значение в другое. А потом, в основном коде, это второе логическое значение каждый раз интерпретируется.
Это конечно не приводит к понижению производительности (даже наоборот! это очень хороший в плане производительности подход), но это загромождает код и делает его поддержку очень сложной. Во всей этой куче кода легко сделать ошибку. Эту кучу кода трудно менять.
Но, к счастью, есть и другое решение. Типа такого:
#include <iostream>
#include <unistd.h>
class noout : public std::ostream {};
noout& operator<< (noout& out, const char* s) { return out; }
int main(int argc, char *argv[]) {
// анализируем входные параметры и превращаем их
// сразу в поведение
noout e;
std::ostream *debug(&e);
int r;
while ((r = getopt(argc, argv, "d")) != -1) {
switch ( r ){
case 'd':
debug = &std::cout;
break;
}
}
// больше не надо ветвлений
*debug << "Some debugging" << std::endl;
return 0;
}
Здесь преобразование производится сразу в объект, определяющий, что делать с отладочными сообщениями.
В основном теле программы теперь ветвления не нужны.
Больше того, теперь очень легко развивать этот код. На пример, мы можем добавить новый поток для направления отладочной информации.
#include <iostream>
#include <unistd.h>
class noout : public std::ostream {};
noout& operator<< (noout& out, const char* s) { return out; }
int main(int argc, char *argv[]) {
// с лёгкостью добавляем новый
// поток для отправки отладочной информации
noout e;
std::ostream *debug(&e);
int r;
while ((r = getopt(argc, argv, "de")) != -1) {
switch ( r ){
case 'd':
debug = &std::cout;
break;
case 'e':
debug = &std::cerr;
break;
}
}
// в самой программе как не было необходимости
// в ветвлениях, так её и нет
*debug << "Some debugging" << std::endl;
return 0;
}
Конечно, эта реализация лишь иллюстрирует подход. Конечно я забыл добавить
noout& operator<< (noout& out, const signed char* s);
noout& operator<< (noout& out, const unsigned char* s);
...
и ещё много-много чего. Конечно, было бы правильней использовать полноценное средство для протоколирования. Это всё понятно. Но надеюсь, что саму идею я проиллюстрировал.
И, обратите внимание, и тут объекты живут полиморфной жизнью, но вот полиморфное удаление нам пока не понадобилось (это снова возвращаясь к предыдущей заметке).
</Лирическое отступление>
Отделим интерфейс от реализации
Давайте немного переделаем класс CodeGeneratorAbstractProcessor, заодно продемонстрировав концепцию NVI. Смотрите, как славно отделилась реализация от интерфейса:
#include <string>
#include <stdexcept>
#include <iostream>
class CodeGeneratorAbstractProcessor {
public:
std::string generateCode() { return code(); }
std::string someCodeRelatedThing() { return thing(); }
protected:
~CodeGeneratorAbstractProcessor() {}
private:
virtual std::string code() = 0;
virtual std::string thing() = 0;
};
class CodeGeneratorJavaProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string code() {
return std::string("Java: System.out.println(\"Hello world!\");");
}
std::string thing() {
return std::string("http://www.java.com/");
}
};
class CodeGeneratorCppProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string code() {
return std::string("C++: std::cout << \"Hello world!\";");
}
std::string thing() {
return std::string("http://www.cplusplus.com/doc/tutorial/");
}
};
class CodeGeneratorBadProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string code() {
throw new std::logic_error("Bad language");
return std::string();
}
std::string thing() {
throw new std::logic_error("Bad language");
return std::string();
}
};
void i_like_to_use_generators(CodeGeneratorAbstractProcessor * cg) {
std::cout << cg->generateCode()
<< "\tMore info: "
<< cg->someCodeRelatedThing()
<< std::endl;
}
int main() {
CodeGeneratorJavaProcessor java;
i_like_to_use_generators(&java);
return 0;
}
Открытые методы реализуют интерфейс. Закрытые и виртуальные методы служат для настройки поведения класса. Этими двумя вещами теперь можно легко и непринуждённо жонглировать. Делая и то и другое оптимальным. На пример так:
#include <string>
#include <stdexcept>
#include <iostream>
class CodeGeneratorAbstractProcessor {
public:
std::string generateCode() { return lang() + ": " + code(); }
std::string someCodeRelatedThing() { return thing(); }
protected:
~CodeGeneratorAbstractProcessor() {}
private:
virtual std::string lang() = 0;
virtual std::string code() = 0;
virtual std::string thing() = 0;
};
class CodeGeneratorJavaProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string lang() {
return std::string("Java");
}
std::string code() {
return std::string("System.out.println(\"Hello world!\");");
}
std::string thing() {
return std::string("http://www.java.com/");
}
};
class CodeGeneratorCppProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string lang() {
return std::string("C++");
}
std::string code() {
return std::string("std::cout << \"Hello world!\";");
}
std::string thing() {
return std::string("http://www.cplusplus.com/doc/tutorial/");
}
};
class CodeGeneratorBadProcessor : public CodeGeneratorAbstractProcessor {
public:
std::string lang() {
throw new std::logic_error("Bad language");
return std::string("???");
}
std::string code() {
throw new std::logic_error("Bad language");
return std::string();
}
std::string thing() {
throw new std::logic_error("Bad language");
return std::string();
}
};
void i_like_to_use_generators(CodeGeneratorAbstractProcessor * cg) {
std::cout << cg->generateCode()
<< "\tMore info: "
<< cg->someCodeRelatedThing()
<< std::endl;
}
int main() {
CodeGeneratorJavaProcessor java;
i_like_to_use_generators(&java);
return 0;
}
И интерфейс для пользователя и «интерфейс» для разработчика теперь можно сделать компактным не противоречивым, удобным, логичным. И сам класс теперь диктует всем определённую дисциплину и структуру.
Таким образом, все заявленные в начале статьи пожелания мы выполнили. Пора бы уже остановится. Снимаю шляпу перед всеми, кто дочитал до конца. Спасибо.
А на картинке изображён дольмен, который не имеет никакого отношения к статье. Просто именно им захотелось оформить статью на эту тему.