DevGang
Авторизоваться

Сигналы в Angular и важность декларативного кода

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

Мы хотим сосредоточиться на одной важной вещи: декларативном коде.

Эта статья была вдохновлена примером компонента React от Дэна Абрамова:

// React
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
  }
  return <h1>{heading}</h1>
}

Он принимает массив и значение-заполнитель, выполняет некоторые вычисления и возвращает некоторый текст.

  1. No videos here
  2. 1 Video
  3. 2 Videos

Вот такие комбинации.

Итак: нам нравятся функциональное программирование, и React в некотором роде придерживается функционального мышления.

f(state) = UI

Здесь мы не будем упоминать о hook, так как суть не в этом. Дело в том, что функциональное программирование является подмножеством декларативного программирования. И нам нравится декларативный код. И пример этого конкретного компонента нам не радует глаз.

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

Этот пост предполагает базовые знания сигналов (и немного React для сравнений)!

Функционал декларативный

Давайте вернемся к компоненту:

// React
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
  }
  return <h1>{heading}</h1>
}

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

Чтобы понять, каким может быть heading, мы должны покопаться во всех остальных компонентах.

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

Это был бы декларативный подход:

// React (declarative)
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;

  const heading = count > 0
    ?  count + ' ' + (count > 1 ? 'Videos' : 'Video')
    :  emptyHeading;

  return <h1>{heading}</h1>
}

Каждое объявление содержит всю логику для этой конкретной константы. Когда мы читаем const heading = this_thing, в нашем сознании читаем "Это то, о чем идет речь в heading". И нас не волнует остальная часть компонента.

Недостатком здесь является то, что троичный оператор может быть более трудным для чтения и немного неудобным для записи. Мы также не можем на самом деле использовать промежуточные переменные (такие как noun в предыдущем примере) без странных синтаксисов, подобных IIFE.

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

Это особенно важно для нас как консультанта: нам нужно иметь возможность посмотреть на какой-то код и сразу понять его логику. Почему? Потому что мы не можем позволить себе понять весь проект, как и клиент! Время - деньги.

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

Классы

По сравнению с чистыми функциями класс представляет собой другую ментальную модель. Это не потенциальный поток инструкций, а группа свойств и методов.

Это означает, что предыдущий компонент React (императивный) не может быть написан таким же образом, если мы используем класс. Нам пришлось бы поместить логику внутри метода. Это было бы и некрасиво, и странно.

// Angular (kinda, you get the point)
class VideoListComponent {

  @Input() videos;
  @Input() emptyHeading;

  count = 0;
  heading = '';

  ngOnChanges(changes) {
    this.count = changes.videos?.currentValue.length;
    this.heading = this.count > 0
      ?  this.count + ' ' + (this.count > 1 ? 'Videos' : 'Video')
      :  this.emptyHeading;
  }
}

Сравните это с тем, насколько чистыми были предыдущие примеры React. Мы не хотим касаться методов жизненного цикла. Мы хотим объявить производные значения: написать руководство.

С классами мы можем использовать getters для объявления производных состояний.

class VideoListComponent {

  @Input() videos = [];
  @Input() emptyHeading = '';

  get count() {
    return this.videos.length;
  }

  get heading() {
    if (this.count > 0) {
      const noun = this.count > 1 ? 'Videos' : 'Video';
      return this.count + ' ' + noun;
    }
    return this.emptyHeading;
  }
}

Обратите внимание, что, поскольку getters — это функция, легко использовать промежуточные переменные (noun) и некоторые искры императивного кода (который самодостаточен и не разбросан по всему компоненту).

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

Это скоро закончится, как только мы получим сигналы.

Signal

class VideoListComponent {

  // I think this is what we'll end up with, reactive
  // inputs as signals with default values, or something like that!
  videos = input([]);
  emptyHeading = input('');

  count = computed(() => this.videos().length);

  heading = computed(() => {
    if (this.count() > 0) {
      const noun = this.count() > 1 ? 'Videos' : 'Video';
      return this.count() + ' ' + noun;
    }
    return this.emptyHeading();
  });
}

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

  • Это очень явно
  • Это декларативный

Еще:

  • Это производительность по умолчанию (как и любая другая реализация Signal на самом деле), которая, по нашему мнению, является будущим любого фреймворка. Мы не поклонник установки "оптимизируйте это позже, если вам это понадобится", потому что в больших приложениях можем не осознавать, насколько дорогим будет конкретное вычисление через 3 месяца с гораздо большим количеством кода. И будет трудно вернуться к инкриминируемому коду.
  • Нам больше не понадобится zone.js

Signal имеют множество разновидностей, по одной для каждой использующей их среды. Сравните это с подходом Solid:

// Solid
function VideoList(props) {
  const count = () => props.videos.length;
  const heading = () => {
    if (count() > 0) {
      const noun = count() > 1 ? "Videos" : "Video";
      return count() + " " + noun;
    }
    return props.emptyHeading;
  }
  return <h1>{heading()}</h1>
}

Но у него есть несколько небольших предостережений.

  • Почему везде есть стрелочные функции? Мы должны знать, что в Solid, чтобы сделать переменную реактивной, она должна быть функцией. Но это немного трудно districate на первый взгляд.
  • Видя функцию, мы не знаем, является ли она реактивным производным состоянием или обработчиком событий. Они выглядят одинаково, поэтому нам нужно прочитать код, чтобы узнать, что он делает (или, надеюсь, у переменной отличное имя!).

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

Так почему же нам нравится Angular с сигналами? И почему нам нравится работать с классами таким образом?

Потому что с сигналами Angular мы вынуждены сообщать, что значение является производным состоянием. И мы будем вынуждены сказать, что ввод следует рассматривать как сигнал (если мы этого хотим!). И написание императивного кода, такого как самый первый пример этой статьи, в значительной степени обескураживает природа классов в реактивном контексте. Если ваш код выглядит плохо, вы, вероятно, возненавидите его, может быть, завтра, может быть, через 3 месяца.

С React у нас есть возможность писать императивный код. У нас красивый и короткий синтаксис. Но у нас нет мелкозернистой реактивности, а у Hook есть правила (думаем, будет справедливо сказать, что ментальная модель React великолепна, пока вы не добавите Hook, а они часто используются).

В Solid мы обычно имеем дело с большим количеством анонимных функций, и обращение с props может быть довольно странным (они реактивные, но мы получаем их как обычные значения, деструктурирование — это странно).

С Angular у нас есть четкий синтаксис, и нас подталкивают к декларативному коду. Налог платить немного больше кода, чем альтернативы.

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

Многие люди критикуют классы. У них есть на это веские причины. Так почему же ценят их, даже если люблю функциональное программирование?

Если мы используем классы с немного FP-мышлением (неизменяемость, свойства являются константами), многие их недостатки исчезают.

Радует, что нам больше никогда не понадобится ngOnChanges. Или некоторые установщики ввода, чтобы установить некоторые значения BehaviorSubjects.

PS. Пожалуйста, не воспринимайте это как критику React или Solid.

#JavaScript #Angular #React
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу