Привет, Хабр! Меня зовут Дмитрий Сурков, я iOS-разработчик приложения для среднего и малого бизнеса ПСБ.
Наше приложение состоит из различных модулей и внутренних библиотек, которые связаны между собой, поэтому важно сохранять гибкость и обратную совместимость во время разработки. В этой статье разберемся, как вносимые изменения нарушают эти правила и как это исправить.

Реализация фич осуществляется в отдельных модулях — фреймворках и библиотеках. При разработке мы руководствуемся принципами SOLID. Разработку выделенного модуля важно вести в соответствии с принципом Открытости/Закрытости (система должна быть открыта для расширения, но закрыта для изменения).
Каждое изменение фиксируется определенной версией по СемВер. Изменения в коде, которые изменяют публичный интерфейс, обязывают вносить правки во всех потребителях данного модуля. Чем чаще такие изменения делаем — тем хуже стабильность системы, а время разработки заметно возрастает. Необходимо стараться вносить изменения в код, которые не будут изменять существующий публичный интерфейс.
В данной статье, мы рассмотрим:
-
Пример на библиотеке дизайн-системы с компонентами, так как она используется в основном приложении и дополнительном фреймворке построения UI.
-
Что такое семантическое версионирование и как мы его используем.
-
Анализ и поиск решения наиболее частых нарушений обратной совместимости.
-
Итоги проделанной работы.
Исходная точка
Когда идет активная фаза разработки, мы вносим много изменений. И это важно учитывать на всех этапах работы команды. Чтобы понять текущее положение и оценить в итоге результат, мы подсчитаем время, затраченное на весь процесс внесения изменений и поднятия версии.
Так как наша библиотека дизайн-системы используется сразу на двух клиентах, стоит понимать, что поднятие мажорной версии влечет за собой обновление сразу в двух местах, а далее — по увеличению связи этих клиентов с зависимостью.
Наш ci настроен на многочисленные автоматизации: проверка сборки, покрытие тестами, связи с задачами, автоподнятие версии и т.д. При создании мердж реквеста запускается пайплайн, в котором выполняются все эти проверки. Также стоит брать во внимание человеческий фактор и загруженность.
На основе этого можно выделить следующую цепочку действий:
-
Необходимо завести задачу на поднятие версии (≈ 5 мин).
-
Создать ветку, внести изменения и проверить работоспособность, а еще внести правки, если необходимо (≈ 15-20 мин).
-
Отправить на код-ревью (≈ 2-3 мин).
-
Подвинуть задачу по статусам, в нашем случае необходим ресурс тестировщика (1-∞ мин).
-
Пройти ревью, дождаться успешного выполнения пайплайна (≈ 60 мин).
-
Влить в мастер.
-
И снова по кругу…
Последний пункт говорит о повторении процесса для всех остальных клиентов, для которых также необходимо поднять версию зависимости. В итоге весь процесс может занимать от 2 до 3 часов, потом умножаем на количество клиентов и получаем 4-6 часов.
Это довольно много для простого поднятия версии, поэтому мы решили разобраться, как мы можем сократить это время и свести к минимуму мажорные изменения. Давайте приступать!
Семантическое версионирование
По сути семантическое версионирование — это соглашение об именовании, которое мы применяем к версиям продукта, будь то приложение или собственная библиотека.
После третьей цифры существуют дополнительные компоненты, призванные внести более детальную информацию для версии, но оставим это на самостоятельное изучение, так как в данной статье нас интересуют только основные компоненты.
Основные компоненты версионирования
Мажор версия — почти все изменения, которые затрагивают публичный интерфейс и нарушают обратную совместимость. Т.е. требуют от клиента вносить правки после обновления, провоцируют появление ошибок и могут влиять на сборку проекта.
Минор версия — все изменения публичного интерфейса, которые не нарушают обратную совместимость.
Патч версия — все изменения, которые не затрагивают публичный интерфейс, изменения внутри метода, переименования приватных свойств и т.д.
Мы придерживаемся данных правил. Кроме того, организовали автоподнятие версий.
Перед влитием задачи в мастер, происходит выполнение пайплайна, где в логах отображается результат проверки ABI и выставляется соответствующая версия.
Желтым выделен warning = поднятие минорной версии.
Красным выделен error = поднятие мажорной версии.
А далее идет описание того, что вызвало данные изменения. Если ничего нет, то это обычно патч.
Благодаря такой наглядности мы можем проанализировать всё, что вызывает повышение той или иной версии, и вот что у нас получилось:
Major версия
Изменения в public протоколе:
-
Удаление протокола
-
Изменение названия
-
Расширение новым типом, требующим реализации обязательных методов / свойств / соответствия, без дефолтной реализации в расширении протокола
-
Добавление или удаление метода / свойства
-
Изменение названия метода / свойства
-
Изменение типа возвращаемого значения метода / свойства
-
Изменение сигнатуры метода — добавление обязательных аргументов, изменение типов или названий аргументов
Изменения в public классах и структурах:
-
Удаление класса / структуры
-
Изменение названия
-
Понижение уровня доступа методов, свойств
-
Изменение типа данных свойств
-
Удаление метода или свойства
Изменения в public перечислениях:
-
Удаление случая (case)
-
Добавление случая (case)
-
Переименование случая (case)
-
Изменение ассоциированных значений (associated values)
-
Изменение методов / свойств и их названий / типов / аргументов
Изменения во внешних зависимостях (зависимости SPM пакета):
-
Добавление и удаление зависимости — в случае, когда зависимость используется в коде и вызывает мажорные изменения
Данные изменения публичного интерфейса являются нарушением обратной совместимости и требуют повышения мажорной версии, поскольку влияют на клиента и могут вызывать ошибки. Например, невозможно найти уже используемый метод или класс, типы возвращаемых значений не совпадают и т.д.
Minor версия
Изменения в public протоколе:
-
Добавление нового public протокола
-
Добавление свойства или метода с дефолтной реализацией через расширение протокола
Изменения в public классах и структурах:
-
Добавление нового public класса или структуры
-
Добавление инициализатора
-
Добавление свойств в инициализаторы с дефолтными значениями
-
Добавление свойств с дефолтным значением или вычисляемым свойством
-
Увеличение уровня доступа методов и свойств
Изменения в public перечислениях:
-
Добавление свойств с дефолтным значением или вычисляемым свойством
Изменения во внешних зависимостях (зависимости SPM пакета):
-
Добавление и удаление зависимости — в случае, когда зависимость используется в коде и вызывает минорные изменения
Здесь изменения, которые затрагивают публичный интерфейс, но не нарушают обратную совместимость, добавляют новый функционал, который не требует от клиента обязательной реализации.
Patch версия
Изменения в public протоколе:
-
Расширение типом, который не нарушает обратную совместимость, не требует обязательной реализации и соответствия, а также не добавляет новый функционал, например добавление ‘Sendable’
-
Изменение порядка объявления методов / свойств / типов
-
Изменение дефолтного значения свойства
Изменения в public классах и структурах:
-
Аналогичные ‘Патч‘ изменениям в протоколах
-
Добавление не публичных методов / свойств / сущностей
Изменения в public перечислениях:
-
Аналогичные ‘Патч‘ изменениям в протоколах
-
Добавление непубличных методов / свойств / сущностей
Изменения во внешних зависимостях (зависимости SPM пакета):
-
Удаление и добавление зависимостей — в случае, когда зависимость используется в коде и не вызывает мажорных и минорных изменений
-
Повышение версии зависимости — при условии ненарушения совместимости, которая влечет к поднятию мажорной версии
Обычно происходит при исправлении внутренних ошибок, улучшении или изменении, которые не влияют на публичный интерфейс и обратную совместимость и не вносят дополнительного функционала. А также все внутренние изменения непубличных / открытых интерфейсов.
Проанализировав изменения, которые влияют на повышение версии, мы можем приступать к поиску решения, как обойти нарушения обратной совместимости. И заодно рассмотреть наиболее частые случаи этих нарушений.
Нарушение обратной совместимости
Добавление обязательных методов/свойств в публичный протокол
❌error: ABI breakage: func OldProtocol.makeSomething2() has been added as a protocol requirement
public protocol OldProtocol { func makeSomething() + func makeSomething2() }
Решения:
1. Еще раз подумать, а нужен ли вам этот метод. Это не шутка. Бывает, понимаешь, что данная доработка и вовсе не нужна.
2. Объявление метода в расширении протокола. Простой вариант обратной совместимости, но имеет ограничение — можно использовать только интерфейсы, объявленые в протоколе, нельзя переопределять.
public protocol OldProtocol { func makeSomething() } extension OldProtocol { public func makeSomething2() { // do something makeSomething() } }
3. Дефолтная реализация в расширении протокола. Простой вариант обратной совместимости, можно переопределять, но имеет ограничение — можно использовать только интерфейсы, объявленные в протоколе.
public protocol OldProtocol { func makeSomething() + func makeSomething2() } extension OldProtocol { public func makeSomething2() { // do something makeSomething() } }
4. Создать новый протокол. Такое решение можно применять, если новый метод не имеет зависимостей на старый протокол.
public protocol OldProtocol { func makeSomething() } public protocol NewProtocol { func makeSomething2() } extension SomeViewModel: NewProtocol { public func makeSomething2() { // do something } }
Данное решение позволяет расширять функционал, не затрагивая старых клиентов.
let value: OldProtocol & NewProtocol
5. Сделать метод опциональным. Такое решение можно применять только для objc протоколов, если вы хотите расширить функционал необязательным в реализации методом.
@objc public protocol OldProtocol { func makeSomething() + @objc optional func makeSomething2() }
Добавление обязательных свойств в публичный протокол
Способы аналогичны решению по добавлению методов в публичный протокол.
Добавление параметра в публичный метод или инициализатор
❌error: ABI breakage: constructor init(title:identifier:) has been removed
❌error: ABI breakage: func perform(title:) has been removed
public init( title: String? = nil, + subtitle: String? = nil identifier: String = String(describing: Self.self) ) { self.title = title + self.subtitle = subtitle self.identifier = identifier } public func perform( title: String, + subtitle: String? = nil ) { // do something }
Установка дефолтного значения не помогает избежать ошибки. Решением является создать отдельный инициализатор / метод.
public struct ViewModel { public let title: String? + public let subtitle: String? public let identifier: String // Will be removed in the future major versions. public init( title: String? = nil, identifier: String ) { self.init( title: title, subtitle: nil, identifier: identifier ) } + public init( + title: String? = nil, + subtitle: String? = nil, + identifier: String + ) { + self.title = title + self.subtitle = subtitle + self.identifier = identifier + } // Will be removed in the future major versions. public func perform( title: String ) { // do something } + public func perform( + title: String, + subtitle: String? = nil + ) { + // do something + } }
Для классов подход аналогичен, только старый инициализатор класса обязательно нужно помечать как convenience.
public class ViewModel { public let title: String? + public private(set) var subtitle: String? public let identifier: String // Will be removed in the future major versions. public convenience init( title: String? = nil, identifier: String ) { self.init( title: title, subtitle: nil, identifier: identifier ) } + public init( + title: String? = nil, + subtitle: String? = nil, + identifier: String + ) { + self.title = title + self.subtitle = subtitle + self.identifier = identifier + } // Will be removed in the future major versions. public func perform( title: String ) { // do something } + public func perform( + title: String, + subtitle: String? = nil + ) { + // do something + } }
⚠️ Обратите внимание! При вызове нового инициализатора внутри старого обязательно нужно указывать все параметры, иначе может произойти рекурсивный вызов, и компилятор выведет: warning — Function call causes an infinite recursion.
Удаление public протокола/сущности/метода/свойства
Если функционал устарел и существует новый аналог.
Публичные свойства, методы и кейсы из сущностей стоит помечать ‘deprecated‘, чтобы клиенты не использовали их в новом функционале, а также указывать сообщение или название замены, если такое имеется.
❌error: ABI breakage: protocol MyProtocol has been removed
@available(*, deprecated, message:"Will be removed in the future major versions. Use `MyProtocolV2`") public protocol MyProtocol {...} public protocol MyProtocolV2 { @available(*, deprecated, message:"Will be removed in the future major versions. Use `MyProtocolV2.makeSomething2()`") func makeSomething() func makeSomething2() @available(*, deprecated, message:"Will be removed in the future major versions. Use `MyProtocolV2.somePropertyV2`") var someProperty { get } var somePropertyV2 { get } }
Изменение названия public протокола
❌error: ABI breakage: protocol MyProtocol has been renamed to protocol MyProtocolV2
Добавляем ‘typealias‘ со старым названием и помечаем протокол ‘deprecated‘, чтобы клиенты не использовали в новом функционале.
@available(*, deprecated, message:"Will be removed in the future major versions. Use `MyProtocolV2`") public typealias MyProtocol = MyProtocolV2 public protocol MyProtocolV2 { ... }
Поднятие версии платформы
Поднятие версии платформы, например, версии iOS в настройках SPM пакета — это мажорное изменение. Хоть явного упоминания о мажорной версии может и не быть.
Заключение
Ведя разработку таким образом, мы соблюдаем принцип Open / Closed. Не затрагиваем существующий функционал, а только расширяем его, добавляя дополнительные инициализаторы и методы, делая расширения на протоколы и сущности.
Это позволяет нам иметь более стабильную систему и не провоцировать поток правок во всех частях среды.
Мы проводили анализ в нашей библиотеке дизайн-системы и по ходу дела отлавливали изменения, которые нарушали обратную совместимость, искали причину, находили решение и вливали уже минорную или патч версию.
-
За три месяца в мастер было влито 27 мердж реквестов.
-
Из них 1 Major, 17 Minor, 9 Patch версий
-
Удалось исправить 6 мердж реквестов с мажорных на минорные
-
Учитывая данные, количество времени на процесс поднятия версии у клиента и их количество, мы сэкономили 6 мердж реквестов * 3 часа * 2 модуля ≈ 24 — 36 часов
Надеюсь, что предложенные решения будут вам полезны и помогут избежать повышения мажорной версии. В дальнейшем планирую делиться новыми интересными случаями нарушений совместимости. Также буду благодарен за дополнения!
ссылка на оригинал статьи https://habr.com/ru/articles/873612/
Добавить комментарий