Интерфейс в программировании c#

Отделение интерфейса от реализации имеет много практических преимуществ. Вот простой способ сделать это в стандартном C-коде ANSI. Как вы организуете средние или большие программы на языке Си? Немногие учебники по Си дают какое-либо понимание; они концентрируются на изложении особенностей Си с использованием небольших примеров. Примеры обычно помещаются в один файл исходного кода. Без какого-либо руководящего принципа организации большие программы на языке Си могут стать трудными для понимания и невозможными для поддержания. Модульное программирование — это один из способов управления сложностью.

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

К сожалению, C явно не поддерживает модульное программирование. Архетипический модульный язык программирования-это модуль-2 (и -3) Никлауса Вирта. Модульный язык, такой как Modula, имеет синтаксис для отделения реализации от интерфейса и импорта модулей. Удобно, что некоторые из функций языка Си, как в языке, так и в препроцессоре. Могут быть кооптированы в обеспечение модулярных возможностей. Эти особенности в сочетании с набором условностей делают модульное программирование на языке Си практичным и эффективным методом.

Модульное программирование

Программисты работают с абстракциями каждый день. Абстракция выделяет существенные черты чего — то, игнорируя его детали. Абстракция может включать в себя часть аппаратного обеспечения, программный компонент и т. Д. Например. Последовательный порт может быть абстрагирован до пары функций для чтения и записи байтов в порт; детали UART. Его регистры и адреса. Подавляются. Для наших целей интерфейс модуля-это абстракция его функций. Интерфейс определяет функции модуля и способы их использования.

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

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

Два наиболее важных элемента модуля — это разделение модуля на интерфейс и реализацию и возможность скрывать информацию в реализации.

Синтаксис для создания модуля (аналогичный синтаксису Modula) будет следующим:

МОДУЛЬ ОПРЕДЕЛЕНИЯ foo
EXPORT список функций и данных
объявления экспортируемых функций
и данных
END foo
IMPLEMENTATION MODULE foo
IMPORT список используемых модулей
… код …
END foo

Определение интерфейса и реализация разделены. Определение интерфейса содержит список функций и структур данных для экспорта. А также объявления этих функций и данных. Это единственные части модуля, которые могут использовать клиенты. Реализация перечисляет свои зависимости от других модулей.

Затем он конкретизирует экспортированные функции; они могут использовать скрытые вспомогательные функции и данные.

Как мы можем применить эти методы с C? По умолчанию каждая функция, определенная в C, доступна глобально. Самое близкое, что C имеет к модулю, — это исходный файл (.c). Мы можем инкапсулировать функции и данные в исходный файл, чтобы сформировать часть реализации модуля. Соответствующий файл заголовка (.h) формирует интерфейс к модулю. Давайте посмотрим, как это работает в деталях.

Реализации

Листинг 1 представляет собой реализацию примера модуля с именем foo.

Сначала модуль перечисляет свои зависимости от других модулей; он импортирует интерфейсы к модулям. Которые он использует. Это делается с помощью директивы препроцессора C #include. Модуль foo использует модули x и y . Реализация foo также включает в себя собственное определение интерфейса. Это позволяет компилятору проверить, соответствует ли реализация модуля объявленному интерфейсу. Мы рассмотрим содержимое файлов интерфейса позже.

Листинг 1 foo.c (реализация)

/* foo.c */
/* Импорт необходимых интерфейсов: */
#include “x.h”
#include “y.h”
/* Реализует этот интерфейс: */

#include “foo.h”

int var1;
static int var2;

void Fun1(int *p) { … }

Чтобы скрыть внутренние детали, реализация модуля должна иметь возможность создавать частные данные и функции. По умолчанию C делает каждую функцию, определенную в файле, вызываемой (видимой) из функций. Определенных в любом другом файле. Аналогично, все элементы данных, кроме локальных для функции, доступны из других файлов. Однако мы можем использовать ключевое слово C static, чтобы ограничить область функций и данных одним файлом.

