Pull to refresh

Comments 34

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

Я — евангелист строковых 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 и 2 — бесценны.
Абсолютно непонятно, почему автор оригинального текста решил предпочесть числовые enum. Потом ему кто-то (или даже он сам) добавляет новый элемент в начало enum — и привет, все enum-проверки сломались.
С учетом того, что в TS всё уже хорошо с discriminated unions, вам и строковые енумы нафиг не нужны. Зачем, когда то же самое сделает
type FooBar = 'foo' | 'bar'
покажите пример со switch или if
Всё то же самое, что и с енумом. Да, у вас в коде явно будут написаны все эти 'foo' и 'bar' — но какая разница если это всё тайпчекается. Измените тип — компилятор этим сравнениям (и свичам) даст отлуп и потребует их поменять. Так же, как если вы значение енума переименуете.
Хорошо.Но есть нюанс
someVar  =  GENRE.ROMANTIC

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


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

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

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

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

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

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


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

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


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

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

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


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


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

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


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

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


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

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

В этом случае ещё может помочь as const, которое отключает авторасширение типа: const x = 'value'; // x: 'value'. Работает с любыми значениями, а свойства в объектах ещё и помечает как readonly

`as const` помогает с выводимым типом, но не с автокомплитом.

Я для себя решил, что в подобных случаях (когда нужно создать константу из union type), лучше явно указать ей тип.

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

Да, в 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. Логика связи полей у дата-класса, вынесенная во внешний «трансформер-вычислитель» — сразу закладывает в вашу архитектуру не только гибкость, но и возможность менять формулы и поведение на лету, с теми же коллекциями объектов, на том же экране!
Эти классы используются для передачи и хранения данных. Модификацию полей в них — и всю логику вычисления — производят специальные классы, только у которых есть доступ. Формулы вычислений содержит, скажем, специальный GeometryTransformer.

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

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

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

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


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

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

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

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

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

movie.genre = GENRE.ROMANTIC | GENRE.COMEDY;

лучше так


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

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


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

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

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

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

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

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


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


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


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

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

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

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

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

class Circle {
radius: number;

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

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


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


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

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


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

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


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

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


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

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


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

Sign up to leave a comment.

Articles

Change theme settings