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

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

Здесь мы обсуждаем несколько наших стратегий управления устаревшими версиями, некоторые из которых специфичны для Angular, а другие стали возможны с помощью TypeScript. В этой серии блогов подробно рассматриваются несколько стратегий на тот случай, если вы хотите реализовать то же самое в своих проектах, начиная со стратегий переименования.

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

Переименование ваших общедоступных API

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

Мы добавили в Clarity соглашения об именах, которые мы применяем ко всему общедоступному из нашей библиотеки. Мы хотели ввести новые соглашения об именах, не нарушая при этом изменений, поэтому мы ввели новые имена, сохранив исходные имена. Например, мы используем префикс Clr в именах компонентов, например ClrButton, поэтому очевидно, что все является частью Clarity, и поэтому наши имена не конфликтуют с другими библиотеками. Вы не хотите, чтобы приложение с двумя библиотеками пыталось предоставить Button компонент в Angular в тех случаях, когда вам может потребоваться импортировать кнопку с помощью ViewChild. Это также означает, что когда вы используете автозаполнение, мы не загрязняем список, пока вы не начнете вводить Clr.

Какими бы ни были причины для переименования, вы должны быть уверены, что оно не сломает другие вещи, которые могут его использовать. Например, мы знаем, что некоторые проекты используют @ViewChild(Wizard) wizard: Wizard; в своих проектах, чтобы получить ссылку на мастер, но мы хотели изменить общее имя Wizard на ClrWizard. Следующие шаги покажут вам различные примеры того, как мы ввели совершенно новую номенклатуру без критических изменений, с примерами различных типов.

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

Если вы хотите переименовать компонент, директиву или службу, вы можете решить эту проблему двумя способами. В зависимости от типа изменений, которые вы вносите в компонент, вы можете просто создать ссылку с правильным именем без каких-либо функциональных изменений или создать второй экземпляр с измененными свойствами.

В случае, если вы просто хотите переименовать класс, но не вносить никаких других критических изменений, переименуйте класс, а затем повторно экспортируйте его под исходным именем в качестве нового значения.

// Component or Directive
@Component({ ... })
export class ClrButton {}
/** @deprecated since 1.0 */
export const Button = ClrButton;
// Service
@Injectable()
export class ClrService {}
/** @deprecated since 1.0 */
export const Service = ClrService;

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

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

/** @deprecated since 1.0 */
@Component({ 
  selector: 'button',
  template: `<button [type]="type"><ng-content></ng-content></button>`
})
export class Button {
  protected type = 'button';
}
// New implementation
@Component({ 
  selector: 'clr-button',
  template: `<button [type]="type"><ng-content></ng-content></button>`
})
export class ClrButton extends Button {
  protected type = 'submit';
}

Вы создаете новый класс ClrButton, расширяя прототип Button. Затем вы можете внести изменения в новый класс, например переопределить значения или добавить новые. Это делает новую реализацию ClrButton уникальной по сравнению с оригиналом.

В этом случае необходимо повторно объявить декоратор компонента и метаданные, потому что у вас не может быть двух полных экземпляров компонента с одним и тем же селектором (поскольку они оба принимают одни и те же метаданные). Компилятор Angular не имеет возможности перезаписать только одно из свойств метаданных компонента, вам нужно заново объявить все это (включая шаблон).

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

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

Переименование интерфейса

У Clarity есть ряд интерфейсов, которые придают согласованность и ясность нашему API. Вначале мы неправильно добавляли префиксы для интерфейсов. Чтобы решить эту проблему, мы работали над их переименованием для единообразия.

Интерфейсы можно расширять, как и классы. Начните с присвоения существующему интерфейсу желаемого имени. Затем повторно экспортируйте его со старым именем. В приведенном ниже примере я также реализую в интерфейсе универсальный тип.

export interface ClrDatagridComparatorInterface<T> {
  compare(a: T, b: T): number;
}
/** @deprecated since 1.0 */
export interface DatagridComparator<T> extends ClrDatagridComparatorInterface<T> {}

Вы также можете изменить интерфейс при расширении, если это необходимо, но, как и в примере с классом ранее, я бы не рекомендовал этого, если вы можете этого избежать.

Переименование перечисления

Невозможно повторно экспортировать перечисление. Они определены один раз и не могут быть изменены. Как вы можете видеть ниже, когда мы объявили устаревшими некоторые перечисления в Clarity, нам пришлось переопределить исходное перечисление под обоими именами.

