Как писать код, который невозможно поддерживать
в рубрике Java, Другое, Колонка Спанч Боба, Методологии
Продолжение. Первая часть статьи Roedy Green “Unmaintainable code” опубликована здесь.
Маскировка
Чем дольше ошибка не проявляется, тем труднее ее обнаружить
А. Руди (A. Roedy)
Чтобы овладеть искусством правильно писать код, который невозможно поддерживать, вам нужно достичь определенного мастерства в маскировке. Вы должны научиться умело утаивать некоторые вещи и мастерски выдавать одно за другое. Большинство приемов искусной маскировки основывается на том, что человек воспринимает программный код совсем не так, как это делает компилятор. Существуют еще некоторые нюансы, которые относятся к просмотру кода в режиме текстового редактора. Сейчас мы расскажем об этих техниках обмана.
1. Пишите «оптимизированный» код
Следует повышать производительность лишь тогда, когда это дает ощутимый выигрыш, скажем, 97% по времени; корень зла скрывается в непродуманной оптимизации
Дональд Кнутт
При попытках повышения производительности совершается больше ошибок, чем по какой-либо другой причине, включая человеческую глупость (причем эти попытки не всегда заканчиваются успехом).
В.А. Вульф (W. A. Wulf)
Попытки оптимизации — железная отмазка, работает практически всегда. Ничто не поможет вам скрыть ваши истинные намерения — писать неподдерживаемый код — лучше, чем забота о повышении производительности. Ради этого высокого искусства вам отпустят все ваши грехи, даже когда станет понятно, что своей настоящей цели вы все-таки добились.
2. Приемы маскировки: код или комментарии?
Закомментированный код в некоторых местах маскируйте под незакомментированный. Вот так, например:
for ( j=0; j
total += array[j+0];
total += array[j+1];
total += array[j+2]; /* Main body of
total += array[j+3]; * loop is unrolled
total += array[j+4]; * for greater speed.
total += array[j+5]; */
total += array[j+6];
total += array[j+7];
}
Без функции подсветки кода вы потратите несколько секунд, чтобы разобраться, какие строки здесь закомментированы, а какие — нет.
3. Пространства имен
В языке программирования C struct/union и typedef struct/union принадлежат к разным пространствам имен (а в C++ — нет). Воспользуйтесь этим! Используйте одинаковые имена членов структур и (или) объединений в обоих пространствах имен. При этом постарайтесь добиться почти полного сходства между ними.
typedef struct {
char* pTr;
size_t lEn;
} snafu;
struct snafu {
unsigned cNt
char* pTr;
size_t lEn;
} A;
4. Прячьте определения макросов
Определения макросов легче всего замаскировать, спрятав их в куче бессмысленных комментариев: скорее всего, условный противник устанет их читать, и не заметит ловко замаскированный макрос. Еще лучше, если спрятанный макрос будет использован для замены какого-нибудь корректного с точки зрения языка выражения на что-нибудь одиозное:
#define a=b a=0-b
5. Притворитесь занятым
Часто используйте оператор define для объявления таких функций, которые на самом деле не выполняют ни одной полезной операции. Например, вы можете изящно закомментировать все переменные этой функции.
#define fastcopy(x,y,z) /*xyz*/
…
fastcopy(array1, array2, size); /* does nothing */
6. Правильно переносите строки!
При правильном подходе к делу, перенос строк — незаменимое средство, позволяющее вам достичь поставленной цели.
Вместо
#define local_var xy_z
пишите:
#define local_var xy\
_z // local_var OK
И используйте этот прием почаще! Он весьма эффективен: ведь глобальный поиск “xy_z” по всему тексту кода не выдаст ваших тайн условному противнику. Ну а препроцессор C “склеит” строки, разделенные символом “\”, интерпретируя их как одну строку.
7. Произвольно выбранные имена, замаскированные под зарезервированные слова
При документировании кода, если вам нужно использовать произвольное имя файла в примере, обязательно назовите его file. Просто и без излишеств. Вот не самый полный, но достаточно эффективный список слов, который можно использовать в документации кода в качестве произвольно выбранных имен: “bank”, “blank”, “class”, “const “, “constant”, “input”, “key”, “keyword”, “kind”, “output”, “parameter” “parm”, “system”, “type”, “value”, “var” и “variable “.
Вы должны постараться использовать как можно больше зарезервированных слов в названиях пользовательских переменных и других произвольных именах, когда будете документировать свой код. Чем больше, тем лучше. Ошибки компилятора при попытке пользователей выполнить описываемые действия только посмешат вас. И главное, вы не будете ни в чем виноваты: ведь вы искренне хотели доходчиво объяснить пользователю смысл того, что он делает. Поэтому и выбрали такие имена.
8. Имена в коде != имена в интерфейсе
Запомните: имена переменных в коде не должны иметь ничего общего с именами, которые отображаются на пользовательском интерфейсе и присутствуют в пользовательской документации. Например, если на пользовательском интерфейсе поле названо Postal Code, то его можно хранить в переменной с именем zip.
9. Отставить переименования!
Не используйте глобальное переименование для переменных, чтобы синхронизировать два фрагмента кода. Лучше переопределите их, много раз используя TYPEDEF.
10. Как надежно упрятать “запрещенные” глобальные переменные
Поскольку глобальные переменные есть зло, определите статическую структуру, которая будет содержать все переменные, которые вам понадобятся. Призовите на помощь воображение, когда будете подбирать имя для этой структуры. Скажем, назовите ее EverythingYoullEverNeed. Сделайте так, чтобы во всех функциях использовался указатель на эту структуру (для большего эффекта, назовите этот указатель столь же креативно — handle). Так вы создадите полную иллюзию отсутствия глобальных переменных: ведь вы получаете доступ ко всему через “указатель”.
11. Маскируйте экземпляры одного класса под синонимами
Чтобы проверить, не будет ли каких-либо нежелательных каскадных эффектов при изменениях, условный противник часто использует глобальный поиск по именам переменных. Одурачьте его! Прием прост: конечно же, это столь знакомые нам синонимы.
#define xxx global_var // in file std.h
#define xy_z xxx // in file ..\other\substd.h
#define local_var xy_z // in file ..\codestd\inst.h
Эти операторы #define должны находиться в разных заголовочных файлах. Еще лучше это сработает, если все заголовочные файлы будут находиться в разных директориях. Также к очень эффективным средствам относится использование имени уже существующей переменной в другом контексте. Компилятор, конечно, различит их; а вот человек, пользующийся поиском по имени переменной — не всегда. Право, очень действенный метод. К сожалению, SCID может свести на нет все ваши усилия уже в ближайшем десятилетии, поскольку его редактор кода распознает все эти хитрые штучки ничуть не хуже, чем компилятор.
12. Длинные имена переменных, неотличимые друг от друга
Используйте длинные, очень длинные имена переменных и классов, практически неотличимые друг от друга. Лучше всего, если эти имена будут различаться только одним символом. Еще лучше, если они будут различаться только регистром одного символа. Вот, например, блестящая идея для фрагментов таких имен: swimmer и swimner. Не забывайте, что при выборе некоторых шрифтов определенные символы выглядят практически одинаково, и надо прилагать усилия, чтобы различить их: i, l, I, 1 и | или o, O и 0. Умело используйте это, давая имена переменным и идентификаторам (parselnt и parseInt, D0Calc и DOCalc).
13. Одинаково выглядят и одинаково звучат
Мы уже определили одну переменную с именем xy_z. Так почему бы не определить еще несколько других, использовав удачные имена: xy_Z, xy__z, _xy_z, _xyz, XY_Z, xY_z, и Xy_z.
14. Используйте широкие возможности перегрузки функций
Постарайтесь выжать максимум из оператора #define в C++. Переопределите имена библиотечных функций. То-то будет умора, если условный противник вызовет какую-нибудь из этих функций, надеясь, что она работает так, как было задумано, а тут его будет ждать приятный сюрприз!
15. Перегрузка операторов: ошеломляйте!
Не ограничивайтесь библиотечными функциями. Перегрузка операторов расширяет ваши возможности. Придумайте, как должны работать +, -, /, *. Побольше фантазии! Не ограничивайтесь одной арифметикой, ведь эти операторы при правильном подходе будут выполнять совсем другие действия. Если Страуструп использует операторы сдвига (>> и <<) для потокового ввода-вывода, почему бы вам не пойти по его стопам? Определите "+", к примеру, так, чтобы результаты вычисления двух значений: i = i + 5; и i += 5; были совершенно разными. Определите оператор “!” для одного класса так, чтобы он не имел никакого отношения к логической инверсии и отрицанию. Сделайте так, чтобы он возвращал целочисленное значение. Тогда, чтобы получить его логическое значение, мы должны использовать “! !”. Но это инвертирует логику, и значит (звучит гонг) на самом деле нужно использовать “! ! !”. Не перепутайте оператор “!” (логическое НЕ), который возвращает булеву константу (0 или 1) с оператором ~ (побитовая инверсия). С ним, кстати, тоже можно что-нибудь сделать.
16. Еще раз о перегрузке
Перегрузите оператор “new”, это более эффектно, чем детские шалости с арифметическими операторами. Переопределив поведение этого оператора, вы внесете настоящий хаос и смятение в ряды условного противника. Представляете, что его ожидает, если он захочет создать динамический экземпляр объекта? Еще лучше, если вы продолжите эксперименты с одинаковыми именами и создадите функцию-член, назвав ее New.
17. #define
Директивы препроцессора заслуживают несколько отдельных благодарностей от тех, кто овладевает совершенством писать неподдерживаемый код. Это очень мощное средство. Используйте имена переменных в нижнем регистре, когда пишете выражение с #define — и они замаскируются под обычные переменные. Используйте глобальные #define везде, где только можно. А творчески подойдя к применению #ifdef, можно добиться того, что у вас будут использоваться разные версии функции в зависимости от того, в каком порядке и в каких количествах включены заголовочные файлы. Это особенно забавно, когда один заголовочный файл включен в другой. Попробуйте разобраться в следующем коде:
#ifndef DONE
#ifdef TWICE
// put stuff here to declare 3rd time around
void g(char* str);
#define DONE
#else // TWICE
#ifdef ONCE
// put stuff here to declare 2nd time around
void g(void* str);
#define TWICE
#else // ONCE
// put stuff here to declare 1st time around
void g(std::string str);
#define ONCE
#endif // ONCE
#endif // TWICE
#endif // DONE
Весь трюк в том, что когда в g() передается char*, то, какая версия g() будет вызвана, зависит от числа включений заголовочного файла.
18. Директивы компилятора
Еще одна удобная вещь, которая поможет изменить поведение кода. Например, можно поэкспериментировать с булевыми замыканиями и обработкой длинных строк.
19. Ложный след
Пускайте условного противника по ложному следу, включая в код переменные и методы, которые нигде не используются. Это очень легкий метод, даже делать специально ничего не нужно. Просто не удаляйте старый код, ставший ненужным. Условный противник быстро увязнет в нагромождениях старого “барахла”.
Комментариев: 2 на “Как писать код, который невозможно поддерживать”
Прокомментировать
Вы должны быть авторизованы для комментирования.
:
[...] Наталья пишет: Статьи. 25.07.10 … 4. Прячьте определения макросов. Определения макросов легче всего замаскировать, спрятав их в куче бессмысленных комментариев: скорее всего, условный противник устанет их читать, и не заметит ловко замаскированный макрос. Еще лучше, если спрятанный макрос будет … 9. Отставить переименования! Не используйте глобальное переименование для переменных, чтобы синхронизировать два фрагмента кода. Лучше переопределите их, много раз используя TYPEDEF. … [...]
25 июля, 2010 в 11:34
Сатаров Владимир:
Отличная статья. Но я бы дополнил ее нулевым пунктом. А звучал бы он так: “Напишите программу на любом языке”. В принципе, этого уже достаточно для получения неподдерживаемого кода.
Некоторое время назад в этом блоге я разместил пост “Об исторической роли суперпрограммирования”. Признаться, пост был провокационным и для того, чтобы он прошел в блог, мне пришлось сгладить все остроты и убрать всю критику. Но теперь, после его опубликования, можно продолжить его основную идею дальше. Основная проблема создания кода в том, что структурное, объектно-ориентированное программирование, а также еще множество подходов (за очень редким исключением), а также т.н. “лучшие практики” - все это шаманство, которое к науке имеет очень опосредованное отношение. Это означает, что пока не существует формального метода, который позволял бы определить, хорош наш код или нет, в том числе, с точки зрения поддержки. Это грустно, но это так. Основной гарантией хорошего кода является опыт и вдохновение программиста.
Мне кажется, что в некотором роде можно улучшить положение вещей, если использовать известные математические абстракции. В этом случае наперед совершенно точно известно, как должен работать код. А раз так, то уже не совсем важно, как обозначены переменные и расставлены комментарии. В крайнем случае, если уже совсем запутался, можно переписать код собственными силами.
30 июля, 2010 в 13:45