Код они писали еще до того как многие из нас родились, а многие познакомились с компьютерами. Вас это настораживает?
Разумеется, настораживает. Во времена их активности именно в качестве кодеров многие современные подходы находились на стадии теоретических изысканий — следовательно, не были применимы к их опыту написания кода.
Некто может как никто другой в мире уметь обрабатывать дерево киянкой и долотом, но обязаны ли мы поэтому считать его непререкаемым авторитетом в современной столярной мастерской? А если учесть что этот мастер в последний раз работал с деревом десять с гаком лет назад, а потом "профессионально вырос" до руководства другими людьми?
Полезно, думаю, делать деструктивные изменения в базе делать не сразу, а, например, через релиз, по аналогии с удалением чего-то из PublicAPI проекта. То есть, перестали пользоваться полем в релизе A, пометили само поле как Deprecated, но именно удалять его будем только в релизе A+N. Правда, это в разы сложнее отслеживать, но зато откат проводить можно меньшей кровью.
А не стоит эти ресурсы рассматривать как «потраченные на удовлетворение компилятора», и тогда всё сойдётся. Описание типа, например, некой, уже будет включать в себя как минимум два теста:
тест на то, что при каждом вызове функции в неё будут переданы параметры именно того типа, который она ожидает.
тест на то, что независимо от реализации функции, результатом её вызова будет нечто определённое.
Есть, конечно, случаи, когда по какой-то причине нужный Вам тип невозможно выразить в рамках системы типов некого языка, однако это, по моему глубокому убеждению, такие вещи являются скорее недостатком дизайна, и возникают не настолько часто, чтобы быть аргументом против типизации вообще.
Приведу пример (я буду предполагать что система типов моего языка достаточно развита, чтобы всё это поддерживать):
fun getLength — всё, что мы можем сказать о этой функции — это её имя. Исходя из нашего опыта, мы могли бы предположить, что она возвращает(get) некую длину(Length), но чего — мы не знаем. Также мы не знаем, что такое этот Length на самом деле. Нам нужны несколько групп тестов для каждого из типов, которые мы потенциально собираемся передать в эту функцию в качестве аргумента. Исходя, опять же, из опыта, мы предполагаем, что длина — это неотрицательное целое число. Нам нужны тесты, проверяющие, что функция действительно возвращает число, что оно действительно неотрицательное, и что оно действительно целое (минимум три типа тестов, для которых мы не гарантируем полного покрытия, ведь всегда останется шанс, что getLength("aaa") === 3, но getLength("™") === 3.14). Кроме того, нам нужны отдельные тесты на то, что функция не изменяет состояния своих параметров.
fun getLength: List<Any> => Number — теперь нам уже не нужны вручную написанные проверки на принимаемый тип, ведь мы точно знаем, что если переданный параметр не является списком, то вызывающий код не пройдёт этап компиляции. Если также мы знаем, что базовый тип List не поддерживает мутации, то мы знаем, что вызов этой функции не меняет сам список. Нам всё ещё нужны проверки на то, что результат функции возвращает неотрицательное, и целое число.
Пусть целое число у нас описывается типом Int. Тогда, если мы имеем fun getLength: List<Any> => Int — мы точно знаем, что длина всегда является целым числом, и каким бы ни было тело функции, она не сможет вернуть нецелый результат, или результат больше максимального или меньше минимального значения для Int. Но стоп, мы не знаем, что будет если передать в функцию Null или Undefined. Какова их длина? Нужен тест!
Не совсем. Пусть в нашей системе типов, и Null, и Undefined являются отдельными типами. Так как наша функция определена как fun getLength: List<Any> => Int, она не содержит ни того, ни другого типа (Что выражалось бы, к примеру, как List<Any>|Null). Следовательно, никакой код не сможет передать внутрь Null, и никакая реализация не сможет вернуть Undefined. Потому этот класс тестов нам тоже не нужен. Нам по-прежнему нужны тесты на то, что результат неотрицательный.
Если мы определили fun getLength: List<Any> => Length, а тип Length у нас: typedef Length as Int where $value >= 0 — то мы перекладываем ещё один из наших вручную написанных тестов на плечи компилятора, ведь теперь это его работа — проверять что функция не возвращает значений меньше нуля.
Итого, из первоначальных классов тестов нам останется только проверить, что для списков предопределённой длины вызов getLength(list) вернёт строго ожидаемое значение. Так система типизации позволила нам писать тесты только одного типа вместо шести или семи, которые я обрисовал вначале, все остальные группы тестов «написаны» и выполнены компилятором.
Более того, обычно я даже не задумываюсь над тем, насколько огромно на самом деле количество тестов, которые я не написал вручную из-за систем типизации, но которые, тем не менее, постоянно выполняются в моём коде.
Но в этой статье не сравнивались покрытие тестами и сильная типизация, только сам факт наличия или отсутствия тестов (если я не прав, то укажите, пожалуйста, конкретное место в статье). По сути, статья является описанием опыта введения TDD в команду.
Кстати, замечу, что в отсутствие строгой типизации упомянутых в статье 2930 юнит-тестов, 100 тестов производительности и >400 интеграционных тестов было бы явно недостаточно, так как пришлось бы писать ещё пару тысяч тестов на то, что DeviceFactory::createLightBulbDevice действительно возвращает нечто, что ведёт себя в соответствии контракту LightBulbDevice, а DeviceEvent::getType никогда не возвращает строку.
Прочитал я это всё и задался вопросом: почему это я про Ceylon почти ничего не слышу в своих кругах? Это ведь, должно быть, просто мечта для программиста, стремящегося к подходу «если в коде баг, то код не компилируется».
Потому и следует начинать с написания тестов. Чтобы если/когда задор ушёл — выхлопом были хотя бы тесты, а не наполовину переписанный код.
Причём, с моей точки зрения, тесты в этом всём — как раз самое скучное, поэтому когда у меня снова случится обострение — большая часть тестов уже будет написана, и тут уже отвертеться много сложнее. :)
Получится, что нового ничего нет, а старое уже сломали
Как так? Неужели совершенно нет истории изменений и/или всё переписывание ведётся на мастер-ветке? Если так, но не в переписывании проблема, и даже не в увольнении сотрудника.
С точки зрения бизнеса жить нужно сегодняшним днем и обеспечивать непрерывность получения доходов
Действительно ли так много бизнеса, следующего этим постулатам? А как же пресловутый Customer Retention, над которым теперь все так трясутся? Это, само собой, к теме статьи относится только косвенно.
Ну почему так категорично. Предикация и проекция — две операции, которые не так чтобы близко были связаны между собой. Для малых кардинальностей, логически, есть возможность оптимизировать запрос так, чтобы этот столбец был "Ленивым" и вычислялся вообще прямо перед "Show" каждой строки (в последний момент), что будет ценно как раз при ограничении по памяти, т.к. позволяет при вообще не хранить в столбце значения — только "рецепт" и кеш-таблицу. Оптимизация менее применима при наличии сортировки по такому столбцу, но не снимается полностью даже в этом случае.
Про LINQ: Там в картинке явно указывается в тексте запроса, что, что некие сущности следует брать из кеша.
Не то чтоб это было плохо само по себе, но требует телодвижений в каждом use-site. Впрочем, принципиальную возможность это уже доказывает, следовательно, где-то должен быть способ то же самое делать более централизованно. Благодарю за просвещение.
С моей колокольни исходная фраза выглядит как «поведение вот такое, но почему именно — я найти не сумел».
Что я воспринял как «официальная документация не включает объяснений такому поведению». Иными словами, в моей интерпретации звучит как UB, тогда как, по видимости (и в комментариях уже много раз сказано) поведение в точности соответствует спецификации (i.e. «почему» — «по спецификации»). Это, безусловно, ошибка трансляции с моей стороны, но теперь я убеждён, что автор статьи тоже причастен. Ещё раз спасибо.
Мелкие справочники можно грузить только если они не часть предиката. Если они появляются в предикате — никакой особой разницы не будет, их всё равно придётся подключать. Опять же, всегда был под впечатлением, что для join с малой кардинальностью сама база вполне в состоянии сложить у себя в памяти hashTable из значений, и прирост тогда будет только для нескольких конкурирующих запросов с одного и того же сервера, а иначе расходом памяти и процессора на такой join с таблицей можно пренебречь на фоне основного запроса.
Кстати, в порядке просвещения меня: может ли LINQ сделать такую отдельную загрузку для малых таблиц и потом использовать эти таблицы в проекциях без настоящего join'a на стороне БД? Хотя бы в теории?
Но если гарантии нет, то не получится здесь совсем не смотреть в базу. Если у меня имеется некий кеш сущностей, и я хочу данный кеш пофильтровать по значению одного из атрибутов (не являющемуся единственным identity-ключом), то смотреть в базу мне всё равно придётся, чтобы дочитать потенциально недостающие элементы.
А если в запросе при этом есть сортировка по соседнему атрибуту, то кэш я вообще не имею права использовать (исключение — кеш содержит результаты запросов по ключу из текста самого запроса, и даже в этом случае не всегда).
Да, это может работать если клиенту возможно скармливать данные по кускам, как, например, Flux<T> в реакторе. Тогда при запросе можно быстро выдавать из кэша что есть, и если этого не хватило — выдать уже из базы данных. Так можно сильно уменьшить время первоначального отклика на запрос, если отклик на запросы клиентов часто содержит повторяющиеся данные.
К сожалению, плохо будет работать сортировка — в общем случае, в любой момент остаётся шанс, что в базе находится некий элемент, который по данному критерию ниже, чем начало кешированной страницы.
Согласен. С другой стороны, мелкие данные можно выгрузить в память один-единственный раз (если они иногда меняются, то писать обратно после изменения чем-нибудь неблокирующим) и потом работать с ними без запросов вообще. Это налагает свои ограничения, разумеется (например, все операции придётся писать императивно, если нет желания ставить SQL-совместимую in-memory-DB), но подход проверенный и работающий.
В исходной формулировке с уточнениями, правда, выходит, что запрос «хавал» очень много ресурсов сервера БД, а малое количество данных так не делает.
Можно, к примеру, делать break из проименованного if (вообще-то любого блока, но не суть).
И единственное, почему это мало кто любит — это то, что конструкция выглядит непривычно. А выглядит она непривычно потому, что так мало кто делает. А делают так мало потому, что не любят. Ну и так далее. А началось с того, что это не любил Дийкстра. Весьма нерациональный подход, на мой вкус.
забавные штуки, которые позволяют строить запросы, которые отрабатываются по уже загруженным данным
Расскажите подробнее, пожалуйста. Какие группы запросов поддерживаются — произвольные, или только фильтрация по PrimaryKey? Если произвольные запросы, то чем обеспечивается доказательство, что из базы уже всё прочитано в память, и в базу лезть не требуется?
Просто если здесь только фильтрация по PrimaryKey, то такие штуки по факту являются банальным кэшем, и ввиду этого абсолютно не забавны. А вот если там поддержка произвольных запросов (и не просто кэширование результатов) — то это да, интересно неимоверно.
Так мы вроде и обсуждаем здесь случай, когда данных много? Иначе зачем для их хранения нужна база данных? Малые объёмы можно держать и в оперативной памяти где-нибудь.
А ваше «был бы только рад» намекает на то
… что я уже взвешивал несколько раз возможность "вытянуть всё и на месте разобраться в коде". И ни разу это не оказалось приемлемой стратегией. Более того — несколько раз мне приходилось такие подходы из кода вычищать калёным железом, так как в раунде PSR-тестирования выявлялось сильное падение отзывчивости системы, и причиной были именно, закаты солнца вручную на сервере вместо написания нормального запроса в базу данных.
> Пример: рендер изображения в сетевой игре происходит на клиенте, а не на сервере
Плохой пример: если изображение это данные, то их совершенно правильно не передают с сервера, вместо этого клиенту передаётся описание вычислений, необходимых для получения правильных данных (поток событий в матче).
С другой стороны, в ММО, опять, же не передают все данные из базы на клиента, дабы клиент сам определился, какие из них ему действительно нужны и сделал необходимые вычисления. Переводя на язык СУБД, в таком случае вся фильтрация и join'ы произведены на сервере (в базе данных), клиенту приходит только проекция. Когда такая схема нарушается по любой причине, появляется возможность сжульничать — например, убрать «туман войны» в стратегии (применимо только для стратегий с сервером-арбитром правда, обойтись без дупликатов данных в P2P играх, насколько я знаю, пока не получалось ни у кого).
ORM сияет обычно при записи, потому что не нужно думать, какие сущности сохранять первыми, какие вторыми и так далее. Очень сильно помогает если в схеме присутствуют циклические связи.
Для запросов и их результатов ORM подходит куда меньше, хотя бы потому, что не позволит просто так пойти и высчитать среднее, бегущую сумму или какой-нибудь изврат с оконными функциями — а это обычно самые интересные данные с точки зрения заказчика.
Уж поверьте, как кодер я был бы только рад унести логику обработки данных из «холодной, безразличной» и требующей многословных запросов базы данных (где, например, попытки переиспользовать код обычно натыкаются на ощутимые тормоза ввиду переключения контекстов между SQL и PL) в уютный для меня любимый язык программирования. Но сетевой доступ к хранилищу не может происходить по путям бесконечной ёмкости, а значит, чем больше работы я сделал вдали от данных, тем больше данных было передано по сети. А это вещь ненадёжная и медленная.
Я не исключаю что в каких-то системах такой подход может не работать и сервер базы загружен на 98%, но с другой стороны, если мы можем поднять сотню аппсерверов (для чего, кстати? вручную делать join'ы и закат солнца до кучи?), то почему мы не можем поднять «клон» для чтения, к примеру? Или поделить данные между несколькими базами, для начала, а склеивать уже результат от полученный от одинакового запроса к разным базам?
Разумеется, настораживает. Во времена их активности именно в качестве кодеров многие современные подходы находились на стадии теоретических изысканий — следовательно, не были применимы к их опыту написания кода.
Некто может как никто другой в мире уметь обрабатывать дерево киянкой и долотом, но обязаны ли мы поэтому считать его непререкаемым авторитетом в современной столярной мастерской? А если учесть что этот мастер в последний раз работал с деревом десять с гаком лет назад, а потом "профессионально вырос" до руководства другими людьми?
Есть, конечно, случаи, когда по какой-то причине нужный Вам тип невозможно выразить в рамках системы типов некого языка, однако это, по моему глубокому убеждению, такие вещи являются скорее недостатком дизайна, и возникают не настолько часто, чтобы быть аргументом против типизации вообще.
Приведу пример (я буду предполагать что система типов моего языка достаточно развита, чтобы всё это поддерживать):
fun getLength— всё, что мы можем сказать о этой функции — это её имя. Исходя из нашего опыта, мы могли бы предположить, что она возвращает(get) некую длину(Length), но чего — мы не знаем. Также мы не знаем, что такое этот Length на самом деле. Нам нужны несколько групп тестов для каждого из типов, которые мы потенциально собираемся передать в эту функцию в качестве аргумента. Исходя, опять же, из опыта, мы предполагаем, что длина — это неотрицательное целое число. Нам нужны тесты, проверяющие, что функция действительно возвращает число, что оно действительно неотрицательное, и что оно действительно целое (минимум три типа тестов, для которых мы не гарантируем полного покрытия, ведь всегда останется шанс, чтоgetLength("aaa") === 3, ноgetLength("™") === 3.14). Кроме того, нам нужны отдельные тесты на то, что функция не изменяет состояния своих параметров.fun getLength: List<Any> => Number— теперь нам уже не нужны вручную написанные проверки на принимаемый тип, ведь мы точно знаем, что если переданный параметр не является списком, то вызывающий код не пройдёт этап компиляции. Если также мы знаем, что базовый тип List не поддерживает мутации, то мы знаем, что вызов этой функции не меняет сам список. Нам всё ещё нужны проверки на то, что результат функции возвращает неотрицательное, и целое число.Int. Тогда, если мы имеемfun getLength: List<Any> => Int— мы точно знаем, что длина всегда является целым числом, и каким бы ни было тело функции, она не сможет вернуть нецелый результат, или результат больше максимального или меньше минимального значения для Int. Но стоп, мы не знаем, что будет если передать в функциюNullилиUndefined. Какова их длина? Нужен тест!Null, иUndefinedявляются отдельными типами. Так как наша функция определена какfun getLength: List<Any> => Int, она не содержит ни того, ни другого типа (Что выражалось бы, к примеру, какList<Any>|Null). Следовательно, никакой код не сможет передать внутрьNull, и никакая реализация не сможет вернутьUndefined. Потому этот класс тестов нам тоже не нужен. Нам по-прежнему нужны тесты на то, что результат неотрицательный.fun getLength: List<Any> => Length, а типLengthу нас:typedef Length as Int where $value >= 0— то мы перекладываем ещё один из наших вручную написанных тестов на плечи компилятора, ведь теперь это его работа — проверять что функция не возвращает значений меньше нуля.Итого, из первоначальных классов тестов нам останется только проверить, что для списков предопределённой длины вызов
getLength(list)вернёт строго ожидаемое значение. Так система типизации позволила нам писать тесты только одного типа вместо шести или семи, которые я обрисовал вначале, все остальные группы тестов «написаны» и выполнены компилятором.Более того, обычно я даже не задумываюсь над тем, насколько огромно на самом деле количество тестов, которые я не написал вручную из-за систем типизации, но которые, тем не менее, постоянно выполняются в моём коде.
Кстати, замечу, что в отсутствие строгой типизации упомянутых в статье 2930 юнит-тестов, 100 тестов производительности и >400 интеграционных тестов было бы явно недостаточно, так как пришлось бы писать ещё пару тысяч тестов на то, что
DeviceFactory::createLightBulbDeviceдействительно возвращает нечто, что ведёт себя в соответствии контрактуLightBulbDevice, аDeviceEvent::getTypeникогда не возвращает строку.Причём, с моей точки зрения, тесты в этом всём — как раз самое скучное, поэтому когда у меня снова случится обострение — большая часть тестов уже будет написана, и тут уже отвертеться много сложнее. :)
Непонятно несколько моментов:
Как так? Неужели совершенно нет истории изменений и/или всё переписывание ведётся на мастер-ветке? Если так, но не в переписывании проблема, и даже не в увольнении сотрудника.
Действительно ли так много бизнеса, следующего этим постулатам? А как же пресловутый Customer Retention, над которым теперь все так трясутся? Это, само собой, к теме статьи относится только косвенно.
Ну почему так категорично. Предикация и проекция — две операции, которые не так чтобы близко были связаны между собой. Для малых кардинальностей, логически, есть возможность оптимизировать запрос так, чтобы этот столбец был "Ленивым" и вычислялся вообще прямо перед "Show" каждой строки (в последний момент), что будет ценно как раз при ограничении по памяти, т.к. позволяет при вообще не хранить в столбце значения — только "рецепт" и кеш-таблицу. Оптимизация менее применима при наличии сортировки по такому столбцу, но не снимается полностью даже в этом случае.
Про LINQ: Там в картинке явно указывается в тексте запроса, что, что некие сущности следует брать из кеша.
Не то чтоб это было плохо само по себе, но требует телодвижений в каждом use-site. Впрочем, принципиальную возможность это уже доказывает, следовательно, где-то должен быть способ то же самое делать более централизованно. Благодарю за просвещение.
Что я воспринял как «официальная документация не включает объяснений такому поведению». Иными словами, в моей интерпретации звучит как UB, тогда как, по видимости (и в комментариях уже много раз сказано) поведение в точности соответствует спецификации (i.e. «почему» — «по спецификации»). Это, безусловно, ошибка трансляции с моей стороны, но теперь я убеждён, что автор статьи тоже причастен. Ещё раз спасибо.
Кстати, в порядке просвещения меня: может ли LINQ сделать такую отдельную загрузку для малых таблиц и потом использовать эти таблицы в проекциях без настоящего join'a на стороне БД? Хотя бы в теории?
А если в запросе при этом есть сортировка по соседнему атрибуту, то кэш я вообще не имею права использовать (исключение — кеш содержит результаты запросов по ключу из текста самого запроса, и даже в этом случае не всегда).
Да, это может работать если клиенту возможно скармливать данные по кускам, как, например,
Flux<T>в реакторе. Тогда при запросе можно быстро выдавать из кэша что есть, и если этого не хватило — выдать уже из базы данных. Так можно сильно уменьшить время первоначального отклика на запрос, если отклик на запросы клиентов часто содержит повторяющиеся данные.К сожалению, плохо будет работать сортировка — в общем случае, в любой момент остаётся шанс, что в базе находится некий элемент, который по данному критерию ниже, чем начало кешированной страницы.
В исходной формулировке с уточнениями, правда, выходит, что запрос «хавал» очень много ресурсов сервера БД, а малое количество данных так не делает.
И единственное, почему это мало кто любит — это то, что конструкция выглядит непривычно. А выглядит она непривычно потому, что так мало кто делает. А делают так мало потому, что не любят. Ну и так далее. А началось с того, что это не любил Дийкстра. Весьма нерациональный подход, на мой вкус.
Расскажите подробнее, пожалуйста. Какие группы запросов поддерживаются — произвольные, или только фильтрация по PrimaryKey? Если произвольные запросы, то чем обеспечивается доказательство, что из базы уже всё прочитано в память, и в базу лезть не требуется?
Просто если здесь только фильтрация по PrimaryKey, то такие штуки по факту являются банальным кэшем, и ввиду этого абсолютно не забавны. А вот если там поддержка произвольных запросов (и не просто кэширование результатов) — то это да, интересно неимоверно.
Так мы вроде и обсуждаем здесь случай, когда данных много? Иначе зачем для их хранения нужна база данных? Малые объёмы можно держать и в оперативной памяти где-нибудь.
… что я уже взвешивал несколько раз возможность "вытянуть всё и на месте разобраться в коде". И ни разу это не оказалось приемлемой стратегией. Более того — несколько раз мне приходилось такие подходы из кода вычищать калёным железом, так как в раунде PSR-тестирования выявлялось сильное падение отзывчивости системы, и причиной были именно, закаты солнца вручную на сервере вместо написания нормального запроса в базу данных.
Плохой пример: если изображение это данные, то их совершенно правильно не передают с сервера, вместо этого клиенту передаётся описание вычислений, необходимых для получения правильных данных (поток событий в матче).
С другой стороны, в ММО, опять, же не передают все данные из базы на клиента, дабы клиент сам определился, какие из них ему действительно нужны и сделал необходимые вычисления. Переводя на язык СУБД, в таком случае вся фильтрация и join'ы произведены на сервере (в базе данных), клиенту приходит только проекция. Когда такая схема нарушается по любой причине, появляется возможность сжульничать — например, убрать «туман войны» в стратегии (применимо только для стратегий с сервером-арбитром правда, обойтись без дупликатов данных в P2P играх, насколько я знаю, пока не получалось ни у кого).
Для запросов и их результатов ORM подходит куда меньше, хотя бы потому, что не позволит просто так пойти и высчитать среднее, бегущую сумму или какой-нибудь изврат с оконными функциями — а это обычно самые интересные данные с точки зрения заказчика.
hadoop.apache.org/docs/r1.2.1/hdfs_design.html
Уж поверьте, как кодер я был бы только рад унести логику обработки данных из «холодной, безразличной» и требующей многословных запросов базы данных (где, например, попытки переиспользовать код обычно натыкаются на ощутимые тормоза ввиду переключения контекстов между SQL и PL) в уютный для меня любимый язык программирования. Но сетевой доступ к хранилищу не может происходить по путям бесконечной ёмкости, а значит, чем больше работы я сделал вдали от данных, тем больше данных было передано по сети. А это вещь ненадёжная и медленная.
Я не исключаю что в каких-то системах такой подход может не работать и сервер базы загружен на 98%, но с другой стороны, если мы можем поднять сотню аппсерверов (для чего, кстати? вручную делать join'ы и закат солнца до кучи?), то почему мы не можем поднять «клон» для чтения, к примеру? Или поделить данные между несколькими базами, для начала, а склеивать уже результат от полученный от одинакового запроса к разным базам?