30 августа, 2010

Keep it simple, stupid

Простота. Краеугольный камень нашей профессии. Наверняка вам приходилось слышать от своих коллег: “это можно сделать гораздо проще”. Или вы говорили это вашим коллегам: “смотри, можно сделать так. Это ведь гораздо проще”, а в ответ получали взгляд полный непонимания, как бы говорящий вам: “и это ты считаешь проще?”.

В программной инженерии определенно произошла инфляция слова “простота”.

Простота — это расстояние

Не существует общепринятого определения простоты. Тем не менее, если мы заглянем в словарь то увидим, что очень часто рядом со словом простота фигурируют такие понятия как: “предсказуемость”, “прямолинейность”, “ясность”, “очевидность”.

Что мы считаем простым? Процессы и объекты чье поведение легко предсказать. Если “оно” ведет себя так как я от “него” ожидаю, значит я понимаю как “оно” работает и скорее всего смогу “его” изменить не поломав. В противоположность этому, если “оно” ведет себя непредсказуемо, значит я не понимаю основополагающих принципов лежащих в “его” основе. И любая попытка “его” изменить может окончится таким же непредсказуемым результатом. В этом случае процесс постулируется слишком сложным.

Такие “предсказания” возможны благодаря тому что в голове человека есть ментальная (поведенческая) модель описывающая этот процесс или объект. И чем сильнее соответствует модель реальности, тем проще расценивается объект. Так что да, простота есть расстояние. Расстояние между ожидаемым и действительным.

Мера простоты — понятие субъективное

Теперь это должно быть очевидно. У двух людей могут быть различные поведенческие модели, а значит и своя мера простоты относительно того или иного процесса. Если вы считаете какой-то процесс простым, это значит что у вас в голове есть удачная модель происходящего. Именно она позволяет вам эффективно понимать и управлять происходящим. Мнение другого человека по этому же вопросу может быть диаметрально противоположенным, и это может обосновываться, например, отсутствием опыта работы в какой-либо сфере.

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

Но более интересна обратная ситуация. Если вы считаете какой-то процесс сложным, это еще не означает что процесс объективно непредсказуем и непонятен. Это может означать что у вас просто нет подходящей поведенческой модели. Возможно не стоит клеймить все вокруг сложным, возможно просто стоит... научится?

Например, если вы считаете что следующий код слишком сложен, так это просто потому что вы не знакомы с функциональным программированием.

def qsort: List[Int] => List[Int] = {
  case Nil => Nil
  case pivot :: tail =>
    val(lessThan, greaterOrEqualThan) = tail partition(_ < pivot)
    qsort(lessThan) ::: pivot :: qsort(greaterOrEqualThan)
}

Это приводит нас к заключению, что если мы хотим создать простой продукт, мы обязаны учитывать ментальные особенности людей которые будут работать с продуктом. И тут возможны два варианта:

  1. создавать процессы близкие к ожиданию пользователей;
  2. создавать в головах пользователей новые поведенческие модели (учить пользователей).
​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​Помните этот комикс? ​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​​

Последний продукт просто ужасен, верно? Или нет? Я незнаю. Мне ясна посылка этого комикса, — существует тенденция к чрезмерному усложнению вещей (или их недостаточному упрощению, как вам больше нравится).

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

Это кабина еще советского Ту-154М, реактивного самолета выпуска середины 80-х. Обычный человек не сможет идентифицировать большинство приборов, рычагов управления и их назначения. Значит ли это что интерфейс сложен для тех для кого он создавался, для пилотов?

Тенденции к упрощению существуют и в авиации, так что мы можем поискать более простой реактивный лайнер (так сказать, "для нубов"). Давайте посмотрим на кокпит широкофюзеляжного A-380, который начал эксплуатироваться в 2007 году. Считается что A-380 более прост в управлении. Об этом говорит даже тот факт что экипаж A-380 составляет два человека (против 4 человек в Ту-154М).

Аналоговых приборов почти не осталось, им на смену пришли цифровые приборы и LCD панели. Рычаги управления стали больше похожи на джойстик. Основным органом управления, судя по всему, стала клавиатура. Но стал ли он проще? Для пилотов может быть, для всех остальных никакой разницы. Если вы чувствуете себя как слон в посудной лавке в кабине Ту-154, тоже самое будет в кабине A-380.

Конечно, если вы можете создать продукт который будет решать задачи пользователя и при этом будет простым как электрочайник, то это просто прекрасно. Но есть сферы где человеку нужен полный контроль над происходящим и масса информации чтобы принять верное решение. И в этом случае может быть не лучшей идеей сводить интерфейс к одной кнопке в UI/одному методу в API и т.д.

Простота многомерна

Но вернемся к программированию. Простота в отношении программного кода обманчиво проста. Эта простота (или ее отсутствие) может проявляться в разных контекстах, в зависимости от процесса разработки.

Простота использования

Если речь идет об API или бибилиотеке, то как просто ее развернуть на клиенте и начать использовать?

Простота написания

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

Простота тестирования

Люди по своей природе не надежны. Наша интуиция не всегда хорошо работает, иногда мы ленимся, иногда теряем концентрацию. Поэтому, мое жизненное кредо заключается в том, что там где это возможно вопросами верификации корректности должны заниматься машины. Человек не всегда может подтвердить что код все еще работает увидеть что код уже не работает. Машина может.

Именно поэтому для меня простота тестирования (тестируемость) — очень важное качество кода. Если я пишу код и не могу сделать так, чтобы верификацией работоспособности за меня занималась машина, то скорее всего этого кода не должно быть.

Простота верификации

Автоматическое тестирование не панацея конечно же, особенно если мы говорим о параллельных приложениях, которые очень тяжело тестировать. Насколько просто вы можете загрузить в голову коллеги такую ментальную модель при помощи которой он мог бы сказать: “да, я уверен, этот код работает”? Это и есть простота верификации.

Простота понимания

Последнее по порядку, но не по значению. Насколько код очевиден, насколько легко понять что он делает, и как он это делает, а следственно и насколько легко изменить его. Простота понимания и верификации очевидно связаны, потому как тяжело верифицировать то, чего ты не понимаешь. Тем не менее я склонен считать это разными факторами.

Это не исчерпывающий список. Вы можете изобразить все это на spider графике. Как и всегда, достичь максимума по всем направлениям вряд ли получится. Надо определить приоритеты и следовать им. Кому-то важнее простота написания, кому-то простота тестирования. Но в любом случае этот выбор должен быть осмыслен. Это то, что в наши дни очень тяжело найти — осмысленный выбор. В большинстве случаев выбор делается неявно в угоду упрощения процесса которым разработчик занимается именно сейчас. Это свойственно всем людям, — идти по пути наименьшего сопротивления.

Проблема в том, что упрощений в чистом виде не так много. Чаще приходится иметь дело с разменом. Упрощая одно, вы автоматически усложняете другое. Например, упрощение тестирования зачастую связанно с “расслоением” системы и усложнением ее понимания. Это размен на который мы идем, неважно отдаем мы себе в этом отчет или нет. Вопрос в том, выгоден ли этот размен для нас? Но это тема совсем другого разговора...

10 августа, 2010

Lock vs. Lease

Представьте себе типичную задачу, есть поток событий и этот поток событий надо разделить между несколькими пользователями системы. Типичный пример: модерация. Скажем, есть три модератора, которые просматривают добавляемый пользователями контент (допустим рекламные кампании). Мы хотим “распилить” поток событий между модераторами, чтобы они не делали одну и ту же работу дважды.

