Любое изменение открытого интерфейса является критическим

Неприятная правда заключается в том, что любое изменение открытого интерфейса потенциально является критическим (breaking change).

Прежде всего, я хочу прояснить, что я имею в виду под «критическим изменением». Если вы выпускаете некоторый компонент для другой компании, тогда «критическое изменение» – это такое изменение, при котором код этой компании, успешно компилирующийся с предыдущей версией библиотеки, не будет компилироваться с новой. (Более строгим определением критического изменения является сохранение компилируемости кода, но при этом его поведение измененяется; на этот раз давайте будем считать, что критическое изменение приводит лишь к появлению ошибок компиляции.) «Потенциальное» критическое изменение – это изменение, которое может привести к ошибке при определенном способе использования вашего компонента. Под «изменением открытого интерфейса» я подразумеваю изменение «публичных метаданных» компонента, таких как добавление нового метода, а не изменение тела существующего метода. (Такое изменение обычно приводит к изменению поведения во время выполнения, а не к ошибке компиляции.)

Некоторые критические изменения публичного интерфейса являются очевидными: изменение области видимости метода с открытой на закрытую и т.п. В таком случае код, вызывающий этот метод, или наследник этого класса перестанет компилироваться. Но многие другие изменения кажутся безопасными; например, добавление нового метода или добавление метода задания значения (setter) существующему свойству только для чтения. На самом же деле, практически любое изменение открытого интерфейса компонента потенциально является критическим. Давайте рассмотрим несколько примеров. Предположим, вы добавляете еще одну перегрузку метода:

 // Старая версия:
public interface IFoo {...}
public interface IBar { ... }
public class Component
{
    public void M(IFoo x) {...}
}

Затем, предположим, вы добавляете метод классу Component

public void M(IBar x) {...}

Предположим у вашего клиента есть следующий код:

// Пользовательский код:
class Consumer : IFoo, IBar
{
   ...
   component.M(this);
   ...
}

Исходный код нормально компилировался с исходной версией компонента, но не будет компилироваться с новой версией из-за неоднозначности определения нужной перегруженной весрии. Упс!

А как насчет добавления нового метода?

 // Исходная версия компонента:
...
public class Component
{
  public void MFoo(IFoo x) {...}
}

И вы добавляете метод

 public void MBar(IBar x) {...}

Никаких проблем, правда? Пользователь ведь не мог использовать метод MBar. Естественно, добавление этого метода не может привести к ошибке компиляции, ведь так?

 class Consumer
{
    class Blah
    {
        public void MBar(IBar x) {}
    }
    static void N(Action<Blah> a) {}
    static void N(Action<Component> a) {}
    static void D(IBar bar)
    {
        N(x=>{ x.MBar(bar); });
    }
}

В исходной версии было два кандидата при разрешении перегрузки метода с именем N. В исходной версии лямбда-выражение не могло быть приведено к Action<Component>, поскольку формальный параметр x типа Component приводил к ошибке в теле лямбда-выражения. Таким образом, эта версия метода отбрасывалась и оставшийся метод оказывался единственным кандидатом, поэтому в теле лямбда-выражения в качестве x использовался тип Blah.

С новой версией класса Component, тело лямбда-выражения не содержит ошибки; таким образом, во время определения перегруженной версии метода будет доступно два равнозначных кандидата, что приведет к ошибке компиляции.

Такой тип критических изменений делает практически любое изменение открытого интерфейса типа потенциально критическим. Но на самом деле, такие изменения являются сомнительными и маловероятными, и мало шансов, что разработчики столкнутся с ними в «реальных» проектах. Когда мы анализируем влияние потенциальных изменений наших пользователей, то мы явно отбрасываем подобный тип критических изменений, как маловероятный. Однако я считаю очень важным принимать это решение осознанно, а не просто игнорировать эту проблему.

Оригинал статьи