Моему сыну, как и многим мальчишкам, нравятся автомобили. Причём чем они больше и необычнее — тем больше нравятся. Когда мы идём по улице, а мимо проезжает эвакуатор или снегоуборочная машина, он неизменно дёргает меня за руку, указывает на заинтересовавший его объект и говорит: «Папа, б-р-р!». Говорит он так потому, что ему один год и вышеуказанные два слова составляют 40% его словарного запаса. Тем ни менее, в общем мысль понятна — обратить внимание на автомобиль. Давайте подумаем, каким образом ребёнок в возрасте 8-10 лет сказал бы своему сверстнику то же самое. Что-то вроде «Ух-ты, смотри какая крутая тачка!», да? Мысль та же, но обратите внимание — уже шесть слов вместо двух. И, наконец, представьте, каким образом то же самое скажет человек лет в тридцать: «Эй, смотри, да это же Ferrari California 2008-го года выпуска с двигателем V8 мощностью в 260 лошадиных сил и 7-ми скоростной коробкой-автоматом! Она до сотни разгоняется за 3.9 секунды!». Да, здесь уже больше деталей, но, если вы не автомеханик или фанат Ferrari — они вам скорее всего не нужны и не важны. Основная же мысль — всё та же, что и в «Ух-ты, смотри какая крутая тачка!» или «Папа, б-р-р!». Но выражена она уже в 30 слов.
Вы заметили, как абстракция «интересный автомобиль» обросла деталями и нюансами, стала занимать существенно больше места в тексте и времени на понимание, анализ и ответ? То же самое происходит и с программным кодом.
О чём вообще идёт речь
По моему мнению, основная характеристика хорошего программиста это не глубокие знания математики, не 100 лет опыта за плечами, не знания кучи языков и библиотек, не %куча_других_неважных_вещей%, а именно умение видеть абстракции. Не только видеть, конечно, а ещё и проектировать, использовать, исправлять и т.д. Но факт есть факт. Успех какого-нибудь популярного сегодня продукта (подставьте сюда свою любимую ОС, браузер, игру — да что угодно) определён именно тем, насколько хорошо спроектирована его архитектура, насколько хорошо высокоуровневые части отделены от низкоуровневых и друг от друга.
Посмотрите на «умершие» проекты. Очень редко они погибают от того, что программист не смог на 10% повысить скорость работы, или потому что не смогли прикрутить нужную библиотеку. Чаще всего причина закрытия формулируется в духе «существующая архитектура делает принципиально невозможным дальнейшее развитие». Вот она, ошибка в видении абстракций. Кто-то когда-то давно не увидел, что несколько сущностей на самом деле являются одной, или что одна может иметь несколько представлений, или что на самом деле не клиент должен дёргать сервер, а наоборот, или что в протокол неплохо бы заложить возможность расширения — и вот он, грянувший спустя годы гром последствий.
Паттерны
В современном мире программирования есть такая штука как «паттерны». Ну, знаете, книга банды четырёх, всякие там фабрикисинглтоныобёрткинаблюдателифасадымосты. Отношение программистов к паттернам неоднозначно. Есть лагерь любителей паттернов, которые справедливо утверждают, что это всё — квинтэссенция десятилетий лучшего программерского опыта, проверенные вещи и надо не тормозить, а использовать наработки по полной. И есть лагерь противников паттернов, которые пеняют им на излишнюю сложность (3-5 классов на реализацию одной идеи — вполне типично для средненького паттерна), говорят, что изучение паттернов подобно школьной зубрёжке — когда просто учишь что-то без понимания причин, следствий и вариантов именно целевого использования.
Мне кажется, что дело тут опять-таки в связи паттернов с абстракциями. Некоторые паттерны целостны и однозначно описывают какую-то одну концепцию. Её можно понять, увидеть, реализовать, инкапсулировать в себе. Не важно, один там будет класс, пять или десять — если они формируют некую сущность, которая не зависит от внешнего окружения, которая может быть помещена в отдельный модуль и дальше использована через простой интерфейс — это хороший паттерн.
Другие паттерны являются откровенным мусором. Просто потому, что вот эти два класса находятся в третьем, унаследованы от четвёртого и вызывают методы пятого — они не создают абстракцию. Возможно, они каким-то образом ускоряют код или обходят какое-то ограничение языка, но вот целостной идеи не формируют. Это трудно понять, невозможно запомнить и это вызывает праведный гнев в рациональном
Инструменты
К сожалению, современные средства разработки не дают хороших автоматических средств для того, чтобы видеть абстракции в проекте. Да, вы можете увидеть интерфейсы или абстрактные классы в коде — но не факт, что это то, что составляет настоящую абстракцию. Какой-то определённый слой логики может содержать в себе десятки интерфейсов, а другой — иметь всего один класс (и тот — просто пустая обёртка вокруг чего-то другого). Мы можем с помощью IDE увидеть классы, методы и переменные — но мы не видим реального разделения проекта на слои. Всё остаётся на совести программиста. К счастью, у нас сегодня есть возможность выносить код в отдельные модули, у нас есть пространства имён, интерфейсы, дельные советы по рефакторингу и инструменты для его осуществления. Написать хорошо разделённый на отдельные модули код — возможно. И это важнее, чем написать быстрый код. Конечно, «модульный код» не на 100% равно «идеальный код», но очень и очень к этому близко.
Пример плохого кода
Несколько лет назад был пик популярности текстового редактора Notepad++. Десятки миллионов загрузок, приятный минималистичный интерфейс, плагины. Начало было очень хорошее, ничто не предвещало беды. За последние пару лет данный текстовый редактор сдулся и фактически застопорился в своём развитии. Вот график его загрузок.
В чём же причины? Я не берусь называть их все, но вот одна. Давайте посмотрим на один файл из его исходников.
NppBigSwitch.cpp
Внимательнее разберём некоторые части этого кода.
Macro m = _macro;
Имя переменной — это не имя регистра процессора. Главная задача имени переменной не адресовать ячейку в памяти, а объяснить, что за данные в ней находятся. Просто для адресации можно было бы один раз в начале выделить массив в памяти и бегать по нему указателями. Именно имя переменной является той абстракцией, которая упрощает понимание кода. Здесь, как видим, не упрощает.
/*
case NPPM_ADDREBAR :
{
if (!lParam)
return FALSE;
_rebarTop.addBand((REBARBANDINFO*)lParam, false);
return TRUE;
}
case NPPM_UPDATEREBAR :
{
if (!lParam || wParam < REBAR_BAR_EXTERNAL)
return FALSE;
_rebarTop.reNew((int)wParam, (REBARBANDINFO*)lParam);
return TRUE;
}
case NPPM_REMOVEREBAR :
{
if (wParam < REBAR_BAR_EXTERNAL)
return FALSE;
_rebarTop.removeBand((int)wParam);
return TRUE;
}
*/
Старому ненужному коду нужно быть удалённым с комментарием в отдельном коммите о том почему он был удалён. Если это ещё не дописанная функциональность — её место в отдельной ветке. Просто так держать нагромождения закомментированого кода в файлах проекта — значит не понимать идеи систем контроля версий.
LRESULT Notepad_plus::process(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam)
{
...
// тут 1800 строк кода
...
}
Ничего так размерчик для одной функции, да? Автор, наверное, пытался сэкономить целый десяток наносекунд на вызов каждого отдельного кусочка кода через отдельную функцию. Молодец, сэкономил. И получил код, в котором противно копаться и нужно тратить кучу времени на понимание и исправление.
nmdlg->Items[i] = 0xFFFFFFFF; // indicate file was closed
...
if ((lParam == 1) || (lParam == 2))
...
long view = MAIN_VIEW;
view <<= 30;
return view|i;
Что такое 0xFFFFFFFF, 1, 2 и 30? Ах да, 0xFFFFFFFF — означает что файл был закрыт. Какой чудесный комментарий, как всё наглядно! Было, видимо, очень быстро и удобно просто набросать цифры в код — компилируется ведь. Нет времени объяснять, поехали дальше, да?
case COPYDATA_FILENAMESA :
{
char *fileNamesA = (char *)pCopyData->lpData;
CmdLineParams & cmdLineParams = pNppParam->getCmdLineParams();
#ifdef UNICODE
WcharMbcsConvertor *wmc = WcharMbcsConvertor::getInstance();
const wchar_t *fileNamesW = wmc->char2wchar(fileNamesA, CP_ACP);
loadCommandlineParams(fileNamesW, &cmdLineParams);
#else
loadCommandlineParams(fileNamesA, &cmdLineParams);
#endif
break;
}
case COPYDATA_FILENAMESW :
{
wchar_t *fileNamesW = (wchar_t *)pCopyData->lpData;
CmdLineParams & cmdLineParams = pNppParam->getCmdLineParams();
#ifdef UNICODE
loadCommandlineParams(fileNamesW, &cmdLineParams);
#else
WcharMbcsConvertor *wmc = WcharMbcsConvertor::getInstance();
const char *fileNamesA = wmc->wchar2char(fileNamesW, CP_ACP);
loadCommandlineParams(fileNamesA, &cmdLineParams);
#endif
break;
}
}
return TRUE;
}
В каждом месте, где имеет значение в юникоде скомпилирована программа или нет, мы видим конструкции, нагружающие
::MoveWindow(_rebarTop.getHSelf(), 0, 0, rc.right, _rebarTop.getHeight(), TRUE);
...
::SendMessage(_statusBar.getHSelf(), WM_SIZE, wParam, lParam);
Каждый раз, когда нам нужно передвинуть окно, спрятать его или выполнить какое-либо иное действие с элементом интерфейса — дёргаются напрямую функции Win32 API. Никаких библиотек интерфейса, никаких обёрток, классов — ничего. В итоге — куча лишнего и дублирующегося кода, абсолютная непортируемость на другие ОС, все недостатки Win32 API — прямо внутри нашего кода. Причём авторы выставляют такой подход как преимущество продукта, дескать, никаких лишних компонентов! Это просто ужас. Сотые доли процента прироста производительности (в лучшем случае) — и ад в исходниках.
Как результат всего вышеперечисленного, разработка Notepad++ является катастрофически сложным и медленным делом. Мой патч, исправляющий пару важных для меня багов висит в списке «на рассмотрении» уже полгода, вместе с почти 200 другими патчами. Автор, конечно, время от времени что-то из них принимает, но вы сами понимаете — делать это быстро с такой кодовой базой абсолютно невозможно. Мне очень жаль показавшийся мне когда-то хорошим редактор, но вы видите сами — смерть неизбежна.
Пример хорошего кода
Возможно, вам известна такая популярная библиотека как Qt — о ней в последнее время много пишут на Хабре. Я, опять-таки, не берусь утверждать, что знаю лучше всех все причины её успеха, но вот вам одна из них. Вся библиотека построена на прекрасном ядре абстракций: тут есть абстракции от платформы, от сети, от элементов интерфейса ОС, от кодировок, да практически от чего угодно. Взглянув на любой компонент Qt нам не нужно лезть сильно глубоко вниз или вверх, чтобы понять как он работает. Причём всё это не благодаря хорошей документации, а из-за кода самой библиотеки.
Давайте посмотрим на один из заголовочных файлов библиотеки Qt.
#ifndef QPDFWRITER_H
#define QPDFWRITER_H
#include <QtCore/qobject.h>
#include <QtGui/qpagedpaintdevice.h>
QT_BEGIN_NAMESPACE
class QIODevice;
class QPdfWriterPrivate;
class Q_GUI_EXPORT QPdfWriter : public QObject, public QPagedPaintDevice
{
Q_OBJECT
public:
explicit QPdfWriter(const QString &filename);
explicit QPdfWriter(QIODevice *device);
~QPdfWriter();
QString title() const;
void setTitle(const QString &title);
QString creator() const;
void setCreator(const QString &creator);
bool newPage();
void setPageSize(PageSize size);
void setPageSizeMM(const QSizeF &size);
void setMargins(const Margins &m);
protected:
QPaintEngine *paintEngine() const;
int metric(PaintDeviceMetric id) const;
private:
Q_DISABLE_COPY(QPdfWriter)
Q_DECLARE_PRIVATE(QPdfWriter)
};
QT_END_NAMESPACE
#endif
Не пугайтесь возможно незнакомых вам макросов — не о них речь. Я хочу обратить ваше внимание на другую вещь. Заметьте, в этом классе нет приватных свойств. И почти ни в одном другом классе Qt — тоже нет. Вернее, на самом деле в каждом из них есть ровно по одной приватной переменной — это неявно объявленный через макрос Q_DECLARE_PRIVATE указатель на подкласс, в котором уже и находятся все свойства и часть приватных методов. Сделано это по официальному объяснению для обеспечения бинарной совместимости — получается что данный класс в любой версии Qt имеет один и тот же размер (поскольку место для хранения одного указателя константно в пределах платформы) и его можно спокойно передавать туда-сюда между модулями, не боясь каких-нибудь там segmentation fault.
На самом деле для меня вся прелесть этого решения в другом. Смотрите — мы открываем заголовочный файл и что же мы видим? Только публичные методы. Вы впервые видите этот заголовочный файл (да и вообще библиотеку Qt, может быть) — но вы ведь уже поняли, что это за класс и как его использовать, правда? Все внутренности изящно скрыты в приватном подклассе. К сожалению, классический С++ заставляет программиста смешивать в заголовочном файле и то, что нужно внешнему пользователю класса и его внутренности. Но в Qt при чтении заголовочного файла — мы видим чёткое послание от разработчиков Qt: «Внутренности этого класса тебе не нужны. Не твоё дело что там и как, это абстракция — используй её через публичные методы.». И это прекрасно, чёрт возьми! Я хочу видеть все библиотеки в подобном стиле. Покажите мне нужные мне вещи сразу и спрячьте ненужные так, чтобы их нужно было искать долго и трудно. За этим подходом будущее, я уверен.
Выводы
Большинство хороших советов для программистов типа «Используйте рефакторинг», «Применяйте хорошие паттерны», «Преждевременная оптимизация — зло», «Не пишите больших функций», «Не заводите глобальных переменных» на самом деле являются выводами из более общего совета «Умейте видеть абстракции».
Приятного программирования.
Автор: tangro