API как набор интерфейсов
СтруктурированиеЕсли API разбит на отдельные интерфейсы - это
- автоматически определяет ее структуру
- позволяет искать решение задачи спуском по дереву API-интерфейс-метод
- позволяет отслеживать зависимости от API по интерфейсам
Автоматическое освобождение
Интерфейсы Win32 позволяют в любой реализации организовать подсчет ссылок на компонент и инициировать его автоматическое освобождение. Не нужно для этого предусматривать специальные функции, да и клиентский код средствами Delphi, C++, C# легко избавляется от необходимости ручного вызова методов для подсчета ссылок.
Стандартность
Интерфейсы Win32 поддерживаются операционной системой на уровне двоичного кода, при использовании стандартных соглашений вызовов методов и стандартных же типов данных - можно снять целый класс вопрос по организации взаимодействия модулей.
Описание интерфейсов
Вариант использованияСамое главное в описании интерфейса - для чего он предназначен. Да, хорошо если назначение интерфейса интуитивно понятно, но еще лучше - когда оно еще и документировано явно. Это должно избавить и авторов реализаций, и пользователей интерфейса от попыток забивать гвозди микроскопом, а также облегчит ориентацию в недрах API.
Пример:
IReadableStream - последовательное чтение двоичных данных
Возвращаемые ошибки
Описание каждого метода, возвращающего ошибки, должно иметь их полный перечень с перечислением условий возникновения. Без этого API будет неиссякаемым источником труднопроверяемых сбоев.
Пример:
Метод Read может выкинуть исключение EStreamError при ошибках чтения.
Побочные эффекты
Если метод изменяет то или иное состояние, имеющее смысл для клиентского кода - это должно быть документировано явно.
Пример:
После выполнения метода Read значение текущая позиция внутреннего указателя на данные увеличивается на число прочитанных байт.
Внешнее состояние
Если интерфейс позволяет управлять или наблюдать за изменяемым состоянием - это состояние должно быть документировано явно.
Пример:
IReadableStream имеет внутренний указатель на данные, перемещаемый при чтении
Прочие требования к реализации
Все, что должна делать реализация, не описанное в сигнатурах методов и не входящее в предыдущие пункты, также должно документироваться явно
Пример:
Нуль прочитанных методом Read байт еще не означает, что данных больше вообще не будет (возможно пополнение) - за это отвечает EndOfData.
Требования к интерфейсам
Причина созданияНеобходимое и достаточное условие для создания нового интерфейса - новый вариант использования, не покрываемый текущим API.
Пример:
Необходимо обеспечить однократный последовательный доступ к бинарным данным большого объема, не требующий соответствующего объема ОЗУ. Решение - интерфейс IReadableStream.
Минимализм
Интерфейс должен обладать минимальным объемом методов, достаточным для варианта использования.
Интерфейс не должен проектироваться для двух вариантов использования, каждый из которых задействует лишь часть его методов. Такая практика приводит к следующим негативным последствиям:
- Сложность чтения - чем больше у интерфейса методов и состояний, тем сложнее он для понимания и использования
- Завышенные требования к реализации - приходится поневоле делать швейцарский нож
- Худшая тестируемость - интерфейс с тремя методами требует в среднем в три раза больше тестов, чем три интерфейса с одним методом вместе взятые
- Завышенная связность - даже если клиентскому коду не требует читать настройки, он будет зависеть от интерфейса, умеющего их писать. Чтобы найти в большом проекте места, которые реально пишут - придется перелопатить на порядок больше кода
Наблюдаемость
Если интерфейс имеет изменяемое сторонними источниками состояние, то необходим механизм оповещения клиентов об изменениях, обычно путем реализации паттерна "публикатор-подписчики".
Неизменяемость
Опубликованные интерфейсы не изменяются - категорически не стоит нарушать это правило Microsoft. Любое нарушение (причем изменение не только сигнатур, а любых документированных особенностей) рушит обратную совместимость API на корню.
Если интерфейс еще не опубликован - все равно лучше не изменять, а делать новый. Еще лучше - обойтись добавлениями интерфейсов для конкретных задач и изменениями реализаций в рамках имеющихся соглашений.