Статьи

Программирование, как Кент Бек

Трое из нас, а именно Стиг, Кшиштоф и Якуб, имели удовольствие провести неделю с Кентом Беком во время Iterate Code Camp 2012, работая вместе над проектом и изучая лучшие практики программирования. Мы хотели бы поделиться ценными уроками, которые мы извлекли и которые сделали нас лучшими программистами (или, по крайней мере, так хотелось бы думать).

Значения, лежащие в основе стиля программирования

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

связь

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

Простота

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

гибкость

«Принятие выполнимых решений сегодня и сохранение гибкости, чтобы изменить свое мнение в будущем, является ключом к хорошей разработке программного обеспечения». — Шаблоны реализации, Кент Бек

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

Резюме

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

Ключевые уроки 1. Вам это не нужно!

Какая сегодня демка? С какого теста начать?

Прежде чем мы прибыли, мы остановились на теме, которая, по нашему мнению, была бы интересной и сложной. Мы решили попробовать свои силы в создании прототипа распределенной базы данных с высокой степенью масштабируемости. Мы ожидали, что по прибытии проведем несколько часов, обсуждая подходы к тому, как мы должны это реализовать. В конце концов, есть много вещей, которые нужно учитывать: стратегии репликации, согласованное хеширование, автообнаружение членства в кластере и разрешение конфликтов, и многие другие. Если у вас есть всего 5 дней, вам нужно спланировать, как реализовать это самым простым способом. Что делать. Что пропустить. Правильно? Неправильно. Вместо какого-либо планирования Кент просто спросил нас, какую демонстрацию мы хотели бы показать в конце дня. И его следующий вопрос был о том, какой тест написать.

Это оказался очень разумный подход, потому что мы на самом деле реализовали лишь очень небольшое подмножество возможных функций, ежедневно решая, что делать дальше, основываясь на нашем опыте решения проблемы. Любое обсуждение с самого начала, более 10 минут, было бы потрачено на 90% времени. Это не означает, что планирование плохое (хотя получающиеся планы обычно бесполезны ), это лишь означает, что мы обычно делаем гораздо больше, чем нам нужно на самом деле. Получение реальной обратной связи и основание наших решений на этом, на реальных данных, гораздо более практично и ценно, чем любые предположения.

Поэтому предпочтите текущее планирование, основанное на обратной связи и опыте, обширному предварительному планированию. Спросите себя: что я собираюсь представить на следующей демонстрации / встрече / дне? Какой тест нужно написать, чтобы отразить это и тем самым направить мои усилия по разработке?

2. Напишите высокоуровневые тесты для руководства разработкой

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

1
2
3
4
5
6
7
8
List<Graft> grafts = Graft.getTwoGrafts();
Graft first = grafts.get(0);
Graft second = grafts.get(1);
 
first.createNode().put("key", "value")
first.kill();
 
assertNotNull(second.getNodeByProperty("key", "value"));

(API, конечно, позже развился в нечто более общее.)

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

Раньше я думал о TDD на уровне единиц / классов, но это TDD на гораздо более высоком уровне. У него есть некоторые интересные свойства:

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

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

3. Лучшие практики для [модульного] тестирования.

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

Это применение ключевого принципа фокуса.

Пишите реализацию в тестах, потом рефакторинг

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

Основные принципы: Фокус, избегая преждевременного принятия решений.

Дизайн снизу вверх

Избегает:

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

Начните с реализации небольших частей функциональности. Затем объедините их, чтобы сформировать более сложное поведение. Не отвлекайтесь на зависимости, напишите для них простые заглушки, которые вы позже замените реальными реализациями. Используя эту технику, вы не обязаны проектировать решения, принятые в начале, как в подходе «сверху вниз». Это требует немного интуиции и некоторого опыта, но в сочетании с TDD это помогает улучшить дизайн и реализацию.

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

Действуй и утверждай на одном уровне абстракции

Наша Graft DB имеет telnet-подобный интерфейс для приема команд от пользователей. Рассмотрим следующие два (упрощенных) варианта теста addComment :

1
2
3
4
5
// Test 1
Graft db = ...; this.subject = new CommandProcessor(db);
subject.process("addComment eventId my_comment");
 
assertThat(subject.process("getComments eventId")).isEqualTo("my_comment");
1
2
3
4
// Test 2 (same setUp)
subject.process("addComment eventId my_comment");
 
assertThat(db.getComments("eventId")).containsOnly("my_comment");

