Без сомнения, главнейшее условие успешного создания крупных программ заключается в применении надежных методов проектирования. Широкое распространение при написании программ получили следующие три метода: нисходящий (сверху вниз), восходящий (снизу вверх) и специальный (на данный конкретный случай). В случае нисходящего метода вы начинаете созидательный процесс с программы высокого уровня и спускаетесь до подпрограмм низкого уровня. Восходящий метод работает в обратном направлении: вы начинаете с отдельных специальных подпрограмм, постепенно строите на их основе более сложные конструкции и заканчиваете самым верхним уровнем программы. Специальный подход не имеет заранее установленного метода.
Поскольку С является структурированным языком программирования, то лучше всего он сочетается с нисходящим методом проектирования. Подход сверху вниз позволяет производить ясный, легко читаемый программный код, который в дальнейшем не вызовет трудностей и при сопровождении. К тому же данный подход помогает прояснить и сделать понятной всю структуру программы в целом до кодирования функций более низкого уровня. Такой подход позволяет уменьшить потери времени, обусловленные неудачными или ошибочными начинаниями.
Структурирование программы
Как и для любой общей схемы, при применении нисходящего метода начинают с общего описания программы, а затем переходят к проработке ее конкретных элементов. На практике при разработке программы лучше всего сначала точно определить, что программа будет делать на самом высоком уровне, и только после этого погружаться в подробности, касающиеся каждого действия. Предположим, к примеру, что необходимо написать программу, предназначенную для ведения списка рассылки. Во-первых, необходимо составить перечень действий, которые будет выполнять такая программа. Каждая строчка данного перечня должна содержать только один функциональный элемент. (На этом этапе под функциональным элементом понимается «черный ящик», который выполняет только одну задачу.) Тогда такой перечень может быть представлен примерно в следующем виде:
- Ввести новый адрес
- Удалить новый адрес
- Печать списка
- Найти имя
- Сохранить список
- Загрузить список
- Завершить выполнение программы
После того как вы определили все функциональные возможности программы, можно в общих чертах описать подробные свойства каждого функционального модуля, начиная с основного цикла. Ниже приведен один из возможных вариантов такого представления, позволяющий реализовать основной цикл программы работы со списком рассылки:
main loop
{
do {
вывести меню
определить выбор пользователя
выполнить выбранное действие
} while выбор != quit
}
Такая разновидность алгоритмического представления (иногда называемая псевдокодом) может помочь внести ясность в общую структуру программы; ее необходимо выполнить до кодирования. С-подобный синтаксис был использован по той простой причине, что он вам уже знаком, но можно воспользоваться и любым другим подходящим синтаксисом.
Аналогичные определения необходимо дать каждому функциональному элементу. Например, вы можете описать функцию, которая осуществляет запись списка рассылки в файл на диске примерно следующим образом:
save to disk {
open disk file
while есть данные write {
write данные на диск
}
close disk file
}
На этом уровне абстракции функция «записать-на-диск» обращается к новым функциональным модулям более низкого уровня. Эти модули открывают файл на диске, записывают на него данные, а затем закрывают дисковый файл. В дальнейшем необходимо будет еще определить и каждый из этих модулей. Если при их описании будут создаваться новые функциональные элементы, они также должны быть определены и т.д. Этот процесс закончится, когда при описании больше не будет создан ни один новый функциональный элемент. Тогда остается всего лишь сесть и написать реальный С-код, который реализует эти действия. Например, модуль, который закрывает дисковый файл, вероятно, будет транслирован в вызов функции fclose().
Обратите внимание, что при подобном определении совсем ничего не упоминается о структуре данных и переменных. Это сделано умышленно, потому что до сих пор вы хотели определить только то, что должна делать ваша программа, но не то, каким образом она реально будет это осуществлять. Такой описательный процесс помогает выбрать эффективную структуру данных. (Естественно, структуру данных необходимо будет описать перед кодированием функциональных элементов.)
Выбор структуры данных
После определения общей структуры прикладной программы необходимо решить, какой будет структура данных. Выбор структуры данных и ее реализация имеют чрезвычайно важное значение, поскольку она помогает определить проектные ограничения вашей программы.
Список рассылки, как правило, содержит следующую информацию: имена, названия улиц, городов, штатов и почтовые индексы. При нисходящем подходе это немедленно предполагает применение определенной структуры для хранения этой информации. И тут же возникает вопрос: а как такие структуры будут храниться и обрабатываться? Для программы, работающей со списком рассылки, можно было бы использовать массив структур фиксированного размера. Но массив фиксированного размера имеет один серьезный недостаток: размер массива жестко и безапелляционно ограничивает длину списка рассылки. Более удачное решение заключается в динамическом выделении памяти для каждого адреса. Тогда каждый адрес будет сохраняться в динамической структуре данных определенной формы (например, в связном списке), которая может при необходимости расти или уменьшаться. В таком случае список может быть огромным или очень маленьким в зависимости от конкретных обстоятельств.
Хотя на данном этапе мы отдали предпочтение динамическому распределению памяти, а не массиву фиксированного размера, все же точная модель представления данных все еще не определена. Существует несколько возможных вариантов. Можно использовать однонаправленный связный список, двунаправленный связный список, двоичное дерево и даже метод хэширования. Каждый из этих методов имеет свои достоинства и недостатки. Ограничим круг обсуждения и предположим, что конкретно нашему приложению, обрабатывающему список рассылки, предъявляется требование достичь минимального времени поиска. Исходя из этого, мы выбираем двоичное дерево. Теперь можно определить структуру, которая содержит в списке каждое имя и адрес, как показано ниже:
struct addr {
char name[30] ;
char street[40];
char city[20];
char state[3];
char zip[ll];
struct addr *left; /* указатель на левое поддерево */
struct addr *right; /* указатель на правое поддерево */
};
Теперь, после определения структуры данных, можно приступить к кодированию всей программы. Для этого надо всего лишь определить все подробности, описанные в общей структуре созданного ранее псевдокода. Если вы последуете нисходящему методу, программы будут не только легко читаться, но и потребуют меньше времени на разработку и сопровождение.