Форматирование кода. Как выглядит красивый HTML код? Зачем нам нужен красивый код
Порой наш код не всегда бывает идеален. А так хочется, чтобы он был не только рабочий, но и красиво оформлен и отформатирован. Время - это наш главный враг, оно редко позволяет следить нам за правилами оформления кода. Мы стараемся побыстрее закончить верстку, или описать десяток css-стилей, и делаем это в ущерб читабельности. В этой статье я приведу список сервисов, которые помогут вам отформатировать ваш код так, чтобы на него было приятно смотреть.
Довериться программе или делать все ручками?
Наверняка у вас возникал вопрос: как сделать код читабельным? Может ли программа грамотно расставить все отступы и переносы строк так, чтобы человеческий глаз получал он вида кода лишь эстетическое удовольствие? Конечно, может! Не стоит заблуждаться, что, раз работу за вас делает, по сути, робот, то сделана она будет "грязно". Сервисы, которые будут представлены в статье ниже, не раз спасали меня. Например, в ситуациях, когда необходимо было скопировать тот же html код с другого сайта, а теги при вставке оказывались просто в каком-то хаотическом порядке расставлены по строкам: куча табуляций, не логические переносы строк, абсолютно не видно никакой вложенности! Наверное, многим такое знакомо. И очень хочется, чтобы на своем сайте у вас было по-другому: аккуратно и хорошо читаемо. Ведь, в первую очередь, мы делаем это для себя, для удобства дальнейшей поддержки того или иного кода.
Давайте же отформатируем ваш код
Меньше слов, больше дела. Как показала практика, форматирование кода онлайн происходит достаточно просто. Вам нужно лишь скопировать ваш "грязный" код и вставить его в специальные текстовые поля на одном из указанных сайтов. Затем нажать кнопку, немного подождать и - вуаля! Вы получаете прекрасный, отформатированный и легко читабельный код.
Вот список всех известных мне "пурификаторов" кода для различных языков.
Чтобы все дальнейшие описание было Вам более понятным, скачайте этот PDF фаил или просто откройте в новой вкладке Вашего браузера это изображение .
Иногда интересно обратить внимание на то, всегда ли в красивом внешне вебсайте использован "красивый" html код, т.е. точный, структурированный, безошибочный. Html код это своеобразное искусство. Он не такой развитый и многообразный, как динамический язык, и тем не менее может быть написан довольно изящно и мастерски.
Вот список тех особенностей с помощью которых можно определить написан ли код с мастерством или нет.
1. DOCTYPE, определенный должным образом. Эта строка позволяет не только определить ваш код, но и "говорит" браузеру о том, как следует отобразить вашу страницу.
2. Опрятный раздел в тегах
Заголовок установлен. Кодировка объявлена. Таблицы стилей подключены. Скрипты подключены, а не написаны полностью.
3. Присвоение ID вашему документу позволяет создавать свойства CSS, которые уникальны для каждой страницы. Например, вы можете захотеть, чтобы Ваш
тэг выглядел по-особенному, к примеру, на домашней странице. В таблице стилей CSS вы можете написать: #home h2 {}, чтобы достигнуть этого.
4. Чистое меню. Вы, наверно, часто встречались с тем, что в качестве меню используются списки. Это очень удобно, т.к. дает вам возможность контролировать его.
5. Заключение всего содержания страницы в главный тег 6. Важное нужно размещать в начале.
Новости и важные события в коде должны следовать первыми, а меню, маловажное содержание следует размещать в конце. 7. "Включение" элементов сайта.
Большинство элементов сайта повторяются на каждой странице, значит их нужно включать, с помощью php-команды include. 8. Код должен быть табулирован по разделам.
Если каждый раздел кода будет выделен отступом, то структура кода будет намного понятнее. Код, который выровнен по левому краю трудно читать и понять. 9. Правильные заключительные теги
. Исключите заключительные теги в непарных тегах, и проверьте, чтобы парные теги были закрыты. 10. Используйте теги заголовков
. Использование заголовков необходимо, т.к. они показывают иерархию разделов, и больше упорядочивают их. 12. Никаких стилей!
Ваш html код должен быть сосредоточен на структуре и содержании, а не на оформлении страницы! Все стили выносите в отдельный файл CSS. Роберту Мартину удалось идеально описать измерение качества кода кода: Единственным ценным измерением качества кода является WTF/мин. Объясню чуть подробнее. Когда я провожу code review, у меня бывает только три эмоции: Что же влияет на нас первым делом, когда мы видим любой код? Это чистота и красота его написания. Создание чистого и красивого кода - это знак отличного мастера. В изучении этого ремесла есть два направления: знание и работа. Знание учит вас шаблонам, принципам и методам, которые вам нужны для того, чтобы стать лучше в профессии. Но это знание нужно применять на постоянной практике и в упорной работе. Вот несколько способов, которые могут помочь вам в искусстве написания чистого и красивого кода. Кендрик Ламар отлично сказал: Если я захочу рассказать реальную историю, то я начну со своего имени. Названия находятся в программе повсеместно. Мы называем наши функции, классы, аргументы и много других вещей. Мы называем файлы с исходниками, директории и всё, что внутри них. Мы постоянно придумываем новые имена, пока они не начинают засорять наш чистый код. Название должно показывать намерение. Выбор хороших названий требует времени, но в итоге сохраняет его вам в будущем. Поэтому думайте о названиях и изменяйте их, если вдруг вы придумали имя получше. Помните, что название каждой переменной, функции или класса должно отвечать на три вопроса: почему оно существует, что оно делает и для чего используется. Это требует не только хороших навыков описания, но и широкого культурного бэкграунда, и этому можете научиться только вы сами. Луис Салливан однажды сказал: Форма следует за функцией. Каждая система создается на основе предметно-ориентированного языка, который создан программистами для возможности точного описания. Функции являются глаголами этого языка, а классы - существительными. Функции должны быть первыми в очередь на организацию в любом языке программирования, и создание хороших функций - это суть написания хорошего кода. Существует только два золотых правила создания чистых функций: Это означает, что в вашей функции не должно содержаться вложенных структур. Таким образом, уровень отступа в функции не должен быть больше, чем один или два. Этот метод делает код проще для чтения и понимания. В дополнение к этому, нам нужно убедиться, что утверждения в нашей функции находятся на одном уровне абстракции. Смешивание уровней абстракции в функции приводит к коду, который нельзя будет исправить. Мастера-программисты думают о функциях, как об историях, которые нужно рассказать, а не как о коде, который нужно написать. Они используют удобства выбранного языка, чтобы создавать выразительный и чистый блок кода, который будет хорошо рассказывать нужную историю. Винус Уильямс заметила: Каждый оставляет свои комментарии. Так рождаются слухи. Комментарии - это обоюдоострый нож. Хорошо размещенный комментарий может быть очень полезен. Но с другой стороны, ничего так не засорит код, как бесполезные комментарии. И ничего не может так распространять дезинформацию, как комментарии. Так что комментарии - это необходимое зло. Не всегда, но в большинстве случаев. Чем старше комментарий, тем сложнее становится его поддерживать, и многие программисты не выравнивают комментарии со своим кодом. Код двигается и развивается. Части кода перемещаются туда и сюда, а комментарии - нет, и это становится проблемой. Всегда помните, что чистый и выразительный код с несколькими комментариями лучше, чем засоренный и сложный код с множеством комментариев. Не тратьте время на объяснение созданного беспорядка, лучше уберите его. Форматирование кода - это коммуникация, а коммуникация - это приоритет для профессионального разработчика, - отмечает Роберт Мартин. Отформатированный код - это окно в ваш разум. Мы хотим, чтобы люди были впечатлены нашим стремлением к порядку, вниманием к деталям и ясностью мысли. Но если они увидят непонятную массу кода без выраженного начала или окончания, это несомненно пошатнет вашу репутацию. Если вы думаете, что главное, чтобы все работало, вы не правы. Функциональность, которую вы создаете сегодня, могут заменить в следующем релизе, но читаемость вашего кода не изменится. Всегда знайте, что вас будут помнить за стиль и дисциплину, а не за ваш код. Поэтому вы должны позаботиться о том, чтобы ваш код был хорошо отформатирован и чтобы он подчинялся простым правилам, которые понимают все члены вашей команды. Жорж Кангилем правильно сказал: Ошибаться - это по-человечески, постоянно ошибаться - это бесчеловечно. Программисты постоянно справляются с ошибками. Входные данные могут быть ненормальными, и устройства могут выходить из строя. Как разработчики, мы должны убедиться, что код делает то, что ожидается. Однако проблема заключается не в обработке ошибки, проблема заключается в обработке ошибки с сохранением чистого читаемого вида. Часто исправление ошибок сильно меняет код. Все становится таким разрозненным, что понять цель и логику главного кода становится сложно. Это неправильно. Код должен быть чистым и надежным, а ошибки должны быть исправлены с грацией и стилем. Это признак мастера в разработке. Один из способов добиться этого - правильное размещение всех ошибок в блоках try-catch. Эти блоки определяют объем вашего кода. Когда вы выполняете часть кода в части try, вы утверждаете, что это выполнение может прерваться в любой момент времени и продолжиться в catch. Поэтому, когда вы пишете код, хорошей практикой будет начать с утверждения try-catch-finally. Это поможет вам определить, чего ожидает пользователь, независимо от того, что произойдет неправильно с кодом в секции try. Помните, что для каждого исключения вы должны описать достаточно контекста для того, чтобы определить источник и место ошибки. Творческие информативные сообщения об ошибках запомнятся надолго после того, как код был написан, а программисты ушли из организации. Каким словом можно обобщить все сказанное здесь? Это чувство кода, аналог здравого смысла в программном обеспечении. Согласно Роберту Мартину, «написание чистого кода требует дисциплинированного использования мириад маленьких техник, примененных для ощущения чистоты. Эти маленькие методы вместе образуют чувство кода». Некоторые из нас рождаются с ним, а некоторые упорно приобретают его через практику. Это чувство кода не только помогает нам различать хороший и плохой код, но и позволяет нам формировать стратегии трансформации плохого кода в хороший. Чувство кода помогает программисту выбирать лучшие инструменты из доступных, чтобы создавать ценный, чистый и красивый код. Подвести итог можно словами Гарольда Абельсона: Программы должны быть написаны прежде всего для людей, которые будут их читать, и только потом для машин, которые будут их выполнять. Доброго времени суток всем, кто сейчас читает данную публикацию. Сегодня я хочу рассказать вам об одном из инструментов сайтостроения, без которого ни один веб-ресурс не может обойтись. Это меню сайта, или как еще говорят карта сайта. На сегодняшний день существует безграничное множество видов и подвидов меню. Разработчики интернет-магазинов, блогов, обучающих сервисов и других ресурсов экспериментируют и создают все более новые и необычные карты. После прочтения статьи вы узнаете, на какие основные группы делятся все виды панелей навигации, сможете опробовать каждую из них, а также научиться писать код меню для сайта html. А теперь перейдем непосредственно к делу! В языке разметки существует несколько способов создания меню. Основная их концепция заключается в использовании ненумерованного списка. Таким образом, в привычном для нас html 4 разработчики прописывают на станице теги Как оговаривалось в предыдущих публикациях, парный элемент
Навигация сайта Однако с появлением платформы язык разметки пополнился дополнительными тегами. Именно поэтому меню современных веб-сайтов создается при помощи специального тега <
menu>
. В использовании этот элемент ничем не отличается от маркированных списков. Вместо единицы <
ul>
прописывается <
menu>
. Однако существенные различия появляются если судить со стороны работы . Так, второй пример ускоряет работу поисковых программ и роботов в . При анализе структуры сайта они сразу понимают, что данный кусок кода отвечает за карту сайта. Бывают горизонтальные, вертикальные и выпадающие меню. Иногда панель навигации оформляют в виде изображения. Так как сегмент технологий расширился, веб-сервисы делают адаптивными, т.е. структура страниц автоматически адаптируется под размер экрана девайсов. Рассмотрим же перечисленные группы меню. Такой вид навигации наиболее популярен. При горизонтальном оформлении панели все пункты меню расположены в шапке страницы или в «подвале» (иногда навигационные элементы дублируются, отображаясь одновременно и сверху, и снизу). В качестве примера мы создадим горизонтальную панель, пункты меню которого будут оформлены при помощи css (каскадных таблиц стилей), а точнее трансформированы. Так, каждый отдельный элемент будет находится в скошенном прямоугольнике. Заинтриговал? Для трансформации мы используем свойство css под названием transform
. Чтобы указать трансформацию, используется встроенная функция skewX
, в которой угол наклона указывается в градусах. К сожалению, каждый браузер работает с данным свойством по-своему, не смотря на прописанные стандарты. Поэтому для обозначения того или были созданы специальные префиксы: А теперь полученные знания применим к написанию примера.
Для второй программы используем за основу предыдущий код. Я захотел, чтобы пункты моего вертикального меню были не скошены, а с округленными углами. Для этого я воспользовался еще одним свойством css border-radius
. В предыдущих статьях я уже работал с данным параметром, так что сложностей с пониманием его функционирования, думаю, не возникнет.
Как вы уже заметили, главное изменение в этом коде – это отсутствие объявления display: inline-block
, который собственно и отвечал за горизонтальное расположение пунктов навигации. Мы с вами рассмотрели основные группы навигационных панелей, однако существует еще несколько разновидностей или лучше сказать дополнений. Иногда возникают такие ситуации, когда некоторые из пунктов дополняют основные. В этом случае не обойтись без выпадающих списков. Они создаются путем преобразований инструментами css. Ниже я прикрепил код небольшой программки, в которой реализуется данный подход.
В данном примере я разделил единицы меню на два класса: Первый класс отвечает за основное меню, а s-menu – за подменю. В коде можно встретить такой прием, как .m-menu > li:hover
или .m-menu > li.
Так, при помощи:hover указывается, как будет вести себя элемент при наведении на него курсора. При этом знак «>» видоизменяет селектор так, чтобы блочно-строчными были только объекты, относящиеся к верхнему уровню. Изначально подменю было задано display:
none
, что оповещает обработчик скрывать данный объект. После наведения на элемент навигации с указанием hover
, значение свойства display
меняется на block
и поэтому открывается выпадающий список. Как видите, реализация такого приема очень простая. Теперь вы освоили основные виды навигационных панелей и можете самостоятельно их видоизменять, дополнять и модернизировать. Если вам понравилась моя статья, то подписывайтесь на обновления блога и делитесь источником знаний с друзьями и коллегами. Пока-пока! С уважением, Роман Чуешов Прочитано: 1010 раз
Сравнительно быстро можно обучить человека пользоваться необходимым инструментарием и документацией, правильной коммуникации с заказчиком и внутри команды, правильному целеполаганию и расстановке приоритетов (ну, конечно, в той мере, в которой сам всем этим владеешь). Но когда дело доходит собственно до кода, все становится гораздо менее однозначно. Да, можно указать на слабые места, можно даже объяснить, что с ними не так. И в следующий раз получить ревью с абсолютно новым набором проблем. Профессии программиста, как и большинству других профессий, приходится учиться каждый день в течение нескольких лет, а, по большому счету, и всю жизнь. Вначале ты осваиваешь набор базовых знаний в объеме N семестровых курсов, потом долго топчешься по различным граблям, перенимаешь опыт старших товарищей, изучаешь хорошие и плохие примеры (плохие почему-то чаще). Говоря о базовых знаниях, надо отметить, что умение писать красивый профессиональный код - это то, что по тем или иным причинам, в эти базовые знания категорически не входит
. Вместо этого, в соответствующих заведениях, а также в книжках, нам рассказывают про алгоритмы, языки, принципы ООП, паттерны дизайна… Да, все это необходимо знать. Но при этом, понимание того, как должен выглядеть достойный код, обычно появляется уже при наличии практического (чаще в той или иной степени негативного) опыта за плечами. И при условии, что жизнь “потыкала” тебя не только в сочные образцы плохого кода, но и в примеры всерьез достойные подражания. В этом-то и заключается вся сложность: твое представление о “достойном” и “красивом” коде полностью основано на личном многолетнем опыте. Попробуй теперь передать это представление в сжатые сроки человеку с совсем другим опытом или даже вовсе без него. Но если для нас действительно важно качество кода, который пишут люди, работающие вместе с нами, то попробовать все же стоит! Но являются ли эстетические качества кода фактором, положительно влияющим на вышеперечисленные показатели? Это так, потому что красивый код, вне зависимости от субъективной трактовки понятия о красоте, обладает следующими (в той или иной степени сводимыми друг к другу) важнейшими качествами: А теперь, чтобы от общих слов перейти к конкретике, давайте сделаем обратный ход и скажем, что именно читаемый и управляемый код обычно воспринимается нами как красивый и профессионально написанный. Соответственно, на обсуждении того, как добиться этих качеств, мы далее и сосредоточимся. Вне зависимости от конкретного языка программирования и решаемых задач, для того, чтобы фрагмент кода в достаточной степени обладал этими двумя качествами необходимо, чтобы он был: Это все, конечно, хорошо и важно, но, говоря о реализации бизнес-логики в реальных проектах, обычно приходится иметь дело с алгоритмами совсем другого свойства, больше напоминающими иллюстрации к детским книжкам по программированию. Что-то типа такого (взято из ): Таким образом, с линейностью я связываю не столько асимптотическую сложность алгоритма, сколько максимальное количество вложенных друг в друга блоков кода, либо же уровень вложенности максимально длинного подучастка кода
. Например, идеально линейный фрагмент: Do_a();
do_b();
do_c();
Do_a();
if (check) {
something();
} else {
anything();
if (whatever()) {
for (a in b) {
if (good(a)) {
something();
}
}
}
}
Примечание
: поскольку здесь и далее нам потребуется примеры кода для иллюстрации тех или иных идей, сразу условимся, что они будут написаны на абстрактном обобщенном C-like языке, кроме тех случаев, когда потребуются особенности конкретного существующего языка. Для таких случаев будет явно указано, на каком языке написан пример (конкретно будут встречаться примеры на Java и Javascript). Попробуем сделать это на основе “алгоритма авторемонта” на диаграмме выше: Listen_client();
if (!is_clean()) {
...
}
check_cost();
if (!client_agree()) {
...
}
find_defects();
if (defects_found()) {
...
}
create_request();
take_money();
bye();
Listen_client();
if (!is_clean()) {
...
}
check_cost();
if (client_agree()) {
find_defects();
if (defects_found()) {
...
}
create_request();
take_money();
} else {
...
}
bye();
If (!client_agree()) {
...
} else {
find_defects();
if (defects_found()) {
...
}
create_request();
take_money();
bye();
}
If (!client_agree()) {
...
return;
}
find_defects();
if (defects_found()) {
...
}
create_request();
take_money();
bye();
Разумеется, неверным был бы вывод, что вообще никогда не нужно использовать оператор else
. Во-первых, не всегда контекст позволяет поставить break
, continue
, return
или throw
(хотя часто как раз таки позволяет). Во-вторых, выигрыш от этого может быть не столь очевиден, как в примере выше, и простой else
будет выглядеть гораздо проще и понятней, чем что-либо еще. Ну и в-третьих, существуют определенные издержки при использовании множественных return
в процедурах и функциях, из-за которых многие вообще расценивают данный подход как антипаттерн (мое личное мнение: обычно преимущества все-таки покрывают эти издержки). Поэтому эта (и любая другая) техника должна восприниматься как подсказка, а не как безусловная инструкция к действию. Поэтому продемонстрируем технику на основе “плохого” примера из начала главы: Do_a()
if (check) {
something();
} else {
anything();
if (whatever()) {
for (a in b) {
if (good(a)) {
something();
}
}
}
}
Procedure do_on_whatever() {
for (a in b) {
if (good(a)) {
something();
}
}
}
do_a();
if (check) {
something();
} else {
anything();
if (whatever()) {
do_on_whatever();
}
}
Однако следует иметь в виду, что та же самодокументированность
сильно пострадает, если у вынесенной части кода нет общей законченной задачи, которую этот код выполняет. Затруднения в выборе правильного имени для процедуры могут быть индикатором именно такого случая (см. п. 6.1
). If (check) {
do_a();
something();
if (whatever()) {
for (a in b) {
if (good(a)) {
something();
}
}
}
} else {
do_a();
anything();
if (whatever()) {
for (a in b) {
if (good(a)) {
something();
}
}
}
}
Do_a();
if (check) {
something();
} else {
anything();
}
if (whatever()) {
for (a in b) {
if (good(a)) {
something();
}
}
}
Бороться с этим лучше всего, минимизируя размер участка внутри блока. Т.е. все строки, не предполагающие появление исключения, должны быть вынесены за пределы блока. Хотя в некоторых случаях с точки зрения читаемости более выигрышным может оказаться и строго противоположный подход: вместо того, чтобы писать множество мелких блоков try..catch
, лучше объединить их в один большой. Кроме того, если ситуация позволяет не обрабатывать исключение здесь и сейчас, а выкинуть вниз по стеку, обычно лучше именно так и поступить. Но надо иметь в виду, что вариативность тут возможна только в том случае, если вы сами можете задавать или менять контракт редактируемой вами процедуры. If (a) {
if (b) {
do_something();
}
}
If (a && b) {
do_something();
}
If (a) {
var1 = b;
} else {
var1 = c;
}
Var1 = a ? b: c;
If (a) {
var1 = b;
} else if (aa) {
var1 = c;
} else {
var1 = d;
}
Var1 = a ? b:
aa ? c: d;
Заметим, что инициализация переменной var1
теперь осуществляется одной единственной операцией, что опять же сильно способствует самодокументированности (см п. 6.8
). Можно было бы решить проблему через try...finally
, но это не совсем правильно, т.к. в данном случае не идет речи об обработке исключений. Да и делу линеаризации такой подход бы сильно помешал. Давайте сделаем так (на самом деле, я тут применил п. 5.1
еще до того, как его написал): Procedure negotiate_with_client() {
check_cost();
if (!client_agree()) {
pay_for_wash();
return;
}
find_defects();
if (defects_found()) {
say_about_defects();
if (!client_agree()) {
pay_for_wash_and_dyagnosis();
return;
}
}
create_request();
take_money();
}
listen_client();
if (!is_clean()) {
wash();
}
negotiate_with_client();
bye();
В этом смысле, идеальное инженерное решение - это, когда ничего не сделано, но все работает, как требуется. Разумеется, в реальном мире крайне редко доступны идеальные решения, и поэтому у нас, программистов, пока еще есть работа. Но стремиться стоит именно к такому идеалу. Разумеется, самым очевидным методом борьбы с проблемой является вынесение переиспользуемого кода в отдельные процедуры и классы. Стоит также упомянуть весьма полезную технику устранения дублирования, описанную в п. 4.3
. Procedure proc1() {
init();
do1();
}
procedure proc2() {
init();
do2();
}
proc1();
proc2();
Init();
do1();
do2();
Особенно это касается, конечно же, проверок на null
. Как правило, вызвано подобное извечным страхом программистов перед вездесущими NPE
и желанием лишний раз от них перестраховаться. Далеко не редким видом ненужной проверки является следующий пример: Obj = new Object();
...
if (obj != null) {
obj.call();
}
Сюда же можно включить десятки других видов проверок на заведомо выполненное (или заведомо не выполненное) условие. Obj = factory.getObject();
obj.call1();
// если бы obj был null, мы бы уже умерли)
if (obj != null) {
obj.call2();
}
Третий пример чуть менее очевиден, чем первые два, но распространен просто повсеместно: Procedure proc1(obj) {
if (!is_valid(obj)) return;
obj.call1();
}
procedure proc2(obj) {
if (!is_valid(obj)) return;
obj.call2();
}
obj = factory.getObject();
if (is_valid(obj) {
proc1(obj);
proc2(obj);
}
Варианты тут могут быть разными: Еще одним полезным подходом является применение паттерна NullObject
, предполагающего использование объекта с ничего не делающими, но и не вызывающими ошибок, методами вместо “опасного” null
. Частным случаем такого подхода можно считать отказ от использования null для переменных-коллекций в пользу пустых коллекций. Сюда же относятся специальные null-safe
библиотеки, среди которых хотелось бы выделить набор библиотек apache-commons
для Java. Он позволяет сэкономить огромное количество места и времени, избавив от необходимости писать бесконечные рутинные проверки на null
. Большую часть времени перед нами встают задачи или подзадачи, которые уже множество раз были решены, будь то сортировка или поиск по массиву, работа с форматами 2D графики или long-polling сервера на Javascript. Общее правило заключается в том, что стандартные задачи имеют стандартное решение, и это решение дает нам возможность получить нужный результат, написав минимум своего кода. Порой есть соблазн, вместо того, чтобы что-то искать, пробовать и подгонять, быстренько набросать на коленке свой велосипед. Иногда это может быть оправдано, но если речь идет о поддерживаемом в долгосрочной перспективе коде, минутная “экономия” может обернуться часами отладки и исправления ошибок на пропущенных корнер-кейсах. С другой стороны, лишь слегка затрагивая достаточно обширную тему, хотелось бы сказать, что иногда “велосипедная” реализация может или даже должна быть предпочтена использованию готового решения. Обычно это верно в случае, когда доверие к качеству готового решения не выше, чем к собственному коду, либо в случае, когда издержки от внедрения новой внешней зависимости оказываются технически неприемлемы. Тем не менее, возвращаясь к вопросу о краткости кода, безусловно, использование стандартных (например, apache-commons
и guava
для Java) и нестандартных библиотек является одним из наиболее действенных способов уменьшить размеры собственного кода. На деле же, любой код
, в том числе неиспользуемый, требует плату за свое содержание в виде потраченного внимания и времени. Таким образом, удаляя ненужные и неиспользуемые участки, мы не только уменьшаем размеры кода, но и, что не менее важно, способствуем его самодокументированности
. В качестве иллюстрации приведу наиболее, с моей точки зрения, яркий пример для языка Javascript. Очень часто при разборе строковых выражений можно увидеть такие нагромождения: If (obj && obj.s) {
// do something
}
Аналогично, сравните следующие формы записи: If (!a) {
a = defaultValue;
}
A = a || defaultValue;
Читая XML файл мы, даже ничего не зная о контексте, почти всегда можем составить представление о том, что описывает данный файл, что за данные в нем представлены и даже, возможно, как они будут использованы. Распространяя эту идею на программный код, под термином “самодокументированность” хотелось бы объединить множество свойств кода, позволяющих быстро, без детального разбора и глубокого вникания в контекст понять, что делает данный код. Хотелось бы противопоставить такой подход “внешней” документированности, которая может выражаться в наличии комментариев или отдельной документации. Не отрицая необходимости в определенных случаях того и другого, отмечу, что, когда речь идет о читаемости кода, методы самодокументирования оказываются значительно более эффективными. Если поле называется name, там должно храниться именно название объекта, а не дата его создания, порядковый номер в массиве или имя файла, в который он сериализуется. Если метод называется compare()
, он должен именно сравнивать объекты, а не складывать их в хэш таблицу, обращение к которой можно будет найти где-нибудь на 1000 строк ниже по коду. Если класс называется NetworkDevice
, то в его публичных методах должны быть операции, применимые к устройству, а не реализация быстрой сортировки в общем виде. Сложно выразить словами, насколько часто, несмотря на очевидность этого правила, программисты его нарушают. Думаю, не стоит пояснять, каким образом это сказывается на читаемости их кода. Чтобы избежать таких проблем, необходимо максимально тщательно продумывать название каждой переменной, каждого метода и класса. При этом надо стараться не только корректно, но и, по возможности, максимально полно
охарактеризовать назначение каждой конструкции. Если этого сделать не получается, причиной обычно являются мелкие или грубые ошибки дизайна, поэтому воспринять такое надо минимум как тревожный “звоночек”. Очевидно, так же стоит минимизировать использование переменных с названиями i
, j
, k
, s
. Переменные с такими названиями могут быть только локальными и иметь только самую общепринятую семантику. В случае i
, j
, это могут счетчики циклов или индексы в массиве. Хотя, по возможности, и от таких счетчиков стоит избавляться в пользу циклов foreach и функциональных выражений. Примерно то же самое можно сказать и об “обратной” проблеме - когда для одной и той же сущности/операции/алгоритма используется несколько разных имен. Время на анализ такого кода может возрасти по сравнению с ожидаемым в разы. Вывод простой: в своих программах к возникновению синонимов и омонимов надо относиться крайне внимательно и всеми силами стараться подобного избегать. Из этого следует самый прямой вывод: чем меньше сущностей вы введете, тем проще и лучше в итоге окажется ваш код. Типичный пример “лишней” переменной: Int sum = counSum();
int increasedSum = sum + 1;
operate(increasedSum);
...
Int sum = counSum();
operate(sum + 1);
...
Operate(countSum() + 1);
...
Однако применять его стоит лишь в случае, если этот прием способствует самодокументированности, а не противоречит ей. Например: Double descr = b * b - 4 * a *c;
double x1 = -b + sqrt(descr) / (2 * a);
Обобщая принцип, продемонстрированный на данном примере, можно заключить следующее: желательно в своей программе создавать переменные/функции/объекты, только если они имеют прототипы в предметной области. При этом, в соответствии с п. 6.1
, надо стараться, чтобы название этих объектов максимально ясно выражало это соответствие. Если же такого соответствия нет, вполне возможно, что использование переменной/функции/объекта лишь перегружает ваш код, и их удаление пойдет программе только на пользу. Можно привести грубый пример, подразумевая, что жизнь подкидывает подобные примеры совсем не редко. Косвенное условие: If (i == abs(i)) {
}
If (i >= 0) {
}
Простой пример: Object someobj = createSomeObj();
if (some_check()) {
// Don"t need someobj here
} else {
someobj.call();
}
Нетрудно понять, как сделать этот код чуть-чуть лучше: If (some_check()) {
// Don"t need someobj here
} else {
Object someobj = createSomeObj();
someobj.call();
}
Ну или, если переменная нужна для единственного вызова, можно воспользоваться еще и п. 6.3
: If (some_check()) {
// Don"t need someobj here
} else {
createSomeObj().call();
}
Object someobj = createSomeObj();
for (int i = 0; i < 10; i++) {
someobj.call();
}
Такие случаи не отменяют общего принципа, а просто служат иллюстрацией того, что каждая техника имеет свою область применения и должна быть использована вдумчиво. Приводит оно к появлению ненужных зависимостей от состояния объекта в потенциально статических методах, либо вообще к неправильному управлению состоянием объекта. Как следствие - к затруднению анализа кода. Поэтому необходимо стараться обращать внимание и на данный аспект, явно выделяя методы, не зависящие от состояния объекта. Если данной техникой пренебречь, читателю придется для каждого объекта искать по коду, где он был объявлен, нет ли у него где-нибудь инициализации другим значением и т.д. Все это затрудняет анализ кода и увеличивает время, необходимое для того, чтобы в коде разобраться. Именно поэтому, например, функциональные операции из apache CollectionUtils
и guava Collections2
часто предпочтительней встроенных в Java foreach
циклов - они позволяют совместить объявление и инициализацию коллекции. Сравним: Collection Для большей наглядности рассмотрим еще и пример на Javascript (взято отсюда: http://habrahabr.ru/post/154105). Сравним: Var str = "mentioned by";
for(var i =0; l= tweeps.length; i < l; ++i){
str += tweeps[i].name;
if(i< tweeps.length-1) {str += ", "}
}
Var str = "mentioned by " + tweeps.map(function(t){
return t.name;
}).join(", ");
Также как разновидность подобного компромисса следует расценивать необходимость спецификации контракта метода/класса/процедуры при помощи комментариев, если его не получается явно выразить другими языковыми средствами (см п. 5.2
). В нашем случае целью является получение максимально читаемого и управляемого кода. Который одновременно будет приятен с эстетической точки зрения. Изучая примеры, приведенные в текущей статье, можно легко заметить, что, как сами исходные принципы (линейность, минимальность, самодокументированность), так и конкретные техники не являются независимыми друг от друга. Применяя одну технику мы можем также косвенно следовать и совсем другой. Улучшая один из целевых показателей, мы можем способствовать улучшению других. Однако, у данного явления есть и обратная сторона: нередки ситуации, когда принципы вступают в прямое противоречие друг с другом, а также со множеством других возможных правил и догм программирования (например с принципами ООП). Воспринимать это надо совершенно спокойно. В программировании, как и в большинстве других областей человеческой деятельности, не может существовать универсальных инструкций к исполнению. Каждая ситуация требует отдельного рассмотрения и анализа, а решение должно приниматься исходя из понимания особенностей ситуации. В целом, этот факт нисколько не отменяет полезности как вывода и понимания общих принципов, так и владения конкретными способами их воплощения в жизнь. Именно поэтому я надеюсь, что все изложенное в статье может оказаться действительно полезно для многих программистов. Ну и еще пара слов о том, с чего мы начинали - о красоте нашего кода. Даже зная о “продвинутых” техниках написания красивого кода, не стоит пренебрегать самыми простыми вещами, такими, как элементарное автоформатирование и следование установленному в проекте стилю кодирования. Зачастую одно лишь форматирование способно сотворить с уродливым куском кода настоящие чудеса. Как и разумное группирование участков кода с помощью пустых строк и переносов.
Добавить меткиНачните с имени
Функции должны делать одну вещь
Комментарии не исправят плохой код
Форматирование кода - всегда приоритет
Сначала напишите try-catch-finally
Заключение
Инструменты для создания панели навигации
и
создает маркированный список, а
Создадим-ка горизонтальную навигационную модель
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
А теперь вертикально. Я сказал вертикально!
Подпункты в меню: выпадающий список
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2. Зачем нам нужен красивый код?
Обычно, когда мы работаем над конкретным программным продуктом, эстетические качества кода заботят нас далеко не в первую очередь.
Нам гораздо важнее наша производительность, качество реализации функционала, стабильность его работы, возможность модификации и расширения и т.д.
Мой ответ: да, и при этом, одним из самых важных!
Почему эти качества действительно являются важнейшими, и как они способствуют повышению показателей, указанных в начале параграфа, уверен, очевидно любому, кто занимается программированием.3. Три базовых принципа.
Переходя к изложению собственного опыта, отмечу, что, работая над читаемостью и управляемостью своего и чужого кода, я постепенно пришел к следующему пониманию.
Можно бесконечно перечислять различные хинты и техники, с помощью которых можно сделать код красивее. Но я утверждаю, что наилучших, или, во всяком случае, достаточно хороших результатов можно достигнуть, ориентируясь именно на эти три принципа.4. Линеаризация кода.
Мне кажется, что из трех базовых принципов, именно линейность является самым неочевидным, и именно ей чаще всего пренебрегают.
Наверное, потому что за годы учебы (и, возможно, научной деятельности) мы привыкли обсуждать от природы нелинейные алгоритмы с оценками типа O(n3)
, O(nlogn)
и т.д.
И совсем не линейный:
Именно “куски” второго типа мы и будем пытаться переписать при помощи определенных техник.4.1.
Техника 1. Выделяем основную ветку алгоритма.
В подавляющем большинстве случаев в качестве основной ветки имеет смысл взять максимально длинный успешный линейный сценарий алгоритма.
Именно эта основная ветка у нас в идеале должна быть на нулевом уровне вложенности.
Давайте для сравнения рассмотрим вариант, где на нулевом уровне находится альтернативная ветка, вместо основной:
Как видно, уровень вложенности значительной части кода вырос, и смотреть на код в целом уже стало менее приятно.4.2.
Техника 2. Используем break
, continue
, return
или throw
, чтобы избавиться от блока else
.
Плохо:4.3.
Техника 3. Выносим сложные подсценарии в отдельные процедуры.
Т.к. в случае “алгоритма ремонта” мы довольно удачно выбрали основную ветку, то альтернативные ветки у нас все остались весьма короткими.
Лучше:
Обратите внимание, что правильно выбрав имя выделенной процедуре, мы, кроме того, сразу же повышаем самодокументированность кода. Теперь для данного фрагмента в общих чертах должно быть понятно, что он делает и зачем нужен.4.4.
Техника 4. Выносим все, что возможно, во внешний блок, оставляем во внутреннем только то, что необходимо.
Плохо:
Лучше:4.5.
Техника 5 (частный случай предыдущей). Помещаем в try...catch
только то, что необходимо.
Надо отметить, что блоки try...catch
вообще являются болью, когда речь идет о читаемости кода, т.к. часто, накладываясь друг на друга, они сильно повышают общий уровень вложенности даже для простых алгоритмов.4.6.
Техника 6. Объединяем вложенные if-ы.
Тут все очевидно. Вместо:
пишем:4.7.
Техника 7. Используем тернарный оператор (a? b: c
) вместо if
.
Вместо:
пишем:
Иногда имеет смысл даже написать вложенные тернарные операторы, хотя это предполагает от читателя знания приоритета, с которым вычисляются подвыражения тернарного оператора.
пишем:
Но злоупотреблять этим, пожалуй, не стоит.4.8.
Суммируя вышесказанное, попробуем написать полную реализацию алгоритма ремонта максимально линейно.
listen_client();
if (!is_clean()) {
wash();
}
check_cost();
if (!client_agree()) {
pay_for_wash();
bye();
return;
}
find_defects();
if (defects_found()) {
say_about_defects();
if (!client_agree()) {
pay_for_wash_and_dyagnosis();
bye();
return;
}
}
create_request();
take_money();
bye();
На этом можно было бы и остановиться, но не совсем здорово выглядит то, что нам приходится 3 раза вызывать bye()
и, соответственно, помнить, что при добавлении новой ветки, его придется каждый раз писать перед return (собственно, издержки множественных return).
Если вы думаете, что мы сейчас записали что-то тривиальное, то, в принципе, так и есть. Однако уверяю, что во многих живых проектах вы увидели бы совсем другую реализацию этого алгоритма…5. Минимизация кода.
Думаю, было бы лишним пояснять, что уменьшая количество кода, используемого для реализации заданного функционала, мы делаем код гораздо более читаемым и надежным.5.1.
Техника 1. Устраняем дублирование кода.
О копипасте и вреде, от него исходящем, сказано уже так много, что добавить что-то новое было бы сложно. Тем не менее, программисты, поколение за поколением, интенсивно используют этот метод для реализации программного функционала.
При этом всегда возникает проблема выделения общего из частного. Зачастую даже не всегда понятно, чего больше у похожих кусков кода: сходства или различий. Выбор тут делается исключительно по ситуации. Тем не менее, наличие одинаковых участков размером в пол-экрана сразу говорит о том, что данный код можно и нужно записать существенно короче.
Распространить ее можно дальше одних лишь операторов if. Например, вместо:
запишем:
Или обратный вариант. Вместо:
a = new Object();
init(a);
do(a);
b = new Object();
init(b);
do(b);
запишем:
procedure proc(a) {
init(a);
do(a);
}
proc(new Object());
proc(new Object());
5.2.
Техника 2. Избегаем лишних условий и проверок.
Лишние проверки - зло, которое можно встретить практически в любой программе. Сложно описать, насколько самые тривиальные процедуры и алгоритмы могут быть засорены откровенно ненужными, дублирующимися и бессмысленными проверками.
Не смотря на свою очевидную абсурдность, встречаются такие проверки с впечатляющей регулярностью. (Cразу же оговорюсь, что пример не касается тех редких языков и сред, где оператор new()
может вернуть null
. В большинстве случаев (в т.ч. в Java) подобное в принципе невозможно).
Вот, например, такой случай:
Встречается в разы чаще, чем предыдущий тип проверок.
Как видно, автор данного отрезка панически боится нарваться на невалидный объект, поэтому проверяет его перед каждым чихом. Не смотря на то, что иногда такая стратегия может быть оправдана (особенно, если proc1()
и proc2()
экспортируются в качестве API), во многих случаях это просто засорение кода.
Может быть, на первый взгляд, эти варианты кажутся чем-то невозможным или даже страшным, тем не менее, в реальности с их помощью можно существенно повысить читаемость и надежность кода.5.3.
Техника 3. Избегаем написания “велосипедов”. Используем по максимуму готовые решения.
Велосипеды - это почти как копипаст: все знают, что это плохо, и все регулярно их пишут. Можно лишь посоветовать хотя бы пытаться бороться с этим злом. 5.4.
Техника 4. Оставляем в коде только то, что действительно используется.
“Висящие” функции, которые никем нигде не вызываются; участки кода, которые никогда не выполняются; целые классы, которые нигде не используются, но их забыли удалить - уверен, каждый мог наблюдать такие вещи в своем проекте, и может быть, даже воспринимал их, как должное.
Поскольку неиспользуемый код реально не выполняется и не тестируется, в нем могут содержатся некорректные вызовы тех или иных процедур, ненужные или неправильные проверки, обращения к процедурам и внешним библиотекам, которые больше ни для чего не нужны, и множество других сбивающих с толку или просто вредных вещей.5.5.
Техника 5. Используем свои знания о языке и полагаемся на наличие этих знаний у читателя.
Одним из эффективных способов сделать свой код проще, короче и понятней является умелое использование особенностей конкретного языка: различных умолчаний, приоритетов операций, кратких форм записи и т.д.
if (obj != null && obj != undefined && obj.s != null && obj.s != undefined && obj.s != "") {
// do something
}
Выглядит пугающе, в том числе и с точки зрения “а не забыл ли автор еще какую-нибудь проверку”. На самом деле, зная особенность языка Javascript, в большинстве подобных случаев всю проверку можно свести до тривиальной:
Дело в том, что благодаря неявному приведению к boolean, проверка if (obj) {}
отсеет:
В общем, она отсеет большинство тривиальных значений, которые мы можем себе представить. К сожалению, повторюсь, программисты используют эту особенность достаточно редко, из-за чего их “перестраховочные” проверки выглядят весьма громоздко.
и
Второй вариант выглядит проще, благодаря использованию специфичной семантики логических операций в скриптовых языках.6. Самодокументированный код.
Термин “самодокументированность” наиболее часто употребляется при описании свойств таких форматов, как XML или JSON. В этом контексте подразумевается наличие в файле не только набора данных, но и сведений об их структуре, о названиях сущностей и полей, задаваемых этими данными.6.1.
Техника 1. Тщательно выбираем названия функций, переменных и классов. Не стоит обманывать людей, которые будут читать наш код.
Самое главное правило, которое следует взять за основу при написании самодокументированного кода - никогда не обманывайте своего читателя
.
Переменные же с названиями ii
, i1
, ijk42
, asdsa
и т.д, не стоит использовать никогда. Ну разве что, если вы работаете с математическими алгоритмами… Нет, лучше все-таки никогда.6.2.
Техника 2. Стараемся называть одинаковые вещи одинаково, а разные - по-разному.
Одна из самых обескураживающих трудностей, возникающих при чтении кода - это употребляемые в нем синонимы и омонимы. Иногда в ситуации, когда две разных сущности названы одинаково, приходится тратить по несколько часов, чтобы разделить все случаи их использования и понять, какая именно из сущностей подразумевается в каждом конкретном случае. Без такого разделения нормальный анализ, а следовательно и осмысленная модификация кода, невозможны в принципе. А встречаются подобные ситуации намного чаще, чем можно было бы предположить.6.3.
Техника 3. “Бритва Оккама”. Не создаем сущностей, без которых можно обойтись.
Как уже говорилось в п. 5.4., любой участок кода, любой объект, функция или переменная, которые вы создаете, в дальнейшем, в процессе поддержки, потребует себе платы в виде вашего (или чужого) времени и внимания.
Очевидно, переменная increasedSum
является лишней сущностью, т.к. описание объекта, который она хранит (sum + 1)
характеризует данный объект гораздо лучше и точнее, чем название переменной. Таким образом код стоит переписать следующим образом (“заинлайнить” переменную):
Если далее по коду сумма нигде не используется, можно пойти и дальше:
Инлайн ненужных переменных - это один из способов сделать ваш код короче, проще и понятней
.
В этом случае инлайн переменной descr
вряд ли пойдет на пользу читаемости, т.к. данная переменная используется для представления определенной сущности из предметной области, а, следовательно, наличие переменной способствует самодокументированности кода, и сама переменная под “бритву Оккама” не попадает.
Прямое условие:
Оценить разницу в читаемости, думаю, несложно.6.5.
Техника 5. Все, что можно спрятать в private (protected), должно быть туда спрятано. Инкапсуляция - наше все.
Говорить о пользе и необходимости следования принципу инкапсуляции при написании программ в данной статье я считаю излишним.
Хотелось бы только подчеркнуть роль инкапсуляции, как механизма самодокументирования кода. В первую очередь, инкапсуляция позволяет четко выделить внешний интерфейс класса и обозначить его “точки входа”, т. е. методы, в которых может быть начато выполнение кода, содержащегося в классе. Это позволяет человеку, изучающему ваш код, сохранить огромное количество времени, сфокусировавшись на функциональном назначении класса и абстрагировавшись от деталей реализации.6.6.
Техника 6. (Обобщение предыдущего) Все объекты объявляем в максимально узкой области видимости.
Принцип максимального ограничения области видимости каждого объекта можно распространить шире, чем привычная нам инкапсуляция из ООП.
Очевидно, такое объявление переменной someobj затрудняет понимание ее назначения, т. к. читающий ваш код будет искать обращения к ней в значительно более широкой области, чем она используется и реально нужна.
Отдельно хотелось бы оговорить случай, когда данная техника может не работать или работать во вред. Это переменные, инициализируемые вне циклов. Например:
Если создание объекта через createSomeObj()
- дорогая операция, внесение ее в цикл может неприятно сказаться на производительности программы, даже если читаемость от этого и улучшится.6.7.
Техника 7. Четко разделяем статический и динамический контекст. Методы, не зависящие от состояния объекта, должны быть статическими.
Смешение или вообще отсутствие разделения между статическим и динамическим контекстом - не самое страшное, но весьма распространенное зло.6.8.
Техника 8. Стараемся не разделять объявление и инициализацию объекта.
Данный прием позволяет совместить декларацию имени объекта с немедленным описанием того, что объект из себя представляет. Именно это и является ярким примером следования принципу самодокументированности.
c:
// getting “somestrings” collection somehow
...
Collection
Если мы используем Java 8, можно записать чуть короче:
Ну и стоит упомянуть случай, когда разделять объявление и инициализацию переменных так или иначе приходится. Это случай использования переменной в блоках finally
и catch
(например, для освобождения какого-нибудь ресурса). Тут уже ничего не остается, кроме как объявить переменную перед try
, а инициализировать внутри блока try
.6.9.
Техника 9. Используем декларативный подход и средства функционального программирования для обозначения, а не сокрытия сути происходящего.
Данный принцип может быть проиллюстрирован примером из предыдущего пункта, в котором мы использовали эмуляцию функционального подхода в Java с целью сделать наш код понятнее.
c:
Ну а примеры использования функционального подхода, убивающие читаемость… Давайте на этот раз сэкономим свои нервы и обойдемся без них.6.10.
Техника 10. Пишем комментарии только, если без них вообще не понятно, что происходит.
Как уже говорилось выше, принцип самодокументирования кода противопоставляется документированию с помощью комментариев. Хотелось бы коротко пояснить, чем же так плохи комментарии:
Ну а нужны комментарии в случаях, когда написать код хорошо по тем или иным причинам не представляется возможным. Т.е. писать их надо для пояснения участков кода, без них трудночитаемых. К сожалению, в реальной жизни такое встречается регулярно, хотя расценивать это следует лишь как неизбежный компромисс.7. Философское заключение.
Закончив изложение основных техник и приемов, с помощью которых можно сделать код чуть красивее, следует еще раз явно сформулировать: ни одна из техник не является безусловным руководством к действию. Любая техника - лишь возможное средство достижения той или иной цели
.
Встречаются и такие случаи, когда однозначно хорошего решения той или иной проблемы вообще не существует, или это решение по ряду причин нереализуемо (например, заказчик не хочет принимать потенциально опасные изменения в коде, даже если они способствуют общему улучшению качества).