- PVSM.RU - https://www.pvsm.ru -

Ребусы в коде, и как их расшифровать. Тайная сила идентификаторов

Чистый код читается, как хорошо написанная проза.
Грэди Буч в книге «Чистый код»

Ребус как код

Ребусы в коде, и как их расшифровать. Тайная сила идентификаторов - 1

Что такое ребус? Это зашифрованное послание. Автор ребуса берёт обычный человеческий текст и кодирует его при помощи рисунков, чисел и букв. А мы разглядываем такую шифровку и пытаемся прочесть исходный текст.

У ребуса есть две ипостаси. С одной стороны ребус — это исходный незашифрованный текст, а с другой — шифрорисунки. Текст — это «что» ребуса, его смысл, сообщение. Рисунки — это «как»: как именно зашифровано сообщение, с помощью каких средств. Отгадывая ребус, мы переводим «как» в «что».

Рисунки — это язык ребуса, его арсенал выразительных средств. Ребусник как бы говорит с нами с помощью этих рисунков, сообщает нечто. Использовать нормальные человеческие слова ему не позволено.

Вот как читаются ребусы:

Ребусы в коде, и как их расшифровать. Тайная сила идентификаторов - 2

Код как ребус

У программного кода есть кое-что общее с ребусом: у него тоже есть свои «что» и «как». И его тоже иногда приходится расшифровывать.

«Что» кода — это его назначение, смысл, тот эффект и конечный результат, который мы от него ждём. Что именно он делает.

«Как» кода — каким конкретным способом он выполнит своё «что», какими конкретно присваиваниями, умножениями, сравнениями; реализация алгоритма, инструкции процессору. Это дозволенный язык кода, его арсенал выразительных средств.

Мартин Фаулер рассказывает об этом так («Длина функции» [1], оригинал [2]):

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

Здесь highlight — это «что». В терминологии Мартина — intention: «подсвечивать фрагмент изображения». Название выражает, что делает эта функция. Reverse — это «как», implementation. Как именно выполняется подсвечивание (с помощью инверсии изображения). Вот в чём разница между «что» и «как».

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

Программист, когда пишет программу, подобен ребуснику. Программист шифрует человеческое описание алгоритма с помощью доступных средств языка программирования (более примитивных, чем человеческий язык). Зашифровывает «что» с помощью «как». А позже он сам или его коллега читает код, расшифровывая эти ребусы изначальное описание алгоритма. Если в процессе чтения кода мы не можем сходу понять, к какому результату приведёт выполнение каждого фрагмента, то есть каково назначение, смысл кода, значит этот код — ребус, и его нужно переписать на понятном языке.

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

Расшифровка ребуса по ходу чтения кода — это те самые мысленные преобразования, о которых говорит Тим Оттингер в книге «Чистый код». Правда, там он рассуждает о них в контексте присвоения понятных имён переменным, но проблему затрагивает ровно ту же самую. Слово Тиму:

Как правило, программисты весьма умны. А умные люди иногда любят показывать мощь интеллекта, демонстрируя свои способности к мысленному жонглированию. В конце концов, если вы помните, что переменная r содержит URL-адрес с удаленным хостом и схемой, преобразованный к нижнему регистру, это совершенно очевидно свидетельствует о вашем уме.
Одно из различий между умным и профессиональным программистом заключается в том, что профессионал понимает: ясность превыше всего. Профессионалы используют свою силу во благо и пишут код, понятный для других людей.

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

Итак, чтобы снизить усталость от написания и чтения кода, нужно избегать ребусов. Но как это сделать?

Язык кода. Сила идентификаторов

Я согласен с высказыванием Грэди Буча о том, что чистый код читается, как хорошая проза. Это необходимое, хотя и не достаточное условие. Большинство из нас интуитивно поймут, о чём идёт речь, но хотелось бы получить хоть какое-то определение: что же это такое — хорошая проза.

Я спросил своих друзей-писателей: чем отличается хорошая проза от плохой? Все ответили по-разному, но все так или иначе подчеркнули важность языка: он должен быть богатым, должен создавать в уме и душе читателя ясные образы. Когда у читателя без труда возникает чёткая картинка, которую хотел нарисовать ему автор — мы имеем дело с хорошей прозой.

