Статьи

Как написать легко читаемый код

[Эта статья была написана Дэвидом Старром]

Поскольку мы читаем больше кода, чем пишем, имеет смысл писать код, который легко читать. Но то, что делает код более читабельным, похоже на вопрос «Что делает одного автора более читабельным, чем другого?»

Есть некоторые принципы, с которыми разработчики склонны соглашаться — особенно, как выразить свое намерение в коде Попросите программиста назвать некоторые из них, и вы, вероятно, услышите что-то вроде этого:

  • Хорошо названные переменные, методы и класс
  • Последовательное форматирование
  • Маленькие методы
  • Минимальная вложенность и кодовые ветви

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

Улучшение вашего кода: боулинг

В игре в боулинг есть простой алгоритм для подсчета очков в игре, но я заметил, что большинство игроков в боулинг не знают, как на самом деле их игра оценивается. Базовая оценка проста: вы получаете одно очко за каждый сбитый штифт. Есть бонусы, если вы сбили все десять пинов. Если котелок наносит удар (все 10 кеглей за один бросок), счет увеличивается на десять, плюс добавляется значение следующих 2 бросков. Если котелок получает запасной (все десять кеглей за 2 броска), то счет увеличивается на десять плюс стоимость следующего броска.

Модель ниже показывает игру в боулинг и как она забивается. Забастовки показаны как X, а запасные части показаны как /.

Боулинг

Загружаемый zip-файл содержит 2 рабочих примера решений ( до  и послеката для боулинга Боба Мартина ; если вы не слышали об этом, это отличное упражнение для изучения. Класс Game является предметом этой статьи. Метод Roll () класса Game принимает количество выбитых кеглей, когда мяч катится по переулку, и предоставляет метод GetScore () для определения итоговой оценки боулера.

Код C # в   папке before загружаемого кода органически вырос за набор небольших, но возрастающих требований. Я не улучшил его ни на каком этапе, поэтому, к сожалению, стиль похож на производственный код, который я вижу все время. Если вы не знакомы с оценкой боулинга, посмотрите, сможете ли вы понять это, прочитав алгоритм в коде. Не беспокойтесь, если он слишком ошеломляет, чтобы не отставать от индексации массива, мы исправим эту реализацию, чтобы сделать алгоритм более очевидным.

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

int[] _rolls = new int[21];

int _rollIndex = 0;

internal void Roll(int i)

{

_rolls[_rollIndex++] = i;

}

Следующее, что может улучшить ситуацию, — это имя переменной, переданной в метод Roll. Чего ожидает этот метод? Очевидно, целое число, но что это целое представляет? Вот что показывает моя IDE, когда я смотрю на этот метод с точки зрения вызывающего.

game1

Не самый интуитивно понятный метод подписи, не так ли? Если я хочу узнать, что представляет собой «i», мне, вероятно, нужно пойти в этот класс и посмотреть на детали, чтобы выяснить намерение. Изображение ниже — то, что я вижу после переименования переменной, чтобы лучше соответствовать цели метода Roll (). Это более интуитивный метод подписи. Я вижу, что параметр представляет количество пинов, сбитых за один бросок.

Game2

Теперь давайте обратим внимание на имена переменных в методе GetScore () в  ранее примере, который является сердцем этого класса. Глядя на метод GetScore (), вы можете увидеть, что есть некоторые переменные с менее интуитивными именами. Вот как выглядит метод после переименования для удобства чтения.

int rollIndex = 0;

int score = 0;

for (int frameIndex = 0; frameIndex < 10; frameIndex++)

{

. . .

}

Это немного лучше. Теперь мы видим, что алгоритм работает, хотя массив _rolls по одному кадру за раз, а переменная rollIndex отслеживает броски в каждом кадре. Мы также можем сразу увидеть, что результат, который мы вычисляем, на самом деле является результатом, хотя это, как правило, подразумевается именем метода GetScore (), это все еще добавляет немного ясности.

Пойдя дальше, я приведу некоторые условные коды к методу (называемому «Рефакторинг метода Exrtact Method»). Например, я изменю if (_rolls [rollIndex] == 10) на if (ThisFrameIsAStrike (rollIndex). Теперь код начинает читать как простой разговорный язык. Эти изменения чуть-чуть замедляют запись, но ускоряют читатель очень много — и мы знаем, что большая часть обслуживания читает, а не пишет.

Но как насчет фактической оценки? Требуется немного концентрации, чтобы пробраться сквозь эти элементы массива и понять, что происходит. Давайте выполним еще немного рефакторинга Extract Method, чтобы немного их очистить. Полученный класс содержит больше кода из-за частных методов разъяснения, но логику основного алгоритма GetScore () теперь легко читать. Проверьте  решение после, чтобы увидеть окончательную реализацию класса Game.cs.

for (int frameIndex = 0; frameIndex < 10; frameIndex++)

{

if (FrameIsStrike(rollIndex))

{

. . .

}

else if (FrameIsSpare(rollIndex))

{

. . .

}

else

{

. . .

}

}

Через простое переименование и извлечения в метод мы значительно очистили этот класс, чтобы сделать его более читабельным. Обратите внимание, что код читается почти как предложения.

Давайте поговорим еще об одном примере, прежде чем называть это днем.

В поисках простых чисел

Иногда удобочитаемость заключается не в переименовании, а в структуре самого кода. Этот следующий код представляет собой полное и работающее решение для ката Prime Factors , которая является еще одним отличным инструментом обучения, который можно добавить в ваш набор инструментов.

Метод GetPrimes () принимает целое число и возвращает его простые множители. Переменные уже имеют правильные имена, и если вы потратите некоторое время на чтение кода, вы, вероятно, поймете его намерения.

public int[] Factor(int numToFactor)

{

var factors = new List<int>();

var possiblePrime = 2;



while (numToFactor > 1)

{

while (numToFactor%possiblePrime == 0)

{

factors.Add(possiblePrime);

numToFactor /= possiblePrime;

}

possiblePrime++;

}

return factors.ToArray();

}

Хотя это полное и работающее решение для ката Prime Factors, многие практикующие ката используют этот код, чтобы сделать его еще более коротким и кратким. Фактически, основная логика может быть выражена в 2 коротких строках кода.

public int[] Factor(int numToFactor)

{

var factors = new List<int>();



for (int candidate = 2; numToFactor > 1; candidate++)

for (; numToFactor % candidate == 0; numToFactor /= candidate)

factors.Add(candidate);





return factors.ToArray();

}

Хотя в этой реализации код действительно короче, его труднее читать. Эта реализация берет стандартные структуры цикла for и использует их способами, которые мы не привыкли видеть. Внешний цикл сравнивает numToFactor в слоте, который мы обычно ожидаем увидеть сравниваемым индексатором. Внутренний цикл выглядит немного странно, поскольку он даже не объявляет индексатор. Это вполне допустимо в C #, но это не та конструкция, которую часто можно увидеть.

Найдите минутку, чтобы попытаться найти решение в 2 строки. Чтение заняло больше или меньше времени, чем первое решение, которое использует больше строк кода? Как читатель, я ценю более подробное решение с циклами while.

Ответственность программиста

Делать наш код читаемым чрезвычайно важно для разработчика, который его наследует, и снижает общую стоимость владения кодом. Простые методы, такие как правильное присвоение имен и извлечение кода в методы разъяснения, просты и могут значительно улучшить читаемость без особой работы со стороны разработчика. Умение использовать языковые возможности или сложные алгоритмы может быть полезным, и это, возможно, улучшает статус разработчика, но может легко привести к менее читаемому коду.

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