Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Есть ощущение, что существует некий разрыв между практикой, с которой ты сталкиваешься, начиная работать как программист и теорией, в которую ныряешь. Не противоречие, а именно небольшое белое пятно. Вся теория сфокусирована на структурах данных, алгоритмах, паттернах, парадигмах и принципах. Практика (читай “зарабатывание денег”) же вертится вокруг взаимоотношения и коммуникации людей - сторон контракта об изготовления продукта ПО для решения их насущных бизнес задач, зачастую весьма приземленных. И ситуация становится максимально ироничной, когда со знанием всех этих принципов и лучших практик в задаче изготовления условного интернета магазина, мы получаем ужасно запутанный код и проект сдается как подвиг. Я хочу поделиться идеей-метафорой, которая для меня работает тем пропущенным звеном между высокими материями структур-алгоритмов и приземленными вопросами пользователей “Куда пропала моя любимая кнопка?!!”.
m-Робот
Давайте представим простое приложение: в нем есть две кнопки “Работать работу” и “Показать зарплаты всех сотрудников”. И пожелание заказчика: “Я хочу, чтобы кнопка с “зарплатами” была видна только пользователям с максимальным уровнем доступа (менеджерам)”.
При решении такой задачи в лоб у нас возникнет что-то наподобие такого
button_salaries.Visible = user.Permissions == Permission.Max
Интуитивно есть ощущение, что с ним что-то не так. Да, он буквально исполняет то, о чем нас просят, но ведь скорее всего заказчиком подразумевается система, которая никогда не должна позволять увидеть зарплаты кому-бы то ни было, а не прятать-показывать кнопки. Будем считать мы разгадали истинное требование к продукту - каков будет наш следующий шаг?
Код, который действительно гарантирует, что доступ получат только авторизированные пользователи, будет выглядеть примерно так:
if (user.Permission < Permission.Max) throw new Exception()
Если немного включить фантазию, то легко представить, что мы создаем нечто вроде робота, который обладает некоторыми возможностями - показывать зарплаты, подсчитать кто сколько раз нажимал кнопку “Работать работу”, а также следить за соблюдением ограничений, которым нужно соответствовать, чтобы воспользоваться этими возможностями - обладать нужным уровнем доступа или подавать на вход данные, которые соответствуют правилам валидации. Если ограничения не удовлетворяются - дальнейшее выполнение должно блокироваться.
И возможности и ограничения продиктованы тем, кто платит за создание продукта - его владельцем. Соответственно прямая обязанность этого робота - как верного слуги - заботиться о интересах своего владельца. Назовем его “m-робот” от слова “Machines”. Хотя, возможно, “мизантроп” ему тоже подходит.
По моему опыту предложение самому выбросить исключение в коде всегда вызывает сильное удивление у начинающих программистов - “Стой, так ведь приложение упадет?!!” Да, упадет. И это действительно будет очень неприятно для пользователя - ведь он уже предвкушал, как разузнает кое-что интересненькое. Более того, вполне возможно, что все те разы, когда до этого он жал кнопку “Работать работу” не сохранятся и ему заново придется их нажимать - можно только посочувствовать. Но. Если у владельца системы спросить, какое из двух зол он бы предпочел - расстроить немного кого-то из своих сотрудников или дать утечь важной информации для всего его бизнеса - что он бы выбрал? Реальность такова, что зачастую нужно выбирать не между хорошим и плохим сценарием, а между “плохим” и “очень плохим” - и к этому нужно быть готовым.
h-Робот
Несомненно, постоянно падающие приложения, теряющие пользовательские данные - пусть и ради “высших” интересов - не очень хорошо. Кто заступится за людей, которых так раздражают такие сбои? А давайте тоже сделаем для них робота. Миссией которого будет заботиться о них, вернее об их опыте взаимодействия с системой: оберегать от холодного мира строгих логических проверок и проброшенных исключений, которыми живет m-робот. Давайте назовем этого робота - специалиста по связям с людьми - h-роботом (“Humans”).
При его программировании можно смело и целиком фокусироваться на том, как уберечь людей от того, что не стоило бы делать с точки зрения m-робота. На том, как человеку лучше объяснить, что хочет от него система, что она считает правильным и использовать для этого можно весь набор UI элементов платформы: настольных, мобильных или веб приложений.
Какие варианты, как это сделать в нашем примере? Самый простой способ - вызывать метод Salaries
обернутым в конструкцию типа try..catch
и показать, например, диалоговое окошко, которое нам было недоступно на уровне m-робота. Это все равно будет окошко об ошибке, но по крайнее мере данные не потеряются. Хоть что-то. Пользоваться приложением с постоянными сообщениями, что Вы сделали что-то не так, тоже не очень приятно. Тогда можно пойти дальше и научить нашего h-робота незаметно проверять: срабатывает ли фактически этот метод с зарплатами, и если нет - просто прятать эту несчастную кнопку. Это, наверное, будет работать, но не совсем корректно, так как кнопка спрячется даже если метод не работает из-за технического сбоя.
Ситуация в некотором смысле напоминает процесс подачи документов в посольстве или другом бюрократическом учреждение - ты робко кладешь свою заявку в окошко, а злобная надменная тетка говорит “Неправильно! Исправляйте! Следующий!” и ты вынужден думать-угадывать, что именно не так. Правда было бы здорово, если бы непосредственно тот, кто принимает решение, заранее - до того, пока вы дождались своей очереди - мог сказать, подходит ли Ваша заявка под его требования? А еще лучше - объявить их.
Мы, конечно, не можем исправить надменных теток в важных учреждениях, но мы можем сделать нашего m-робота чуть более общительным. Мы можем научить его заранее объявлять, достойны ли мы воспользоваться возможностями метода Salaries
. Да, это действие будет в интересах людей - самому роботу от этого не лучше, но и не хуже - если это никак не нарушает его основного предназначения. Для этого нужно вынести код проверки в отдельный метод
bool Salary(User user) => user.Permission < Permission.Max;
но прим этом проверка, бросающее исключение, все равно должна остаться.
if (!Permissions.Salary(user)) throw new Exception();
Что мы имеем? У нас два робота, у которых разные “миссии”. При этом мы только что сделали одного из них - m-робота - чуть более разговорчивым. Сделано это было в интересах людей - чтобы h-робот на базе этой информации мог нарисовать более качественный с точки зрения человека интерфейс. При этом мы никак не задели основного назначения m-робота - контроля доступа к информации по зарплатам.
Если бы в примере выше мы просто создали метод проверки прав доступа, но не бросали исключение при попытке им воспользоваться - пользы от такого робота было бы ровно ноль, так как тогда он ничего не гарантирует. Полагаться на него совершенно бессмысленно - Вам все равно нужно было бы писать того какой-то код проверок в каждом месте, где возникнет потребность в доступе к зарплатам.
Но сохранив выбрасывание исключения и предоставив возможность проверки до запроса, мы даем h-роботу узнавать, каково “мнение” системы: можно или нет пользователю получать эти данные. А уже решать, как воспользоваться этой информацией - спрятать кнопку, отключить или просто написать рядом “Не нажимать! Упадет!” будет h-робот согласно лучших UX практик.
Другим примером может быть запрет на редактирование какого-то поля в объекте. m-робот может пояснить, что для данного поля существует всего единственное разрешенное значение и на этом основании h-робот может смело отключать соответствующий ui-элемент, т.к. просто нет смысла позволять человеку менять там значение. А если это единственное возможное значение равно 0 или null - может вообще спрятать этот UI-элемент - все равно он особо информации никакой человеку не дает?
Важно то, что даже если h-робот забьет на все эти проверки или отключения - получить доступ к данным или изменить заблокированное поле в объекте у пользователя не выйдет все равно - пострадает лишь его опыт общения с системой, но не сама система или данные, которые она оберегает.
d-Робот
Теперь давайте взглянем на кнопку “Работать работу”. Согласно требований ее нажатие должно добавить 1 пункт очков, если ее жмет рядовой сотрудник (Permission.Min
) и 2 пункта, если средний (Permission.Middle
).
Код, который заставит m-робота это делать будет выглядеть как:
void Job(Permission p)
{
if (p == Permisson.Min) Counter = Counter + 1;
if (p == Permissiio.Middle) Counter = Counter + 2;
}
Написав подобное, Вы наверняка спросите себя: стоп, а если нажмет эту кнопку менеджер, у которого уровень Мах
- это же как-то нелогично, что его работа просто потеряется? А если добавится промежуточный уровень доступа, что-то вроде BetweenMinAndMiddle
? Давайте лучше уточним у того, кто нам эти требования сформулировал. Высок шанс, что ответ его будет примерно “Ой, да забей - менеджеры в жизни не будет жать эту кнопку. И уровней доступа нам хватает - не будем мы их добавлять”.
По-моему опыту такой ответ вполне обычная ситуация, так как заказчик уверен, что объяснил все что нужно и искренне не понимает, чего Вы еще хотите. Вам не дают добро, чтобы внести изменения в интерфейс - и спрятать кнопку для менеджеров - раз она им вроде как не нужна. Но и продумывать сценарий, если ее нажмут, тоже никто не хочет, потому что “да не будут они ее жать”. В этой ситуации Вы оказываетесь зажаты между своим здравым смыслом и m-роботом. Можно бросить исключение для любого входного p, отличного от разрешенных - так сказать, уйти в глухую оборону защитного программирования, но здравый смысл нам нашептывает: а не чересчур ли это? Кнопка так или иначе была нажата, работа - какая-никакая - но сделана. Ни глупо ли ее вот так просто выбрасывать?
Как же тут быть? Да черт его знает. Но какое бы решение Вы не приняли - добавить 0 очков, добавить по минимуму или по максимуму - его всегда можно и нужно дополнить универсальным ходом в подобной ситуации - уведомить о ненормальной ситуации, даже если Вы смогли продолжить работу после нее. Это позволит Вам с одной стороны не переживать за то, чего возможно и в самом деле никогда не случится - менеджеры начнут работать (в нашем примере). С другой стороны Вы будете на высоте в том случае, если изменения появятся или из-за дефекта по Вашей вине каким-то образом в этот метод начнут приходить некорректные параметры. У Вас будет шанс заметить и исправить ситуацию до того, как она всплывает на поверхность.
В таком случае, раз уж у нас есть специалист по связям с людьми - почему бы нам не завести спеца по связям с разработчиками, скажем d-робота? Помимо канала коммуникации людей с системой, нам нужен канал для коммуникации m-робота с разработчиками, где ему можно было бы “жаловаться” на странные входные данные, не прибегая к таким радикальным методам, как проброс исключения. В некотором смысле, d-робот - это просто любая библиотека логирования, которая предоставляет минимально необходимый набор возможностей, чтобы классифицировать эти сообщение как Fatal, Error, Warning и так далее. (Для логгирования я использую подход, который описал в отдельной статье Алгоритм ранжирования ошибок.)
Да, кстати, h-робот - тоже может по необходимости жаловаться на странные обстоятельства, если он такие обнаружит. Скажем, легко можно представить метод его самодиагностики, Test()
(!Permission.Salary(user) && button_salary.Visible) Warning(“Кнопка зарплат видна, хотя я же ее прятал!”)
Без d-робота не совсем ясно как использовать результаты этой самодиагностики. В смысле, кому сообщать о результатах? Уж ведь точно не пользователю показывать уведомление “Кнопка, которую Вы не должны видеть - видна!”.
HMD схема
В начале статьи я завел про воспоминания о прекрасных временах с простыми консольными приложениями. Прелесть консоли в том, что в ней фактически все три робота доступны в одном контексте. У вас не возникает никаких проблем, чтобы обработать и пользовательский ввод и вывод, остановить выполнение программы без исключений или сообщить об аномалиях - прямо в саму консоль. В реальных же проектах, где необходимо переиспользовать код, с более сложными (но и красивыми) интерфейсами, эта возможность теряется и заодно с ней та легкость кодирования.
Разработчики, скованные пониманием, что приложение не должно падать, начинают уродовать сигнатуры методов и всячески извращаться, добавляя выходные параметры, которые должны обозначать ошибки. Или еще хуже - затыкают пустыми try..catch
места, где возможны исключения - что вообще врагу не пожелаешь. Простое осознание, что ты сейчас находишься на уровне m-робота, позволяет снять эту зажатость в мышлении.
Другая крайность заключается в том, что исключения начинают бросать по любому чиху и вопреки здравому смыслу, даже когда можно было бы продолжить работу. Это тоже своего рода зажатость, потому что есть ощущения, что нельзя просто так игнорировать аномалию, но без d-робота - просто некому о ней сообщить.
На мой взгляд, возможная причина запутанности даже простых проектов заключается в том, что разработчики не осознают какую часть системы они меняют и в чьих интересах. Трюк заключается в том, что с hmd-схемой - за счет определенного очеловечивания частей программы - представления ее как роботов с интересами - становится проще рассуждать о задачах и раскладывать их на части.
Чтобы добиться более понятного пользовательского интерфейса, нам нужно начать немного в неожиданном месте - усовершенствовать m-робота - создать протокол, по которому h-робот может получить от него более подробную информацию о правилах, которые будут применяться. На их основе он может нарисовать более качественный и дружественный интерфейс.
Для технического персонала нам возможно понадобиться научить h-робота измерять время, которое занимает загрузка интерфейса и сообщать эту информацию d-роботу. Тот мог бы его анализировать, контролируя, что оно в допустимых пределах. Но грузить самого h-робота этим анализом точно не стоит.
Программируя h-робота, Вы понимаете, что должны приложить максимум, чтобы избежать крэша приложения. Но у Вас есть все возможности UI-элементов, чтобы как-то разобраться с неожиданной ситуацией “по-людски”.
Для m-робота у Вас нет ничего для взаимодействия с людьми. Ну и черт с ними, с людьми - здесь Вы можете заниматься алгоритмами и структурами в чистом виде и смело использовать "принцип самурая" в любом месте.
Вся суть заключается в том, что при таком подходе в каждом конкретном моменте, Вы можете - не поступаясь здравым смыслом - предусматривать все возможные сценарии, который могут возникать на области определения этого алгоритма. Говоря “предусматривать” я имею виду обрабатывать как и ожидаемые сценарии, так и явно пресекать выбросом исключения запрещенные, если в них нет никакого смысла.
На мой взгляд, именно это ощущение завершенности - что Вами “закрыты” - обработаны или явно заблокированы все “ветки” развития - действительно повышает качество кода в целом и дает то чувство удовлетворенности от процесса программирования в частности.
Заключение
Конечно же, можно с легкостью отказаться от терминологии с роботами и использовать понятия вроде “уровень системы”, “уровень UI” и что-то типа “технический уровень”.
Также, если помните, мы начинали с цитаты, как типичный владелец продукта может сформулировать к нему требования: “чтобы кнопка была скрыта”. Несомненно, “команда высококлассных профессионалов” без труда поймет, чего он хочет на самом деле. Но это также вполне реальный сценарий, когда подобное требование заказчика Ваши коллеги будут воспринимать буквально и реализовывать буквально. Необходимость разложить задачу в схему hmd может работать как прием, позволяющий обнаружить подобные странности на ранней стадии. Например, то, что на функциональность, выдающую ценные данные, не наложено никаких ограничений. А также как формат общения между программистами, когда один пытается объяснить другому, каков его план или как что-то работает.
В моем понимании для изготовления хорошего ПО нужно преследовать две цели: правильно изготовить каждый отдельный его компонент и затем эти компоненты правильно соединить. Хочется верить, что метафора из этой статьи сможет помочь Вам с обоими пунктами.