Технология структурного программирования является наиболее правильной с точки зрения управляемости, безошибочности и качества получаемого программного кода. Другие подходы, не будучи столь идеальными, тем не менее имеют право на существование хотя бы в качестве дополнений к основному методу. Назовем их «неправильным программированием». Открывает этот перечень «историческое программирование», подробно рассмотренное в 3.2. Исторический подход исходит не из логической структуры алгоритма, а из его развертки во времени: программа пишется в той же последовательности, что и выполняется (блок-схема).
«Грязное» программирование #
Под «грязным» программированием обычно понимается написание программы, грубо воспроизводящей требуемое поведение. Такая программа может быть быстро разработана и отлажена, а затем использована для уяснения последующих шагов, либо для наложения «заплаток» с целью получения требуемого результата. Хотя этот «не есть хорошо» с точки зрения технологии проектирования, но может быть оправдано при следующих условиях:
«грязная» программа воспроизводит требуемое поведение на самом верхнем уровне.
в дальнейшем в нее могут встраиваться контексты и фрагменты, не меняющие ее поведения, но конкретизирующие ее в нужном направлении.
Основным отличием правильно разрабатываемой «грязной» программы от программы, разрабатываемой «как попало» является соблюдение соотношений, которые она устанавливает в процессе ее работы. Эти соотношения необходимо сохранять при включении в программу новых фрагментов, они являются ее инвариантами (см. 3.7, 7.1).
Пример «грязного» программирования. Обработка строки. Функция заменяет в строке последовательность одинаковых символов на константу - счетчик и один такой символ (например, qwertyaaaaaaaaaaaatybbbbbbbbgg – qwerty12aty8bgg).
«Грязная» программа моделирует основные свойства процесса обработки строки: за один шаг цикла просматривается один неповторяющийся символ или цепочка повторяющихся. Цикл просмотра цепочки является «заглушкой», заменяющей еще не написанный фрагмент ее свертывания. Инвариант программы – переменная цикла на каждом шаге должна устанавливаться на начало следующего фрагмента.
C
//------------------------------------------------------35-01.cpp//---------" Грязная" программа просмотр повторяющихся цепочекvoidproc(char c[]){for(int i=0; c[i]!=0; i++)// 1 шаг - 1 символ или 1 цепочка{if(c[i]!=' '&& c[i]==c[i+1]){// заглушкаputchar('*');while(c[i]==c[i+1]) i++;}elseputchar(c[i]);}}
Достоинство такой программы – она может быть проверена и отлажена, хотя и бесполезна. Следующий шаг – замена заглушки на требуемый фрагмент. Он включает в себя последовательность действий:
определение длины последовательности k и установку индекса на ее последний символ j;
запись двух цифр счетчика в начало последовательности в символьном виде;
сохранение в строке одного символа из повторяющихся;
сдвиг «хвоста»;
установка индекса i на последний символ полученного фрагмента с целью сохранения инварианта внешнего цикла.
C
//------------------------------------------------------35-02.cpp//----------- Свертка цепочек повторяющихся символовvoidproc(char c[]){for(int i=0; c[i]!=0; i++)// 1 шаг - 1 символ ???{if(c[i]!=' '&& c[i]==c[i+1]){// старая заглушка// putchar('*' ); while (c[i]==c[i+1]) i++;// 1 - длина k// Начало нужно - не трогаем i// Конец фрагмента - jfor(int j=i,k=1; c[j]==c[j+1]; k++,j++);// j - на последний из 'aaaaa'// 2 - k - записать в c[] в виде 2 цифр// i - сдвинуть так, чтобы// он оказался там, где надоif(k>=10) c[i++]=k/10+'0';
c[i++]=k%10+'0';// 3 - оставить 1 символ - уже стоим там !!!!// 4 - сдвинуть хвост -// перенос с использованием 2 индексовint i1;for(j++, i1=i+1; c[j]!=0; j++, i1++) c[i1]=c[j];
c[i1]=0;// 5 на конец полученного фрагмента -// уже стоим там !!!!// свойство - i - на оставленном символе}}}// i++ => на следующий фрагмент
Восходящее программирование #
Восходящее проектирование от внутренней конструкции к внешней имеет под собой объективную основу: частности всегда «на виду» и за них в первую очередь хватается начинающий. Поэтому можно попытаться выстроить программу «изнутри», в этом случае каждый следующих шаг проектирования проще увидеть.
В качестве примера рассмотрим сортировку выбором. Из всего массива выбирается минимальный элемент и меняется местами с первым, затем процесс повторяется с оставшимися элементами (1..n-1) и т.д., пока не останется один элемент. Если сразу схватиться за видимые контексты выбора минимального и обмена, то для начала (для первого шага) нужно написать так:
C
//------------------------------------------------------35-03.cpp//----------- Сортировка выбором – восходящее программирование. Шаг 1.int j,k;for(k=0,j=1;j<n;j++)// Поиск минимального - сохранение индексаif(A[j]<A[k]) k=j;int c=A[0]; A[0]=A[k]; A[k]=c;// Обмен первого A[0] и минимального
Справедливости ради стоит заметить, что такое программирование является одновременно и историческим – текст программы соответствует ее развертке во времени. Поскольку записанный фрагмент многократно повторяется, то он становится телом цикла. В заголовке цикла появляется переменная i, ее смысловое наполнение выглядит как «начало неотсортированной части». Соответственно, изменения должны быть внесены в уже написанный цикл, начальные значения переменных j=i+1 и k=i.
Здесь мы видим недостаток восходящего программирования. При включении уже написанного фрагмента в обрамляющую его конструкцию нам приходится превращать «частный» случай, для которого он написан, в «общий», в котором он получает дополнительные параметры, «смысл» которых задает внешняя конструкция.
Модульность и восходящее программирование. Разделение программы на модули позволяет преодолеть основное противоречие структурного программирования: процесс детализации программы состоит в движении от общего к частному, но в то же время наиболее очевидными являются, наоборот, фрагменты нижнего уровня. При большом количестве уровней проектирование сверху-вниз становится в тупик: слишком много фактов, причем внешние из них плохо просматриваются. В этом случае можно выделить некоторый промежуточный уровень, оформив нижележащие фрагменты в виде функции, а в вышележащий поместить ее вызов.
Можно проектировать «снизу-вверх» программный комплекс целиком, наращивая слой за слоем набор функций, в которой каждая вышележащая использует уже разработанные. Общие принципы структурного проектирования каждой отдельной функции сохраняются.