22 мая 2018 г.

Программирование как дизайн

Нередко в команде software development проекта рано или поздно наступает момент, когда начинают вестись ярые споры и обсуждения вокруг соглашений о кодировании, об архитектурных решениях, о том, что важно, а что нет. Часто это переходит в формат холиваров или неконструктивной аргументации типа “надо делать так, потому что у ХХХ сделано так”, “надо делать так, потому что в YYY написано так”, “мне так кажется лучше и удобнее”. Попробую в этой статье немного порассуждать вокруг этих вещей. Как, мне кажется, лучше думать и рассуждать о коде и архитектуре? На что опираться в своих доводах? Начну издалека и немного в стиле капитана очевидность, но надеюсь, что к концу это прояснит мою мотивацию, поможет участникам диалога лучше понимать друг друга и конструктивнее договариваться.

Какую роль выполняют язык программирования, соглашения о кодировании, архитектура кода, вот это всё? Они выполняют роль посредника между человеком и компьютером, между программистом и алгоритмом. Это средство выражения и средство отображения мыслей, желаний человека и процессов реального мира в алгоритмы, в архитектуру железа компьютера. На это средство переноса человеческих идей в машину Тьюринга естественным образом налагаются ограничения. В первую очередь, налагаются ограничения со стороны компьютера - выполнимость, ресурсы, скорость и алгоритмическая сложность операций. Когда придумываешь программу - нужно беспокоиться о том, чтобы она остановилась, о контроле ресурсов и об алгоритмической скорости работы. Это вроде понятно. Но откуда возникла и продолжает возникать вся эта возня с языками высокого уровня, модульностью, декомпозицией, паттернами, стилями, архитектурой кода? Ведь ничего из этого в железе компьютера нет, там просто ячейки памяти и инструкции. Алгоритму ведь абсолютно не нужна архитектура или модульность, чтобы выполниться на машине Тьюринга. Зачем разработчики компилятора заморачиваются обнаружением мёртвого кода? Зачем нам средства обнаружения утечек памяти? Зачем счётчики ссылок или сборщики мусора? Зачем каждому языку программирования нужна стандартная библиотека структур данных и функций для работы с ними с гарантиями быстродействия? Зачем вот эта вся странная эволюция языков и средств разработки, длящаяся уже более полустолетия? На мой взгляд, это всё нужно из-за ограничений “архитектуры” на другом конце - из-за ограничений человеческого мозга, особенностей нашего мышления, особенностей работы наших органов чувств. Всё вышеперечисленное, языки высокого уровня, стили, паттерны, … - это всё следствия ограничений нашего мозга. Даже некоторые архитектурные решения на уровне кода, казалось бы, диктуемые практической целесообразностью, как, например, декомпозиция функций, препятствующая комбинаторному взрыву вариантов тестирования, - это тоже, как мне кажется, следствие взаимных ограничений алгоритмов и мозга. Наш мозг неспособен мысленно проверить сложный алгоритм на корректность и выполнимость, ему сложно обнаруживать мёртвые участки, нам сложно мысленно для большого алгоритма проконтролировать использование ресурсов, нам сложно отслеживать и гарантировать быстродействие - наш мозг абсолютно не приспособлен мыслить машинами Тьюринга, хотя последние и являются его творением.

