Что произойдёт с внешними типами, реализующими публичный интерфейс, если в этот интерфейс добавить новый метод?
Добавление метода в публичный интерфейс может нарушить совместимость: внешние типы, у которых нет этого метода, перестанут удовлетворять интерфейсу. Ошибка проявится на этапе компиляции в местах присваивания, передачи или возврата таких типов как значения интерфейса.
Сам тип не становится некорректным: Go не требует явно объявлять реализацию интерфейса. Но его набор методов больше не соответствует расширенному контракту.
В Go интерфейсы реализуются неявно: тип считается реализующим интерфейс, если его методный набор содержит все требуемые методы. Это уменьшает связанность пакетов и позволяет описывать зависимости через поведение, а не через конкретные типы.
Обратная сторона неявной реализации — автор интерфейса не видит всех внешних реализаторов. Поэтому расширение уже опубликованного интерфейса изменяет контракт для неизвестных потребителей и может вызвать ошибки в чужом исходном коде.
Предположим, библиотека публикует интерфейс с одним методом, а пользователь реализует его в собственном типе. Если библиотека добавит второй обязательный метод, пользовательский тип больше нельзя будет передать туда, где ожидается обновлённый интерфейс.
Риск особенно велик для интерфейсов, которые принимаются функциями, возвращаются из фабрик или используются в объявлениях полей. Ошибка может появиться не в объявлении пользовательского типа, а значительно позже — в конкретной точке присваивания.
Проверка соответствия выполняется по методному набору типа. После добавления метода компилятор проверяет, содержит ли тип и новый метод; если нет, операция присваивания интерфейсу становится недопустимой.
В этом примере service имел бы корректное соответствие интерфейсу до появления Close, но после расширения интерфейса проверка var _ Logger = service{} завершается ошибкой компиляции. Такая проверка часто используется как явный сигнал о соответствии типа интерфейсу.
Обычно публичные интерфейсы делают небольшими и стабильными. Потребитель может определить собственный узкий интерфейс только с нужными ему методами, а библиотека — предоставлять дополнительные возможности через отдельные интерфейсы. Это уменьшает вероятность того, что добавление новой функции сломает существующих реалบизаторов.
Нельзя решить проблему добавлением метода только в интерфейс: каждый внешний тип, который должен продолжить соответствовать контракту, нужно адаптировать или изменить. Обёртка-адаптер сохраняет совместимость, но добавляет код и слой делегирования.
Команда поддерживает библиотеку обработки сообщений. Изначально публичный интерфейс требует только Handle, а затем возникает желание добавить обязательный HealthCheck, чтобы вызывающий код мог проверять состояние обработчика.
Вариант с расширением существующего интерфейса прост для самой библиотеки, но ломает всех внешних обработчиков без HealthCheck. Вариант с добавлением метода в каждый пользовательский тип сохраняет единый контракт, однако библиотека не контролирует эти типы и не может быстро обновить их.
Выбранное решение — оставить исходный интерфейс минимальным, а проверку здоровья выразить отдельным дополнительным интерфейсом. Код, которому нужна только обработка сообщений, продолжает принимать старый контракт; код, которому нужна проверка состояния, отдельно проверяет поддержку расширенного поведения.
Так сохраняется совместимость существующих реализаций, а новые реализации могут поддерживать дополнительную возможность. Цена решения — вызывающему коду нужно явно обрабатывать случай, когда дополнительный интерфейс не поддерживается.
Нет. Сам тип остаётся допустимым и может использоваться независимо от этого интерфейса. Ошибка возникает только в операции, где компилятор должен доказать соответствие типа расширенному интерфейсу: например, при присваивании, передаче аргумента или возврате значения.
Нет. Если новый интерфейс встраивает старый и добавляет метод, внешнему типу всё равно необходимо иметь все методы нового интерфейса. Встраивание переиспользует описание требований, но не добавляет отсутствующее поведение пользовательскому типу.
Следует определить отдельный интерфейс с новым методом и использовать его только там, где это поведение действительно нужно. Конкретное значение можно проверить на поддержку дополнительного интерфейса во время выполнения; при этом базовый интерфейс остаётся совместимым, а расширенная возможность становится опциональной.
Такой дизайн лучше широкого интерфейса, если разные потребители используют разные части поведения. Компромисс состоит в том, что вызывающий код должен иметь понятную политику на случай отсутствия дополнительной возможности: пропустить её, применить запасной путь или вернуть ошибку.