2015-09-20

О схеме

Все мы любим схемы. Схемы данных. Схемы данных в реляционных БД. Схемы вызовов и сущностей в API. И прочее, и прочее.
Началось, всё, пожалуй, со структур. Структур в сишном смысле, ну или записей в паскалевом смысле. До структур у нас были примитивные типы да коллекции, вроде лиспов-списков. Структура же объединяет несколько значений примитивного типа в нечто цельное и обладающее отдельным смыслом. Структура — это схема. Схема размещения данных в памяти. Чтобы к каждому примитивному значению можно было удобно получить доступ по человекопонятному имени. И без этого, пожалуй, никуда. Из структур, как известно, выросли ООП классы, как минимум в некоторых языках.
Как нам добавить еще одно поле в структуру? Буковка O в SOLID принципах говорит нам о том, что нам нельзя просто взять и добавить поле в существующую структуру. И это правильно. Мало ли, куда нам заблагорассудится засунуть это поле. А схема размещения данных в памяти изменится. И ранее сохраненные данные уже будут не совместимы с новой схемой. Ой.
В классических сишных структурах поэтому, как правило, допустимо добавлять поля только в конец структуры. А в объектно-ориентированном C++ для расширения структур/классов придумали наследование. Наследуем базовый класс и добавляем в наследника новые поля. Хитрый C++ размещает поля наследника строго после полей родительского класса. Это нужно для выполнения буковки L тех же SOLID принципов. Если уж мы имеем указатель на область памяти наследника, то код, который умеет работать только с родительским классом, должен успешно поработать и с этим указателем.
Однако, сейчас в моде динамические языки, вроде Python и Ruby. И у них объекты в памяти, внезапно, не представлены жёсткой структурой. Объект — это словарь, он же карта, он же ассоциативный массив. Имя поля (строка) связано со значением этого поля. Как это физически представлено в памяти, уже не важно. Важно, что у нас есть чётко определённый способ получить значение по имени.
И тут возникает феномен утиной типизации. Какому-либо коду, работающему с объектом, уже не важна схема размещения свойств объекта в памяти (класс), а достаточно лишь наличия некоторых свойств с определёнными именами. В разном коде требуются разные свойства. Но класс объекта действительно уже вторичен. Именно поэтому в том же Python почти не встречаются длинные иерархии классов, в отличие от Java. И это удобно.
Duck
Схемы объектов в памяти — это еще полбеды. В конце концов, когда мы меняем код, мы перезапускаем процесс, и всё содержимое памяти теряется. Запускаем новый код, и всё хорошо. Но есть ведь данные, которые живут гораздо дольше, в базах данных.
Реляционные базы тоже начались со строгой схемы. Возможно, тоже из соображений производительности. Мы обязаны указать набор таблиц, набор колонок в каждой таблице, типы колонок. Позднее появились констрейнты, чтобы жёстко связать данные не только в рамках одной таблицы, но и между таблицами. Ну вы знаете, все эти 100500 нормальных форм предназначены как раз для того, чтобы всё было жёстко и красиво.
Как добавить новую колонку в БД? На то есть ALTER TABLE. Но не всё так просто. Как минимум, для старых данных нужно предусмотреть разумное значение по умолчанию, и не всегда NULL — это хорошо. Как максимум, у нас может оказаться не один сервер БД, мы захотим выкатить изменение не только лишь для одной колонки, а всё, что накопилось с предыдущего релиза. Для автоматизации всей этой возни придумали миграции. Подумать только, их даже можно откатить, если что-то пошло не так. Но также представьте, что в момент релиза, на какой-то, пусть не сильно продолжительный, момент, все ваши базы данных вынуждены будут заниматься такой глупостью, как миграция, вместо того, чтобы честно отвечать за запросы.
Ну вот ребята, придумавшие NoSQL, точнее, в контексте данного разговора, документо-ориентированные БД, так и сказали: долой схемы и миграции, мы будем schemaless. Вместо жёстко определенного набора колонок в таких БД имеются документы, которые могут содержать произвольный набор полей. Поля идентифицируются именами. Понятно, что некоторый код, обрабатывающий документы, имеет дело с определенным набором полей. Ожидает, что у обрабатываемого документа есть, например, id и name. Но на наличие или отсутствие других полей ему совсем наплевать. И он должен быть готов напороться на документ, в котором и ожидаемых полей нет. В общем, привет, утиная типизация, всё тут абсолютно так же.
Но как же миграции? Как добавить новое поле в документ? Ну, для начала, просто добавить. Просто в новых документах, добавляемых в БД, будет новое поле. А в старых документах, которые находились в БД ранее, этого поля не будет. Можно, по примеру реляционных БД, пройтись по всем имеющимся документам и добавить к ним новое поле, его значение по умолчанию. Но это, очевидно, не эффективно.
Можно не обновлять документы вообще. Как мы помним, согласно SOLID, мы не должны ни модифицировать старый код, ни выкидывать его. Пусть старые документы обрабатываются старым кодом, а новые документы — новым кодом.
Но еще интереснее обновлять документы по мере надобности. Взяли документ из БД, когда нам он понадобился. Смотрим, а он старый, и нового поля в нем нет. Обновим документ, добавим новое поле. Запишем обновление в БД. А дальше в обработку передадим уже обновлённый документ.
Для удобства распознования старого и нового документа имеет смысл добавить версию. Версию схемы данного документа :)
Самое удивительное в том, что строгую и утиную типизацию в БД можно совмещать. Теперь, даже в рамках одной СУБД. Великолепный PostgreSQL умеет эффективно хранить JSONы в jsonb. Вот и можно всякий хлам свалить в JSON колонку, а серьёзные данные, со всякими внешними ключами, выделить в отдельные классические реляционные колонки.
А еще более удивительно то, что таблицы в PostgreSQL можно наследовать. В том самом сиплюсплюсном смысле, с добавлением полей/колонок. Получается, что можно не делать ALTER TABLE, а просто создавать новые таблицы-наследники, когда нужно расширить схему. SOLIDно.
Open closed
Данные, объекты да структуры в памяти и в БД — это еще не всё. Ещё мы любим это дело передавать по сети, в виде каких-то сообщений, удалённых вызовов и т.п. Давным давно было принято сериализовать объекты в XML. Причём, так как сериализовали строгие классы в стиле C++, то придумали схему XSD, с таким же наследованием и возможностью добавления новых элементов при наследовании только после элементов базового типа. И был такой RPC, где всё сериализовывалось в XML, а передаваемые данные описывались в XSD. И назывался он SOAP. Бойтесь протоколов со словом Simple в названии, а тем более — от Microsoft. SOAP и до сих пор жив, он является дефолтным протоколом в WCF — популярном коммуникационном фреймворке для .NET.
Что нужно сделать, чтобы добавить поле в какую-нибудь сущность, передаваемую, допустим, через SOAP? Правильно, определить новую схему, подправить код, опубликовать новую версию RPC API, обновить клиентский код, оповестить всех клиентов о появлении новой версии. Никто в трезвом уме и здравой памяти не захочет делать такие вещи слишком часто.
Вы можете заметить, что нынче у нас моден REST и, вместо громоздкого XML, повсеместно используется JSON. Хоть для JSON тоже придумали схему, слава богу, никто ею особо не пользуется. Однако, сам по себе REST — это тоже довольно жёсткая схема. Ведь у нас есть сущности, есть коллекции сущностей, всё по строго определенным адресам, со строго определенными методами доступа. Чем не схема? С теми же проблемами обновления. Новое поле, новая сущность, новая версия API, обновление клиентов и т.д.
На самом деле, даже в WCF возможна утиная типизация. Пусть сервер ожидает от клиента вызова некоторых методов с передачей в качестве параметров или результатов определённых сущностей. Пока у нас совпадает неймспейс, имена методов, имена классов сущностей, имена и типы полей сущностей, а также пропущенные поля допускают null значения, мы можем передавать серверу вовсе не те классы, которые определены в его API, а какие-то наши собственные классы, с подмножеством полей. Пока сериализация и десериализация работает — всё ок. Конечно, это не типичный, не документированный, и не рекомендуемый способ использования WCF. Никто так не делает.
Потому что делают обычно наоборот. Обычно ленятся придумывать схемы, т.е. набор минимально необходимых полей, для каждого случая использования данных. А ведь передача по сети, обработка и сохранение — это совсем разные случаи. А рисуют универсальные сущности/классы, навешивают на них магические атрибуты/аннотации и используют повсюду. Эти сущности используются для генерации WCF или REST API и заглушек. Эти же сущности используются для генерации и миграции схемы БД (да, это ORM). И вроде все счастливы.
WCF
Знаю один такой настоящий проект, где вся предметная область красиво расписана в классах. Эти классы используются в описании WCF интерфейсов и сериализуются по сети. Эти же классы используются в NHibernate и напрямую сохраняются в БД, которая, конечно же, имеет аж третью нормальную форму. Всё прекрасно, всё по учебнику, всё по официальным рекомендациям. Но, когда приходит простейший на вид запрос: добавить еще одно поле в одну из сущностей, — наступает ад. Надо поменять сами сущности; надо поменять код, выгребающий эти сущности, все его три слоя; надо поменять код WCF сервера, принимающего сущности, все его репозитории, команд басы и прочее; надо поменять код веб сервера, отображающего сущности, чтобы добавить колонку в отображаемую таблицу. И при этом не требуется ничего специального, надо просто выгрести значение оттуда, где оно есть, перенести туда, где оно отображается, и отобразить.
Какая альтернатива? Ну смотрите, нам нужно собрать, передать и отобразить набор полей. Всё. Ну так давайте представим каждую сущность каким-нибудь JSON, без жёсткой схемы, Давайте передадим и сохраним этот JSON, не вникая в детали того, что мы тут передаём и сохраняем. Давайте отобразим этот JSON как просто набор полей/колонок, произвольный набор. Всё, что нам нужно здесь — это различать типы данных в этих колонках, чтобы корректно сортировать и корректно отображать те же даты. И всё. Добавление одного поля — это небольшое изменение кода, извлекающего данные. А далее, коду, передающему данные, коду, сохраняющему данные, коду, отображающему данные, уже всё равно, этот код — универсален. Понятно, что в дальнейшем, для бизнес логики, придется выделить некоторые поля, стандартизировать их, описать в схеме. Но лишь некоторые необходимые поля. Утиная типизация.
Одни и те же данные в разное время, в разных контекстах, могут и должны выглядеть по-разному. Это нормально и естественно. Называйте это утиной типизацией. Называйте это schemaless. Не пытайтесь придумать универсальную третью нормальную форму для всего. Всё течёт, всё изменяется. Выделите нечто максимально общее, что не будет меняться, как бы не менялись сами данные и требования. Часто это будут документы или сообщения.