Код рассказывает процессору, что тот должен делать. А хороший код одновременно рассказывает программисту — и притом предельно правдиво! — чем он, код, тут занимается. То есть излагает свой алгоритм максимально близко к тому, как это сделал бы сам автор на естественном языке. Наш код обязан делать это очень хорошо, иначе к нам домой может прийти необузданный маньяк с бензопилой или дробовиком [3]. Код не должен быть ребусом.

Какими средствами располагает код, чтобы не быть ребусом?

Рассказ от лица кода будет легко понятен человеку, если код сам заговорит на человеческом языке. Добиться этого можно только с помощью идентификаторов: названий функций, классов, переменных и констант — потому что только в идентификаторах мы можем использовать любые нужные нам слова человеческого языка.

Конечно, ключевые слова языка программирования — это тоже человеческие слова, но их словарь слишком убог. Что-то вроде языка Эллочки-Людоедки — хорошей прозы на нём не напишешь.

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

Посмотрите, насколько просто читаются строчки кода, когда хорошо подобраны названия переменных и методов:

pageData.hasAttribute("Test")
dom_tree.to_html()
emails_str.split(',')

Глядя на эти короткие фразы, легко понять, о чём они рассказывают. Мы знаем, какой результат получим, потому что идентификаторы сообщают нам об этом. А теперь представьте, что на месте каждого такого вызова находится его реализация — насколько снизится скорость чтения такого «зашифрованного» кода?

Многие простейшие приёмы рефакторинга: именованные константы, выделение метода, замена переменной вызовом метода, поясняющая переменная, расщепление временной переменной и т. д. — все они про то, как заставить код говорить на человеческом языке, другими словами, как избежать ребусов.

Метод ребуса

Когда я читал «Чистый код», меня периодически посещала мысль: «Какого чёрта!».

С высоты своего 40-летнего опыта Роберт Мартин даёт нам советы, как сделать код лучше. Например:

Первое правило: функции должны быть компактными. Второе правило: функции должны быть еще компактнее.

И тут же признаётся, что не может научно обосновать своё утверждение. Честно говоря, ненаучно у него тоже плохо получается. Требование компактности функции уже начинает смахивать на догму — вот почему споры [4] вокруг вопроса о длине функции не утихают столько десятилетий.

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

Более прагматично подходит к вопросу Мартин Фаулер.

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

Оригинал

The argument that makes most sense to me, however, is the separation between intention and implementation. If you have to spend effort into looking at a fragment of code to figure out what it's doing, then you should extract it into a function and name the function after that “what”. That way when you read it again, the purpose of the function leaps right out at you, and most of the time you won't need to care about how the function fulfills its purpose — which is the body of the function.

Уже лучше. Понимаете теперь, что хотел сказать Мартин в этом отрывке? Он имел в виду: давайте устранять ребусы. Пусть код сам рассказывает нам, какой результат будет получен, а каким образом — пусть это будет спрятано где-нибудь подальше, в определении функции. Пусть все ребусы будут расшифрованы. Нет ребусов — нет усилий.

Ни в коем случае не следует вслепую применять методы рефакторинга. Это так очевидно, но как понять, когда рефакторинг действительно нужен, а когда — нет? Метод ребуса гласит: если после рефакторинга ребус не исчез, то рефакторинг не нужен.

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

Пример расшифровки ребусов в коде (не очень удачной)

В качестве такового приведу полюбившийся мне фрагмент из книги «Чистый код». До рефакторинга мы видим код, полный ребусов. Рефакторинг выполнен автором книги в соответствии с продвигаемыми им правилами хорошего кода, и — вот же совпадение — отрефакторенный код выглядит в точности как код, в котором расшифрованы ребусы.

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

Например, метод include, самый часто используемый в этом отрывке

private void include(String pageName, String arg) throws Exception {	
	WikiPage inheritedPage = findInheritedPage(pageName);
	if (inheritedPage != null) {
		String pagePathName = getPathNameForPage(inheritedPage);
		buildIncludeDirective(pagePathName, arg);
	}
}

Название совершенно не отражает того, что происходит в реализации. Что include и куда?

Глядя на вызов этого метода:

include("TearDown", "-teardown");

