Як правильно розробляти API з підтримкою зворотної сумісності. Семінар в Яндексі

Привіт! Мене звуть Сергій Константинов, в Яндексі я керую розробкою API Карт. Нещодавно я поділився досвідом підтримки зворотної сумісності зі своїми колегами. Моя доповідь складався з двох нерівних частин. Перша, велика, присвячена тому, як правильно розробляти API, щоб потім не було болісно боляче. Друга ж про те, що робити, якщо вам потрібно щось рефакторіть і не зламати по дорозі зворотну сумісність.



Якщо заглянути у Вікіпедію, то про зворотну сумісність там буде написано, що це збереження інтерфейсу системи при випуску нових версій. Насправді, для кінцевих користувачів зворотна сумісність означає, що код, написаний для попередньої версії системи, працює функціонально так само і в наступній версії.

Для розробника зворотна сумісність в першу чергу передбачає, що одного разу прийняте зобов'язання надавати якусь функціональність неможливо скасувати, виправити або перестати підтримувати.

Навіщо потрібно брати на себе зобов'язання? По-перше, ви економите час і гроші своїм користувачам. Наївно думати, ніби не підтримувати зворотний сумісність дешевше. Насправді, ви просто розмазуєте вартість підтримки клієнтам. Один факап в продакшені може коштувати набагато більше, ніж вся розробка всього продукту.

По-друге ви підтримуєте свою карму. Свідомий злам зворотної сумісності засмучує користувачів куди більше, ніж баги. Людям дуже не подобається, коли їм явно демонструють байдужість до їхніх проблем.

По-третє, зворотна сумісність — це конкурентна перевага. Вона передбачає можливість переходити на більш свіжі версії без витрат на розробку, гарантію того, що сервіс не зламається продакшені.

Зворотна сумісність: правильна архітектура
Що можна зробити на етапі проектування, щоб потім не було болісно боляче? Тут потрібно зробити три попередні зауваження. По-перше, зворотна сумісність не буває безкоштовною. Побудова правильної архітектури тягне за собою накладні витрати. Вам доведеться більше думати, вводити більше сутностей, писати надлишковий код.

По-друге, перш ніж приступати до розробки, потрібно визначити сферу відповідальності, чітко визначити, що саме буде підтримуватися. Зводьте до мінімуму ситуації, коли якийсь публічно доступне API не описано в документації. Ніколи не давайте на читання (і тим більше на запис) сутності, формат яких не описаний.

По-третє, передбачається, що ваш API правильно спроектований і структурований за рівнями абстракції.

Припустимо, що все це ми зрозуміли і усвідомили. Пора переходити до правил, які ми винесли з нашого майже пятилетноего досвіду.

Правило №1: більше інтерфейсів
В межі вашої публічної документації повинно бути ні однієї сигнатури, що приймає конкретні типи, а не інтерфейси. Виняток може бути зроблено для базових глобальних класів і явно підлеглих компонент.

interface IGeoObject :
IChildOnMap, ICustomizable,
IDomEventEmitter, IParentOnMap {
attribute IEventManager events;
attribute IGeometry geometry;
attribute IOptionManager options;
attribute IDataManager properties;
attribute IDataManager state;
}
Map getMap();
IOverlay getOverlay();
IParentOnMap getParent();
IGeoObject setParent(IParentOnMap parent)

Чому це допомагає уникнути втрати зворотної сумісності? Якщо у сигнатурі заявлений інтерфейс, у вас не буде проблем, коли у вас з'явиться друга (третя, четверта) реалізація інтерфейсу. Атомизируется відповідальність об'єктів. Інтерфейс не накладає умов, що повинен бути переданий об'єкт: він може бути спадкоємцем стандартного об'єкта, так і самостійною реалізацією.

Чому це корисно при проектуванні API? Виділення інтерфейсів в першу чергу необхідно розробнику для наведення порядку в голові. Якщо ваш метод приймає як параметр об'єкт з 20 полями і 30 методами, дуже рекомендується задуматися, що конкретно необхідно з цих полів і методів.

В результаті застосування цього правила ви повинні отримати на виході багато дрібних інтерфейсів. Ваші сигнатури не повинні вимагати від вхідного параметра більше 5±2 властивостей або методів. Ви отримаєте уявлення про те, які властивості ваших об'єктів важливі в контексті загальної архітектури системи, а які — ні. Як наслідок, знизиться надмірність інтерфейсів

Правило №2: ієрархія
Ваші об'єкти повинні бути побудовані в ієрархію: хто з ким взаємодіє. Коли на цю ієрархію наложатся інтерфейси, які ви пред'явили до своїх об'єктів, ви отримаєте певну ієрархію інтерфейсів. Тепер найважливіше: об'єкт має право знати лише про об'єкти сусідніх рівнів.

Чому це допомагає уникнути втрати зворотної сумісності? Знижується загальна зв'язаність архітектури, менше зв'язків — менше side-ефектів. А при зміні якого-небудь об'єкта ви можете зачепити тільки його сусідів по дереву.

Домогтися цього очевидними способами можна не завжди. Потрібні методи і властивості потрібно прокидати по ланцюжку через проміжні ланки (з урахуванням рівня абстракції, зрозуміло!). Тим самим ви автоматично отримаєте набір точок розширення, які потім можуть стати в нагоді.

Правило №3: контексти
Розглядайте будь-яку проміжну щабель ієрархії як інформаційний контекст для нижчою щаблі.

Приклад:
Map
= картографічний контекст (спостережувана область карти + масштаб).
IPane
= контекст позиціонування в клієнтських координатах.
ITileContainer
= контекст позиціонування в тайловых координатах.