2015-09-06

О блоге

Первая статья в тутошнем блоге появилась 6 ноября 2013 года. Почти два года назад. Давненько. За это время его страницы смотрели 23 тысячи раз. Негусто. Самой популярной остается статья про Ingress, хотя в сам Ingress я уже не играю. Так бывает.
Случалось так, что один и тот же вопрос мне задавали несколько раз. И мне приходилось повторять одни и те же объяснения несколько раз. Также становилось очевидным, что если уж я несколько раз что-то объяснил, то может появится еще кто-то, кому придётся объяснять снова. И я подумал, а почему бы все эти штуки не записывать. Например, в блог. И, в общем, не прогадал. Теперь я использую свой блог в таких репликах: «Ну что я буду тебе это объяснять, вот же я в блоге про это уже писал». :)
Так и повелось. Большинство постов — это какие-то базовые объяснения, введение в область каких-то знаний. Причем я не ограничиваю эти знания какой-либо тематикой. Мой блог — не тематический. Мой блог — персональный. Поэтому я объясняю и кто такие понечки и муми-тролли, и какие бывают ворчуны, и что такое ZeroMQ, и как прошивать Android, и прочее, и прочее. Всегда публикую свои публикации, как только это становится возможным. Часто пишу отзывы и впечатления о местах и конференциях, где бываю, или даже о продуктах, которыми пользуюсь.
За время существования блога сложились некоторые традиции написания статей. Название статьи должно начинаться с предлога «О» (или «Об», «Обо») и, по возможности, одного слова. В тексте статьи не должно быть никаких заголовков, только поток мысли, разделенный на абзацы.
Должно быть много ссылок, но ссылки должны быть едины с текстом. Должно быть можно читать текст, не переходя по ссылкам. Не возбраняется давать ссылки на такие попсовые источники, как Википедия и Луркоморье. Набор ссылок в статье должен давать хороший список для дальнейшего чтения, если тема статьи заинтересовала читателя и требуется дальнейшее погружение.
Картинки в статье нужны и обязательны. Хорошо, если они иллюстрируют идею статьи или абзаца, но можно воткнуть просто что-нибудь абстрактное и созвучное. Статья должна читаться и без картинок, не должно быть явных отсылок на картинки в тексте. Я предпочитаю давать ссылки на внешние картинки, найденные через Гугль, но внешние ссылки имеют привычку умирать через полгодика :(
Самое главное правило: писать по статье не реже, чем раз в две недели. Это правило позволяет держаться в тонусе и совсем не забросить бложик. Пусть это будет какая-нибудь фигня, типа рефлексии по поводу ведения блога, но статья должна быть.
В отличие от Дорофеева, я не пишу статьи заранее. Заранее я только собираю идеи для написания статьи. Если идея интересная, то, бывает, накапливаются какие-то заметки, о чем надо упомянуть в статье. Но сам текст пишется на выходных. За один присест. На это уходит два-четыре часа.
Blogger
Блог хостится на Блоггере (он же Блогспот). Благодаря чему я уже поймал привет от Роскомнадзора. Но один дельный совет, заключающийся в том, чтобы воспользоваться бесплатным CDN от CloudFlare, помог унести блог на другой диапазон IP адресов, которые еще не заблокированы.
Сначала я писал статьи непосредственно в админке Блоггера. Но, после того, как пару раз наполовину написанная статья успешно пропадала из-за глюков JS в браузере, я решил таки воспользоваться полноценной системой контроля версий и стал писать в любимом Markdown, в приватном репозитории на Bitbucket. После некоторого ряда мучений в переводе маркдауна в HTML, который не взрывался бы редактором Блоггера, родился скриптик на Питоне, которым я теперь и пользуюсь. Натравливаю его на .md файл и вставляю получившийся в буфере обмена HTML прямо в редактор Блоггера.
Markdown
И да,
в маркдауне я пишу предложения,
перенося каждую часть предложения на новую строчку.
Где-то я наткнулся на такой способ написания текстов.
Так их действительно удобнее редактировать
и хранить в системах контроля версий.
Знаки препинания только мешаются :)