export enum ClrDatagridSortOrder {
  Unsorted = 0,
  Asc = 1,
  Desc = -1,
}
/** @deprecated since 1.0 **/
export enum SortOrder {
  Unsorted = 0,
  Asc = 1,
  Desc = -1,
}

Это касается большинства общих объектов и типов TypeScript. Давайте перейдем к переименованию нескольких конкретных примеров Angular.

Переименование углового ввода

Входы Angular имеют встроенную функцию переименования, но она ограничена и не обязательно то, что нам нужно. Например, здесь мы можем переименовать вход в clrTitle, но исходный title больше не будет работать.

@Input() title: string; // This is the original
@Input('clrTitle') title: string; // This will break anyone using it

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

@Input() clrTitle: string;
/** @deprecated since 1.0 */
@Input()
set title(text: string) {
  this.clrTitle = text;
}
get title(): string {
  return this.clrTitle;
}

Теперь ваш компонент будет поддерживать привязку как к clrTitle="text", так и к title="text". Помните, что если кто-то использует оба атрибута, будет использоваться последний определенный атрибут, поскольку они оцениваются по порядку.

Также обратите внимание, что в приведенном выше примере мы включили геттер в исходное свойство title. Это решает проблему для потребителей, получающих доступ к этому значению в более старой версии Clarity. Если бы мы не предоставили геттер, это значение не существовало бы для них, и это было бы критическим изменением. Предоставление геттера дает нам возможность избежать критических изменений до тех пор, пока не закончится период устаревания.

Переименование вывода Angular

Выходные данные Angular функционально похожи на то, как мы переименовали вход, за исключением того, что они, по сути, являются свойствами только для чтения, на которые вы можете подписаться и получать генерируемые события.

Сначала определите новый вывод как обычный вывод, используя EventEmitter. Затем определите другой вывод с исходным именем и, наконец, сделайте его merge с другим выводом, как вы видите здесь.

@Output() clrClickEvents = new EventEmitter<boolean>();
/** @deprecated since 1.0 */
@Output()
get clickEvents(): Observable<boolean> {
  return this.clrClickEvents
}

Это позволяет нам подписаться на выход (clickEvents)="listen(state)" или (clrClickEvents)="listen(state)", что эквивалентно.

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

Переименование методов и свойств

Когда у вас есть объект с набором вызываемых свойств (методов / функций), вы можете захотеть изменить имена этих свойств. Мы уже сделали это с примером ввода Angular, но вот простой способ справиться с этим случаем, чтобы поддержать оба имени.

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

class ClrButton {
  clrType = 'button';
  /* @deprecated since 1.0 */
  set type(value: string) {
    this.clrType = value;
  }
  get type(): string {
    return this.clrType;
  }
}

Если бы я позвонил ClrButton.type = 'submit';, то технически он бы установил значение свойства clrType, но все равно предоставил бы мне исходный API без критических изменений. Позже я могу безопасно удалить эти вызовы геттеров и сеттеров, не нарушая реализацию, когда буду готов.

Тестирование ваших устаревших рекомендаций

Ясность должна гарантировать, что устаревший код останется доступным до тех пор, пока он не будет намеренно удален для следующего выпуска. Чтобы облегчить это, мы добавили файл модульного теста специально для проверки того, что устаревшие версии все еще доступны. По сути, мы запускаем набор тестов, чтобы убедиться, что переименованный элемент соответствует оригиналу.

// Test class equality
expect(TreeNode).toEqual(ClrTreeNode);
// Can't test interfaces directly, so just verify if they are exported and can be applied.
class ComparatorTest implements Comparator<any> {
  compare(a, b) {
    return 0;
  }
}
expect(new ComparatorTest()).toBeTruthy();
// Test enum values are as expected
expect(SortOrder.Unsorted).toEqual(ClrDatagridSortOrder.Unsorted);

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

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

describe('since v0.11, remove in 0.12', () => {
  // All tests for things deprecated in 0.11
});

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

Пометка устаревших комментариев комментариями

Последний шаг, который вы увидите в приведенных выше примерах, - это использование комментариев для отметки устаревшего API. Это важно для любого разработчика, использующего IDE или редактор, который может анализировать эти комментарии и предоставлять информацию о том, как тот или иной API устарел.

Чтобы упростить использование этих функций, добавьте следующий комментарий в строку прямо над объявлением свойства или класса.

/** @deprecated since VERSION */

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

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