Ключевое слово static кажется особенно названным и заставило меня задуматься. Когда я впервые изучал C. static-это спецификатор класса хранения. По умолчанию локальные переменные в функции распределяются динамически, то есть в стеке. Добавление статики к определению локальной переменной приводит к тому. Что она становится статической и выделяется таким образом. Что она постоянно сохраняет значения между вызовами функции. В этом есть смысл. Где все запутывается, так это в определении нелокальных данных. Здесь статика влияет на область действия данных; данные статичны и распределены независимо от того. Статичны ли они присутствует или нет. Нелокальные данные, объявленные статическими, ограничены по объему файлом, в котором они определены (технически. От точки объявления до конца файла). статика также может быть применена к функциям. Чтобы ограничить их область действия одним файлом.

Мы будем использовать статическое ключевое слово C, чтобы скрыть информацию в реализации модуля. То есть в его исходном файле. Возвращаясь к листингу 1, отметим, что var1 доступен извне модуля. В то время как var2 полностью является внутренним элементом реализации foo. Аналогично, функция Fun1 может быть вызвана извне модуля, но Fun2 может быть вызвана только из других функций внутри модуля foo. Использование статики для сокрытия информации требует определенной самодисциплины. Забыв поставить статику на данные, которые должны быть внутренними для модуля, позволяет данным “утекать” из модуля.

Листинг 2 foo.h (интерфейс)

/* foo.h */
#define var1 Foo_var1
#define Fun1 FooFun1

extern int var1;
extern void Fun1(int *);

Интерфейсы

В листинге 2 показано определение интерфейса нашего примера модуля. Игнорируйте #define s вверху; мы скоро вернемся к ним. Основная часть интерфейса объявляет внешние, общедоступные части модуля. Клиентский модуль может получить доступ к целочисленной переменной var1 . Или он может вызвать функцию Fun1 с аргументом. Который является указателем на целое число.

Рассмотрим простую программу на языке Си:

#включить

При запуске на моей рабочей станции Sun эта программа выдает результат e = 0.000000 , а не e = 2.718282 .

Программа компилируется без ошибок и предупреждений. Так в чем же дело? В программе есть простая ошибка. Без отсутствующего #include , компилятор C предположил. Что функция exp ожидает целочисленный аргумент и возвращает целочисленный результат. Добавление прототипов функций в C позволило избежать подобных ошибок. Прототипы функций позволяют компилятору проверять типы аргументов. Передаваемых функциям. Даже если функция определена в другом файле. Однако C не требует. Чтобы вы объявляли или использовали прототипы. Я злонамеренно пропустил #include ; чтобы проиллюстрировать один момент: если можно намеренно опустить прототипы. Можно сделать это случайно.>

Предположим. Что модуль, показанный в листинге 3, хочет воспользоваться услугами. Предоставляемыми нашим модулем foo. К сожалению, программист забыл импортировать определение интерфейса foo, то есть нет #include “foo.h” . Далее предположим. Что программист забывает. Что Fun1 принимает указатель на целое число. Но вместо этого передает целое число. Предположительно. Эта ошибка не будет обнаружена. Как это было в случае с exp . Однако с помощью нашей модульной техники ошибка обнаруживается. Хотя ошибочный модуль программиста будет компилироваться, компоновщик не сможет найти Foo1, и в результате появится сообщение об ошибке.

Листинг 3 клиент.c (реализация)

/* client.c */
/* Импорт необходимых интерфейсов: */
#include “z.h”
/* Реализует этот интерфейс: */
#include “client.h”

static void ClientFun(void)
{
int z;

Fun1(z);

}