В самом простом случае, мы можем использовать mq в любом его проявлении. Пользователи просто берут сообщение из головы и обрабатывают его. Эта схема хорошо работает, но к сожалению не всегда применима. Например, если пользователи имеют возможность указывать критерии событий которые хотят обрабатывать. Последнее требование очень часто в том или ином виде всплывает в подобных задачах, потому что люди (точно так же, как и процессоры) плохо переносят context switch. Например, если я настроился на модерацию авто тематики, то я фигурально выражаясь загрузил в кеш ключевые слова относящиеся к этой тематике и буду очень расстроен если мне подсунут объект с тематикой относящийся к сотовым телефонам.

Окей, мы не можем использовать mq, что тогда? Далее приходит на ум следующее — хранить события в реляционной таблице (чтобы иметь возможность фильтрации) и брать shared lock на каждое из событий/объектов при их обработке. В данном случае lock’и позволяют исключить дублирующую обработку события. Ничего криминального в использовании локов для решения этой задач нет, но если вы начнете имплементировать подобное решение, то вы очень быстро прийдете к тому что lock’и обладают семантикой отличной от той которая необходима вам. И вот почему...

Lock timeout

Если мы говорим про web-приложение, то необходимо понимать что факт закрепления события за конкретным пользователем должен иметь timeout. HTTP — это stateless протокол. После того как вы закрепили событие за пользователем, он может просто закрыть браузер и пойти по своим делам. Если мы хотим чтобы любое событие было рано или поздно обработано, мы обязаны аннулировать устаревшие локи.

Но традиционный механизм lock’ов не имеет timeout’ов, и это решение вполне обосновано, так как обычно lock’и или используются внутри одного адресного пространства (а следственно проблемы исчезновения lock owner’а нет) или используются в distributed окружении, но имеют механизм autorelease’а (при разрыве соединения текущего владельца блокировки и координатора автоматически происходит release).

Это означает в традиционных use case’ах использовании lock’ов между текущим владельцем лока и координатором всегда есть канал связи, чего нет в нашем случае.

Immediate locking

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

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

Lease

Проблема в том, что люди видя эту семантическую разницу пытаются расширить интерфейс локов для того чтобы покрыть им данный use case использования. Это опасно, так как в механизм добавляется элемент autorelease’а, который легко может нарушить целостность данных. Представьте, что если при традиционном использовании локов (скажем, параллельном изменении коллекции элементов) autorelease произойдет до того момента когда владелец лока успеет выполнить операцию?

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

Помните DHCP? Это протокол автоматической конфигурации сетевого стека компьютера. Сегодня его можно найти буквально в любом домашнем маршрутизаторе. DHCP позволяет компьютеру автоматически получать IP-адрес и автоматически конфигурировать сетевой стек в зависимости от топологии сети в которой находится компьютер.

Обратите внимание, когда компьютер получает IP-адрес по DHCP, он не посылает запрос “дайте мне IP 192.168.1.15”. Он посылает запрос: “дайте мне любой свободный адрес”. В ответ он получает lease. Lease — это аренда адреса. Аренда имеет timeout, по истечении которого, если lease не был продлен, то аренда аннулируется и IP-адрес возвращается в список доступных. Вполне подходит под нашу задачу, верно?

В случае DHCP, клиенту по большому счету все равно с каким IP-адресом работать. В нашей задаче клиенту все равно какие именно события обрабатывать. Главное чтобы они удовлетворяли формальным требованиям о которых мы говорили раньше.

Модель аренды гораздо ближе к природе задачи. Во-первых, timeout’ы, которые нам просто необходимы, лежат в природе механизма аренды. Во-вторых, аренда не подразумевает владения объектом. Фактически в этот же момент с этим же объектом может происходить какая-то другая операция (например, индексация), которая скорее всего имеет возможность выполнятся параллельно, а значит данные находящиеся в объекте не являются предметом защиты от конкурентного доступа. Это сигнализирует о том что локи в данном случае не уместны. В этой задаче lock’и могут служить только инструментом обеспечения целостности списка арендованных объектов, но не более того.

P.S. интересно услышать к какими способами решения схожих задач приходилось сталкиваться вам?