У нас мизерная оперативная память - мы способны в один момент времени держать в уме не более 3-5 кусочков информации. Мы с трудом, после лет тренировок, способны мысленно, без подручных средств, отслеживать линейную логическую цепочку заключений и выводов длиной до десятка и более. Лишь малому проценту нормальных людей удаётся в уме отслеживать цепочку логических заключений одновременно по нескольким признакам - это хорошо демонстрирует известная Задача Эйнштейна и её разные вариации. Мы неспособны концентрироваться на нескольких смысловых контекстах одновременно - в этом смысле наш мозг однозадачен. Это из недостатков. Но что наш мозг умеет хорошо? Для чего он приспособлен эволюцией? Мы очень быстро привыкаем к любым однородным, регулярным и повторяющимся вещам, какими бы абсурдными они нам в начале не казались. Наш мозг прекрасно, на уровне подсознания, с параллельной обработкой, распознаёт уже знакомые / привычные образы (паттерны) в визуальной и речевой информации - и мы способны на уровне автоматизма реагировать на привычные вещи, и нам это, как правило, приятно. Наш мозг прекрасно выявляет неоднородности, отступления от правил, нарушения в однородных и привычных образах (визуальных и речевых) - вспомните это ощущение, когда само собой внезапно повышается внимание, происходит раздражение или возбуждение - глаз как будто сам цепляется за дисгармонию или исключение, не вписывающееся в общую закономерность или привычный фон. Это всё вероятно из-за того, что у нас в мозге есть специализированные “сопроцессоры” для анализа картинки (зрительная кора) и речи (области Брока и Вернике) - мозг людей специализируется на быстрой, бессознательной и эффективной обработке визуальной и речевой информации.

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

Хороший дизайн использует сильные стороны мозга - делает для пользователя привычными и бессознательными анализ ситуации и совершение типичных действий. Или наоборот, использует слабые - может специально вызывать нервозность и сознательное напряжение в ситуациях требующих повышенного внимания. Проектирование интерфейса - это как ориентация на существующие привычки пользователей, так и формирование у них новых. Продуктивный интерфейс должен помогать вырабатывать “полезные” привычки и препятствовать появлению “вредных”.

На что опираются в рассуждениях, когда дискутируют и принимают решения при обычной разработке графического интерфейса? Есть бизнес-функционал и его нужно выразить на экране для взаимодействия. Стараются брать привычные для пользователя элементы управления. Если и возникает желание сделать нетипичный элемент, то оценивают, действительно ли он значительно упрощает операции - к примеру, позволяет ли достигать результата за меньшее число кликов, действительно ли он значительно лучше отображает информацию - уменьшает ли неопределённость и делает ли восприятие легче. Если выгоды небольшие, то разумно применять привычные решения, а не заставлять пользователя понимать и привыкать к незнакомым. Также стараются избегать слишком большого количества элементов, ведь каждый элемент - это потенциальный источник неопределённости и когнитивного напряжения при работе с интерфейсом. Стараются использовать визуальное и структурное расстояние - чтобы элементы, расположенные близко друг к другу или сгруппированные вместе, несли общую смысловую нагрузку, а элементы с разным предназначением были отделены друга от друга пустым пространством или какой-либо структурой. Всё, что необходимо для работы с одним объектом или смыслом, стараются расположить в рамках одного диалога (экрана, раздела), чтобы мозгу пользователя не приходилось при работе резко переключать восприятие на другое изображение / обстановку. Каждый смысл и каждое действие стараются выразить везде визуально одинаковым образом - недопустимо, чтобы одно и то же в разных местах выглядело или работало по-разному. Стараются в каждой области интерфейса, в каждом элементе визуально предоставить всю необходимую информацию для принятия решения человеком, максимально уменьшить неопределённость - лишь в случае однозначности и ясности человеку легко анализировать и работать. В конце концов, стараются, чтобы при работе с интерфейсом смена контекста, например, переход от одного экрана/диалога к другому, происходила предсказуемым и легко отслеживаемым в уме образом - чтобы граф переходов был не сложнее дерева небольшой глубины, а не напоминал ментальный или реальный лабиринт. Чтобы человек на уровне бессознательной привычки мог ориентироваться, где он сейчас находится и куда ему надо “перейти” для достижения цели.

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

Пример 1. При работе с кодом нужно быстро понимать предназначение любой именованной сущности (константы, переменной, функции, …). Для этого, во-первых, придумывают стили именования, использующие визуальную информацию, - правила смешения больших и маленьких букв, специальных символов. Тогда мозг вырабатывает привычку автоматически и бессознательно трактовать изображения названий на экране. Во-вторых, полезно использовать соглашения для грамматических форм слов - придумать паттерны для использования существительных, глаголов, причастий, суффиксов, префиксов и т.п. Это эксплуатирует возможности речевого распознавания мозга. Визуальный анализ и чтение названий должны быть осмысленны и однородны. Делать отступления от внедрённых правил - плохо, это будет вызывать когнитивное напряжение и нервозность, аналогично тому, как если бы в 90% интерфейса главные кнопки диалогов назывались глаголами с большой буквы (“Сохранить”, “Отменить”, ..), но в остальных 10% использовались бы другие визуальные и грамматические формы (“запуск”, “ЗАКРЫТИЕ”).