Как это возможно? Чем отличается этот пример от примера exp? Посмотрите на #define s в определении интерфейса foo, которое мы пропустили ранее. #define for Fun1 приведет к тому. Что любое использование Fun1 будет заменено на FooFun1 . Обратите внимание. Что #define стратегически расположен перед прототипом для Fun1 . Следовательно. Прототип предназначен для функции с именем FooFun1 . Поскольку интерфейс модуля включается в исходный файл реализации модуля. Фактическим определением функции будет FooFun1 . Когда foo.c будет скомпилирован. Функция будет называться FooFun1 в объектном файле. Если клиентский модуль не может импортировать (#define ) определение интерфейса для foo. Его использование Fun1 будет компилироваться в Fun1 . Компоновщик не может подключить Fun1 к FooFun1 , поэтому связывание не удастся. Как только программист исправит клиентский модуль для импорта интерфейса foo. Его использование Fun1 станет FooFun1 . Теперь компилятор может обнаружить несоответствие типов.

Обратите внимание. Что замена FooFun1 на Fun1 основана на токенах. А не на тексте. Другими словами. Подстановка не происходит подобно команде поиска и замены в текстовом редакторе. Поэтому нет никакой опасности. Что OtherFun1 станет OtherFooFun1 . Это происходит потому. Что препроцессор C понимает C. По крайней мере. На уровне лексических токенов. Следовательно. Только фактическое использование функции Fun1 будет заменено на FooFun1 . Это работает. Даже если функция не использует никаких аргументов. Фиксированное число аргументов или переменное число аргументов. Он также работает при создании указателя на функцию.

Я определяю новые имена для всех функций. Доступных в интерфейсе модуля. Новое имя состоит из имени функции с префиксом имени модуля. Для наших целей здесь было бы достаточно префиксировать некоторую фиксированную строку вместо этого, M_ например. Независимо от имени модуля. Однако добавление имени модуля ко всем глобальным функциям увеличивает вероятность того, что имя останется уникальным. Если модуль помещен в библиотеку. Менее важно определить конкретные имена модулей для открытых данных. Поскольку переименование функции обычно достаточно для обнаружения сбоя включения интерфейса. Однако добавление имени модуля ко всем общедоступным данным, как и для имен функций. Уменьшает вероятность столкновения имен. Если модуль будет добавлен в библиотеку.

До сих пор в наших примерах интерфейсов были только функции и данные. А как насчет типов данных? Если публичная функция использует или возвращает агрегированные данные. Такие как структура. Нам обычно нужно будет объявить эту структуру данных и в интерфейсе. Объявление структуры данных в интерфейсе предоставляет клиентам доступ к ее внутренним компонентам. Это часто разумно и необходимо; например. Когда клиент хочет передать большой объем данных функции. Может быть более ясным поместить данные в структуру и передать указатель на структуру. А не передавать все данные в качестве отдельных аргументов. В этом случае клиент должен знать имена компонентов структуры. Чтобы он мог заполнить их значения.

Но что, если мы хотим скрыть представление структуры данных от клиентов? Modula называет это непрозрачным экспортом: интерфейс предоставляет клиентам имя структуры данных и функции для создания и работы с экземплярами структуры данных. Но ничего не раскрывает о компонентах или макете структуры данных. Оказывается. Мы можем сделать это и на языке Си. В листинге 4 показан интерфейс модуля. Обеспечивающего простую приоритетную очередь. Клиент приоритетной очереди может создавать очередь. Ставить данные в очередь и удалять данные из очереди в порядке приоритета. Интерфейс объявляет Priority_queue тип данных, состоящий из указателя на фактическую структуру очереди приоритетов. Три прототипа функций используют тип Priority_queue. Однако нигде в интерфейсе не объявлена фактическая структура приоритетной очереди. Это называется неполным типом в C. Где-то тип должен быть завершен; это происходит в части реализации приоритетной очереди. Это позволяет нам скрыть представление структуры данных от клиентов.

Листинг 4 priqueue.h (интерфейс)

/* priqueue.h */
#define Enqueue PriEnqueue
#define Dequeue PriDequeue
#define createQueue PriCreateQueue

