Руководство для новичков

Это ранняя версия главы из Ваш первый год в коде, книги с практическими инструкциями и советами для начинающих разработчиков. Если вы подумываете о карьере в области программного обеспечения, посетите https://leanpub.com/firstyearincode .

Когда вы начинаете программировать, вы обычно проводите год или два, совершенно не обращая внимания на правила «хорошего кода». Вы можете слышать такие слова, как «элегантный» или «чистый», но не можете дать им определение. Это нормально. Для программиста без опыта есть только один показатель, за которым стоит следить: работает ли он?

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

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

Разница между «хорошим кодом» и «плохим кодом» обычно не зависит от того, как он влияет на вас, когда вы его пишете. Код всегда является общим ресурсом: вы делитесь им с другими сопровождающими с открытым исходным кодом, или с другими разработчиками в вашей команде, или с человеком, у которого будет ваша работа в будущем, или с «будущим вы» (кто не будет иметь представление, о чем думал «настоящий»), или даже просто «отлаживая вас», кто просматривает ваш свежий код в поисках ошибок и чертовски расстроен. Все эти люди будут благодарны, если ваш код будет иметь смысл. Это сделает их работу проще и менее напряженной. Таким образом, написание хорошего кода - это проявление профессиональной вежливости.

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

Условия

Несколько быстрых определений, прежде чем мы начнем:

  • состояние: данные, хранящиеся в памяти во время работы вашей программы. Каждая присваиваемая вами переменная является частью состояния программы.
  • рефакторинг: для изменения кода программы без изменения ее поведения (с точки зрения пользователя). Обычно цель рефакторинга - сделать код более простым, организованным и читаемым.

1. Разделение проблем

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

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

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

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

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

Лучшая форма кода - это разделение рецепта на подрецепты, обычно называемые в коде «модулями» или «классами». Каждый модуль связан с одной связанной операцией или фрагментом данных. Шеф-повар по овощам не должен беспокоиться об ингредиентах соуса, а тому, кто готовит пасту, не следует беспокоиться о терке для сыра. Их проблемы разделены (следовательно, разделение проблем).

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

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

2. Глобальные переменные (плохие)

Вернемся к вашей username переменной. Когда вы создали форму входа в приложение, вы поняли, что вам нужно будет отображать имя пользователя в нескольких местах, например, в заголовке и на странице настроек. Итак, вы выбираете путь наименьшего сопротивления: вы создаете его как глобальную переменную. В Python он объявляется с ключевым словом global. В JavaScript это свойство объекта window. Вроде хорошее решение. Везде, где вам нужно показать имя пользователя, вы просто вставляете переменную username, и вы уже в пути. Почему не все переменные поддерживаются таким образом?

Тогда дела идут боком. В коде есть ошибка, связанная с username. Несмотря на доступность инструмента мгновенного поиска кода в большинстве IDE, это займет некоторое время, чтобы исправить это. Вы будете искать username и найдете сотни или тысячи результатов; некоторые будут глобальной переменной, которую вы установили в начале проекта, некоторые будут другими переменными, которые также имеют имя username, а некоторые будут словом «имя пользователя» в комментарии, имени класса, имени метода и т. д. вперед. Вы можете уточнить поиск и уменьшить количество беспорядка, но отладка все равно займет больше времени, чем следовало бы.

Решение состоит в том, чтобы поместить username туда, где он принадлежит: внутри контейнера (например, класса или объекта данных), который вводится или передается в качестве аргумента классам и методам, которые в нем нуждаются. Этот контейнер также может содержать аналогичные фрагменты данных - все, что установлено при входе в систему, является хорошим кандидатом (но не храните пароль. Никогда не храните пароль). Если вы так хотите, вы можете сделать этот контейнер неизменяемым, чтобы после установки username его нельзя было изменить. Это значительно упростит отладку, даже если username используется в вашем приложении десятки тысяч раз.

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

3. "СУХОЙ"

Давайте на секунду поговорим об отношениях.

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