Ваше дерево об'єктів можна буде розглядати як ієрархію контекстів. Кожний рівень ієрархії повинен відповідати якомусь рівню абстракції.

Чому це допомагає уникнути втрати зворотної сумісності? Правильно побудоване дерево контекстів практично ніколи не зміниться при рефакторинге: потоки інформації можуть з'являтися, але навряд чи пропадуть. Правило контекстів дозволяє ефективно ізолювати рівні ієрархії один від одного.

Це корисно при проектуванні API, так як тримати в голові інформаційну схему проекту істотно простіше, ніж повне дерево. А опис об'єктів у термінах наданих ними контекстів дозволяє правильно виділяти рівні абстракції.

Правило №4: consistency
В даному випадку я використовує термін consistency в парадигмі ACID для баз даних. Це означає, що між транзакціями стан об'єктів завжди повинно бути валідним. Будь-який об'єкт повинен надавати повний опис свого стану в будь-який момент і повний набір подій, що дозволяє відстежувати всі зміни свого стану.

Подібні патерни порушують consistency:

obj.name = 'щось';
// do something
obj.setOptions('щось');
// do something
obj.update();

зокрема, звідси випливає правило: уникайте методів update, build, apply.

Це допомагає уникнути втрати зворотної сумісності, тому що зовнішній спостерігач завжди може повністю відновити стан та історію об'єкта за його публічному інтерфейсу. Крім того, такий об'єкт завжди можна підмінити або скопіювати, не володіючи знанням про його внутрішній устрій.

Коли у вас організовано таку взаємодію, що є state об'єкта і подія про його зміну, номенклатура методів і подій ваших об'єктів стає менш різноманітною і більше консистентним. Вам стане простіше виділяти інтерфейси і тримати все це в голові.

Правило №5: події
Організуйте взаємодія між об'єктами за допомогою подій, причому в обидві сторони.

Розглянемо два приклади, як можна організувати взаємодію між кнопкою та макетом:

button.onStateChange = function () {
layout.setCaption(state.caption); }
layout.onClick = function () {
button.select(); }

vs

button.onStateChange = function () {
this.fire('statechange'); }
layout.onClick = function () {
this.fire('click') }

Друга схема взаємодії виходить нативно при дотриманні вимоги consistency:

  • кожен з об'єктів знає, коли стан іншого об'єкта змінилося;
  • кожний з об'єктів може повністю з'ясувати стан іншого.


У першому випадку кнопка і макет знають подробиці про внутрішній устрій один одного, у другому — ні.

Це допомагає уникнути втрати зворотної сумісності, оскільки події необов'язкові до виконання для обох об'єктів: ви легко зможете підтримувати такі реалізації обох об'єктів, які реагують тільки на частину подій і відображають лише частину другого стану об'єкта. Якщо у вас з'явиться третій об'єкт, якому необхідно реагувати на дію — у вас не буде проблем.

Якщо ви правильно виконали і попередні чотири кроки, у вас виходить стандартний патерн: у вас є, state, події про його зміну, нижележащий об'єкт, який слухає цю подію і реагує на нього якимось чином. Ваша організація взаємодії між об'єктами значно уніфікується. Взаємодія між об'єктами таким чином базується на загальних методах та події, а не приватних, тобто буде містити набагато менше специфіки конкретних об'єктів

Правило №6: делегування
Шосте правило логічно випливає з перших п'яти. Ви побудували всю систему, у вас є інтерфейси і події, рівні абстракції. Тепер потрібно наскільки це можливо перенести всю логіку на нижній рівень абстракції. Оскільки найчастіше змінюється реалізація і функціональність саме нижнього рівня абстракції (верстка, протоколи взаємодії, etc), інтерфейс до нижнього рівня абстракції повинен бути максимально загальним.

При такому підході зв'язку між об'єктами стають наскільки це можливо абстрактними. Ви зможете безболісно переписати об'єкти нижнього рівня абстракції цілком при необхідності

Правило №7: тести
Пишіть тести на інтерфейс.

Правило №8: зовнішні джерела
В абсолютній більшості випадків найбільші проблеми із збереженням зворотної сумісності виникають внаслідок незбереження зворотної сумісності іншими сервісами. Якщо ви не контролюєте суміжний сервіс (джерело даних) — заведіть до нього версионируемую обгортку на своїй стороні.

Зворотна сумісність: рефакторинг
Перш, ніж приступати
Проясніть ситуацію:

  • Якщо заявлена функціональність не працювала ніколи, ви можете прийняти будь-яке рішення: полагодити, змінити, викинути;
  • Якщо щось виглядає як баг — це ще не привід впадати його лагодити;
  • Перевірте тести на інтерфейс об'єкта, який збираєтеся рефакторіть, і пов'язаних об'єктів;
  • Якщо тестів немає — напишіть їх;
  • Ніколи не починайте ніякої рефакторинг без тестів;
  • Тестування повинно включати в себе перевірку відповідності поведінки старої і нової версії API;


Півтора прийому рефакторінгу:

  • Якщо ви все зробили правильно і взаємодія об'єктів зроблено за схемою «стан — подія зміни стану», то, часто, ви зможете переписати реалізацію, залишивши старі поля і методи для зворотної сумісності;
  • Використовуйте в інтерфейсах необов'язкові поля, методи і fallback-і — правильно підібрані умовчання дозволять вам нарощувати функціональність.


Від релізу до релізу
Заведіть собі блокнотик душевного спокою:

  • Якщо ви неправильно назвали сутність — вона буде неправильно називатися до наступного мажорного релізу
  • Якщо ви зробили архітектурну помилку — вона буде існувати до наступного мажорного релізу
  • Запишіть собі проблему в блокнотик і постарайтеся не думати про неї до наступного мажорного релізу


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.