Мой прошлый пост вызвал огромный резонанс. Комментариев было не много, но я получил множество писем, а некоторые читатели выступили даже с открытыми заявлениями (там, правда, преобладают наезды на меня лично и на хабр в целом, но есть и мысли по существу вопроса). Поэтому я решил продолжить писать в жанре «мои мысли по поводу вопросов известной компании». Этим постом я постараюсь решить две задачи: (i) ответить на вопросы и возражения читателей предыдущего поста и (ii) толкнуть в некотором смысле философскую мысль о безIFовом программировании. Букв получилось довольно много, но те, кому интересно только что-то одно из поста, могут пропустить половину.
И ещё: этот топик (как и прошлый) — не наезд ни на кого. Просто интересно порассуждать об интересных вопросах. Здесь нет подтекста, намёка, вызова. Параноиков и сторонников теорий заговоров попрошу расслабиться.
В этот раз хотел бы взглянуть на вопрос 4.
Вот его код. Немного причёсанный так, чтобы он собирался
Давайте наметим, от чего тут хочется избавиться:
Для начала, разнесём всё, что касается отдельных языков по отдельным классам. Ничего нового мы не сделали, но разрабатывать и тестировать стало чуть-чуть легче.
Не будем долго задерживаться на обсуждении этого чисто механического улучшения.
Давайте создадим для всех языков единый интерфейс. Теперь мы можем хранить в CodeGenerator не код языка, а указатель на класс генератора этого языка. Преобразование кода языка в класс происходит один раз.
Я так понимаю, что именно о таком дизайне говорилось в ответе на предыдущую мою заметку. Автор сомневался, что тут можно ещё хоть что-то улучшить. Что ж, давайте посмотрим.
Давайте рассмотрим, что делает теперь CodeGenerator. Всего две вещи:
То есть, предполагается, что мы по жизни тащим enum, и преобразуем его в алгоритм там, где нам вдруг это понадобится. Со всеми накладными расходами: создание/удаление объектов, проверка корректности нашего enum-a, обработка исключений… Может быть разумней было бы один раз преобразовать enum в объект и дальше жить с ним? Это решило бы множество проблем и дало бы множество преимуществ. Скажем, единый сквозной объект мог бы собирать статистку о своей работе, что-то кэшировать… Как вам такое решение:
Здесь мы действительно отказались от CodeGenerator.
Конечно, такой решительный шаг не обошёлся без изменения интерфейса, но за-то сколько преимуществ мы получили!
Кстати (возвращаясь к обсуждению предыдущей заметки), обратите внимание, как только мы отказались от бомбы _language и похожей не её бомбы processor, операция создания и удаления генератора на столько приблизились друг к другу, что необходимость полиморфного удаления объектов пропала.
И сейчас самое время ввернуть лирическое отступление.
<Лирическое отступление>
Среди программистов, склонных ко всяким хаскелям, есть любопытные движения борцов против if и других ветвлений.
Проблема if-ов в том, что они множатся как зараза. Приведу пример, который можно очень часто встретить в реальной жизни. Допустим вы пишите программу с возможностью включения отладки.
Часто это делается так:
Примеры такого кода можно найти в исходниках cdrtools, Python'a, git, cmake, m4 (переменная debug), firefox (debug_mode), mpalyer/mencoder (поля типа debug, b_debug в различных структурах), x11vnc (debug/crash_debug/debug_pointer)… посмотрите исходники вашей любимой программы и вы найдёте дополнительные иллюстрации.
Проблема в том, что первый if преобразует одно логическое значение в другое. А потом, в основном коде, это второе логическое значение каждый раз интерпретируется.
Это конечно не приводит к понижению производительности (даже наоборот! это очень хороший в плане производительности подход), но это загромождает код и делает его поддержку очень сложной. Во всей этой куче кода легко сделать ошибку. Эту кучу кода трудно менять.
Но, к счастью, есть и другое решение. Типа такого:
Здесь преобразование производится сразу в объект, определяющий, что делать с отладочными сообщениями.
В основном теле программы теперь ветвления не нужны.
Больше того, теперь очень легко развивать этот код. На пример, мы можем добавить новый поток для направления отладочной информации.
Конечно, эта реализация лишь иллюстрирует подход. Конечно я забыл добавить
и ещё много-много чего. Конечно, было бы правильней использовать полноценное средство для протоколирования. Это всё понятно. Но надеюсь, что саму идею я проиллюстрировал.
И, обратите внимание, и тут объекты живут полиморфной жизнью, но вот полиморфное удаление нам пока не понадобилось (это снова возвращаясь к предыдущей заметке).
</Лирическое отступление>
Давайте немного переделаем класс CodeGeneratorAbstractProcessor, заодно продемонстрировав концепцию NVI. Смотрите, как славно отделилась реализация от интерфейса:
Открытые методы реализуют интерфейс. Закрытые и виртуальные методы служат для настройки поведения класса. Этими двумя вещами теперь можно легко и непринуждённо жонглировать. Делая и то и другое оптимальным. На пример так:
И интерфейс для пользователя и «интерфейс» для разработчика теперь можно сделать компактным не противоречивым, удобным, логичным. И сам класс теперь диктует всем определённую дисциплину и структуру.
Таким образом, все заявленные в начале статьи пожелания мы выполнили. Пора бы уже остановится. Снимаю шляпу перед всеми, кто дочитал до конца. Спасибо.
А на картинке изображён дольмен, который не имеет никакого отношения к статье. Просто именно им захотелось оформить статью на эту тему.
И ещё: этот топик (как и прошлый) — не наезд ни на кого. Просто интересно порассуждать об интересных вопросах. Здесь нет подтекста, намёка, вызова. Параноиков и сторонников теорий заговоров попрошу расслабиться.
В этот раз хотел бы взглянуть на вопрос 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;
}
И интерфейс для пользователя и «интерфейс» для разработчика теперь можно сделать компактным не противоречивым, удобным, логичным. И сам класс теперь диктует всем определённую дисциплину и структуру.
Таким образом, все заявленные в начале статьи пожелания мы выполнили. Пора бы уже остановится. Снимаю шляпу перед всеми, кто дочитал до конца. Спасибо.
А на картинке изображён дольмен, который не имеет никакого отношения к статье. Просто именно им захотелось оформить статью на эту тему.