При разработке вещей для использования людьми очень важно помнить, что пользователи являются частью системы, и когда ваша вещь ведет себя не так, как ожидает пользователь, они удивлены. Удивленные пользователи склонны винить себя за неправильное использование вашей вещи и могут в конечном итоге почувствовать себя глупо из-за непонимания того, как на самом деле работает ваша вещь. Если вы не schadenfreudesüchtig†, вы, вероятно, не хотите, чтобы ваши пользователи чувствовали себя глупо.

Многие вещи могут удивить пользователей вашей вещи, особенно пользователей, которые с ней незнакомы. Когда я впервые начал программировать на Java, я не был знаком с моделью памяти Java и сделал массу ошибок. К счастью для меня, меня окружали выдающиеся инженеры, которые были терпеливы со мной и рассказали мне о некоторых темных уголках Java. Со временем удивление улетучилось, когда я ближе познакомился с Java. Дело не в том, что модель памяти Java была «очевидно разработана идиотами!», она настолько отличалась от моего предыдущего опыта работы с C/C++, что мне пришлось построить новую ментальную модель для написания кода Java. Это не то удивление, о котором я говорю. Новые, инновационные вещи по определению отличаются от существующих вещей, что приводит к незнакомству. Это сильно отличается от удивления от использования чего-то знакомого и ведет себя не так, как ожидалось.

Удержание пользователей от удивления приводит к важному принципу юзабилити-дизайна:

Принцип наименьшего удивления

Люди являются частью системы. Дизайн должен соответствовать опыту, ожиданиям и ментальным моделям пользователя.

(Из Principles of Computer System Design: An Introduction Джерома Х. Зальцера и М. Франса Каашука.)

POLA применяется ко всем областям дизайна интерфейса, от материального (например, дверь) до абстрактного (пользовательский интерфейс вашего любимого приложения). Как разработчики программного обеспечения, мы склонны ассоциировать POLA с дизайном пользовательского интерфейса, забывая, что у всего программного обеспечения, которое мы пишем, есть пользователи. Ваши пользователи могут быть ограничены вашей командой (в случае внутреннего интерфейса, реализующего детали реализации проекта, за который отвечает ваша команда), другими командами внутри вашей компании (в случае внутренней библиотеки, за которую отвечает ваша команда) , или других инженеров по всему миру (в случае библиотеки с открытым исходным кодом, которую поддерживает ваша команда). Не дайте себя одурачить; не все пользователи одинаковы. Пользователи Twitter’s REST API сильно отличаются от пользователей Twitter’s UI, но потребность в минимизации удивления в обеих группах сохраняется. Пользователи REST API имеют различный опыт, ожидания и ментальные модели, поэтому REST API должен соответствовать этим ожиданиям.

По счастливой случайности, несколько дней назад Let’s Encrypt столкнулась с бизнес-концом некоторого удивительного поведения, в результате чего они, по-видимому, слили 7618 адресов электронной почты владельцам тех же 7618 адресов электронной почты.

Том ван дер Вурдт быстро указал на особенность с некоторым примером кода Python, которая привела к утечке. То, что он опубликовал, было похоже на следующее:

from email.MIMEText import MIMEText
m = MIMEText("Hello World!")
m['To'] = '[email protected]'
m['To'] = '[email protected]'
print m

сгенерировал сообщение с двумя заголовками «Кому»:

From nobody Sun Jun 12 15:18:28 2016
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: [email protected]
To: [email protected]

Hello World!

Почему это проблема? Рассмотрим контекст, в котором пакет электронной почты, вероятно, использовался Let’s Encrypt: рассылка бюллетеня всем 383 000 их пользователей, при этом каждый пользователь получал идентичное электронное письмо. Вполне разумно создать шаблон сообщения, затем для каждого получателя изменить заголовок «Кому» и доставить сообщение:

from email.MIMEText import MIMEText

addrs = ['[email protected]', '[email protected]']

m = MIMEText('Hello list subscriber!')
for addr in addrs:
    m['To'] = addr
    print m

Любой опытный хакер Python, незнакомый с пакетом электронной почты, вероятно, ожидает, что это будет работать так, как задумано. Они (правильно) ожидали бы, что заголовок To будет заменяться во время каждой итерации цикла. В python, как и во многих других языках, при индексировании коллекции используются скобки для обозначения индекса. В качестве lvalue коллекция обновляется путем замены значения, хранящегося в индексе (если есть), новым значением. Как rvalue, он оценивается как значение, хранящееся в индексе.

Вооружившись этими ожиданиями, опытный взломщик Python будет ожидать следующие электронные письма:

From nobody Sun Jun 12 15:22:54 2016
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: [email protected]

Hello list subscriber!

и

From nobody Sun Jun 12 15:22:54 2016
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: [email protected]

Hello list subscriber!

К их большому удивлению, второе сообщение электронной почты будет включать оба адреса электронной почты:

From nobody Sun Jun 12 15:22:54 2016
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: [email protected]
To: [email protected]

Hello list subscriber!

Это ошибка? Что тут происходит? К сожалению, это не ошибка. Пакет электронной почты разработан таким образом. Нарочно. Поведение задокументировано.

Если вы знакомы с RFC-822 и RFC-2822, поведение почти понятно. Поскольку стандарт допускает многократное появление заголовка, ожидается, что пакет электронной почты поддерживает эту функцию. Используя синтаксический сахар, предоставляемый python, этот API неожиданно превратил набор в дополнение. Вместо этого API, подобный следующему, был бы намного понятнее:

class MIMEHeaders(object):
    # Other stuff

    def set(self, name, value):
        # Replace all existing headers of type "name" with one with
        # value "value".

    def add(self, name, value):
        # Add a new header of type "name" with value "value", while
        # retaining existing headers of type "name".

class MIMEText(object):
    def __init__(self):
        self._headers = MIMEHeaders()

    @property
    def headers(self):
        return self._headers

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

Делайте все возможное, что в ваших силах, чтобы оправдать ожидания ваших пользователей.

Вот лишь несколько разумных вещей, которые вы можете сделать.

Убедитесь, что поведение очевидно, последовательно и предсказуемо. «Очевидное» имеет тенденцию быть субъективным; то, что очевидно для хакера Python, может не быть очевидным для гуру Java или волшебника C++. Будьте последовательны с платформой, для которой вы пишете, и доменом, в котором вы работаете. Рассмотрите другие классы/методы вашего пакета.

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

Именование важно. Имена должны четко сообщать, что делает класс/метод. Будьте последовательны с похожими классами/методами, чтобы ваш новый класс/метод казался вашим пользователям знакомым.

Всякое бывает! Сбой при возникновении неисправимой ошибки не является аномалией. Однако удивительно, когда что-то не выходит из строя, когда должно. Рассмотрим десериализатор, который возвращает наполовину десериализованные объекты при возникновении ошибки десериализации. Может показаться, что это мило для пользователя, потому что ему не нужно иметь дело с ошибкой, но когда он обрабатывает частичные данные, как если бы они были полными, отладка, скорее всего, станет кошмаром.

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

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

† schadenfreudesüchtig: (существительное) Злорадство. Тот, кто имеет склонность искать удовольствия от несчастья другого человека.