Чистый код для TypeScript — Часть 1

Наткнувшись на материал по принципам чистый код для TypeScript и прочитав его решил взяться за его перевод. Здесь я хочу поделиться с вами некоторыми выдержками из этого перевода, так как некоторые моменты чистого кода для TypeScript повторяют такие же принципы для JavaScript, я их здесь описывать не буду, если будет интересно перевод для JS уже публиковался на хабре(@BoryaMogila) или же можете ознакомится с ними в первоисточнике.



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


Переменные


Используйте enum для документирования


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


Плохо:


const GENRE = {
  ROMANTIC: 'romantic',
  DRAMA: 'drama',
  COMEDY: 'comedy',
  DOCUMENTARY: 'documentary',
}

projector.configureFilm(GENRE.COMEDY);

class Projector {
  // delactation of Projector
  configureFilm(genre) {
    switch (genre) {
      case GENRE.ROMANTIC:
        // some logic to be executed 
    }
  }
}

Хорошо:


enum GENRE {
  ROMANTIC,
  DRAMA,
  COMEDY,
  DOCUMENTARY,
}

projector.configureFilm(GENRE.COMEDY);

class Projector {
  // delactation of Projector
  configureFilm(genre) {
    switch (genre) {
      case GENRE.ROMANTIC:
        // some logic to be executed 
    }
  }
}

Функции


Избегайте проверки типов


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


Плохо:


function travelToTexas(vehicle: Bicycle | Car) {
  if (vehicle instanceof Bicycle) {
    vehicle.pedal(currentLocation, new Location('texas'));
  } else if (vehicle instanceof Car) {
    vehicle.drive(currentLocation, new Location('texas'));
  }
}

Хорошо:


type Vehicle = Bicycle | Car;

function travelToTexas(vehicle: Vehicle) {
  vehicle.move(currentLocation, new Location('texas'));
}

Используйте итераторы и генераторы


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


  • отделяет вызываемый объект от реализации генератора в том смысле, что вызываемый объект решает сколько элементов
    иметь для доступа
  • ленивое выполнение, элементы передаются по требованию
  • встроенная поддержка итерации элементов с использованием синтаксиса for-of
  • итераторы позволяют реализовать оптимизированные паттерны итераторов

Плохо:


function fibonacci(n: number): number[] {
  if (n === 1) return [0];
  if (n === 2) return [0, 1];

  const items: number[] = [0, 1];
  while (items.length < n) {
    items.push(items[items.length - 2] + items[items.length - 1]);
  }

  return items;
}

function print(n: number) {
  fibonacci(n).forEach(fib => console.log(fib));
}

// Print first 10 Fibonacci numbers.
print(10);

Хорошо:


