3.5. Неправильное программирование: историческое, восходящее, грязное

    «Марксизм – не догма,

    а руководство к действию».

    В.И.Ленин

    Технология структурного программирования является наиболее правильной с точки зрения управляемости, безошибочности и качества получаемого программного кода. Другие подходы, не будучи столь идеальными, тем не менее имеют право на существование хотя бы в качестве дополнений к основному методу. Назовем их «неправильным программированием». Открывает этот перечень «историческое программирование», подробно рассмотренное в 3.2. Исторический подход исходит не из логической структуры алгоритма, а из его развертки во времени: программа пишется в той же последовательности, что и выполняется (блок-схема).

    «Грязное» программирование

    Под «грязным» программированием обычно понимается написание программы, грубо воспроизводящей требуемое поведение. Такая программа может быть быстро разработана и отлажена, а затем использована для уяснения последующих шагов, либо для наложения «заплаток» с целью получения требуемого результата. Хотя этот «не есть хорошо» с точки зрения технологии проектирования, но может быть оправдано при следующих условиях:

    • «грязная» программа воспроизводит требуемое поведение на самом верхнем уровне.

    • в дальнейшем в нее могут встраиваться контексты и фрагменты, не меняющие ее поведения, но конкретизирующие ее в нужном направлении.

    Основным отличием правильно разрабатываемой «грязной» программы от программы, разрабатываемой «как попало» является соблюдение соотношений, которые она устанавливает в процессе ее работы. Эти соотношения необходимо сохранять при включении в программу новых фрагментов, они являются ее инвариантами (см. 3.7, 7.1).

    Пример «грязного» программирования. Обработка строки. Функция заменяет в строке последовательность одинаковых символов на константу - счетчик и один такой символ (например, qwertyaaaaaaaaaaaatybbbbbbbbgg – qwerty12aty8bgg).

    «Грязная» программа моделирует основные свойства процесса обработки строки: за один шаг цикла просматривается один неповторяющийся символ или цепочка повторяющихся. Цикл просмотра цепочки является «заглушкой», заменяющей еще не написанный фрагмент ее свертывания. Инвариант программы – переменная цикла на каждом шаге должна устанавливаться на начало следующего фрагмента.

    C
    //------------------------------------------------------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]);
    
                  }}

    Достоинство такой программы – она может быть проверена и отлажена, хотя и бесполезна. Следующий шаг – замена заглушки на требуемый фрагмент. Он включает в себя последовательность действий:

    1. определение длины последовательности k и установку индекса на ее последний символ j;

    2. запись двух цифр счетчика в начало последовательности в символьном виде;

    3. сохранение в строке одного символа из повторяющихся;

    4. сдвиг «хвоста»;

    5. установка индекса i на последний символ полученного фрагмента с целью сохранения инварианта внешнего цикла.

    C
    //------------------------------------------------------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) и т.д., пока не останется один элемент. Если сразу схватиться за видимые контексты выбора минимального и обмена, то для начала (для первого шага) нужно написать так:

    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.

    C
    //------------------------------------------------------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;
    
                }

    Здесь мы видим недостаток восходящего программирования. При включении уже написанного фрагмента в обрамляющую его конструкцию нам приходится превращать «частный» случай, для которого он написан, в «общий», в котором он получает дополнительные параметры, «смысл» которых задает внешняя конструкция.

    Модульность и восходящее программирование. Разделение программы на модули позволяет преодолеть основное противоречие структурного программирования: процесс детализации программы состоит в движении от общего к частному, но в то же время наиболее очевидными являются, наоборот, фрагменты нижнего уровня. При большом количестве уровней проектирование сверху-вниз становится в тупик: слишком много фактов, причем внешние из них плохо просматриваются. В этом случае можно выделить некоторый промежуточный уровень, оформив нижележащие фрагменты в виде функции, а в вышележащий поместить ее вызов.

    Можно проектировать «снизу-вверх» программный комплекс целиком, наращивая слой за слоем набор функций, в которой каждая вышележащая использует уже разработанные. Общие принципы структурного проектирования каждой отдельной функции сохраняются.