Советы по написанию хороших программ..
Содержание:
- Критерии хорошести программ
- Общие соображения
- Методы оптимизации программ
- Методы увеличения "читаемости" программ
- Методы нахождения и исправления ошибок
- Часто встречаемые ошибки
- Ассемблерный код
- Создание объектных моделей
- Проектирование баз данных
- Организация работы
Примечание: Данные правила собирались в течение достаточно долгого времени, и к настоящему моменту часть их может оказаться устаревшей или слишком специфической, а может быть и просто банальной.
Критерии хорошести программ
- Скорость работы или быстродействие.
- Размер кода.
- Читаемость исходников, комментированность.
- Возможность легко вносить изменения.
- Занимаемые ресурсы.
- Помехоустойчивость = Защита от дурака = Обработка ошибок.
- Удобство для пользователя.
- Доступный для дурака интерфейс.
- Актуальность.
- Переносимость и платформонезависимость (чем хороши Java и html).
- Возможность повторного использования кода.
Общие соображения
- Надо страться все делать как можно более универсальным.
- Очень важно часто делать резервные копии проекта.
- Нужно как можно чаще нажимать F2 (сохранять внесенные изменения).
- Зачем надо использовать навороченные структуры данных и процедуры, когда можно обойтись без этого? При продуманных структурах данных лучше видна идея алгоритма, а при плохой структуре эта идея теряется за мелочами. Т.е. если структуры данных отражают реальные объекты, и операции над ними сделаны как отдельные процедуры, то при реализации алгоритма ты просто вызываешь эти процедуры, и по тексту виден алгоритм. Если не делать структур данных и операций над ними, то можно реализовать абсолютно тот же алгоритм, но в тексте программы его не будет видно.
- Программу лучше писать маленькими кусочками: ставишь себе маленькую цель, например, продумать такую-то структуру данных, написать такую-то процедуру, собрать вместе такие-то куски, и достигаешь этой цели, причем в конце ты должен каждый раз быть уверен в том, что ты этой цели достиг, что все работает, работает так как надо, и не содержит ошибок. Цели лучше ставить так, чтобы ее можно было достичь за пару-тройку дней или за неделю, хотя лучше всего иметь развитую структуру целей; когда тобой руководит одна большая цель, ты разбиваешь ее на подцели и последовательно их достигаешь, потом подцели первого уровня подразделяешь на подцели второго уровня, и так далее, где самыми мелким разбиением будут элементарные операции, с которыми ты можешь работать свободно, например, написать оператор присваивания, или расписать цикл. (Человек обычно это делает автоматически, и задумываться над мелкими целями такого уровня не надо, но стоит осознать, что именно так все и делается. Еще это зависит от человека и его опыта: одним элементарной операцией будет целый класс со всеми методами, а для другого именно оператор присваивания или описание переменной.)
- Стоит читать книги о хороших стилях и подходах к программированию, например, Бентли "Жемчужины творчества программистов", Буч "Объектно-ориентированный анализ и проектирование".
- Надо грамотно выбирать: среду, компилятор, железо, модель памяти, язык программирования и т.д. Например, для 3D-графики оптимальным будет Watcom C++, 32-bit Flat Model, Fast PCI VideoCard (типа ET6000), ассемблерные вставки.
- Закон 80% - 20%
80% отладки приходится на 20% модулей
80% функциональности приходится на 20% модулей
80% ресурсов занимает 20% программы
80% работы выполняет 20% людей
80% пива выпивает 20% людей
Методы оптимизации программ
- Перейдите на более оптимизирующий компилятор (тот же Watcom).
- Обычно при оптимизации надо найти маленький кусочек, который жрет 90% ресурсов, и уж его заоптимизировать до смерти (см. закон 80% - 20%).
- Меньше мспользовать динамическое выделение памяти. Стандартные менеджеры памяти обычно медленны. В некоторых случаях даже имеет смысл написать свой менеджер.
- Полезно пройтись по программе Turbo Profiler'ом, найти критические места и соптимизировать их или переписать на ассемблер.
- Все сложные вычисления выносить из цикла и проводить один раз.
Например, вместоfor(i=0;i<100;i++) a[i]=i*320+500;
лучше написать:for(i=0,j=500;i<100;i++,j+=320) a[i] = j;
а еще лучше:for(b=a,i=500;i<32500;i+=320) *b++ = i;
- Если какую-то формулу, которую нужно вычислять часто, можно загнать в
табличку - стоит так и сделать.
Пример - синус и косинус. - Во многих компиляторах имеет смысл меньше использовать байтовые переменные, т.к. операции над ними все равно будут производиться в DWord-регистрах, и теряется время на преобразование байта в Word или DWord
if( a*d > c*b )
лучше (если нет проблем со знаком), чемif( a/b > c/d )
- так не теряется точность и нет деления на ноль.Inc( a, b )
илиa += b
работает быстрее, чемa = a+b;
- Стоит передавать функции меньше параметров - тогда Watcom сможет их всех передать через регистры.
- Поскольку деление осуществляется медленнее умножения, то для деления на
небольшие числа можно завести табличку чисел
1/X
, и вместо деления умножать на1/X
из таблицы. - Меньше использовать действительные числа, а больше целые.
Кстати, в паскале очень тормозит тип
Real
, т.к. он нестандартен для сопроцессора. Хотя на пентиуме типfloat
уже не медленнее. - При работе с графикой меньше использовать видеопамять - она медленная (хотя есть и акселерированные видеокарточки, но все равно все идет по шине). Если каждый раз перерисовывается немного, то можно кидать в видеопамять только "разницу" между экранами (особенно для Trident 9000 и других медленных карт).
- Меньше работать с XMS. (Это требует переключения в защищенный режим). При работе с блоками 640 байт она медленнее обычной в 2.5 раза, при блоках 40 байт - в 7 раз.
Методы увеличения "читаемости" программ
- Обязательно делать блочные отступы, т.е. все операторы внутри блока, цикла и т.д. сдвигать вправо, и все операторы внутри одного блока писать начиная с одной и той же позиции. Если один оператор занимает несколько строк - последующие строки тоже стоит сделать с отступом.
- Логические части функций можно разделать пустой строкой или логически
связанные операторы можно писать в одной строке.
Например,{ c=a; a=b; b=c; }
даже будет лучше понятен в одной строке. - Не стоит нагромождать одну функцию больше чем на лист, т.е. 60 строк.
- Хорошо продумывать типы данных - структуру классов (статику). Это треть программы.
- Продумывать разбиение алгоритма на функции - взаимодействие классов (динамику). Это еще треть программы. Оставшуюся треть - собственно код - при этом будет писать несложно.
- Писать комментарии. А то потом сам не сможешь разобраться в своей же программе через год-два.
- Использовать объекты и грамотно продумывать классы.
- Не использовать оператора
GoTo
. Он сильно ухудшает читаемость. - Разумно разбивать функции с данными на модули.
- Использовать очевидные имена функций и глобальных переменных. А для локальных лучше использовать короткие имена и комментарии.
- Определите правила для выбора названий и придерживайтесь их.
Например: имена классов пишутся с большой буквы, члены класса - с маленькой, а константы - большими буквами. Имена интерфейсрв в Java можно начинать с буквы 'I'.
Методы нахождения и исправления ошибок
- Программу писать постепенно: написать одну функцию, отладить, убедиться что все работает, потом еще чуть-чуть, а не писать сразу сотню строк и искать в ней ошибки.
- Двигаться постепенно от варианта, когда программа работает к варианту, когда она не работает, и смотреть, что изменяется.
- Правило: чем яснее алгоритм, тем труднее в нем ошибиться.
- Включить все предупреждения и посмотреть программу.
Так легко вылавливаются
ошибки типа
if(a=b)
вместоif(a==b).
- По всей программе пишутся
printf()
и смотрится на каком шаге глюки
Замечание: Главное - не ошибиться в самомprintf()
, а то будет еще хуже. - Посмотреть соответствие скобок (это есть во многих редакторах). Если компилятор ругается совсем непонятно на что, то возможно где-то выше не закрыта скобка.
- Использовать инварианты - условия, которые должны всегда выполняться. В отладочной версии можно понавставлять вызовов функции проверки инвариантов, который будет вываливать ошибку при нарушении инварианта.
- Писать тесты! Лучше потратить время на написание теста для каждого маленького кусочка, чем потом мучаться искать ошибки в большом проекте.
- Посмотрите следующий раздел - может быть ваша ошибка там уже описана.
Часто встречаемые ошибки
int a[10]; for(i=0;i<=10;i++) a[i];
или другие варианты выхода за пределыif(a=b)
- Не работать с нулевыми указателями!!!
unsigned a,b; if( a-b < 0 ) ;
f(x)/*W;
- то ли делить на ссылку, то ли комментарий- Watcom: при использовании
intr / in386x
обнулять сегментные регистры - Может глючить вариант
/*/
- Создаешь указатель, не выделяешь память и потом туда пишешь. Эффект потрясающий! Или, более общий случай - неинициализированная переменная (Java такие вещи отслеживает).
- Borland C плохо работает с указателями типа
huge
- Watcom: При чтении
fscanf("%d",&i)
из файла без проверки наfeof
и без пустой строки в конце, последнее значение считывается дважды. Лечится:fscanf("%d\n",&i)
- Перед делением стоит проверить а не на ноль ли мы делим.
- После целочисленного умножения (и сложения) значение не всегда влазит в переменную или регистр.
- Не надо забывать
abs
иfabs
, и не надо их путать. - Помнить про преобразование типов.
Еслиval
преобразовать кint
, то при сравненииif( val > eps )
всегда маленькоеval
будет нулем :-( - Если в языке нет автоматического преобразование к типу, то надо внимательно следить за типами.
- Пишешь в неоткрытый файл или читаешь из оного.
- Использование данных больше 64K в 16-битной программе.
int i; scanf( "%d", i );
const a = 0.001;
(по умолчанию тип int)for( unsigned i=100; i>=0; i-- );
- Многие новые компиляторы стали по умолчанию выравнивать структуры
данных на границу двойных слов, поэтому при описании внешних структур
данных, например формата заголовков графических файлов, структуры VESA,
и т.д. надо отменить это выравнивание при помощи
#pragma pack( 1 )
- Ассемблер: Перед делением регистра на регистр
забываешь обнулить
edx
.
Ассемблерный код
- Избегать медленных команд -
IDiv, Div, IMul, Mul, Jmp, Jcc
- В 32-битной модели стараться не использовать 16-битных регистров
- Стоит писать код так, чтобы Pentium мог выполнять команды парами
- Будьте проще! (и за вами потянутся массы):
Избегайте сложных команд типаMovzx, Loop, LodSD, ...
Dec ecx; Jnz ...
работает быстрее, чемLoop ...
Neg edx:eax = Neg edx; Neg eax; Sbb edx,0
- Проверка четырех условий с укладкой командой
SETcc reg8; SHL reg32,8;
иCMP reg32,1010101h; JZ метка;
работает ощутимо быстрее, чем 4Jcc
или:Cmp ;(SetB) Rcl eax,8;
илиShl eax,8 -> Ror eax,1
bx = ax + bx*320
для 8086 (320 = 1.0100.0000b):Xchg bh,bl; Add ax,bx; Shr bx,1; Shr bx,1; Add bx,ax
286:Add ah,bl; Shl bx,6; Add bx,ax
- Умножение
eax
на 5:Lea eax,[eax+eax*4]
- Деление
eax
на 3:Mov edx,0x55555555; IMul edx;
Shl eax,imm8
сильно быстрееShl eax,cl
. Лучше модифицировать код.
Создание объектных моделей
- Используйте абстракции. Имеет смысл требуемую функциональность описать в виде абстрактного класса (или интерфейса в Java) и в других классах работать только с ним, а реализацию (implementation) делать в унаследованных классах (в Java ее можно вынести в отдельный пакет).
- Используйте фабрики. Удобно для создания объектов использовать фабрики (это может быть глобальная функция, метод класса или специальный класс) тогда легко перейти с одной реализации на другую - достаточно поменять код фабрики.
- Для фабрики можно создать конфигурационный файл, и менять тип реализации вообще без перекомпиляции кода.
- Используйте образцы (patterns).
В них можно найти много стандартных отношений и взаимодействий объектов.
Например, www.oi.com/handbook - Создавайте собственные библиотеки классов. При проектировании объектной модели задумывайтесь, где еще можно будет использовать некоторые классы, и выделяйте их в библиотеку, что сэкономит в будущем немало времени.
- Используйте стереотипы. Кроме стандартных, для каждого проекта можно создавать специфические стереотипы, отражающие специфику предметной области.
- Используйте цвета на диаграммах. Можно сопоставить каждому стереотипу свой цвет, и диаграмма становится более выразительной.
- Скрывайте данные класса, делайте их доступными только через методы. Это может помочь, например, если вы убираете один атрибут, который может быть вычислен через другие. Или если вам стало необходимо сопровождать изменение атрибута определенными действиями.
- Если вам дорога память, а часть атрибутов не используется, или имеет значение "по умолчанию", то имеет смысл выделять под них память не в конструкторе, а в методе, изменяющем их значение, а в методе - селекторе возвращать значение "по умолчанию" если атрибут не инициализирован.
- Не загромождайте диаграммы. Известно, что человек может одновременно держать внимание на 7 +/- 2 объектах, поэтому большее количество классов, методов или атрибутов тяжело понять.
Проектирование баз данных
- Если в процессе работы делается выборка по нескольким полям, то стоит делать групповые индексы, а не несколько индексов.
Организация работы
- В процессе работы необходимо осозновать, что главной целью является создание программного продукта, а все остальное - второстепенно. Т.е. установкой и "опробованием" новых средств, методов, и т.д. заниматься стоит, но главным является сдать проект в срок.
- Работу стоит разделять на "итерации" длительностью порядка одной недели. И раз в неделю собираться всей командой разработчиков, обсуждать результаты и проблемы, решать продолжать работу и определять цели следующей итерации.