невозможно сказать, какого результата собирался тут достичь автор кода.

Далее: что делает buildIncludeDirective? Судя по названию, он должен составить некую директиву включения, и что? Вернуть её? А вот нет. Он сразу добавляет её к общему результату.

А вот ещё updatePageContent. Что нам говорит updatePageContent о том, какой результат мы получим после вызова метода? Да ничего. Какой-то там page content будет заменён вообще неизвестно чем. Чего ради здесь выполнялся рефакторинг под названием выделение метода? Помог ли он избавиться от ребуса? Не помог, а только сильнее запутал код. Здесь мы имеем то самый случай, когда тело метода предпочтительнее. Конструкция

pageData.setContent(newPageContent.toString());

куда понятнее загадочного updatePageContent().

В качестве развлечения предлагаю читателям поискать, какие ещё есть неудачные места в отрефакторенном коде [5].

В оправдание Боба могу сказать, что в нынешней версии FitNesse этого кода уже нет. Видимо, в свою очередь, он тоже был когда-то отрефакторен.

Заключение

Длина функции — слишком нечёткий критерий для определения качества функции. «Короткие функции» не равно «хорошие функции». Длина функции — не критерий, всё, забудьте.

Хороший код должен давать ответы на вопросы программиста — зачем он (код) здесь, какого чёрта он тут делает результата добивается. Эти ответы могут быть даны только с помощью идентификаторов.

В качестве примера того, какие ответы код давать не должен, хочу привести отрывок из одной весёлой книжки.

– Я Ронан, Победитель Зла, – медленно проговорил он. – А это Тарл. Хотим тебе кое-какие
вопросы задать. Если станешь лгать, умрешь. Понял?
– Я, дяденька, завсегда, – выдохнул он. – Пожалуйста. Все-все-все скажу.
– Вот и славно, – продолжил Ронан. – Имя?
– Ронан, Победитель Зла.
– Да не мое, идиот!
– А, да-да, тогда Тарл, – извиняющимся тоном ответил орк.
– И не мое! – пробормотал Тарл. – Твое имя, дубина! Имя!
– Имя – это название, которым я пользуюсь, что бы отличать себя от других, – забубнил орк.
– Ну так давай сюда это название! – завопил Тарл.
Орка вдруг осенило.
– А! Прыщ!
– Итак, Прыщ, что ты здесь делаешь?
– В штаны кладу, – последовал правдивый ответ.
Ронан с отвращением сморщил нос.
– Нет, я спрашиваю, что ваша банда орков здесь делает!
Глаза Прыща стремительно завращались, озирая сцену.
– Большинство тут без голов валяется, – пробормотал он.
Тарл тронул Ронана за плечо.
– Дай я попробую, – уверенно произнес он и повернулся к перепуганному орку. – Скажи,
Прыщ, – продолжил он, – зачем ты здесь?
– Ой, дяденька, и не спрашивай. Экзистенциальная философия для меня просто лес темный.
– Слушай, ты, драконья отрыжка, – глухо прорычал он. – У вашей банды орков была особая
причина сюда прийти. Что здесь такого, в лесу?
– Здесь деревьев очень много.
Ронан выпучил глаза, а Тарл отвернулся. Прыщ, чувствуя, что дал не тот ответ, которого ждали, принялся бубнить дальше.
– А если вы хотите знать про причину, а не про лес, так все потому, что тот человек в пивной
заплатил нам, чтоб мы сюда пришли и тебя убили.
Джеймс Бибби, «Ронан-варвар»

Автор: SergeyGalanin

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/288338

Ссылки в тексте:

[1] «Длина функции»: https://habrahabr.ru/post/316708/

[2] оригинал: https://martinfowler.com/bliki/FunctionLength.html

[3] необузданный маньяк с бензопилой или дробовиком: https://habr.com/post/347166/#comment_10628798

[4] споры: https://habr.com/company/nixsolutions/blog/341034/

[5] отрефакторенном коде: https://github.com/unclebob/fitnesse/blob/7491001db74ad1b577e3e3e44e3b87c93e881c02/src/fitnesse/html/SetupTeardownIncluder.java#L6

[6] Источник: https://habr.com/post/419269/?utm_campaign=419269