typedef struct priority_queue_struct * Priority_queue;

extern void Enqueue(Priority_queue. Int priority. Void *data);
extern void *Dequeue(Priority_queue);
extern Priority_queue createQueue(void);

Примеры определений интерфейсов в этой статье имеют мало комментариев. Реальные определения интерфейса должны содержать всю документацию, необходимую программисту для использования модуля. Однако эта документация не должна раскрывать никаких подсказок о том. Как на самом деле реализован модуль. Хранение документации с объявлениями прототипов дает программисту один-единственный шанс узнать. Как использовать модуль. Можно легко представить себе инструмент, который, учитывая имя модуля, находит соответствующие определения интерфейса. Извлекает только объявления и соответствующие комментарии и представляет их программисту в красивом формате. Это будет аналог инструмента javadoc. Который используется Java-программистами.

Обсуждение

Напомним, что модульное программирование состоит из отделения реализации от интерфейса и сокрытия информации в реализации. В C это достигается путем размещения определения интерфейса в заголовочном файле и реализации в исходном файле. Дисциплинированное использование статики используется для сокрытия деталей реализации. Определение интерфейса формирует связь между модулем и его клиентами. Модуль включает в себя собственное определение интерфейса. Чтобы подтвердить. Что он реализует объявленный интерфейс; клиентский модуль импортирует/включает определение интерфейса. Чтобы убедиться. Что он использует интерфейс правильно.

Использование заголовочных файлов для экспорта информации из исходного файла языка Си на первый взгляд может показаться странным. Когда я впервые изучил язык Си. Я поместил определения констант и объявления структуры данных в заголовок для включения в соответствующий исходный файл Си. Теперь, если константа или структура данных используется только одним исходным файлом. Я помещаю их в этот исходный файл. Есть еще место для другого стиля заголовочного файла. Если константа или тип данных используются во всем приложении, они принадлежат традиционному заголовочному файлу.

Я уже некоторое время использую этот стиль модуля. Однако использование препроцессора C для переименования функций. Чтобы заставить клиентов импортировать определения интерфейса. Является недавним дополнением. На самом деле я переоборудовал его в существующее приложение и обнаружил. Что действительно забыл импортировать определения интерфейса в нескольких местах. Единственный негативный аспект этой техники. Который я обнаружил до сих пор. Заключается в том. Что отладчики используют измененные имена функций. Это может привести к некоторой путанице, например. Когда трассировка стека показывает FooFun1 вместо ожидаемого Fun1 .

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

Я включил некоторые ссылки на дополнительное чтение по модульному программированию. Статья Парнаса является классическим справочником по этому вопросу. Книга Хансона дает многочисленные примеры интерфейсов и реализаций в редком сочетании элегантности и практичности. Наконец, я рекомендую книгу Вирта о Модуле-2. Хотя у меня никогда не было возможности программировать в Modula-2, я узнал некоторые ценные методы программирования из этой книги. Я всегда считаю целесообразным исследовать альтернативные методы программирования. Воплощенные в других языках.

Джон Хейс работает в Лаборатории прикладной физики Университета Джона Хопкинса. Он занимается разработкой встроенных систем для космических аппаратов почти два десятилетия. Он получил степень бакалавра в Виргинском политехническом институте и Государственном университете. А также степень магистра в Университете Джона Хопкинса. Вот его электронный адрес .

Рекомендации:

1. Парнас Д. Л. “О критериях, используемых при разложении систем на модули”, Communications of the ACM, 5(12). December 1972, pp.

2. Хансон, Д. Р. C Интерфейсы и реализации-Методы создания многоразового программного обеспечения. Рединг, Массачусетс: Аддисон-Уэсли, 1997.

3. Вирт, Н. Программирование в модуле-2. Берлин: Springer-Verlag, 1982.

Вернуться к Оглавлению за декабрь 2001 года

Поделитесь этим: