Это главы 47-48 раздела «SDK и UI-библиотеки» моей книги «API». На этом второе издание книги завершено, все шесть разделов готовы. Если эта работа была для вас полезна, пожалуйста, оцените книгу на GitHub, Amazon или GoodReads. English version on Substack.

Вернёмся к одной из проблем, описанных в главе «Проблемы встраивания UI-компонентов»: наличие множественных линий наследования усложняет кастомизация компонентов, поскольку подразумевает, что они могут наследовать важные свойства по любой из вертикалей.

Пусть у нас имеется кнопка, которая получает одно и то же свойство iconUrl по двум вертикалям — из данных [т.е., в случае нашего примера, из результатов поиска предложений] и из настроек отображения:

class Button {
  static DEFAULT_OPTIONS = {
    …
    iconUrl: <иконка по умолчанию>
  }
  constructor (data, options) {
    this.data = data;
    // Разрешаем переопределять
    // опции по умолчанию
    this.options = extend(
      Button.DEFAULT_OPTIONS,
      options
    )
  }
  render() {
  …
  this.iconElement.src =
    this.data.iconUrl ||
    this.options.iconUrl
  }
}

При этом мы можем легко представить себе, что по обеим иерархиям свойство iconUrl было получено от кого-то из родителей по любой из вертикалей:

  • опции по умолчанию могут быть определены в базовом классе, от которого унаследован Button;

  • данные, на которых строится кнопка, могут быть общими для группы кнопок или сами по себе быть иерархическими (например, если мы будем группировать предложения сети кофеен и наследовать иконку именно родительской группы);

  • в целях облегчить кастомизацию визуального стиля компонент мы можем разрешить переопределять иконку вообще всем кнопкам через задание свойств по умолчанию для всего SDK.

В этой ситуации у нас возникает вопрос: если значение определено сразу в нескольких иерархиях (например, и в данных предложения, и в опциях по умолчанию), каким образом задавать приоритеты, чтобы выбирать одно из них?

Простой подход «в лоб» к этому вопросу — попросту запретить наследование и заставить разработчика копировать все нужные ему свойства. То есть в нашем примере сам разработчик должен написать что-то типа:

const button = new Button(data);
if (data.createOrderButtonIconUrl) {
  button.view.iconUrl = 
    data.createOrderButtonIconUrl;
} else if (data.parentCategory.iconUrl) {
  button.view.iconUrl = 
    data.parentCategory.iconUrl;
}

Достоинства простого решения очевидны — разработчик сам имплементирует ту логику, которая ему нужна. Недостатки тоже очевидны — во-первых, это лишний и зачастую дублирующийся код; во-вторых, разработчик быстро запутается в том, какие правила он реализовал и почему.

Чуть более сложный подход к проблеме — разрешить наследование, но строго зафиксировать приоритеты (скажем, заданное в опциях отображения значение всегда важнее заданного в данных, и они оба всегда важнее любого унаследованного свойства). Однако в достаточно сложном API результат будет тот же самым: если разработчику необходим другой порядок приоритетов, ему придётся задавать нужные свойства вручную, т.е. в итоге писать код, подобный вышеприведённому.

Альтернативный подход — это предоставить возможность задавать правила, каким образом для конкретной кнопки определяется её иконка, декларативно или императивно:

// Декларативный подход:
// описываем правила в каком-то формате
{
  "button.checkout.iconUrl": "@data.iconUrl"
}
// Императивный подход — программно
// добавляем функцию вычисления значения
api.options.addRule(
  'button.checkout.iconUrl',
  (data, options) => data.iconUrl
);

Наиболее последовательная реализация этого подхода — CSS1. Мы не то чтобы рекомендуем использовать CSS-подобные правила в библиотеках компонентов (в силу потрясающей сложности их имплементации и, в большинстве случаев, избыточности), но осторожно замечаем, что поддержка какого-то простого подмножества подобного рода правил значительно облегчает кастомизацию визуальных компонент для разработчиков.

Вычисленные значения

Очень важно не забыть предоставить разработчику не только способы задать приоритеты параметров, но и возможность узнать, какой же из вариантов значения был применён. Для этого мы должны разделить заданные и вычисленные значения:

// Задаём значение в процентах
button.view.width = '100%';
// Получаем реально применённое
// значение в пикселях
button.view.computedStyle.width;

Хорошей практикой также будет предоставлять доступ не только к вычисляемым значениям, но и к событию изменения этого значения.

Примечания

Глава 49. Заключение 

Предыдущие восемь глав были написаны нами, чтобы раскрыть две очень важные мысли:

  • разработка качественной UI-библиотеки — это отдельная и весьма непростая инженерная задача;

  • и эта задача не сводится к автоматической генерации SDK по спецификации / модели данных.

Оглядываясь на всё написанное, мы с трудом можем сказать, что нашли лучшие примеры и самые понятные слова для описания такой сложной предметной области. Мы, тем не менее, надеемся, что сделали вашу жизнь — и жизнь ваших пользователей — чуточку проще.

Комментарии (2)


  1. ritorichesky_echpochmak
    25.09.2023 10:00
    +1

    Человек делает бесплатно книгу, выкладывает... его совершенно молча минусуют. Хабраклассика. Потому что пенальти за сливание стат - это "не конструктивно", в отличие от безнаказанного сливания


    1. forgotten Автор
      25.09.2023 10:00

      Ну, уже снова 0.