Первый тест, при тестировании команды addComment, использует другую команду — getComments — для проверки результирующего состояния. Он использует только одну точку входа API — субъект — в течение всего теста. Второй тест напрямую обращается к базовому экземпляру базы данных и его API для получения тех же данных, т. Е. Помимо субъекта он использует также базовый БД .

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

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

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

4. Фокус!

  • Поместите всплывающие задачи в список «Позднее» вместо того, чтобы выполнять их сразу
  • Сосредоточьтесь на исправлении теста в первую очередь — однако уродливый и простой (и рефакторинг позже)
  • Фокус на текущих потребностях — нет преждевременной абстракции

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

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

Некоторые другие вещи, которые мы изучили Parallel Design

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

Примером высокоуровневого параллельного проектирования является замена СУБД на базу данных NoSQL. Вы начнете с реализации кода для записи в новую БД, затем будете использовать его и записывать как в старый, так и в новый, затем вы также начнете читать из нового (возможно, сравнивая результаты со старым кодом). проверить их правильность), используя данные старой БД. Затем вы начнете фактически использовать данные БД NoSQL, продолжая записывать / читать из старой БД (чтобы вы могли легко переключиться обратно). Только когда новая БД себя оправдает, вы постепенно удалите старую БД.

Примером параллельного проектирования на микроуровне является замена параметров метода (сообщения и т. Д.) Объектом, из которого они получены (Edge), как мы сделали для notifyComment :

1
2
3
4
5
- public void notifyComment(String message, String eventName, String user) {
-    notifications.add(user + ": commented on " + eventName + " " + message);
---
+ public void notifyComment(Edge target) {
+    notifications.add(target.getTo().getId() + ": commented on " + target.getFrom().getId() + " " + target.get("comment"));

Шаги были:

  1. Добавление ребра в качестве другого параметра (Refactor — Изменить метод подписи)
  2. Замена одного за другим использования исходных параметров свойствами целевого Edge (Infinitest запускает тесты автоматически после каждого изменения, чтобы убедиться, что мы все еще в порядке)
  3. Окончательно удаляем все исходные параметры (Refactor — Change Method Signature)

Хорошо, что ваш код всегда работает, и вы можете зафиксировать или остановить его в любое время.

Возобновляемый рефакторинг

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

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

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

Рефакторинг на зеленый, по желанию

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

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

Мы бы закрыли эту тему, цитируя Кента, рассказывающего о том, сколько тестов нужно сделать :

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

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

Симметрия в коде

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

Код с симметриями легче понять, чем асимметричный код. Это легче читать и понимать. Так что же, в частности, симметричный код? Чтобы процитировать Кента снова:

Симметрия в коде — это то, где одна и та же идея выражается одинаково везде, где она появляется в коде.

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

И что ты знаешь, даже больше …

  • Управляйте своей энергией — осознавайте свою энергию и остановитесь, прежде чем устать. Не забывайте делать перерывы. Отдыхающий разработчик в несколько раз продуктивнее, чем уставший. (Дж. Б. Рейнсбергер из журнала «Экономика разработки программного обеспечения» рассказывает о том, как работал так интенсивно, что он стал истощенным и совершенно непродуктивным).
  • Парное программирование — это навык, который нужно осознанно усвоить (и он может быть более сложным для некоторых типов личности, которые следует уважать)
  • Предпочитаю рефакторинги IDE ручным изменениям — например, никто из нас никогда раньше не использовал встроенный рефакторинг, в то время как Кент использует его постоянно. Как только вы справитесь с рефакторингами, они станут намного более эффективными, чем изменение вещей вручную, и, что более важно, они избегают малой, но ненулевой вероятности что-то сломать (помните, что парень Мерфи, который сказал — что может сломаться, сломается)

Код

Вы можете найти код для Iterate Code Camp 2012 на GitHub — bit.ly/codecamp2012

Вывод

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

Связанные ресурсы

  • В блоге Якуба « Принципы создания ремонтопригодных и эволюционируемых тестов» обобщены некоторые дополнительные принципы написания тестов, которые он выучил у Кента.
  • Rich Hickey: Simple Made Easy — отличный доклад, который объясняет принципиальное различие между «простым» (против сложного) и «простым» и тем, что наши языки и инструменты не так просты, как должны быть, часто потому, что они пытаются быть легко

Ссылка: программирование Как Кент Бек от нашего партнера JCG Якуба Холи в блоге Holy Java .