Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

4.5 Принципы интерфейса

В предыдущих параграфах мы прорабатывали детали некоего интер­фейса. Сформулируем теперь в общих чертах, что же такое интерфейс. Интерфейс — это детализированная, описанная граница взаимодействия между кодом, предоставляющим некоторые возможности, и кодом, ко-который эти возможности использует. Интерфейс определяет, что именно предоставляет своему пользователю некоторый законченный блок кода, таким образом функции (а может быть, и какие-то элементы данных) из этого блока могут быть использованы в остальной части программы. Интерфейс CSV предоставляет пользователю три функции: чтение строки, получение поля и возврат количества полей. Кроме них, пользователь не может получить от нашего кода ничего.

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

Прячьте детали реализации. Реализация, которая стоит за интерфейсом, должна быть скрыта от остальной части программы — с тем чтобы ее можно было изменять, не затронув при этом ничего снаружи. Для это­го принципа существует несколько терминов: сокрытие информации (hiding), инкапсуляция, абстракция, модульность и т. п.; все они описы­вают в общем одни и те же идеи. Интерфейс должен скрывать те детали реализации, которые не имеют отношения непосредственно к клиенту (пользователю интерфейса). Скрытые детали можно изменять, никак не затрагивая этим клиента: таким образом, можно постепенно улучшать интерфейс, наращивать его возможности и даже целиком заменить всю реализацию.

Базовые библиотеки большинства языков программирования дают хорошо известные примеры реализации этого принципа, хотя и не все­гда удачно разработанные. Одна из наиболее известных среди них — это стандартная библиотека ввода-вывода в С, в ней содержится несколько десятков функций для открытия, закрытия, чтения, записи и другой обработки файлов. Реализация файлового ввода-вывода скрыта в типе данных FILE*; на самом деле его свойства можно даже посмотреть (они нередко высказаны в <stdio. h>), но использовать не стоит.

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

Избегайте глобальных переменных; всюду, где это возможно, лучше передавать ссылки на данные через аргументы функций.

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

extern FILE __iob[_NFILE];

#define stdin (&__iob[0])

#define stdout (&__iob[1])

#define stderr (&__iob[2])

Таким образом, реализация получается абсолютно прозрачной, при этом, несмотря на то что stdin, stdout и stderr выглядят как переменные, присваивать им никаких значений нельзя. Специфическое имя__iob ос­новано на соглашении ANSI С, гласящем, что два подчеркивания ис­пользуются в начале тех имен служебных переменных, которые должны быть видны. Таким образом, выбранное нами имя, скорее всего, не будет конфликтовать с именами внутри самой программы.

Классы в C++ и Java — еще более удачные механизмы для сокрытия информации; их можно считать центральными средствами правильного использования этих языков. Классы-контейнеры из STL для C++, кото­рые мы использовали в главе 3, заходят еще дальше: за исключением не­которых данных о производительности, никакой информации о деталях реализации не имеется, — следовательно, разработчики библиотеки мо­гут использовать абсолютно любые механизмы.

Ограничьтесь небольшим набором независимых примитивов. Интер­фейс должен предоставлять все необходимые возможности, но не более того; части интерфейса по возможности не должны перекрывать друг друга в плане функциональности. С одной стороны, лучше иметь большое количество функций в библиотеке — тогда можно подобрать любую необходимую комбинацию. С другой стороны, чем больше интерфейс, тем труднее его написать и поддерживать в дальнейшем, а кроме того, неоправданно большие размеры могут привести к тому, что его будет трудно изучить и, следовательно, использовать оптимально. "Интерфейсы прикладных программ" (Application Program Interfaces, или API) зачастую настолько велики, что ни один смертный, похоже, не в состоянии освоить их целиком.

Для удобства использования некоторые интерфейсы предоставляют несколько способов для выполнения той или иной операции; с этой тенденцией надо бороться. Стандартная библиотека ввода-вывода С пре­дставляет как минимум четыре разные функции для вывода символа в выходной поток:

char с;

putc(c, fp);

fputc(c, fp);

fprintf(fp, "%c", c);

fwrite(&c, sizeof(char), 1, fp);

Если потоком является stdout, то существует еще несколько возможностей. B принципе, удобно, но не все эти возможности так уж необходимы.

