Принципы интерфейса
В предыдущих параграфах мы прорабатывали детали некоего интерфейса. Сформулируем теперь в общих чертах, что же такое интерфейс. Интерфейс – это детализированная, описанная граница взаимодействия между кодом, предоставляющим некоторые возможности, и кодом, который эти возможности использует. Интерфейс определяет, что именно предоставляет своему пользователю некоторый законченный блок кода, каким образом функции (а может быть, и какие-то элементы данных) из этого блока могут быть использованы в остальной части программы. Интерфейс CSV предоставляет пользователю три функции: чтение строки, получение поля и возврат количества полей. Кроме них, пользователь не может получить от нашего кода ничего.
Для того чтобы оказаться удобным, интерфейс должен отвечать некоторым базовым требованиям: быть простым, общим, стандартным, предсказуемым, надежным, а также нести в себе возможность без потерь адаптироваться к изменениям запросов пользователей и своей внутренней реализации. В основе хороших интерфейсов лежат несколько принципов. Принципы эти тесно взаимосвязаны, а иногда даже противоречивы, но они помогут нам описать, что же происходит при пересечении границы между двумя частями программы.
Прячьте детали реализации.
Реализация, которая стоит за интерфейсом, должна быть скрыта от остальной части программы – с тем чтобы ее можно было изменять, не затронув при этом ничего снаружи. Для этого принципа существует несколько терминов: сокрытие информации (hiding), инкапсуляция, абстракция, модульность и т. п.; все они описывают в общем одни и те же идеи. Интерфейс должен скрывать те детали реализации, которые не имеют отношения непосредственно к клиенту (пользователю интерфейса). Скрытые детали можно изменять, никак не затрагивая этим клиента: таким образом, можно постепенно улучшать интерфейс, наращивать его возможности и даже целиком заменить всю реализацию.
Базовые библиотеки большинства языков программирования дают хорошо известные примеры реализации этого принципа, хотя и не всегда удачно разработанные. Одна из наиболее известных среди них – это стандартная библиотека ввода-вывода в С, в ней содержится несколько десятков функций для открытия, закрытия, чтения, записи и другой обработки файлов. Реализация файлового ввода-вывода скрыта в типе данных FILE*; на самом деле его свойства можно даже посмотреть (они нередко высказаны в <stdio.h>), но использовать не стоит.
Если заголовочный файл содержит только название структуры, а не полное ее описание, то такой тип называют иногда непрозрачным типом, поскольку свойства его неизвестны, а все операции осуществляются через указатель.
Избегайте глобальных переменных; всюду, где это возможно, лучше передавать ссылки на данные через аргументы функций.
Мы настоятельно рекомендуем не делать видимыми никаких данных ни в каком виде, – если пользователи смогут по своему желанию менять значения переменных, то чересчур сложно будет сохранять целостность и непротиворечивость данных. С помощью интерфейсов функций достаточно просто задавать жесткие правила доступа, однако этот принцип часто нарушается. Предопределенные потоки ввода-вывода вроде stdin и stdout практически всегда определяются как элементы глобального массива структур FILE:
Таким образом, реализация получается абсолютно прозрачной, при; этом, несмотря на то что stdin, stdout и stderr выглядят как переменные, присваивать им никаких значений нельзя. Специфическое имя __iob основано на соглашении ANSI С, гласящем, что два подчеркивания используются в начале тех имен служебных переменных, которые должны быть видны. Таким образом, выбранное нами имя, скорее всего, не будет конфликтовать с именами внутри самой программы.
Классы в C++ и Java – еще более удачные механизмы для сокрытия информации; их можно считать центральными средствами правильного использования этих языков. Классы-контейнеры для C++, которые мы использовали в главе 3, заходят еще дальше: за исключением некоторых данных о производительности, никакой информации о деталях реализации не имеется, – следовательно, разработчики библиотеки могут использовать абсолютно любые механизмы.
Ограничьтесь небольшим набором независимых примитивов.
Интерфейс должен предоставлять все необходимые возможности, но не более того; части интерфейса по возможности не должны перекрывать друг друга в плане функциональности. С одной стороны, лучше иметь большое количество функций в библиотеке – . тогда можно подобрать любую необходимую комбинацию. С другой стороны, чем больше интерфейс, тем труднее его написать и поддерживать в дальнейшем, а кроме того, неоправданно большие размеры могут привести к тому, что его будет трудно изучить и, следовательно, использовать оптимально. "Интерфейсы прикладных программ" (Application Program Interfaces, или API) зачастую настолько велики, что ни один смертный, похоже, не в состоянии освоить их целиком.