Я повесил у себя в подвале боксерскую грушу, приклеил на нее стоковое фото типичного менеджера и запихал внутрь динамик, чтобы он проигрывал фразы, которые меня злят. Например, груша говорит: «Бизнесу не нужен твой идеальный код. Ему нужно решить проблему так, чтобы прибыль покрыла затраты. Если для этого нужен говнокод, значит будет говнокод». И начинаю дубасить.
Недавно я добавил в грушу запись: «Типы — это сложно и ненужно». В этот момент я бью так сильно, что рука чуть не ломается. Потому что с меня хватит. Пару месяцев назад я пережил один из самых вопиющих кейсов в своей карьере.
Мой друг Антоха попросил меня помочь с решением для одной большой-большой корпорации. Я согласился, и мы влезли в бездонную пучину корпоративного абсурда, кранча, войны с ничего не понимающими коллегами и всеми видами несправедливости. Нам ничего нельзя говорить, поэтому мы будем говорить про типы, чтобы такая фигня никогда ни у кого не повторялась.
1
— (Антоха rcanedu ) Мне поручили разработку внутренней платформенной тулзы — библиотеки для выражения программных сущностей в виде объектно-ориентированных моделей и для однообразной работы с API сервисов. То есть, инструмента для взаимодействия с данными, которые ходят от источника к отображению и обратно.
В этом пути огромное количество форматирований, конвертаций, расчетов и преобразований. Громадные структуры, сложные иерархии, многочисленные связи всего со всем. В таких паутинах очень легко потеряться. Ты видишь куски данных и не знаешь, что с ними можно делать, а что нельзя. Тратишь кучу времени, чтобы разобраться. Это сильно затягивает разработку. Проблему решает только хорошее описание типов.
Из-за отсутствия подходящей типизации во многих решениях становится труднее достигать одинакового поведения программы во время исполнения и во время компиляции. Типы должны давать полную уверенность, что все то же самое и так же будет происходить и во время исполнения. То же могут дать и тесты. Лучше всего опираться и на то и на другое. Но если выбирать между типами и тестами — типы гораздо надежнее и дешевле.
Когда делаешь фронт, есть два источника проблем — пользователь и бекенд. С пользователем все просто — есть много удобных, абстрагирующих от пользовательского I/O библиотек и фреймворков (react, angular, vue и другие).
Во взаимодействии с бэкендом другая история. Его виды многочисленны, а реализаций — тьма. Один стандартный подход к описанию данных не определишь. Из-за этого придумали костыли вроде «нормализации структуры данных», когда все входящие данные приводятся к жесткой структуре, и если что-то пошло не так, начинаются исключения или работа в нештатном режиме. Это должно ускорять и упрощать разработку, но на деле требуется куча документации, UML-диаграммы, описание фич.
Архитектурная проблема во взаимодействии клиентской и серверной части возникла, потому что фронтенд повзрослел. Он стал полноценным делом, а не просто версткой поверх бэкенда. Раньше взаимодействие клиента и сервера задавали только разработчики серверной части. Теперь фронты и бэки вынуждены договариваться и плотно сотрудничать. Нужен инструмент, который позволит структурировать работу с API сервисов-источников данных, избежать потери истинности данных, и в то же время упростить дальнейшие преобразования. Эта проблема должна решаться всем сообществом.
— (Фил) Если ты бэкендер, у тебя полно взрослых решений и практик по моделированию данных для приложения. Например, в C# ты фигачишь класс модели данных. Берешь либу, какой-нибудь EntityFramework, либа поставляет тебе атрибуты, которыми ты обмазываешь свои модели. Говоришь либе, как ей достучаться до базы. Дальше юзаешь ее интерфейс для манипулирования этими данными. Эта штука называется ORM.
Во фронтенде мы так и не определились, как лучше это делать — ищем, пробуем, пишем нелепые статьи, потом все опровергаем, начинаем заново и все никак не придем к единому решению.
— (Антоха) Все что я писал раньше имело одну большую проблему — узкую специализацию. Каждый раз библиотека разрабатывалась с нуля и каждый раз она была заточена под один вид клиент-серверного взаимодействия. Все это происходит из-за отсутствия подходящей типизации.
Я считаю, без статической типизации сложно себе представить универсальную библиотеку для работы с API и выражения предметной области. В ней будет много рефлексии, она будет содержать огромное количество документации, будет обрастать разными приложениями с указанием практик под тот или иной вид. Это не упрощение.
Хороший универсальный инструмент такого рода должен давать полное представление о данных на любом срезе, чтобы всегда точно знать, как работать с этими данными, и зачем они нужны.
— (Фил) Нужна либа, которая даст возможность подробного описания и управления каждой сущности, получения данных для этой сущности с разных ресурсов с разным интерфейсом, от REST API и json-rpc до graphQL и NQL. Которая позволит держать разрастающуюся кодовую базу и структуру в строгости и порядке. Простой и понятной в использовании. В любой момент предоставляющая полное и точное описание состояния сущностей. Мы хотим абстрагировать модули своих пользователей от слоя данных настолько, насколько это возможно.
Первым делом мы посмотрели существующее. Нам ничего не понравилось. Почему-то все библиотеки для работы с данными сделаны или на js, или с кучей any наружу. Эти any все портят. Они закрывают глаза разработчикам, говоря, что такая библиотека тебе мало чем поможет, что ты не сможешь ориентироваться в типах своих данных, не сможешь выразить их связи. Всё становится ещё хуже, когда используется несколько API разных видов или оно неоднородно.
Все эти библиотеки не были достаточно защищены типами. Поэтому они создают больше проблем, чем решают. Проще их не использовать, а делать свои доменно-специфичные решения.
Поэтому, вместо узкозаточенной либы, на которую стоял таск, мы решили делать гораздо более мощную и абстрактную — подходящую для всего.И мы невероятно верили в свою правоту, потому что только так и создаются по-настоящему хорошие вещи.
2
— (Антоха) Как часто бывает, я ждал все виды доступа, чтобы меня пустили в репозиторий компании. А такое может длиться и по несколько недель. В моем случае заняло всего одну. В это время, основываясь на своем опыте создания аналогичных библиотек, я декомпозировал задачу, оценивал сроки.
Проблема в том, что раньше, как и все, я делал очень узкоспециализированные решения. Работа над универсальным инструментом влекла дополнительные проблемы. Система типов выходила крайне сложной, а опыта проектирование подобного не было ни у меня, ни у кого либо еще. Хуже того — разработчики вокруг меня вообще не представляли себе статическую типизацию.
Но я начал делать так, как считал правильным. На дейли я рассказывал, что и зачем делаю, но меня в принципе никто не понимал. Мои вопросы команде касательно возникшей проблемы всегда оставались без ответа. Меня словно и не существовало. Чувак, который делает что-то очень сложное и непонятное.
Я понимал, что javaScript здесь не подойдет никак. Нужен был ЯП с мощной моделью типизации, отличным взаимодействием с javaScript, имеющий большое комьюнити и серьёзную экосистему.
— (Фил) Я долго ждал дня, когда Антоха поймет прелесть typeScript.
— (Антоха) Но и в нем есть ряд проблем, которые сводят с ума. Типизация есть, но идеально соответствия между выполнением программы у меня и выполнением у пользователя все равно нет. Поначалу Тайпскрипт кажется сложным. Его приходится терпеть. Всегда хочется взять и кинуть какой-нибудь object или any. Постепенно углубляешься в язык, в его систему типов, и тут начинает происходить интересное. Он становится волшебным. Можно типизировать вообще все.
— (Фил) Впервые в жизни мы в чем-то сошлись и приступили.
Первый шаг — пользователь должен описать схему данных. Прикидываем, как оно выглядит. Что-то вроде такого:
type CustomerSchema = {
id: number;
name: string;
}
const Customer = new Model<CustomerSchema>(‘Customer’);
Этот код означает, что есть схема данных, описывающая кастомеров. У каждого кастомера есть id, представленный в виде целого числа, и имя, в виде строки.
Следом создается модель по этой схеме, переданной дженериком. Главная проблема такого подхода в том, что мы определяем тип данных с помощью дженерика, передавая туда тайпскрипт-тип. Так делать нельзя, потому что типы тайпскрипта исчезают после компиляции, а они нам нужны.
Хорошо, что мы быстро откинули такой вариант. Сама схема — это в каком-то смысле и тип, и значение. Когда мы это поняли, стало необходимо не просто декларировать свойства, но и использовать их. Нам сразу пришло в голову: ведь в языке есть такие штуки, которые одновременно и типы, и значения. Это конструкторы. Теперь объявление схемы приобрело такой вид:
/**
name: String
String - встроенный в js тип-значение: StringConstructor
для нас было очень важно не вводить для этих целей свои типы
чтобы упростить понимание и использование библиотеки
*/
const customerSchema = Schema.create({
id: Number,
name: String,
});
Вот это уже можно сделать. Здесь дженерик тоже есть, но он выводится из объекта, который был скормлен схеме. А самое главное, Number и String существуют в рантайме. Но тут тоже есть проблема: юзер может скормить сюда всё, что он хочет. Например:
const customerSchema = Schema.create({
id: 1,
name: 2,
});
Потому что мы ничего не типизировали. Можно написать код нашей `Schema.create` так, чтобы она плевалась исключениями времени выполнения. Всякие `if (!(property instanceof String)) throw new Error(«читай спеку, мудак»)`. Но это плохой подход.
Во-первых, спеки никто не читает, во-вторых, эта ошибка может произойти когда угодно. И делая либу, ты не можешь предсказать масштабы потенциальных бедствий. Этот рантайм эксепшн может убить человека, затопить жилой район, или предоставить пользователям ошибочные данные об их штрафах.
Есть очевидный способ подобного избежать. Мы просто сделаем свое дело и опишем тип данных, которые принимает Schema.create.
Примерно так:
// вспомогательный тип для рекурсивного определения
type Map<T> = {
[key: string]: T | Map<T>;
};
/**
кусок возможных типов значений декларации,
существующие как во время компиляции,
так и в рантайме.
*/
type NumberType = Template<NumberConstructor, number, 'number'>;
type StringType = Template<StringConstructor, string, 'string'>;
type SymbolType = Template<SymbolConstructor, symbol, 'symbol'>;
type BooleanType = Template<BooleanConstructor, boolean, 'boolean'>;
type DateType = Template<DateConstructor, Date, 'date'>;
interface ArrayType extends Array<ExtractTypeValues<Types>> {};
type Types = {
Number: NumberType;
String: StringType;
Symbol: SymbolType;
Boolean: BooleanType;
Date: DateType;
Array: ArrayType;
};
// алиас для нашего типа
type MapTypes= Map<ApplyTemplate<Types>>;
// алиас для типов декларации - конструкторы
type Declaration = ExtractInputTypes<MapTypes>;
interface Schema<...> {
// Дефинишн типа функции, которая создает схему.
create: <T extends Declaration>(
declaration: ConvertInstanceTypesToConstructorTypes<T>
) => Schema<T>;
};
Здесь Антоха психанул и добавил возможность иметь отдельно типы схем, а также обеспечил вывод типов в привычном виде. Используя структурную типизацию на всю катушку, он, как великий комбинатор, путем десятков конвертаций позволил пользователю видеть типы атрибутов схемы в примитивах, а не в конструкторах.
даже этот абзац написал он сам, потому что я бы не позволил себе такого задротского слога
type CustomerSchema = {
id: number;
name: string;
};
const customerSchema: CustomerSchema = Schema.create({
id: Number,
name: String,
});
Сейчас мы можем свободно маппить конструкторы на примитивы и наоборот. Кажется, что это довольно простой кейс, но я был поражен количеством добавленных типов.
Теперь у пользователя есть только одна возможность неправильно использовать создателя схем. Он может скастить всё к any. Это очень интересный момент — скармливать чужой либе any, когда не знаешь на 100%, что это сработает, может только конченный придурок.
Но такие придурки существуют, причём существуют в том же самом мире, где есть другие придурки, пропускающие это говно на кодревью. Поэтому мы должны защититься и от этого тоже. Мы добавляем внутрь `Schema.create` наши проверки на инстансоф. У 99% людей это лишний оверхед на производительность, остальные выявят проблему чуть раньше. Мы долго думали, нужно ли это делать. Я считаю — а пошли они! Но мы сделаем, потому что мы профессионалы.
Идём дальше. У нас есть инструмент, чтобы описать схему данных, но у нас нет инструментов, чтобы её изменять. Здесь есть проблемы. Сейчас мы можем пользоваться схемой так:
const customerSchema = Schema.create({
id: Number,
name: String,
});
// вот тут vscode предлагает нам: id, name
Schema.raw(customerSchema).
// если мы напишем
// это скомпилируется без проблем.
Schema.raw(customerSchema).id;
// такой код будет подчеркнут красным и не скомпилится.
Schema.raw(customerSchema).neId;
Но. Если дать пользователю возможность добавлять в схему свойства с помощью мутации объекта схемы:
const customerSchema = Schema.create({
id: Number,
name: String,
});
if (true) {
customerSchema.add({gender: String});
}
// здесь мы уже не знаем во время компиляции,
// что у схемы есть свойство gender.
// Это значит, что вся наша работа по выводу типов не имеет никакого
// смысла.
Schema.raw(customerSchema).
То есть, если у схемы будет мутабельное апи, наша проверка типов не сможет работать, и дураки легко сломают наш код. А если пользователь может присвоить схеме свойство gender, считай, что это свойство у схемы есть (да, в тайпскрипте есть магия, меняющая тип this внутри метода!). Он может сделать это внутри какого-нибудь условия или вообще в другой части кодовой базы. Компилятор не сможет точно знать, в какой момент у схемы есть это свойство, что неизбежно приведет к ошибкам времени выполнения.
Я понял это не сразу, и это было большим сюрпризом. Мозг подкинул мне бомбу замедленного действия, соврав, что у него есть видение. Работа уже была проделана, поэтому пришлось искать выход. Мы нашли его в иммутабельности.
Вот так:
const customerSchema = Schema.create({
id: Number,
name: String,
});
// customerSchema.add({gender: String});
// Пусть эта штука создаёт новую схему, а не меняет предыдущую.
// Вот так:
const customerWithGenderSchema = customerSchema.add({gender: String});
// теперь все ок.
// Пишем:
Schema.raw(customerWithGenderSchema).
// видим id, name, gender
// Пишем
Schema.raw(customerSchema).
// видим id, name
Тут мы используем иммутабельный интерфейс, для того чтобы во время компиляции точно знать, какое состояние у схемы. И совершенно не важно, как оно работает под капотом.
Вывод типов после создания схемы и вообще сами схемы нужны вот зачем:
const customerSchema = Schema.create({
id: Number,
name: String,
});
const repository = RepositoryManager.create(openApiDriver, {
// config
});
const Customer = Model.create(repository, customerSchema);
Customer.getAll().first().
// вот тут ide будет знать, что у инстанса данных есть поля id, name и gender.
// для нас это значит, что вот так
Customer.getAll().first().age;
// написать будет нельзя. Этот код не скомпилится, бага не поедет на прод,
// все в плюсе.
Мы выводим тип getAll из типа схемы.
Примерно так:
type MapSchemaToDriver<S extends Schema, D extends Driver> =
InferSchemaDeclaration<S> extends SchemaDeclaration
? InferDriverMethods<D> extends DriverTemplate<IMRReader, IMRWriter>
? Repository<InferSchemaDeclaration<S>, InferDriverMethods<D>>
: never
: never;
interface Repository<D extends Driver, S extends Schema> {
get: <T extends DSLTerm>(dsl: ParseDSLTerm<T>) => MapSchemaToDriver<S, D> extends RepositoryTemplate ? ApplyDSLTerm<MapSchemaToDriver<S, D>, T> : Error<’type’, ‘Type error’>;
}
То есть, тайпскрипт умеет так:
«Эй, тип B, сейчас я передам тебе тип A, и потом у тебя будут такие же имена свойств, как у него, но их значения будут иметь другой тип, производный от соответствующего свойства типа A. Вот такой. Вычисли-ка мне это во время компиляции».
Это магия. Настоящий конвейер прокси и декораторов.
Мы научили свой тип «репозиторий» предоставлять методы, считывающие данные, и предоставляющие пользователю доступ к этим данным с проверкой типов во время компиляции. А типы мы выводим, исходя из типа схемы.
Дальше ещё круче. Схема существует и в рантайме, а значит мы можем генерировать проверки данных во время выполнения. Чувак будет писать:
«Эй либа, у меня есть хранилище по адресу *бла-бла-бла.ком*, там лежат тысячи записей о клиентах. Я думаю, у каждого клиента есть id и имя. Достань-ка мне оттуда первых сто клиентов, и если данные валидные, выведи их на экран».
Почти идеал.
2.5
Мы не написали код, мы просто описали типы, и теперь, по этим лекалам легко сошьем рабочее решение. Проектирование типами в деле.
Мы не знаем, какие данные будут у пользователя — кастомеры, заказы, статистика полетов космических кораблей, или что угодно еще. Мы смогли научить либу работать с любыми данными. Пользователь просто опишет свой домен, и библиотека позаботиться о том, чтобы всё было корректно.
Голова идёт кругом от того, насколько с этим дизайном сильно можно улучшить решение. Мы можем автоматически валидировать данные на основании схемы, заставлять пользователя обрабатывать все возможные исключения, поставлять в составе библиотеки типы, которые позволят пользователям создавать модули, работающие с определенными источниками данных. Мы можем позволять пользователю передавать в запросах только валидные селекторы, потому что мы знаем, какие именно данные он хочет получить.
Наша библиотека не ODM и не ORM — она IMR (Isomorphic Model Representation)
Более абстрактная, она может работать с различными хранилищами, различными API одного или разных сервисов. Ей неважно, как структурированы данные. В отличие от аналогов, мы не реализуем select-where сами, предоставляя эту возможность пользователю.
Самое главное — мы формируем систему, в которой пользователь либы будет точно знать, в каком состоянии его данные в каждой ветви выполнения приложения.
Эти принципиально. На бэкенде тебе не нужно абстрагироваться от слоя данных. Бекенд и есть слой данных. На клиентской стороне, в сложных системах, тебе нужен единый фасад над всеми сторонними источниками данных, кроме пользователя. В идеале, ты берёшь своё фронтенд приложение, и переносишь его на апи с совершенно другой структурой, меняешь несколько строк кода, и всё работает.
И мы были уверены, что корпорация — махина с неограниченными бюджетами на разработку, когда дело доходит до реально полезных вещей — расцелует нам ноги, будет носить на руках и даст карт-бланш на всю дальнейшую работу.
Как бы не так.
3
— (Антоха) Мы продолжали делать либу, но я чувствовал, что могу сорвать сроки. Поэтому мы решили, жестко перерабатывать, чтобы успеть собрать вариант, который всех убедит. Реально работать днями и ночами. Я тратил все восемь часов, приходил домой, списывался с Филом, который заканчивал свою основную работу и убивал еще полночи на этот проект.
Но когда я что-то заливал в репозиторий, никто не мог понять, «что там еще за сложные типы». Я объяснял на дейли снова и снова, но натыкался на полный игнор. Энтузиазм и вера в правоту настолько придавали сил, что был почти уверен — скоро результат переломит недопонимание. Наконец в репо заглянул лид, и увидев там только типы, посчитал, что я нихера не делаю
Хрен знает, чем он слушал на дейли.
Лид надавил на сроки. Сказал, раз время задекларировано для бизнеса, двигать ничего нельзя. Но я видел, как и над чем здесь работают люди, я прекрасно понимал, что на сроки всем плевать. Иногда малейшие фичи пилятся годами. Лид просто не принимал идею сильной типизации. Он не привык к типам, потому что у него их нет. Индустрия, направление и все вокруг меняется, и вся эта нетипизированная мешанина, плодящая ворох проблем, должна уйти в прошлое.
— (Фил) Антоху наняли как суперсиньора с видением, чтобы он пришел и рассказал команде, как решить проблему. Так ему сказали на собесе. А теперь выставили так, что нанимали аджайлового солдата, который должен пилить в срок, что скажут.
Они увидели непривычное решение и вместо того, что рассмотреть его, отказались сразу. Не захотели идти против привычек. Это было явно политическое решение, а не инженерное.
— (Антоха) В итоге возник конфликт, мы обменялись гневными письмами. Меня обвинили в том, что я ничего не делал все это время, попросили закончить несколько сабтасков. Я доделал и ушел. Отдохнул пару дней, полностью удалил все исходники библиотеки со своих репозиториев и локальных хранилищ. Недели бессонных ночей ушли в трубу из-за простых человеческих эмоций, конфликтов, лени, высокомерия и прочей чуши, которая вечно отравляет развитие инженерных дел.
И в этот момент я понял, для чего на самом деле нужны софт-скиллы в разработке. Не для того чтобы быть котиком и со всеми дружить, не для психологического комфорта команды и веселых тимбилдингов. Софт скиллы нужны, чтобы защищать инженерные решения.
Потому что они беззащитны перед непониманием, интригами, соперничеством и всякими подковерными играми. Чтобы делать хорошие дела, надо уметь убеждать людей в своей правоте. И у меня это не получилось.
4
— (Фил) Когда Антоха мне сказал, чем все закончилось — я охренел. Сделано слишком мало! Что?! Засранцы получили двух разрабов вместо одного, я тратил кучу своего времени, Антон работал по восемь часов, потом созванивался со мной, снова работал, а потом мы вместе шли читать про все эти ODM/ORM и прочее дата-рилейтед говно, чтобы не допустить глубоких ошибок. Мы делали инструмент, которым будут пользоваться тысячи наших коллег. Короче, я мог бы понять любую претензию по качеству решения, но не «Чё-то вы нихера не сделали, пацаны».
То, что Антоха все стер, меня вообще убило. Этот инструмент был важнее просто работы по найму. Фиг с ними, с корпорациями, их жирными офферами и комфортной средой для сложной разработки. Такая либа нужна.
Поэтому мы решили начать с чистого листа, закончить с типами, сделать что-то минимально работающее, и заопенсорсить. Сейчас наш бюджет — это энтузиазм, который чертовски быстро кончается.
У нас есть приватный репо с кучей типов, но без реализации. В итоге мы хотим получить что-такое:
/*
Мы делаем сервис доставки пиццы роботами.
Под капотом здоровая платформа — микросервисы.
Нам нужно отобразить оператору таблицу со свободными роботами.
Вот так это выглядит в коде
*/
import { View } from '@view';
import { Logger } from '@utils';
// Модель робота — основной инструмент, который мы поставляем юзеру.
// Ей была передана схема данных и способ их получения.
import { Robot } from '@domain/models';
// Функция, которая использует либу для получения с бека
// ближайших свободных роботов
function foo() {
Robot.fetch({
location: {
area: 2,
free: true,
key: 'f49a6712', // все эти штуки - compile-time checked
}
})
.then(View.Grid.display)
.catch(Logger.error);
}
Мы с Антохой чуть не поубивали друг друга, когда обсуждали стратегию обработки ошибок. Я топил за монаду, он — за идиоматичные для js/ts промисы. В итоге решили сделать так и так, и управлять этим на уровне конфига либы. То-есть где-то в другом файле мы один раз решили, как хотим работать в случае неудачного запроса, и теперь здесь система типов точно знает, что у нас — промис или Result монада.
Также мы добавили возможность на уровне схемы данных определять стратегию обработки ошибок. Это позволит пользователю один раз написать какой-нибудь Logger.Error, не повторяя эти действия при каждом запросе.
/*
Приходит бизнес требование:
если мы подключены к такой то ноде,
при запросах к роботам нужно использовать дополнительный параметр - тип батареи.
и исключать другой параметр - скорость
И в таком случае мы должны использовать только роботов с определенным типом батарей.
Вот как это можно описать:
*/
import { Schema, Model } from 'imr';
import { Logger } from '@utils';
import { View } from '@view';
import { robotSchema } from '@domain/schemas';
import { batteryStationService } from '@domain/infrastructure/services/batteryStation';
import { Robot } from '@domain/models';
import { batteryNode } from '../services/nodeNames';
// Мы на лету расширяем предыдущую схему робота
// говорим, что у таких роботов есть тип батареи, который может иметь одно из двух значений,
// и говорим, что у этих роботов не существует параметра «скорость»
const robotSchemaWithBattery =
robotSchema
.add('battery', Schema.union('li-ion', 'silicon'))
.remove('speed');
// Функция, которая использует либу для получения с бека
// роботов на основании заданной ноды:
function foo(nodeName) {
// Это наш бизнес-кейс: если нода такая-то, значит наши данные выглядят по другому
if (nodeName === batteryNode) {
// Мы создаём штуку, которая получает данные по новой схеме
const CustomerRobot = Model.create(robotSchemaWithBattery, batteryStationService);
CustomerRobot
// Тут у нас компайл тайм поддержка параметров фильтра.
// Написать, например, 'li-notIon' не получится
.fetch({ filter: { battery: 'li-ion' } })
// Скармливаем данные гриду.
// фишка в том, что если грид писали умные люди, он будет декларировать тип данных, с которыми работает.
// Это значит, что если грид хочет рендерить скорость,
// а мы её выпилили из схемы, мы получим здесь ошибку времени компиляции.
.then(View.Grid.display)
.catch(Logger.error)
} else {
Robot
.fetch()
.then(View.Grid.display)
.catch(Logger.error)
}
}
5
— (Фил) Жизнь научила меня, что каждый раз, когда ты думаешь, что изобрел что-то в программировании, ты жестоко ошибаешься. В этом случае мы не думаем, что что-то изобрели, но мне кажется, мы первые попробовали типизировать абстрактный инструмент для работы с данными на таком глубоком уровне.
Но вот что главное я для себя вынес.
Нравится мне это, или нет, разработка — прикладная штука. Мы обслуживаем интересы бизнеса. Но когда ты пишешь библиотечный код, когда твоё решение станут использовать десятки команд — это тот редкий кейс, когда ты лучше всех знаешь, что нужно бизнесу. Потому что ты всю жизнь учился этому. То, какая должна быть библиотека, её дизайн и идеология — это не бизнесовый вопрос, он технический.
В следующий раз мы потратим больше сил на то, чтобы убедить менеджмент: нам нужно качественное решение с максимальной защитой от дурака. Потому что иначе нельзя. Потому что иначе вы потеряете в тысячу раз больше денег в будущем.
Я люблю большие корпорации — у них всегда есть бюджет на качественные решения. Просто нам надо уметь не только делать, но и доносить.
Над статьей также работали: rcanedu, arttom
Теперь вместе с arttom я веду подкаст «Мы обречены». Там все как в статьях — максимально напрямую о разработке, индустрии, бабле, собесах.