Узкие, специализированные интерфейсы предпочтительнее, чем гло­бальные, расширенные. Делайте что-то конкретное, и делайте это хоро­шо. Не добавляйте что-либо в интерфейс только потому, что это неслож­но сделать; не исправляйте интерфейс из-за ошибок в реализации. Например, вместо того чтобы использовать memcpy как скоростной вари­ант и memmove как вариант надежный, удобнее было бы иметь одну функ­цию, которая всегда была бы безопасна, а также быстра — когда это воз­можно.

Не делайте ничего "за спиной" у пользователя. Библиотечная функция не должна создавать никаких таинственных файлов и переменных или без предупреждения менять глобальные данные. Весьма аккуратно и об­думанно надо относиться к изменению вообще любых данных в вызыва­ющей программе. Наша функция strtok не отвечает некоторым из пере­численных критериев. Например, сюрпризом для пользователя явится вписывание пустых байтов в середину введенной строки. Использова­ние пустого указателя для обозначения места окончания предыдущего захода является потенциальным источником ошибок, а кроме того, ис­ключает возможность одновременного использования нескольких эк­земпляров функции. Более логичным было бы создание одной функции, которая делила бы на лексемы исходную строку. Кстати, по аналогия ным причинам наша вторая версия на С не может быть использована для работы с двумя входными потоками (вернитесь к упражнению 4-8).

Использование одного интерфейса не должно повлечь за собой при­менение других интерфейсов только для удобства разработчика интерфейса или реализации. Наоборот, интерфейс должен быть по возможно­сти самодостаточным; если же такой у вас не получается, вы должны абсолютно явно описать все необходимые внешние услуги. В противном случае окажется, что вы взвалили бремя ответственности за поддержку интерфейса на своего клиента (пользователя). В качестве характерного примера можно вспомнить муки управления огромными списками заго­ловочных файлов в программах на С и C++ — заголовочные файлы мо­гут содержать тысячи строк и содержать ссылки на десятки других заго­ловочных файлов.

Всегда делайте одинаковое одинаково. Очень важно обеспечить после­довательность и систематичность интерфейса. Схожие действия долж­ны выполняться схожими способами. Основные функции str ... в биб­лиотеке С нетрудно использовать даже без описания, поскольку все они ведут себя практически одинаково: поток данных идет справа налево, так же, как и в операции присваивания, и все они возвращают результи­рующую строку. Однако в стандартной библиотеке ввода-вывода С предсказать порядок аргументов в функциях трудно. В одних из них ар­гумент FILE* расположен первым, в других— последним; различается также порядок задания размера и количества элементов. А вот правила интерфейса алгоритмов для контейнеров STL хорошо унифицированы, так что предсказать, как будет вести себя незнакомая функция, совсем просто.

Надо стремиться и к внешнему согласованию интерфейса, то есть к сходству с другими, уже известными интерфейсами. Например, функ­ции mem. . . в библиотеках С проектировались позднее, чем функции str. . ., и следуют их стилю. А стандартные функции ввода-вывода fread и fwrite было бы куда проще использовать, если бы они больше походили на свои прообразы — read и write. В Unix ключи командной строки предваряются символом "минус", однако один и тот же ключ может иметь совершенно различный смысл — даже в родственных про­граммах.

Если командный интерпретатор операционной системы всегда под­ставляет в текст шаблоны поиска вроде * в *. ехе, поведение будет едино­образным. Но если эту подстановку будут делать отдельные программы, то единообразия ожидать трудно. Web-браузеру для перехода на ссылку достаточно однократного щелчка мыши, а во многих других приложениях для вызова программы и для перехода на ссылку применяется двой­ной щелчок; в результате многие пользователи совершенно автомати­чески используют двойные щелчки и в web-браузерах.

В одних программных средах изложенных принципов придерживаться проще, чем в других, однако стремиться к их претворению в жизнь надо всегда. Так, например, в С довольно трудно скрыть все детали реализа­ции, но хороший программист не станет злоупотреблять открытостью де­талей, поскольку интерфейс не должен быть привязан к частностям — это противоречит принципу сокрытия информации. Комментарии в за­головочных файлах, имена особого вида (вроде__iob) и тому подобные вещи помогут максимально приблизиться к достойному поведению ва­шего интерфейса в тех случаях, когда вы не можете сделать этого строги­ми методами.

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]