Что еще хуже, представьте, что через несколько месяцев вы узнаете новую информацию о своей истории любви с первого взгляда: вы думали, что это была счастливая случайность, но это вовсе не случайность. Общий знакомый после месяцев тщательного планирования успешно организовал это первое «привет» и использовал подсознательное внушение, чтобы вы понравились друг другу. С одной стороны, все получилось, и вы оба счастливы. С другой стороны, вы уже несколько месяцев рассказываете совершенно неполную историю. Когда люди узнают, что произошло на самом деле, они могут подумать, что вы солгали им (возможно, это невинная ложь, но все же неловко).

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

Это не только отличный способ смягчить одну из самых сложных проблем взаимоотношений, но и лучший способ кодирования: кодировать каждую операцию (каждый алгоритм, каждый элемент представления, каждое взаимодействие с внешним интерфейсом) только один раз и всякий раз, когда другой фрагмент кода должен знать об этой операции, обратитесь к нему по имени. Каждый раз, когда вы копируете и вставляете код в свою базу кода, вы должны спрашивать себя, делаете ли вы что-то не так. Если рассказ «как объект LonelyUser был сопоставлен с объектом MarriedUser» (или любой другой рассказ ) не раз сказано, пора рефакторинг.

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

4. Скрытие сложности

У меня есть машина, которую я хочу вам продать. Вам потребуется некоторое обучение, чтобы научиться им пользоваться.

Чтобы завести автомобиль, возьмите красный провод №2 и белый провод №7 и соедините их вместе, одновременно ударяя ногой по вертушке оборотов двигателя и заливая соответствующее количество топлива в инжектор, который вы найдете под центральной консолью. Как только автомобиль завелся, залезьте в коробку передач и прижмите промежуточный вал к первой передаче на валу дифференциала. Для ускорения увеличьте подачу бензина в форсунку. Чтобы затормозить, прижмите ноги к шинам.

Надеюсь, ты ненавидишь эту машину так же сильно, как и я. Теперь перенесите эту ненависть на элементы кода с чрезмерно сложными интерфейсами.

Когда вы создаете класс или метод, первое, что вы пишете, должен быть interface: часть, о которой другой фрагмент кода (вызывающий объект) должен знать, чтобы использовать класс или метод. . Для метода это также называется подписью. Каждый раз, когда вы ищите функцию или класс в документации API (например, на MDN или jquery.com), вы видите интерфейс - только то, что вам нужно знать для его использования, без какого-либо кода, который он содержит.

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

Это плохой интерфейс:

function addTwoNumbersTogether(number1, number2, memoizedResults, globalContext, sumElement, addFn) // returns an array

Это хороший интерфейс:

function addTwoNumbersTogether(number1, number2) // returns a number

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

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

Помните, что все методы и данные, принадлежащие классу, но доступные за пределами этого класса, являются частью его интерфейса. Это означает, что вы должны сделать как можно больше методов и полей приватными. В JavaScript переменные, объявленные с помощью var, let или const, автоматически становятся частными для функции, в которой они объявлены, до тех пор, пока вы не возвращаете их или не присваиваете их объекту; во многих других языках есть ключевое слово private. Это должен быть твой лучший друг. Публикуйте данные только по служебной необходимости.

5. Близость

Объявляйте вещи как можно ближе к тому месту, где они используются.

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

function () {
  var a = getA(),
      b = getB(),
      c = getC(),
      d = getD();
  doSomething(b);
  doAnotherThing(a);
  doOtherStuff(c);
  finishUp(d);
}

getA() и его соотечественники не определены в этом сегменте, но представьте, что они возвращают полезные значения.

В таком небольшом методе вас простят за то, что вы думаете, что код хорошо организован и легко читается. Но это не так. d по какой-то причине объявлен в строке 4, хотя он не используется до строки 9, что означает, что вам нужно прочитать почти весь метод, чтобы убедиться, что он больше нигде не используется.

Более лучший способ выглядит так:

function () {
  var b = getB();
  doSomething(b);
  var a = getA();
  doAnotherThing(a);
  var c = getC();
  doOtherStuff(c);
  var d = getD();
  finishUp(d);
}

Теперь ясно, когда будет использоваться переменная: сразу после ее объявления.