Пример 2. Что является аргументом в пользу правила, что функции нужно стараться делать не более 40-60 строк? Ограничения мозга. Хорошо, чтобы код функции или алгоритма помещался на один экран, чтобы была возможность анализировать его изображение охватывая взглядом целиком, на полную мощность задействуя визуальную кору. Что является аргументом в пользу того, что следует ограничивать цикломатическую сложность функции? Тоже особенность мозга - линейность мышления. Цикломатическая сложность - это как раз количество линейных путей через код, то есть количество ментальных путей логического анализа алгоритма.

Пример 3. Перед программистом часто возникает вопрос “А вот стоит ли вот этот повторяющийся кусочек функционала вынести в отдельную функцию или же стоит оставить дублирование кода?”. Много написано о вреде дублирования кода. Но я бы хотел сказать, что и чрезмерное увлечение декомпозицией и небольшими функциями - это тоже нехорошо. Легко впасть в крайность и заводить функцию на каждый чих, и потом сгибаться под тяжестью их поддержки и понимания. И главное - непонятно, на какие чувства или аргументы стоит опираться при решении. Мне кажется, что и тут стоит подумать о работе мозга и полезно представлять аналогии с графическим интерфейсом. Каждая новая функция - это как бы новая кнопка или контрол. И аналогичный вопрос в графическом интерфейсе звучал бы так “Стоит ли вот на это повторяющееся действие завести отдельную кнопку/контрол, со своим дизайном и изображением? Или же стоит оставить выполнение действия через старые, возможно, более привычные, контролы?”. Думаю, чтобы принять разумное решение, нужно подумать о таких вещах как осмысленность и понятность этого действия для человека - можно ли удачно назвать одним словом или словосочетанием, атомарность действия - могут ли в процессе возникать ошибки, повторяемость, необходимость модификации… В любом случае, впадать в крайность и заводить по умолчанию на каждое действие по отдельной кнопке или функции - на мой взгляд это было бы неразумно.

Пример 4. Хороший программист при работе с кодом находится в постоянном поиске улучшений и пересмотре концепций и абстракций. А если это ещё и надо для целей проекта - код с налётом истории, с рядом недостатков, подлежит рефакторингу - то такой постоянный поиск и пересмотр становится непосредственной обязанностью программиста. Для того, чтобы оценивать удачность декомпозиции, удачность того или иного архитектурного решения, человек при работе с кодом должен ясно понимать и отслеживать зависимости. Какая функция или объект из какого модуля импортированы - это в данном случае важная информация. Значит, нужно позаботиться о том, чтобы эта важная информация была доступна на визуальном изображении кода, в локальном контексте, прямо там, где она необходима - в самой строчке, где используется функция или объект. В разных языках программирования это делается специальным синтаксисом - вызовом через точку или двоеточие, например. Чтобы человек мог охватить взглядом экран, изображение кода и сразу же легко, на уровне подсознания анализировать зависимости, отсеивать валидные и выявлять проблемные. Можно ли не выражать зависимости в коде, а полагаться лишь на возможность IDE - кликнуть на функцию, чтобы показало её происхождение? Давайте опять проведу аналогию с графическим интерфейсом. Представьте, что вы делаете интерфейс с инфографикой или табличным отображением данных. И часть данных вы показываете сразу, а другую, не менее важную, часть вы делаете доступной только в контекстном меню, выпадающем по клику - адекватный ли это интерфейс? На мой взгляд, IDE в данном случае играет роль не более чем “костыля”, слегка сглаживающего недостатки самого кода, а уж никак не интерфейсного решения.

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

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

Комментариев нет:

Отправить комментарий