|
Персональные инструменты |
|||
|
Поиск и чтение унаследованного кодаМатериал из CustisWikiИгорь Шаталкин, наш разработчик .NET, в корпоративном блоге на «Хабрахабре» рассказал об особенностях поиска и чтения унаследованного кода. Почему умение искать и понимать код так важно для программиста? Какие существуют методы поиска и понимания исходников? Какими инструментами удобнее всего пользоваться при поиске и улучшении кода? Ответы на эти вопросы — в материале «Поиск и чтение унаследованного кода» на сайте. Вася — молодой программист. Получив задачу и засучив рукава, он берется за написание кода. Уже через день решение задачи готово, Вася запускает его... И сталкивается с неожиданной досадной ошибкой. Вася старательно исправляет ее и повторно запускает решение. В результате — снова неприятная ошибка. Так Вася долго наступает на грабли, пару раз значительно переписывает программу, пока, наконец, через неделю задача окончательно не решена. Петя — опытный программист. Получив постановку, он ищет, нет ли исходного кода, решавшего аналогичную задачу. Пару дней Петя проводит, читая материалы и разбираясь в чужих модулях, а на третий — запускает свое решение, основанное на уже существующем коде. В нем есть пара незначительных ошибок, которые удается быстро исправить. Ура! Все работает так, как нужно, уже на третий день. Как найти нужный вам кусок исходного кода? Как его понять? А главное — зачем все это делать? В поиске ответов на эти вопросы добро пожаловать под кат. Содержание[убрать]Почему важно уметь искать и читать код?Д. Томас и Д. Спинеллис в книге «Анализ программного кода на примере проектов Open Source» формулируют несколько важных тезисов.
Со стороны кажется, что основная работа программиста состоит в написании исходников, но это не так. Чаще всего код приходится читать, а не писать. Вчера ваш товарищ написал модуль, а сегодня перед вами стоит задача усовершенствовать его или исправить ошибку, что невозможно без предварительного изучения трудов вашего товарища.
С цифрой 90 %, как мне кажется, можно поспорить, но основная мысль ясна: при чтении (в данном случае — рецензировании) программы можно (и нужно) выявлять и устранять ошибки.
И напрасно, поскольку без чтения существующих исходников приходится постоянно изобретать велосипед. Даже если вы можете написать что-то с нуля, гораздо эффективнее будет осмотреться вокруг и использовать чужой исходный код напрямую или как аналогию. Чужой код проверен, отлажен и, скорее всего, уже содержит какие-то «фичи», которые будут полезны и при решении вашей задачи. Итак, читайте код, чтобы не писать велосипедов. Читайте код, чтобы вносить изменения быстрее. Читайте код, чтобы в программах было меньше ошибок. О чем эта статья?Основная цель статьи заключается в том, чтобы систематизировать существующие подходы к поиску и чтению кода. Знание конкретных подходов дает возможность применять те из них, которые наиболее приемлемы в том или ином случае. Кроме того, статья содержит обзор инструментов, которые позволят эффективно применять описанные в статье подходы. Стоит отметить, что искать и понимать исходный код можно и без специальных инструментов, однако их применение делает процесс более наглядным и менее рутинным. Я расскажу о наиболее универсальных концепциях: иными словами, эти концепции применимы ко всем или почти всем языкам программирования. Некоторые инструменты, которые облегчают поиск и чтение исходников, также универсальны, однако большинство актуально только для Visual Studio и .NET. Тем не менее, тот факт, что большая часть инструментов применяется исключительно в VS, не означает, что для других IDE нет подобных средств: я уверен, что они есть, поскольку приведенные в статье подходы универсальны. Также оговорюсь, что все описанные инструменты бесплатны, исключение составляет только ReSharper. Приведенные в статье примеры кода взяты из приложений ShareX и Greenshot (оба распространяются под лицензией GNU GPL) из репозитория ShareX по состоянию на 30 июня 2015. Я благодарю авторов этих приложений за возможность воспользоваться их кодовой базой. Также благодарю моих коллег, в особенности Дениса Гаврилова и Владислава Иофе, за высказанные ими замечания во время работы над статьей. Поиск кодаПоиск кода представляет собой процесс, результатом которого выступают части исходного кода, связанные с тематикой решаемой задачи. Поиск вне кодаНачинать искать исходники можно и нужно вне кодовой базы. Предварительное общение с командой, чтение документации и баг-трекера могут существенно упростить непосредственный поиск кода. Подробнее о работе с артефактами можно узнать из семинара Д. Гаврилова «Практики командной работы: о пользе письменных артефактов». Помимо чтения документации и общения с командой полезно вспомнить, что вы уже знаете о том, что предстоит найти. Например, если предстоит найти экранную форму, то стоит вспомнить, что название экранных форм в вашем проекте обычно заканчивается на Form, а все формы лежат в сборе ”UI”. Можно запустить приложение на исполнение и искать экранную форму перебором. При переборе программист открывает в случайном (или не очень) порядке формы до тех пор, пока не найдет окно, кнопку, пункт меню или любой другой элемент интерфейса, который, как кажется, использует искомый функционал. Перебор применяется, когда задача поиска размыта или программист плохо знаком с системой. Например, вы ни разу не работали с приложением, а от вас требуется добавить функцию, которая бы экспортировала все открытые картинки в буфер обмена. Логичным представляется для начала исследовать приложение: посмотреть, умеет ли оно экспортировать картинки в буфер обмена по одной, есть ли какие-то настройки, связанные с буфером или экспортом и т. д. Поиск по ключевым словамКлючевое слово — это слово или словосочетание из текста документа или запроса, несущее существенную смысловую нагрузку с точки зрения информационного поиска. Необходимая для поиска подборка ключевых слов формируется через общение с пользователями, просмотр экранных форм перебором и другие методы, описанные выше в качестве поиска «вне кода». Ключевые слова можно взять из описания задачи, экранных форм и действий пользователя, текста ошибки или сообщения, SQL-запроса, то есть из любых артефактов, связанных с решаемой задачей. Чтобы поиск по ключевым словам был максимально эффективным, необходимо подбирать редкие ключевые слова. Например, если вы работаете над проектом, в котором много разных документов (Document), то поиск по слову Document даст множество лишних результатов. Однако бывают ситуации, когда подобрать редкие ключевые слова нет возможности. В этом случае сначала нужно найти множество потенциально полезной информации, а затем сузить поиск. Например, найти все существующие упоминания Document, а затем исключить из результатов тесты, поискать в найденных файлах дополнительные ключевые слова и т. д. В целом следует стремиться найти хотя бы что-то (чуть больше или чуть меньше), а не именно то, что нужно. В большинстве случаев для поиска по ключевым словам достаточно встроенного в IDE поиска (обычно он вызывается сочетанием клавиш Ctrl + F). Иногда может быть полезен файловый менеджер — для поиска в папках, которые не входят в программное решение, — или поисковые движки: например, OpenGrok и DocFetcher (о них речь пойдет ниже). Так, ReSharper содержит полезную функцию Search by Pattern, с помощью которой можно найти, например, все свойства классов, объявленные как нулябельные перечисления (public TEnum? Enum { get; set; }). Конечно же, не стоит забывать и про мощный механизм регулярных выражений; да, он требуется редко, но иногда здорово выручает. Давайте поговорим чуть подробнее про поисковые движки, работающие с исходным кодом. Главное преимущество поисковых движков состоит в том, что они позволяют осуществлять гибкий поиск. Например, нужно найти все классы, в которых есть упоминания Clipboard и Image. Обычные средства, встроенные в IDE, дадут вам возможность найти либо файлы с Clipboard, либо файлы с Image. Здесь за скобками оставляем пресловутый механизм регулярных выражений: он может помочь, но конструкция поиска будет громоздкой. Более сложный пример, с которым встроенные средства поиска не справятся: необходимость найти свойства классов, помеченные атрибутом A, но не помеченные атрибутом B. Помимо гибкости поисковые движки увеличивают скорость поиска. Если вашей IDE нужна пара-тройка, а иногда и не один десяток секунд, чтобы пробежаться по всем файлам проекта, то поисковый движок имеет индекс, позволяющий делать то же самое в доли секунды. Существуют различные поисковые движки по исходному коду, можно почитать их обсуждение на StackOverflow или ознакомиться с подборкой на сайте beyondgrep.com. В своей работе я использую OpenGrok: он бесплатный и умеет работать с различными языками, такими как C#, C, PHP, SQL и с десяток других. Движок состоит из 2-х модулей: модуля индексации исходников и веб-приложения для поиска и навигации по коду. Первый модуль индексирует папки проекта в файловой системе. Веб-приложение работает под Apache, позволяет вводить разные поисковые запросы и просматривать полученные результаты. OpenGrok написан на Java, доступен для скачивания на официальном сайте проекта, а в соответствующих статьях можно прочитать о том, как установить движок под Windows и под Linux. OpenGrok поддерживает различные виды поиска: по тексту всего документа или только в определениях, то есть названиях классов, функций. Кроме того, он позволяет задавать фильтрацию папок и названия искомого файла, выбирать язык, на котором должен быть написан код и т. д. Этот движок показывает результаты в наглядной форме, сгруппированными по файлам, а также позволяет в один клик перейти к нужной строчке в файле и умеет показывать код с подсветкой или в «сыром» виде. У OpenGrok есть несколько ограничений: во-первых, отсутствует интеграция с Visual Studio, во-вторых, он не умеет работать с кириллицей (по крайней мере, мне не удалось добиться этого при помощи настроек). Если вам часто приходиться искать русский текст, то можно использовать DocFetcher: данный инструмент не заточен напрямую под поиск в исходниках, но умеет индексировать любые текстовые файлы и отлично справляется с русским языком. В таблице ниже представлено сравнение возможностей поисковых движков OpenGrok и DocFetcher. С помощью этой таблицы можно выделить для себя необходимые параметры поиска и выбрать наиболее подходящий инструмент.
Поиск по классификаторамКлассификатор — это систематизированный перечень наименованных объектов. Наиболее востребованным программистами классификатором, на мой взгляд, является список всех классов программного решения. Примерами других классификаторов могут быть список членов классов, упорядоченных по классам, список файлов, упорядоченных по папкам и др. С одной стороны, классификатором можно пользоваться как инструментом работы с древовидным списком. Вы «разворачиваете» базовое пространство имен программного решения, далее «разворачиваете» пространство имен нужного модуля и так далее, пока, наконец, не доберетесь до нужного класса. С другой стороны, внутри классификатора можно осуществлять сквозной поиск. Например, вы набираете *Document*, а классификатор показывает все классы, в названиях которых встречается Document. Если вы работаете с ReSharper, то наверняка знаете про функцию Go to Everything, которая позволяет перейти к любому классу, методу, свойству или файлу во всем программном решении. В Visual Studio «из коробки» есть следующие классификаторы: Solution Explorer (классифицирует сборки, папки, файлы, классы и их члены), Class View (классифицирует пространства имен, классы и их члены) и Object Browser (классифицирует сборки, пространства имен, классы и их члены). Solution Explorer и Class View выдают перечень классов внутри вашего решения, а Object Browser — как в вашем решении, так и в сторонних используемых сборках. OpenGrok позволяет искать по определениям (Definition), в том числе с использованием символов подстановки (*Document*). Еще один классификатор — это обычный файловый менеджер, который отображает артефакты, упорядоченные по папкам. Когда следует применять классификаторы? Чтобы успешно использовать классификатор, программист должен хорошо знать программный продукт. Особенно важно, в частности, знать соглашения об именах (например, *Form — это экранная форма, а DocumentNameListForm — списочная форма документов типа DocumentName), а также принципы, по которым классы упорядочиваются в папках и пространствах имен (в Dictionaries лежат справочники, а в Documents — документы). Еще одна возможность применения поиска по классификатору — ситуация, когда известно точное название класса или его члена. Поиск ошибкиПериодически возникает задача найти строчку кода, в которой возникает некоторая ошибка. Как вариант, нужную строчку можно найти по известному номеру ошибки. В данном случае номер ошибки — ключевое слово, по которому осуществляется поиск (про поиск по ключевым словам было рассказано выше). В иной ситуации может быть известна трассировка стека ошибки. В ReSharper есть удобный инструмент Stack Traces, который позволяет просматривать стек и перемещаться по методам, которые вызывались перед тем, как произошла ошибка. Чтобы использовать Stack Traces, необходимо скопировать стек ошибки в буфер обмена, после чего в Visual Studio нажать Shift + Ctrl + E. Если у вас нет информации ни о номере ошибки, ни о трассировке стека, можно запустить проект и включить прерывания по ошибке. Для Visual Studio создано удобное дополнение Exception Breaker, позволяющее включать и выключать прерывание по ошибке одним кликом мыши. По действию пользователяИногда требуется узнать, какой метод вызывается при нажатии кнопки или другом действии пользователя. В этом случае можно вначале найти класс, в котором находится вызываемый метод (например, поиском по ключевым словам или по классификатору), а затем расставить точки останова во всех членах этого класса и запустить приложение. В веб-приложениях в некоторых случаях бывает полезен Fiddler, в частности, в нем можно посмотреть название вызываемой WCF-функции и значения переданных в нее параметров. ИтогиПеред тем, как приступать к поиску кода, стоит потратить немного времени на общение с командой, чтение документации и перебор экранных форм. Это необходимо для того, чтобы собрать ключевые слова, узнать названия классов и методов, без которых дальнейший поиск будет затруднительным. Начинать его стоит с самого простого, при этом можно найти чуть больше или чуть меньше, чем требуется. Искать исходники также можно при помощи ключевых слов или классификаторов. Для поиска ошибок и функций, вызываемых по действию пользователя, есть специальные методики. В следующей таблице представлены рассмотренные выше методы поиска кода.
Понимание кодаПоиск кода неразрывно связан с его пониманием. Понимание необходимо, чтобы выделить из всех найденных действительно полезные для решения задачи блоки исходников, чтобы понять, как надо модифицировать и как можно использовать полученные при поиске результаты. Понимание без чтения кодаВне кодовой базы начинается не только поиск кода, но и процесс его понимания. Запуск приложения, чтение документации, баг-трекера, лога приложения или системы контроля версий, общение с командой и (или) пользователями дают хорошее представление о том, что происходит внутри приложения. Так же, как и при поиске исходников, перед непосредственным чтением кода важно вспомнить, что уже известно про разбираемую область. Чтение «черного ящика»Чтение кода как «черного ящика» предполагает, что по некоторым внешним признакам мы пытаемся установить, что происходит внутри функции или класса. О назначении объекта можно догадаться, посмотрев на его имя, принимаемые параметры и возвращаемое значение. Например,
по всей видимости, помещает изображение image в буфер обмена, а информацию о произошедших ошибках помещает в возвращаемый ResultOrError. Для чтения «черного ящика» важно знать соглашения об именах, то есть о принятых в проекте префиксах, постфиксах и других частях названий. К «черному ящику» относится чтение описаний или комментариев внутри функций. Чтобы убедиться в правильности предположений о назначении функции, можно изучить ее использование. Также можно найти, какие действия пользователя приводят к вызову функции (например, пользователь нажал на кнопку «Копировать в буфер») и на основе этих знаний понять, что пользователь ожидает получить в результате выполнения функции (в нашем случае, очевидно, скопировать некий объект в буфер обмена). ReSharper предлагает мощные инструменты Inspect This и Find Usages, которые позволяют найти места, из которых вызывается функция. Inspect This представляется более удобным, поскольку позволяет легко и наглядно осуществлять дальнейший поиск фрагментов кода, из которых вызываются места, где вызывается функция, и так далее. В самой Visual Studio «из коробки» доступен инструмент Call Hierarchy, который имеет возможности, аналогичные Inspect This.
Чтение «белого ящика»Если в «черном ящике» код метода был для нас «закрыт» (мы изучаем исходники на границе и вокруг функции), то в «белом ящике» предполагается изучение кода внутри исследуемого метода. При «белом ящике» важен порядок чтения исходников. С одной стороны, чтобы понять код, не надо читать его целиком, поэтому начинать чтение стоит с наиболее важных его частей, пропуская менее значимые строки. С другой стороны, от последовательности чтения зависит быстрота понимания. Чтение слева направо и сверху вниз — не лучший способ погружения в исходники. Один из приемов чтения «белого ящика» состоит в чтении «вглубь»: вначале читаем код первого уровня, затем переходим ко второму и так далее по уровням. Для Visual Studio есть дополнение C# outline, которое позволяет сворачивать и разворачивать текст внутри if, for и других управляющих конструкций («из коробки» Visual Studio умеет сворачивать и разворачивать пространства имен, классы, регионы и методы, но не if, for и подобные им конструкции). Другой способ чтения кода как «белого ящика» состоит в чтении «от ключевых мест». Как правило, это места, где функция возвращает или вычисляет результат, каким-то образом использует входные параметры, делает вывод в файл, на экран или другое устройство. Ключевые места в разных функциях могут быть различными. Важно уметь их найти и прочитать в первую очередь. Для Visual Studio есть дополнение RockMargin, которое подсвечивает в редакторе и на полосе прокрутки все места, в которых встречаются вхождения выбранного ключевого слова. Например, вы два раза щелкаете на taskSettings, а RockMargin подсвечивает все места, где встречается taskSettings в файле: В вашем приложении, скорее всего, есть много однотипных, шаблонных кусков. Это могут быть как общеиспользуемые шаблоны (например, Singleton), так и шаблоны, специфичные для вашего проекта (например, какие-то особенности в построении UI). Понять, что делает шаблон, можно по двум-трем ключевым словам, поэтому шаблоны надо учиться читать «целиком», без «разбора букв». Например, на картинке ниже — класс-Singleton, о чем можно догадаться по двум строкам кода, которые выделены красным. Еще одна последовательность, которую следует соблюдать при чтении исходников, состоит в чтении от простого к сложному. Вначале разберитесь с тем, что понятно и так, а затем переходите к более сложным конструкциям. В заключение раздела о «белом ящике» я бы хотел сказать несколько слов о чтении при рецензировании кода. Перед непосредственным чтением (особенно если прорецензировать предстоит объемную доработку) следует просмотреть названия всех измененных файлов, а затем понять, в каких файлах произошли ключевые изменения. Чтение необходимо начинать с файлов с ключевыми изменениями, из которых вы схватите общую суть и лучше поймете все остальные доработки. Улучшение кодаЧтобы понять исходный код, можно не только читать его, но и улучшать. Самый простой способ улучшить восприятие кода — правильно расставить отступы. Другой способ улучшить читабельность — рефакторинг. Простейшие переименования и разделение длинных функций на несколько коротких позволяют быстро сделать текст программы более понятным. Для понимания сложных логических конструкций можно применять законы де Моргана:
В качестве инструмента по улучшению кода можно порекомендовать ReSharper, который справляется с улучшениями разной степени сложности. Режим отладкиРежим отладки дает возможность проверить сделанные во время чтения исходников предположения. Чтение в режиме отладки предполагает использование таких инструментов, как точки останова (в том числе точки останова «по условию», например, срабатывающие только при определенном значении переменных), просмотр трассировки стека, пошаговое выполнение программы, просмотр и модификация значений переменных. Общая картинаПри чтении важно поддерживать общее представление о том, что происходит в разбираемом модуле, и уметь быстро перемещаться по разным местам «паззла». Чтобы удержать в голове общую картину, можно использовать диаграммы, как уже существующие в проекте, так и новые. Некоторые диаграммы можно строить автоматически, особенно это актуально для SQL-запросов. В PL/SQL Developer, например, есть Query Builder, который показывает, какие таблицы участвуют в запросе и как они между собой связаны. Автоматизированно можно строить диаграммы классов, к примеру, в Visual Studio для этого применяется инструмент View Class Diagram. Однако чаще всего достаточно даже нарисованной от руки схемы. Читать вызываемые методы удобно при помощи ReSharper, Inspect This или встроенного в Visual Studio View инструмента Call Hierarchy. В разделе про «черный ящик» эти инструменты уже рассматривались, но там исследовались места, из которых вызывается функция. Здесь же речь идет о просмотре методов, которые вызываются внутри функции. Inspect This и View Call Hierarchy дают целостную картину — вы видите дерево вызовов целиком — и позволяют быстро по нему перемещаться. Для быстрых переходов по произвольным блокам кода можно закреплять вкладки (чтобы отличать те 5–10 классов, с которыми вы плотно работаете, от других 20–100, которые пришлось открыть в процессе работы), делать закладки в редакторе или записи от руки, например, выписывать названия классов и их членов. Для быстрых переходов внутри файла будут полезны: в ReSharper — File Structure (отображает члены текущего класса), в Visual Studio — Member (аналог File Structure) или RockMargin (отображает уменьшенный исходный код на широкой полосе прокрутки, подкрашивает закладки и точки останова). ИтогиПеред непосредственным чтением исходников важно обратиться к различным источникам вне кодовой базы: пообщаться с командой, почитать документацию, запустить приложение на выполнение. Это даст некоторое базовое понимание приложения, которое поможет в непосредственном чтении кода. Читать исходники можно как «черный ящик», анализируя названия методов, принимаемые ими параметры и т. д., как «белый ящик», производя разбор того, что находится внутри метода. Для облегчения восприятия кода его можно улучшить, например, расставить отступы. Запуск приложения дает возможность проверять сделанные предположения в режиме отладки. Наконец, при чтении важно удерживать общую картину в голове и уметь быстро перемещаться по разным частям исследуемого модуля. В следующей таблице представлены рассмотренные выше методы понимания кода.
ЗаключениеПоиск и понимание кода — важный этап в работе над программным продуктом. Поиск и понимание начинаются за пределами репозитория: с общения с командой, чтения документации, баг-трекера, запуска приложения на исполнение. Поиск и понимание необходимы как при доработке существующих модулей (чтобы доработать существующий модуль, надо понять, как он работает), так и при разработке нового функционала: да, новый модуль иногда можно разработать, не прочитав ни строчки кода, но в проекте, скорее всего, есть отдельные блоки, реализующие схожий функционал, которые можно взять в качестве шаблона для реализации функций и классов. По моему мнению, чтение кода в широком смысле этого слова (то есть как поиск исходников, так и их понимание) должно выделяться разработчиком в отдельный этап работы над задачей. Результатом этого этапа является набор закладок в методах и файлах, который будет полезен при работе над задачей, а также общее представление о работе некоего блока кода. Уже в рамках непосредственного программирования поиск исходников необходимо продолжать. Например, так: встретил новый класс, нашел места, где он используется, обновил список полезных закладок. Возможно, подбор закладок можно выделить отдельной функцией в команде: технический лидер или ответственный разработчик составляет список полезных закладок, после чего программист берет задачу в работу. Как для поиска, так и для понимания исходников важно знать соглашения об именах и способах упорядочивания классов (по папкам, пространствам имен). По опыту автора, такое понимание приобретается в процессе работы в проекте, однако соглашения можно формализовывать и сообщать новым разработчикам так же, как соглашения по оформлению кода (Code Style Guide). Перспективным представляется разработка автоматизированных средств, выявляющих фактически сложившиеся соглашения об именах и способах упорядочивания классов. Как кажется, полезными будут средства, выявляющие наиболее часто встречающиеся названия папок, суффиксы, постфиксы и т. д. При чтении кода, как и при чтении обычной книги, важно осознать и держать в голове цель чтения. Для чтения кода можно применять некоторые приемы из статей о способах чтения обычного текста (см., например: «Как улучшить свою способность понимать прочитанный текст?»). Насколько можно оценить, поиск и понимание кода является темой, слабо затронутой в отечественной и зарубежной литературе. Надеюсь, что представленная статья была полезной в освоении приемов эффективной работы с кодовой базой. СсылкиКниги
СтатьиСеминары
ИнструментыДля поиска кода
Для понимания кода
Общие методы
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||