В большинстве случаев ситуация не так проста; что, если b нужно передать как doSomething(), так и doOtherStuff()? В этом случае ваша задача - взвесить варианты и убедиться, что метод по-прежнему прост и удобочитаем (в первую очередь за счет того, чтобы он был коротким). В любом случае убедитесь, что вы не объявляете b до его первого использования, и используйте его в как можно более коротком сегменте кода.

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

6. Глубокая вложенность (плохо)

JavaScript известен неудобной ситуацией, известной как «ад обратных вызовов»:

Видите этот след )};, идущий по середине страницы? Это визитная карточка ада обратных звонков. Этого можно избежать, но это тема, о которой уже писали многие другие писатели.

Я хочу, чтобы вы подумали о чем-то более похожем на «если ад».

callApi().then(function (result) {
  try {
    if (result.status === 0) {
      model.apiCall.success = true;
      if (result.data.items.length > 0) {
        model.apiCall.numOfItems = result.data.items.length;
        if (isValid(result.data) {
          model.apiCall.result = result.data;
        }
      }
    }
  } catch (e) {
    // suppress errors
  }
});

Подсчитайте пары { фигурных скобок }. Шесть, пять из которых вложены. Слишком много. Этот блок кода трудно читать, отчасти потому, что код вот-вот выползет за правую часть экрана, а программисты ненавидят горизонтальную прокрутку, а отчасти потому, что вам нужно прочитать все if условия, чтобы понять как вы попали в строку 10.

А теперь посмотрите на это:

callApi().then(function (result) {
  if (result.status !== 0) {
    return;
  }
  model.apiCall.success = true;
  if (result.data.items.length <= 0) {
    return;
  }
  model.apiCall.numOfItems = result.data.items.length;
  if (!isValid(result.data)) {
    return;
  }
  model.apiCall.result = result.data;
});

Так намного лучше. Мы можем ясно видеть «нормальный путь» для кода, и только в ненормальных ситуациях код сбивается в if блок. Отладка намного проще. И если мы захотим добавить дополнительный код для обработки условий ошибки, будет легко добавить пару строк внутри этих блоков if (представьте, если бы блоки if в исходном коде имели прикрепленные блоки else! Ужас).

Кроме того, я удалил блок try-catch, потому что вы никогда и никогда не должны подавлять ошибки. Ошибки - ваш друг, и без их помощи ваше приложение будет мусором.

7. Чистые функции

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

Эта функция чистая:

function getSumOfSquares(number1, number2) {
  return (number1 * number1) + (number2 * number2);
}

А этого нет:

function getSumOfExponents(number1, number2) {
  scope.sum = Math.pow(number1, scope.exp) + Math.pow(number2, scope.exp);
}

Если вы хотите отладить первую функцию, все, что вам нужно, тут же. Вы можете вставить его в отдельную среду, например jsfiddle или консоль браузера, и поиграть с ним, пока не обнаружите, что не так.

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

Не все методы могут быть чистыми; если бы у вашего приложения не было состояния, его полезность была бы ограничена. Но вам следует как можно чаще писать чистые функции. Это упростит поддержку и масштабирование вашей программы.

8. Модульные тесты (хороши)

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

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

Заключение

Хороший код - это удовольствие поддерживать, развивать и решать проблемы. Плохой код - это пытка для души. Выбирайте писать хороший код.

Хороший вопрос, который следует задать себе при написании кода: будет ли его легко удалить, когда он нам больше не нужен? Если он глубоко вложен, скопирован и вставлен повсюду, в зависимости от различных уровней состояния и строк кода по всей программе, и в противном случае он ужасен, люди не смогут понять его цель и влияние, и им будет неудобно удалять Это. Но если сразу станет понятно, как он используется и с каким количеством строк кода он взаимодействует, люди смогут уверенно удалить его, когда его полезность иссякнет. Я знаю, что вы любите свой код, но правда в том, что когда-нибудь мир станет лучше без него.

Для более полного обсуждения того, что делает хороший код, я рекомендую книгу Стива МакКоннелла Code Complete. Это толстая книга (и немного устаревшая), но она очень удобочитаема и поможет вам вырасти из программиста «рабочего кода» в программиста «хорошего, чистого, элегантного кода».