3.5. Неправильное программирование: историческое, восходящее, грязное
«Марксизм – не догма,
а руководство к действию».
В.И.Ленин
Технология структурного программирования является наиболее правильной с точки зрения управляемости, безошибочности и качества получаемого программного кода. Другие подходы, не будучи столь идеальными, тем не менее имеют право на существование хотя бы в качестве дополнений к основному методу. Назовем их «неправильным программированием». Открывает этот перечень «историческое программирование», подробно рассмотренное в 3.2. Исторический подход исходит не из логической структуры алгоритма, а из его развертки во времени: программа пишется в той же последовательности, что и выполняется (блок-схема).
«Грязное» программирование
Под «грязным» программированием обычно понимается написание программы, грубо воспроизводящей требуемое поведение. Такая программа может быть быстро разработана и отлажена, а затем использована для уяснения последующих шагов, либо для наложения «заплаток» с целью получения требуемого результата. Хотя этот «не есть хорошо» с точки зрения технологии проектирования, но может быть оправдано при следующих условиях:
«грязная» программа воспроизводит требуемое поведение на самом верхнем уровне.
в дальнейшем в нее могут встраиваться контексты и фрагменты, не меняющие ее поведения, но конкретизирующие ее в нужном направлении.
Основным отличием правильно разрабатываемой «грязной» программы от программы, разрабатываемой «как попало» является соблюдение соотношений, которые она устанавливает в процессе ее работы. Эти соотношения необходимо сохранять при включении в программу новых фрагментов, они являются ее инвариантами (см. 3.7, 7.1).
Пример «грязного» программирования. Обработка строки. Функция заменяет в строке последовательность одинаковых символов на константу - счетчик и один такой символ (например, qwertyaaaaaaaaaaaatybbbbbbbbgg – qwerty12aty8bgg).
«Грязная» программа моделирует основные свойства процесса обработки строки: за один шаг цикла просматривается один неповторяющийся символ или цепочка повторяющихся. Цикл просмотра цепочки является «заглушкой», заменяющей еще не написанный фрагмент ее свертывания. Инвариант программы – переменная цикла на каждом шаге должна устанавливаться на начало следующего фрагмента.
//------------------------------------------------------35-01.cpp
//---------" Грязная" программа просмотр повторяющихся цепочек
void proc(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++;
}
else putchar(c[i]);
}}
Достоинство такой программы – она может быть проверена и отлажена, хотя и бесполезна. Следующий шаг – замена заглушки на требуемый фрагмент. Он включает в себя последовательность действий:
определение длины последовательности
k
и установку индекса на ее последний символj
;запись двух цифр счетчика в начало последовательности в символьном виде;
сохранение в строке одного символа из повторяющихся;
сдвиг «хвоста»;
установка индекса
i
на последний символ полученного фрагмента с целью сохранения инварианта внешнего цикла.
//------------------------------------------------------35-02.cpp
//----------- Свертка цепочек повторяющихся символов
void proc(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
// Конец фрагмента - j
for (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) и т.д., пока не останется один элемент. Если сразу схватиться за видимые контексты выбора минимального и обмена, то для начала (для первого шага) нужно написать так:
//------------------------------------------------------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
.
//------------------------------------------------------35-03.cpp
//----------- Сортировка выбором – восходящее программирование
// Шаг 2.
for (i=0;i<n-1;i++){
int j,k;
for (k=i, j=i+1;j<n;j++)
if (A[j]<A[k]) k=j;
int c=A[i]; A[i]=A[k]; A[k]=c;
}
Здесь мы видим недостаток восходящего программирования. При включении уже написанного фрагмента в обрамляющую его конструкцию нам приходится превращать «частный» случай, для которого он написан, в «общий», в котором он получает дополнительные параметры, «смысл» которых задает внешняя конструкция.
Модульность и восходящее программирование. Разделение программы на модули позволяет преодолеть основное противоречие структурного программирования: процесс детализации программы состоит в движении от общего к частному, но в то же время наиболее очевидными являются, наоборот, фрагменты нижнего уровня. При большом количестве уровней проектирование сверху-вниз становится в тупик: слишком много фактов, причем внешние из них плохо просматриваются. В этом случае можно выделить некоторый промежуточный уровень, оформив нижележащие фрагменты в виде функции, а в вышележащий поместить ее вызов.
Можно проектировать «снизу-вверх» программный комплекс целиком, наращивая слой за слоем набор функций, в которой каждая вышележащая использует уже разработанные. Общие принципы структурного проектирования каждой отдельной функции сохраняются.