// Generates an infinite stream of Fibonacci numbers.
// The generator doesn't keep the array of all numbers.
function* fibonacci(): IterableIterator<number> {
  let [a, b] = [0, 1];

  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

function print(n: number) {
  let i = 0;
  for (const fib of fibonacci()) {
    if (i++ === n) break;  
    console.log(fib);
  }  
}

// Print first 10 Fibonacci numbers.
print(10);

Существуют библиотеки, которые позволяют работать с итераторами так же, как и с собственными массивами, путем цепочки методов, таких как map, slice, forEach и др. Смотрите itiriri пример продвинутой манипуляции с итераторами (или itiriri-async для манипуляции с асинхронными итераторами).


import itiriri from 'itiriri';

function* fibonacci(): IterableIterator<number> {
  let [a, b] = [0, 1];

  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

itiriri(fibonacci())
  .take(10)
  .forEach(fib => console.log(fib));

Объекты и структуры данных


Используйте геттеры и сеттеры


TypeScript поддерживает синтаксис геттеров и сеттеров. Использовать геттеры и сеттеры для доступа к данным объекта гораздо лучше, чем напрямую обращаться к его свойствам. "Почему?" спросите вы. Вот список причин:


  • Если вы хотите реализовать больше, чем просто доступ к свойству, вам нужно поменять реализацию в одном месте, а не по всему коду
  • Валидацию легко реализовать на уровне реализации set
  • Инкапсуляция внутреннего состояния
  • Легко добавить логирование и обработку ошибок на уровне геттеров и сеттеров
  • Вы можете лениво подгружать свойства вашего объекта, например, с сервера

Плохо:


type BankAccount = {
  balance: number;
  // ...
}

const value = 100;
const account: BankAccount = {
  balance: 0,
  // ...
};

if (value < 0) {
  throw new Error('Cannot set negative balance.');
}

account.balance = value;

Хорошо:


class BankAccount {
  private accountBalance: number = 0;

  get balance(): number {
    return this.accountBalance;
  }

  set balance(value: number) {
    if (value < 0) {
      throw new Error('Cannot set negative balance.');
    }

    this.accountBalance = value;
  }

  // ...
}

// Теперь `BankAccount` инкапсулирует логику проверки.
// Если однажды спецификации изменятся, и нам понадобится дополнительное правило проверки,
// нам придется изменить только реализацию `сеттера`,
// оставив весь зависимый код без изменений.
const account = new BankAccount();
account.balance = 100;

Создавайте объекты с приватными/защищенными полями


TypeScript поддерживает public (по умолчанию), protected и private средства доступа к свойствам класса.


Плохо:


class Circle {
  radius: number;

  constructor(radius: number) {
    this.radius = radius;
  }

  perimeter() {
    return 2 * Math.PI * this.radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

Хорошо:


class Circle {
  constructor(private readonly radius: number) {
  }

  perimeter() {
    return 2 * Math.PI * this.radius;
  }

  surface() {
    return Math.PI * this.radius * this.radius;
  }
}

Уважаемые читатели, а какими принципами вы пользуетесь при использовании TypeScript?


Продолжение:
Вторая часть
Третья часть
Полный перевод

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 32

    +10
    Всё ниженаписанное подкреплено опытом написания немаленьких проектов с большим сроком поддержки и сменами с составе команды.

    Я — евангелист строковых Enum. В статье, ключи отличаются от значений — я считаю это за абсолютно нереальный случай. Согласен, что так писать нельзя. И всё же:

    1) Удобно дебажить (удачи в каких-нибудь React Devtools узнать значение в props, когда в Enum 20-30 значений)
    2) Спокойно можно добавлять новые значения, менять местами (можно перзистить значения и спать спокойно, зная, что ничего не сломается)
    3) Меньше размер бандла после компиляции (не катастрофа, но всё же)
    // обычный Enum
    // результат = {0: "BAR", 1: "BAZ", BAR: 0, BAZ: 1} 
    var Foo;
    (function (Foo) {
        Foo[Foo["BAR"] = 0] = "BAR";
        Foo[Foo["BAZ"] = 1] = "BAZ";
    })(Foo || (Foo = {}));
    
    // строковый Enum
    // результат {BAZ: "BAZ", FOO: "FOO"}
    var Bar;
    (function (Bar) {
        Bar["BAZ"] = "BAZ";
        Bar["FOO"] = "FOO";
    })(Bar || (Bar = {}));
    


    Магические геттеры/сеттеры… Хочется вернуться в прошлое и не допустить их появление в JS.
    Просто писать явно getSomething и setSomething.
      +1
      Поддерживаю.
      пп. 1 и 2 — бесценны.
      Абсолютно непонятно, почему автор оригинального текста решил предпочесть числовые enum. Потом ему кто-то (или даже он сам) добавляет новый элемент в начало enum — и привет, все enum-проверки сломались.
        +4
        С учетом того, что в TS всё уже хорошо с discriminated unions, вам и строковые енумы нафиг не нужны. Зачем, когда то же самое сделает
        type FooBar = 'foo' | 'bar'
          0
          покажите пример со switch или if
            +1
            Всё то же самое, что и с енумом. Да, у вас в коде явно будут написаны все эти 'foo' и 'bar' — но какая разница если это всё тайпчекается. Измените тип — компилятор этим сравнениям (и свичам) даст отлуп и потребует их поменять. Так же, как если вы значение енума переименуете.
              0
              Хорошо.Но есть нюанс
              someVar  =  GENRE.ROMANTIC

              дает более полное представление о контексте применения кода, чем
              someVar = 'romantic'


              Хотя бы потому, что романтическим может быть что угодно — от девушек до самолётов.
                +1
                более полное представление о контексте применения кода, чем
                someVar = 'romantic'

                let someVar: FooBar = 'foo'; // ok
                let someOtherVar: FooBar  = 'baz'; // compile error

                Какое еще «более полное представление» вам нужно? Бороться с duck typing в JS/TS не имеет ни малейшего смысла.
                  0
                  я вас не критикую. Вы можете писать как угодно.
                  Обилие строковых литералов в коде на мой взгляд неудобно.

                  В последнем комментарии я имел в виду читабельность кода. Мы смотрим на коммит, там изменилась одна строчка. По выбранному enum мы видим и понимаем смысл кода быстрее, чем просто по строковому литералу.

                  Если вы читаете мало коммитов, для вас это может быть не актуально.
                    0

                    Можно извратиться и совместить, как описано тут:


                    const FooBar = {
                        FOO = "foo",
                        BAR = "bar",
                    } as const;
                    type FooBar = (typeof FooBar)[keyof typeof FooBar];
                    
                    function Baz(f: FooBar) { ... }    // using FooBar type
                    
                    Baz(FooBar.FOO);    // using FooBar's value
                    
            0

            Умный поиск/рефакторинг в IDE гораздо лучше работает с явным enum, чем со строковыми константами.


            Со строковыми литералами компилятор, конечно, выдаст ошибку, если рефакторинг был произведен некорректно. Но с enum рефакторинг просто сразу будет корректным.

              0

              Idea отлично следит за union types

                0

                Хм. А и правда. Такое ощущение, что ситуация сильно улучшилась по сравнению тем, что было еще год назад.


                Есть нюанс, правда.


                const x = "string-from-type"; // type: "string"

                выводится как string (естественно). Нужно явно указывать тип:


                const x: MyType = "string-from-type"; // type: "MyType"

                В случае с enum автоматически выведенный тип переменной будет по как раз типом этого enum


                const x = MyType.stringFromType; // type: "MyType"

                Тут можно возразить, что хрен редьки не слаще и вообще дело вкуса.

          +1
          Геттеры и сеттеры — вопрос сложный и таким кратким текстом его не объяснить.

          Да, в Java с начала нулевых сложилась адовая практика добавлять на каждое поле класса геттеры и сеттеры. Это приводит к тому, что код дико разбухает, а заявленная гибкость требуется только для ничтожного проекта случаев. А 95 процентов геттеров и сеттеров — это просто мертвый лишний код.

          Практика показала, что геттеры и сеттеры в таком примитивном виде — не удобное решение. Гораздо делать отдельно чистые дата-классы и чистые объекты.

          Что такое чистый дата-класс? Это класс без логики, без методов, просто набор типизированных полей.

          class Circle {
            radius: number;
            perimeter: numer;
            surface: number;
          }
          


          Эти классы используются для передачи и хранения данных. Модификацию полей в них — и всю логику вычисления — производят специальные классы, только у которых есть доступ. Формулы вычислений содержит, скажем, специальный GeometryTransformer.

          Это даёт настоящую гибкость, так как геометрию можно теперь спокойно менять на лету, подставив просто нужный объект GeometryTransformer — хоть евклидовый, хоть Лобачевского. При этом все остальное не переписывается и даже не пересоздаётся — чистый hot swap, великолепная возможность кардинально изменить правила игры и любых расчетов на лету.

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

          class Shop {
           getGoodList(): Array<Good>
          }
          


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

          Но есть нюанс — об этом не надо думать, как о геттере, то есть об обертке над полем класса. Это метод, контракт для других классов, которые будут работать с Shop. А откуда магазин берет список товаров — это его личное дело. Там может быть поле. А может быть и обращение к другому классу.

          Краткие итоги:
          1. Не думайте «геттеры» и «сеттеры» — думайте о сокрытии реализации
          2. Для хранения данных используйте data-классы как хранилища полей, без методов
          3. Логика связи полей у дата-класса, вынесенная во внешний «трансформер-вычислитель» — сразу закладывает в вашу архитектуру не только гибкость, но и возможность менять формулы и поведение на лету, с теми же коллекциями объектов, на том же экране!
            +2
            Эти классы используются для передачи и хранения данных. Модификацию полей в них — и всю логику вычисления — производят специальные классы, только у которых есть доступ. Формулы вычислений содержит, скажем, специальный GeometryTransformer.

            Для меня это выглядит как «давайте возьмём плохое из ФП и ООП, объединим, и будем пользоваться».
              0
              если разбивать на поля и логику все подряд, то да, это очень плохое решение. Нужно комбинировать.

              Но такая композиция часто выручает в разработке игр, особенно когда могут меняться правила.
                0
                по поводу Circle — я выбрал не очень удачный пример. Смена геометрии вычислений в обычных приложения нужна очень редко )

                более хорошим и естественным примером дата-класса, который просто несет в себе информацию, это

                class Point{
                 x: number;
                 y: number;
                }
                


                В таком примере очевидно, что для такого класса геттеры-сеттеры избыточны, так как модель точки в пространстве не подразумевает взаимосвязи координат (но такая взаимосвязь возникает, если мы начинаем думать о функциях типа y =f(x).) Все зависит от выбранной модели. В данном кейсе Point — это просто коллекция x и y.
                  –1

                  Геттеры в js удобны тем, что если сделать очепятку в имени, то программа рухнет с исключением. Если использовать свойства, то будет молча пропускать:
                  const prog = { }
                  if (!prog.hasBug()) { // упадет с исключением
                  }
                  if (!prog.hasBug) { // всегда true
                  }

                    –2
                    На то и расчёт, что программа должна работать стабильно. В Вашем же случае, весь софт в дырах.
                0

                Напоминает typeclass из хаскелей. Есть адаптация для тайпскрипта fp-ts.

                +2

                Какие то вредные советы.

                  +2
                  А как же такой вариант?
                  const GENRE = {
                    ROMANTIC: 0x01,
                    DRAMA: 0x02,
                    COMEDY: 0x04,
                    DOCUMENTARY: 0x08,
                  };
                  
                  movie.genre = GENRE.ROMANTIC | GENRE.COMEDY;
                    0

                    лучше так


                    enum GENRE {
                      ROMANTIC = 1,
                      DRAMA = 2,
                      COMEDY = 4,
                      DOCUMENTARY = 8
                    }
                      0

                      Тогда уж лучше так:


                      enum GENRE {
                        ROMANTIC = 0b0001,
                        DRAMA = 0b0010,
                        COMEDY = 0b0100,
                        DOCUMENTARY = 0b1000
                      }

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

                        0
                        Нет, hex-числа в данном случае самые удобные, это проверено практикой целых поколений. Там и позиции битов более-менее видны (в отличие от десятичных), и читаются нормально и опечататься сложнее (не рябит в глазах от кучи нулей, как в двоичных).

                        Ну и прелесть битовых флагов как раз в том и состоит, что потом в коде поштучной возни с битами никакой и нет. Там чисто логические функции: проверить флаг &, добавить |. Всё наглядно и просто. И при этом одновременно компактно и эффективно.
                      +1
                      отличная вещь для флагов, которые могут сочетаться в одном признаке

                      только при отладке придется смотреть на этот список или заучить его наизусть )
                      +2

                      В дополнение к уже написанному, про Enum.


                      Встречал при изучении языка ещё одно мнение и решил его придерживаться — вообще не использовать этот тип. Мысль была в том, что TS стоит рассматривать как типизированный JS (JS+типы), который не оставляет следа в рантайме, Enum же, напару с Namespaces, генерируют свой код. И в том, что любые навыки, приобретённые при разработке на TS, должны остаться полезными и при возврате на JS (ничто не вечно под луной).


                      Ещё одна причина — мнимая типобезопасность при использовании не-строковых enum. Если я хочу, чтобы значением было что-то из enum, то я действительно хочу этого, а не вот этого. Строковые enum в этом плане работают куда более ожидаемо.


                      Ну и в случае, где мне хотелось бы использовать enum, мои потребности покрывает юнион строк.

                        0
                        по поводу следа в рантайме — добавьте const перед enum

                        Он исчезнет, а его используемые значения заинлайнятся

                        1. пример (не уверен, что сохранится)
                        2. документация
                          0

                          В babel к сожалению, это не работает.

                        +3
                        class Circle {
                        radius: number;

                        — плохо в обоих случаях. Не надо в JS/TS тащить это всё матёрое ООП. На него даже в дотнетах и джавах уже забили.
                          –1
                          Жесть, куда катится мир, ну не ужели так много людей разучились думать головой
                            +1

                            Ну, про enum уже обсудили выше. Пройдемся по остальным пунктам.


                            Избегайте проверки типов


                            • Код "хорошо" и "плохо" вообще разный — у классов разный интерфейс же. Совет звучит как "используйте полиморфизм вместо условного выполнения кода". Ничего Typescript-специфичного тут нет.

                            Используйте итераторы и генераторы


                            • А если вот мне не нужная ленивая генерация коллекции? Ну и опять же — а причем тут Typescript?

                            Используйте геттеры и сеттеры


                            • Опять нет ничего Typescript-специфичного
                            • Персональное мнение — категорически против. Это делает код менее очевидным: почему чтение или запись вот конкретно этого свойства класса вдруг привело к сайд-эффектам? При явном вызове методов это будет сюрпризом гораздо в меньшей степени.

                            Создавайте объекты с приватными/защищенными полями


                            • Применимо практически к любому ОО-языку (ладно, с натяжкой можно засчитать — ведь JS модификаторы доступа пока не поддерживает)
                              0

                              "почему чтение или запись вот конкретно этого свойства класса вдруг привело к сайд-эффектам?"


                              Ну, например, потому что требуется некая консистентность состояния объекта. А внешнему классу о ней знать не полагается (инуапсуляция-с). Поэтому вы не думаете о методе, когда пользуетесь типом со свойствами, так что очевидность только растёт.
                              Кроме того, свойства можно писать без опоры на поля. Тогда при использовании типа можно будет обращать внимание только на свойство как значение, а при его разработке не заморачиваться хранением и актуализацией, а оставить всё на откуп вычислимому свойству. Иногда очень удобно

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

                            Самое читаемое