Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, Хаброжители!
Дэвид Фарли, легендарный разработчик и создатель continuous delivery, рассказывает о фундаментальных принципах разработки современного ПО. Пора познакомиться с наиболее эффективными и грамотными методами управления рабочими процессами, которые позволят повысить качество ваших приложений, вашей жизни и жизни ваших коллег.
Ключевые концепции, лежащие в основе эффективной разработки ПО, — это непрерывное обучение и управление сложностью. Дэвид Фарли анализирует их и формулирует принципы, следование которым приведет к улучшению дизайна ПО и качества кода, а также методы и подходы, доказавшие свою эффективность и приводящие к успеху.
Речь идет не об абстракциях, вы освоите реальные приемы, которыми пользуются опытные разработчики ПО. Эти техники эмпирические и итеративные, они основаны на обратной связи и сосредоточены на выполнении кода, то есть соответствуют всем требованиям современной разработки.
Связность (cohesion) в computer science определяется как «степень, в которой элементы внутри модуля связаны друг с другом».
Описывая хороший программный дизайн, я всегда использую слова Кента Бека:
Связность — одно из самых зыбких понятий. Я могу сделать что-то простое, например использовать модульный синтаксис, и в результате заявить, что мой код модульный. Но он таким не станет; простое добавление набора несвязанных вещей в файл не делает код модульным ни в каком смысле, кроме самого банального.
Когда я говорю о модульности, я имею в виду, что в системе есть компоненты, которые скрывают информацию от других компонентов (модулей). Если код внутри самого модуля несвязный, то это не работает.
Проблема в том, что такое утверждение зачастую чрезмерно упрощают. Вероятно, это тот случай, когда мастерство, навыки и опыт программиста-практика имеют значение. Баланс между действительно модульными системами и связностью часто сбивает людей с толку.
Как часто вы видели код, который извлекает некоторые данные, разбирает их, а затем сохраняет в другом месте? Наверняка шаг «сохранить» связан с шагом «изменить»? Разве это не хорошая связность? Эти шаги нужны все вместе, не так ли?
Не совсем. Давайте рассмотрим пример. Предупреждаю: здесь сложно выделить несколько идей. Приведенный код проиллюстрирует поне-многу каждую из идей этого раздела, поэтому я надеюсь, что вы сумеете сосредоточиться на связности и понимающе улыбнетесь, когда я также коснусь разделения ответственности, модульности и так далее.
В листинге 10.1 показан пример довольно грубого кода. Тем не менее он подходит для моей цели — изучить конкретный практический пример. Этот код читает небольшой файл, содержащий список слов, сортирует слова в алфавитном порядке, а затем записывает новый файл с итоговым упорядоченным списком — загружайте, обрабатывайте и сохраняйте!
Это довольно типичный шаблон для множества различных задач: прочитать какие-то данные, обработать их, а затем сохранить результат в другом месте.
Листинг 10.1. Очень плохой код с наивной связностью
Я считаю, что этот код очень плох, и мне пришлось заставить себя написать его таким. Код просто кричит о плохом разделении задач, слабой модульности, жесткой связанности и почти нулевой абстракции, но как насчет связности?
Вся работа здесь реализуется в одной функции. И очень часто выпущенный код выглядит так же, только намного длиннее и сложнее, так что в реальности все еще хуже!
Наивное представление о связности таково: все помещается рядом и поэтому легко заметно. Но если на мгновение отвлечься от других методов управления сложностью, так ли легко прочесть этот код? Сколько времени вам понадобится, чтобы понять, что он делает? Долго ли вам придется гадать, если я не дам подсказку в названии метода?
Теперь взгляните на листинг 10.2, где дела обстоят чуть лучше.
Листинг 10.2. Плохой код, связность немного лучше
Листинг 10.2 по-прежнему не очень хорош, но он более связный; взаимо-связанные части кода более четко прописаны и располагаются ближе друг к другу. Проще говоря, все, что вам нужно знать о readWords, названо и содержится в одном методе. Общий поток метода loadProcessAndStore теперь хорошо просматривается, даже если бы я выбрал менее описательное имя. Информация в этой версии более связна, чем в листинге 10.1. Теперь стало значительно понятнее, какие части кода более тесно связаны друг с другом, хотя код имеет тот же функционал, что и раньше. Все это делает эту версию значительно более удобной для чтения, и, как следствие, ее проще изменять.
Обратите внимание, что в листинге 10.2 больше строк кода. Этот пример написан на Java, довольно многословном языке, и шаблонный код на нем весьма дорог, к тому же придется добавить небольшие накладные расходы на улучшение читаемости. Это не обязательно плохо!
Все программисты стремятся уменьшить объем печатаемого текста. Мы ценим ясность и краткость. Большое значение имеет способность выражаться просто, но простоту не измерить количеством набранных символов. Выражение ICanWriteASentenceOmittingSpaces короче, но менее читаемо!
Оптимизировать код, чтобы уменьшить количество набираемого текста, — ошибка. Это оптимизация не того, что нужно. Код — инструмент коммуникации; мы должны использовать его для передачи информации. Конечно, он также должен быть машиночитаемым и исполняемым, но на самом деле это не основная цель. Если бы она была такой, мы бы до сих пор программировали системы, переключая тумблеры на передней панели компьютеров или с помощью машинного кода.
Основная цель кода — донести идеи до людей. Мы пишем код, чтобы максимально ясно и просто выражать мысли, — по крайней мере, так это должно работать. Никогда нельзя жертвовать ясностью в угоду краткости. Сделать код читаемым — это, на мой взгляд, и профессиональная обязанность, и один из самых важных руководящих принципов управления сложностью. Поэтому я предпочитаю оптимизировать мышление, а не сокращать текст.
Вернемся к коду: этот второй пример явно более легко читается. Стало понятнее, что он делает. Но код все еще ужасен — он не модульный, почти не предусматривает разделения функций, негибкий, с жестко закодированными строками для имен файлов, и его нельзя проверить, кроме как запустить целиком и работать с файловой системой. Но мы улучшили связность. Каждый фрагмент кода теперь выполняет одну часть задачи. Фрагменты имеют доступ только к тому, что им необходимо для ее выполнения. Мы вернемся к этому примеру в следующих главах, чтобы посмотреть, как его улучшить.
Я спросил друга, чьим кодом я восхищаюсь, как лучше показать важность связности, и он порекомендовал видео из «Улицы Сезам», которое можно найти на YouTube: «Один из этих предметов не похож на другие».
В шутливой форме выражается важная мысль. Связность в большей степени, чем другие инструменты управления сложностью, зависит от контекста. В зависимости от контекста «все эти предметы могут быть не похожи друг на друга».
Мы должны сделать выбор, и этот выбор тесно связан с другими инструментами. Невозможно однозначно отделить связность от модульности или разделения ответственности, потому что эти методы помогают определить, что означает связность в контексте разработки.
Одним из эффективных инструментов для принятия такого рода решений является предметно-ориентированное проектирование (domain-driven design, DDD). В предметной области проще определить направление, которое с большей вероятностью принесет прибыль в долгосрочной перспективе. Это касается и мышления, и разработки.
Мне очень не нравятся оба примера кода, которые я привожу в этой главе, и мне неловко показывать их вам, потому что мой инстинкт проектировщика буквально вопит, что разделение ответственности в обоих случаях ужасно. Листинг 10.2 лучше: по крайней мере, каждый метод теперь делает что-то одно, но класс по-прежнему ужасен. Если вы еще этого не видите, я расскажу, почему это важно, в следующей главе.
Наконец, в моем наборе инструментов есть тестируемость. Я начал писать эти плохие примеры кода, как и любой код: с написания теста. Однако мне пришлось остановиться почти сразу же, потому что я не мог, используя TDD, написать такой плохой код! Мне пришлось начать работу заново, и, признаюсь, я чувствовал, что перенесся в прошлое. Я написал тесты для примеров, чтобы проверить, работают ли они так, как я ожидал, но этот код нельзя назвать тестируемым.
Тестируемость означает модульность, разделение ответственности и все, что мы ценим в высококлассном коде. Это, в свою очередь, помогает приблизиться к контексту и абстракции, которые нам нравятся в дизайне, и понять, где можно сделать код более связным.
Заметьте: нет никаких гарантий, что это удастся, — и это основная мысль книги. Не существует простых, шаблонных ответов. В книге я показываю вам интеллектуальные инструменты, которые помогают структурировать мышление, когда не получается найти ответы.
Техники, описанные в этой книге, не дают их; ответы вам придется искать самостоятельно. Эти техники предоставляют набор идей и приемов, чтобы двигаться безопасно, если вы еще не знаете ответов, а вы их не знаете, независимо от сложности любой реально создаваемой системы. Мы никогда не знаем ответов, пока не закончим работу!
Эта стратегия выглядит скорее оборонительной, и она такая и есть, но ее цель в том, чтобы сохранить свободу выбора. Это одно из значительных преимуществ работы над управлением сложностью. По мере получения новых знаний мы можем постоянно улучшать код, используя полученные знания. Мне кажется, «инкрементная» — более точный эпитет, чем «оборонительная».
Мы добиваемся прогресса постепенно, проводя серию экспериментов, и используем методы управления сложностью, чтобы защитить код от серьезных ошибок.
Так работают наука и инженерия. Мы контролируем переменные, делаем небольшой шаг и оцениваем текущее положение. Если оценка показывает, что мы сделали неверный шаг, мы возвращаемся назад и решаем, что делать дальше. Если все нормально, мы контролируем переменные, делаем еще один небольшой шаг, и так далее.
Можно представить разработку как своего рода эволюционный процесс. Программисты добиваются цели в результате направленной эволюции в сфере обучения и разработки.
Плохой код, такой как в листинге 10.1, часто оправдывают тем, что для высокой производительности системы требуется писать более сложный код. В последние годы я занимаюсь разработкой очень высокопроизводительных систем — и уверяю вас, что это не так. Такие системы требуют простого, продуманного кода.
Задумайтесь на мгновение, что означает высокая производительность с точки зрения разработки. Для ее достижения нам нужно выполнять максимальный объем работы при минимальном количестве инструкций.
Чем сложнее код, тем больше вероятность того, что разработанные маршруты не оптимальны, потому что самый простой возможный маршрут через код скрыт сложностью самого кода. Многих программистов это удивляет, но чтобы код работал быстро, он должен быть простым и легко понятным.
Правильность этих рассуждений очевидна, если посмотреть на систему шире.
Вернемся к нашему примеру. Я слышал, как программисты утверждали, что код в листинге 10.1 будет быстрее, чем в листинге 10.2, из-за накладных расходов на вызовы методов, которые появились в листинге 10.2. Но для современных языков это нонсенс. Большинство современных компиляторов просмотрят код в листинге 10.2 и встроят методы. Кроме того, современные оптимизирующие компиляторы способны на большее. Они фантастически оптимизируют код, чтобы он работал эффективно на современном оборудовании. Компиляторы полезны, когда код прост и предсказуем, поэтому чем он сложнее, тем меньше пользы от оптимизатора. Большинство оптимизаторов просто прекращают работу, как только цикломатическая сложность блока кода превышает определенный порог.
Я оценил производительность обеих версий кода. Эти тесты оказались так себе, потому что код плохой. Мы недостаточно контролируем переменные, чтобы действительно понимать, что происходит, но очевидно, что на этом уровне бенчмарка разницы практически не было.
Различия слишком малы, чтобы их удалось дифференцировать от остальных событий. В одном прогоне версия BadCohesion оказалась лучшей, в другом — ReallyBadCohesion. В серии тестовых прогонов для каждой из 50 000 итераций метода loadProcessStore общая разница составляла не более 300 миллисекунд, так что в среднем это дало примерно 6 наносекунд на вызов, чаще — в пользу версии с дополнительными вызовами методов.
Это плохой тест, потому что то, что нас интересует, — стоимость вызовов методов — ничтожно мала по сравнению со стоимостью операций ввода-вывода. Тестируемость — в данном случае тестируемость производительности — снова поможет добиться лучшего результата. Мы обсудим это более подробно в следующей главе.
Столько всего скрыто от глаз, что даже экспертам трудно предсказать исход. Каков ответ? Если вас действительно интересует производительность кода, не гадайте, что работает быстро, а что медленно, — измеряйте!
Если мы хотим сохранить свободу исследования и иногда совершать ошибки, нам нужно побеспокоиться об издержках связанности, или сцепления (coupling).
Связанность: для двух строк кода, A и B, связанность означает, что B должна изменить поведение только потому, что изменилась A, — строки сцеплены.
Связность: строки являются связными, когда изменение А позволяет измениться В, так что обе добавляют новую ценность.
Связанность — слишком общий термин. Различные виды связанности мы более подробно рассмотрим в главе 13.
Несвязанная система — это забавно. Если мы хотим, чтобы две части системы взаимодействовали, они должны быть в какой-то степени сцеплены. Так что, как и связность, связанность — это вопрос степени, а не какой-то абсолютной меры. Однако цена несоответствующих уровней связанности чрезвычайно высока, поэтому при проектировании важно учитывать ее влияние.
Связанность в некотором смысле является платой за связность. Области системы, которые обладают связностью, вероятно, будут иметь и большее сцепление.
Использование автоматических тестов, в частности TDD, для управления дизайном дает много преимуществ. Стремление создать проверяемый дизайн и достаточно абстрагированные поведенческие тесты помогут сделать код связным.
Прежде чем создавать код, описывающий поведение, которого мы ожидаем от системы, мы создаем тестовый сценарий. Это позволяет сосредоточиться на разработке внешнего API/интерфейса для кода. Теперь напишем реализацию, которая будет соответствовать созданной нами небольшой исполняемой спецификации. Если кода слишком много, больше, чем нужно для соответствия спецификации, процесс нарушится и связность реализации снизится. Если кода слишком мало, не удастся реализовать желаемое поведение. Методология TDD стимулирует добиваться связности.
Как всегда, никаких гарантий. Это не механический процесс, и результат по-прежнему зависит от опыта и навыков программиста, но этот подход позволяет добиться прогресса, о котором раньше и помыслить было невозможно, и развивает навыки и опыт.
Ключевой метрикой связности служит степень, или стоимость, изменений. Если вам приходится изменять кодовую базу во многих местах, это не очень связная система. Связность — мера функционального соответствия. Это измерение соответствия цели. И очень зыбкое понятие!
Давайте рассмотрим простой пример.
Класс с двумя методами, каждый из которых ассоциирован с переменной-членом (листинг 10.3), — пример плохой связности, потому что переменные здесь никак не соотносятся. Они принадлежат разным методам, но хранятся вместе на уровне класса, даже если не соотносятся между собой.
Листинг 10.3. Плохая связность
В листинге 10.4 представлено гораздо более красивое и связное решение. Обратите внимание, что эта версия не только более связная, но и более модульная, с лучшим разделением функций. Очевидно, что эти концепции взаимосвязаны.
Листинг 10.4. Улучшенная связность
В сочетании с остальными принципами управления сложностью стремление создать тестируемый дизайн помогает улучшить связность решений. Хороший пример — внимание к разделению ответственности, особенно когда речь идет об отделении случайной сложности от необходимой.
В листинге 10.5 показаны три простых примера улучшения связности кода за счет сознательного разделения необходимой и случайной сложности. В каждом примере мы добавляем товар в корзину, сохраняем его в базе данных и вычисляем стоимость корзины.
Листинг 10.5. Три примера связности
Первая функция — очевидно несвязный код. В нем смешано множество понятий и переменных — необходимой и случайной сложности. Я бы сказал, что это очень плохой код, даже в таком масштабе. Я бы не стал писать такой код, потому что из него трудно понять, что происходит, хотя сценарий чрезвычайно прост.
Второй пример немного лучше. Связность в нем выше. Понятия в этой функции соотносятся друг с другом и представляют более последовательный уровень абстракции, поскольку они в основном связаны с необходимой сложностью задачи. Инструкция «сохранить» выглядит спорно, но по крайней мере этим мы скрыли случайную сложность.
Последний пример интересен. Я бы сказал, что, он, безусловно, связный. Чтобы выполнить полезную работу, следует добавить товар в корзину и сообщить об этом другим потенциально заинтересованным сторонам. Мы полностью разделили задачи хранения и подсчета общей стоимости товаров в корзине. Программа может отреагировать в ответ на уведомление о добавлении, а может и не отреагировать, если соответствующие фрагменты кода не зафиксируют интерес к событию «элемент добавлен».
Код можно считать либо более связным, если в нем заключена вся необходимая сложность задачи, а другие варианты поведения являются побочными эффектами, либо менее связным, если считать события «store» и «total» частями задачи. В конечном счете выбор дизайна зависит от контекста задачи, которую вы решаете.
Связность, пожалуй, наименее поддается количественной оценке как атрибут «инструментов управления сложностью», но она важна. Проблема в том, что когда связность плохая, код и системы становятся менее гибкими, их сложнее тестировать и с ними сложнее работать.
В простом примере в листинге 10.5 влияние связного кода очевидно. Если в коде пересекается функционал, ему не хватает ясности и удобочитаемости, как показано в add_to_cart1. Если функционал широкий, труднее увидеть, что происходит, как в add_to_cart3. Располагая взаимо-связанные идеи рядом, мы добиваемся максимальной удобочитаемости, как в add_to_cart2.
На самом деле у способа проектирования add_to_cart3 есть некоторые преимущества, и такой код, безусловно, удобнее для работы, чем версия 1.
Я считаю, что это оптимальный вариант связности. Если вы смешаете слишком много концепций, вы потеряете связность на уровне деталей. В примере 1 можно утверждать, что вся работа делается внутри одного метода, но это только наивная связность.
На самом деле добавление товара в корзину (основной бизнес-функционал) смешано с другими функционалами, что делает общую картину неясной. Даже в этом простом примере непонятно, как работает код, пока вы не углубитесь в него.
Другой альтернативе, add_to_cart3, хотя она и более гибкая, все еще не хватает ясности. В этом экстремальном случае функционал может быть настолько рассредоточен, что вы не сможете понять общую картину, не прочитав множество строк кода и не разобравшись в нем. Это, наверное, и неплохо, но я считаю, что ограничение в ясности — стоимость слабой связанности и некоторых других преимуществ.
Оба этих недостатка очень часто встречаются в уже готовых системах. На самом деле настолько часто, что в больших сложных системах становятся даже своего рода нормой.
Это ошибка дизайна, и она дорого обходится. Вы с ней наверняка хорошо знакомы, если когда-нибудь работали с «унаследованным кодом».
Существует простой субъективный способ определить плохую связность. Если вы читаете код и думаете: «Я не знаю, что он кодирует», — вероятно, причиной тому плохая связность.
Как и в других случаях, проблемы связности не ограничиваются только кодом, который мы пишем, и системами, которые мы создаем. Связность работает на уровне информации, так что она также важна для построения эффективной структуры в организациях, где мы работаем. Самый очевидный пример — организация работы команд. Выводы из отчета State of DevOps свидетельствуют, что способность принимать собственные решения — без их утверждения у кого-либо вне команды — один из главных признаков высокой производительности, метриками которой служат пропускная способность и стабильность. То есть информация и навыки команды обладают связностью в том смысле, что внутри команды есть все, что требуется, чтобы принимать решения и добиваться успеха.
Связность, вероятно, самый отвлеченный из принципов управления сложностью. Разработчики могут утверждать (и иногда утверждают), что связность — это когда весь код находится в одном месте, одном файле и даже одной функции, но это слишком упрощенное представление.
Код, который таким образом случайно объединяет несколько концепций, не является связным; в нем просто нет структуры. Это плохо, поскольку не позволяет увидеть, что делает код и как его безопасно изменить.
Связность заключается в объединении соотносящихся концепций, которые изменяются в коде совместно. Если они оказались вместе случайно, на самом деле связности между ними нет.
Связность — это детектор модульности и в целом имеет смысл, если рассматривать ее в сочетании с модульностью. Одним из наиболее эффективных инструментов, помогающих найти рабочий баланс между связностью и модульностью, является разделение ответственности.
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Инженерия
Дэвид Фарли, легендарный разработчик и создатель continuous delivery, рассказывает о фундаментальных принципах разработки современного ПО. Пора познакомиться с наиболее эффективными и грамотными методами управления рабочими процессами, которые позволят повысить качество ваших приложений, вашей жизни и жизни ваших коллег.
Ключевые концепции, лежащие в основе эффективной разработки ПО, — это непрерывное обучение и управление сложностью. Дэвид Фарли анализирует их и формулирует принципы, следование которым приведет к улучшению дизайна ПО и качества кода, а также методы и подходы, доказавшие свою эффективность и приводящие к успеху.
Речь идет не об абстракциях, вы освоите реальные приемы, которыми пользуются опытные разработчики ПО. Эти техники эмпирические и итеративные, они основаны на обратной связи и сосредоточены на выполнении кода, то есть соответствуют всем требованиям современной разработки.
Связность
Связность (cohesion) в computer science определяется как «степень, в которой элементы внутри модуля связаны друг с другом».
Модульность и связность: основы дизайна
Описывая хороший программный дизайн, я всегда использую слова Кента Бека:
Отодвиньте друг от друга все, что не связано, и придвиньте ближе друг к другу все, что связано.В этой простой, немного шутливой фразе есть доля правды. Хороший программный дизайн на самом деле зависит от того, как мы организуем код в системах. Все рекомендуемые мной принципы, помогающие управлять сложностью, на самом деле касаются разделения наших систем. Нам нужно создавать системы из более мелких, понятных и легко тестируемых дискретных частей. Чтобы достичь этого, безусловно, требуются методы, которые позволят отодвигать несвязанные вещи дальше друг от друга, но не менее важна необходимость сближать связанные вещи. Здесь в игру вступает связность.
Связность — одно из самых зыбких понятий. Я могу сделать что-то простое, например использовать модульный синтаксис, и в результате заявить, что мой код модульный. Но он таким не станет; простое добавление набора несвязанных вещей в файл не делает код модульным ни в каком смысле, кроме самого банального.
Когда я говорю о модульности, я имею в виду, что в системе есть компоненты, которые скрывают информацию от других компонентов (модулей). Если код внутри самого модуля несвязный, то это не работает.
Проблема в том, что такое утверждение зачастую чрезмерно упрощают. Вероятно, это тот случай, когда мастерство, навыки и опыт программиста-практика имеют значение. Баланс между действительно модульными системами и связностью часто сбивает людей с толку.
Базовое снижение связности
Как часто вы видели код, который извлекает некоторые данные, разбирает их, а затем сохраняет в другом месте? Наверняка шаг «сохранить» связан с шагом «изменить»? Разве это не хорошая связность? Эти шаги нужны все вместе, не так ли?
Не совсем. Давайте рассмотрим пример. Предупреждаю: здесь сложно выделить несколько идей. Приведенный код проиллюстрирует поне-многу каждую из идей этого раздела, поэтому я надеюсь, что вы сумеете сосредоточиться на связности и понимающе улыбнетесь, когда я также коснусь разделения ответственности, модульности и так далее.
В листинге 10.1 показан пример довольно грубого кода. Тем не менее он подходит для моей цели — изучить конкретный практический пример. Этот код читает небольшой файл, содержащий список слов, сортирует слова в алфавитном порядке, а затем записывает новый файл с итоговым упорядоченным списком — загружайте, обрабатывайте и сохраняйте!
Это довольно типичный шаблон для множества различных задач: прочитать какие-то данные, обработать их, а затем сохранить результат в другом месте.
Листинг 10.1. Очень плохой код с наивной связностью
public class ReallyBadCohesion
{
public boolean loadProcessAndStore() throws IOException
{
String[] words;
List<String> sorted;
try (FileReader reader =
new FileReader("./resources/words.txt"))
{
char[] chars = new char[1024];
reader.read(chars);
words = new String(chars).split(" |\0");
}
sorted = Arrays.asList(words);
sorted.sort(null);
try (FileWriter writer =
new FileWriter("./resources/test/sorted.txt"))
{
for (String word : sorted)
{
writer.write(word);
writer.write("\n");
}
return true;
}
}
}
Я считаю, что этот код очень плох, и мне пришлось заставить себя написать его таким. Код просто кричит о плохом разделении задач, слабой модульности, жесткой связанности и почти нулевой абстракции, но как насчет связности?
Вся работа здесь реализуется в одной функции. И очень часто выпущенный код выглядит так же, только намного длиннее и сложнее, так что в реальности все еще хуже!
Наивное представление о связности таково: все помещается рядом и поэтому легко заметно. Но если на мгновение отвлечься от других методов управления сложностью, так ли легко прочесть этот код? Сколько времени вам понадобится, чтобы понять, что он делает? Долго ли вам придется гадать, если я не дам подсказку в названии метода?
Теперь взгляните на листинг 10.2, где дела обстоят чуть лучше.
Листинг 10.2. Плохой код, связность немного лучше
public class BadCohesion
{
public boolean loadProcessAndStore() throws IOException
{
String[] words = readWords();
List<String> sorted = sortWords(words);
return storeWords(sorted);
}
private String[] readWords() throws IOException
{
try (FileReader reader =
new FileReader("./resources/words.txt"))
{
char[] chars = new char[1024];
reader.read(chars);
return new String(chars).split(" |\0");
}
}
private List<String> sortWords(String[] words)
{
List<String> sorted = Arrays.asList(words);
sorted.sort(null);
return sorted;
}
private boolean storeWords(List<String> sorted) throws IOException
{
try (FileWriter writer =
new FileWriter ("./resources/test/sorted.txt"))
{
for (String word : sorted)
{
writer.write(word);
writer.write("\n");
}
return true;
}
}
}
Листинг 10.2 по-прежнему не очень хорош, но он более связный; взаимо-связанные части кода более четко прописаны и располагаются ближе друг к другу. Проще говоря, все, что вам нужно знать о readWords, названо и содержится в одном методе. Общий поток метода loadProcessAndStore теперь хорошо просматривается, даже если бы я выбрал менее описательное имя. Информация в этой версии более связна, чем в листинге 10.1. Теперь стало значительно понятнее, какие части кода более тесно связаны друг с другом, хотя код имеет тот же функционал, что и раньше. Все это делает эту версию значительно более удобной для чтения, и, как следствие, ее проще изменять.
Обратите внимание, что в листинге 10.2 больше строк кода. Этот пример написан на Java, довольно многословном языке, и шаблонный код на нем весьма дорог, к тому же придется добавить небольшие накладные расходы на улучшение читаемости. Это не обязательно плохо!
Все программисты стремятся уменьшить объем печатаемого текста. Мы ценим ясность и краткость. Большое значение имеет способность выражаться просто, но простоту не измерить количеством набранных символов. Выражение ICanWriteASentenceOmittingSpaces короче, но менее читаемо!
Оптимизировать код, чтобы уменьшить количество набираемого текста, — ошибка. Это оптимизация не того, что нужно. Код — инструмент коммуникации; мы должны использовать его для передачи информации. Конечно, он также должен быть машиночитаемым и исполняемым, но на самом деле это не основная цель. Если бы она была такой, мы бы до сих пор программировали системы, переключая тумблеры на передней панели компьютеров или с помощью машинного кода.
Основная цель кода — донести идеи до людей. Мы пишем код, чтобы максимально ясно и просто выражать мысли, — по крайней мере, так это должно работать. Никогда нельзя жертвовать ясностью в угоду краткости. Сделать код читаемым — это, на мой взгляд, и профессиональная обязанность, и один из самых важных руководящих принципов управления сложностью. Поэтому я предпочитаю оптимизировать мышление, а не сокращать текст.
Вернемся к коду: этот второй пример явно более легко читается. Стало понятнее, что он делает. Но код все еще ужасен — он не модульный, почти не предусматривает разделения функций, негибкий, с жестко закодированными строками для имен файлов, и его нельзя проверить, кроме как запустить целиком и работать с файловой системой. Но мы улучшили связность. Каждый фрагмент кода теперь выполняет одну часть задачи. Фрагменты имеют доступ только к тому, что им необходимо для ее выполнения. Мы вернемся к этому примеру в следующих главах, чтобы посмотреть, как его улучшить.
Контекст имеет значение
Я спросил друга, чьим кодом я восхищаюсь, как лучше показать важность связности, и он порекомендовал видео из «Улицы Сезам», которое можно найти на YouTube: «Один из этих предметов не похож на другие».
В шутливой форме выражается важная мысль. Связность в большей степени, чем другие инструменты управления сложностью, зависит от контекста. В зависимости от контекста «все эти предметы могут быть не похожи друг на друга».
Мы должны сделать выбор, и этот выбор тесно связан с другими инструментами. Невозможно однозначно отделить связность от модульности или разделения ответственности, потому что эти методы помогают определить, что означает связность в контексте разработки.
Одним из эффективных инструментов для принятия такого рода решений является предметно-ориентированное проектирование (domain-driven design, DDD). В предметной области проще определить направление, которое с большей вероятностью принесет прибыль в долгосрочной перспективе. Это касается и мышления, и разработки.
Предметно-ориентированное проектированиеЕще один важный инструмент, который помогает нам создавать более совершенные системы, — разделение ответственности. О нем мы поговорим более подробно в следующей главе. А сейчас я лишь скажу, пожалуй, что разделение ответственности — это мой стиль программирования: «Один класс, одна задача; один метод/функция, одна задача».
Предметно-ориентированное проектирование — это подход к проектированию, при котором мы стремимся воссоздать предметную область в коде. Дизайн системы направлен на точное моделирование проблемы.
Этот подход подразумевает ряд важных, ценных идей.
Он позволяет снизить вероятность непонимания. Мы стремимся создать универсальный язык как согласованный и точный способ описания идей в предметной области, когда слова располагаются последовательно и имеют согласованные значения. Затем мы применяем этот язык для описания дизайна систем.
Так что если я говорю о своем продукте и упоминаю, что «лимитный ордер соответствует» («Limit-order matched»), то это имеет смысл с точки зрения кода, где четко представлены понятия «лимитные ордера» и «соответствие», названные LimitOrder и Match. Это те же слова, которые мы используем, описывая сценарий людям, далеким от технической сферы.
Такой универсальный язык эффективно разрабатывается и совершенствуется благодаря сбору требований и использованию высокоуровневых тестовых сценариев, играющих роль исполняемых спецификаций поведения системы.
В рамках DDD также была введена концепция «ограниченный контекст». Это часть системы, которая содержит общеупотребимые понятия. Например, в системе управления заказами понятие «заказ», вероятно, отличается от аналогичного понятия в биллинговой системе, поскольку используется в разных ограниченных контекстах.
Это чрезвычайно полезная концепция, помогающая правильно определять модули или подсистемы при проектировании. Большое преимущество ограниченных контекстов в том, что они более слабо связаны в реальной проблемной области, поэтому с их помощью можно создавать более слабо связанные системы.
Такие концепции, как универсальный язык и ограниченный контекст, применяются, чтобы управлять дизайном систем. Используя их, вы сможете создавать более совершенные системы, а также четко представить основную, существенную сложность разрабатываемой системы и отличить ее от случайной сложности, которая часто мешает понять, что на самом деле делает код.
Если спроектировать систему так, чтобы она симулировала предметную область, как мы ее понимаем, то небольшое изменение в предметной области станет и небольшим изменением в коде. Это полезное свойство.
Предметно-ориентированное проектирование — мощный инструмент для создания более качественных проектов, который включает принципы, организующие наши усилия в проектировании и побуждающие улучшать модульность, связность и разделение функций в коде. В то же время он позволяет создавать крупные слабо связанные модули кода.
Мне очень не нравятся оба примера кода, которые я привожу в этой главе, и мне неловко показывать их вам, потому что мой инстинкт проектировщика буквально вопит, что разделение ответственности в обоих случаях ужасно. Листинг 10.2 лучше: по крайней мере, каждый метод теперь делает что-то одно, но класс по-прежнему ужасен. Если вы еще этого не видите, я расскажу, почему это важно, в следующей главе.
Наконец, в моем наборе инструментов есть тестируемость. Я начал писать эти плохие примеры кода, как и любой код: с написания теста. Однако мне пришлось остановиться почти сразу же, потому что я не мог, используя TDD, написать такой плохой код! Мне пришлось начать работу заново, и, признаюсь, я чувствовал, что перенесся в прошлое. Я написал тесты для примеров, чтобы проверить, работают ли они так, как я ожидал, но этот код нельзя назвать тестируемым.
Тестируемость означает модульность, разделение ответственности и все, что мы ценим в высококлассном коде. Это, в свою очередь, помогает приблизиться к контексту и абстракции, которые нам нравятся в дизайне, и понять, где можно сделать код более связным.
Заметьте: нет никаких гарантий, что это удастся, — и это основная мысль книги. Не существует простых, шаблонных ответов. В книге я показываю вам интеллектуальные инструменты, которые помогают структурировать мышление, когда не получается найти ответы.
Техники, описанные в этой книге, не дают их; ответы вам придется искать самостоятельно. Эти техники предоставляют набор идей и приемов, чтобы двигаться безопасно, если вы еще не знаете ответов, а вы их не знаете, независимо от сложности любой реально создаваемой системы. Мы никогда не знаем ответов, пока не закончим работу!
Эта стратегия выглядит скорее оборонительной, и она такая и есть, но ее цель в том, чтобы сохранить свободу выбора. Это одно из значительных преимуществ работы над управлением сложностью. По мере получения новых знаний мы можем постоянно улучшать код, используя полученные знания. Мне кажется, «инкрементная» — более точный эпитет, чем «оборонительная».
Мы добиваемся прогресса постепенно, проводя серию экспериментов, и используем методы управления сложностью, чтобы защитить код от серьезных ошибок.
Так работают наука и инженерия. Мы контролируем переменные, делаем небольшой шаг и оцениваем текущее положение. Если оценка показывает, что мы сделали неверный шаг, мы возвращаемся назад и решаем, что делать дальше. Если все нормально, мы контролируем переменные, делаем еще один небольшой шаг, и так далее.
Можно представить разработку как своего рода эволюционный процесс. Программисты добиваются цели в результате направленной эволюции в сфере обучения и разработки.
Высокопроизводительное программное обеспечение
Плохой код, такой как в листинге 10.1, часто оправдывают тем, что для высокой производительности системы требуется писать более сложный код. В последние годы я занимаюсь разработкой очень высокопроизводительных систем — и уверяю вас, что это не так. Такие системы требуют простого, продуманного кода.
Задумайтесь на мгновение, что означает высокая производительность с точки зрения разработки. Для ее достижения нам нужно выполнять максимальный объем работы при минимальном количестве инструкций.
Чем сложнее код, тем больше вероятность того, что разработанные маршруты не оптимальны, потому что самый простой возможный маршрут через код скрыт сложностью самого кода. Многих программистов это удивляет, но чтобы код работал быстро, он должен быть простым и легко понятным.
Правильность этих рассуждений очевидна, если посмотреть на систему шире.
Вернемся к нашему примеру. Я слышал, как программисты утверждали, что код в листинге 10.1 будет быстрее, чем в листинге 10.2, из-за накладных расходов на вызовы методов, которые появились в листинге 10.2. Но для современных языков это нонсенс. Большинство современных компиляторов просмотрят код в листинге 10.2 и встроят методы. Кроме того, современные оптимизирующие компиляторы способны на большее. Они фантастически оптимизируют код, чтобы он работал эффективно на современном оборудовании. Компиляторы полезны, когда код прост и предсказуем, поэтому чем он сложнее, тем меньше пользы от оптимизатора. Большинство оптимизаторов просто прекращают работу, как только цикломатическая сложность блока кода превышает определенный порог.
Я оценил производительность обеих версий кода. Эти тесты оказались так себе, потому что код плохой. Мы недостаточно контролируем переменные, чтобы действительно понимать, что происходит, но очевидно, что на этом уровне бенчмарка разницы практически не было.
Различия слишком малы, чтобы их удалось дифференцировать от остальных событий. В одном прогоне версия BadCohesion оказалась лучшей, в другом — ReallyBadCohesion. В серии тестовых прогонов для каждой из 50 000 итераций метода loadProcessStore общая разница составляла не более 300 миллисекунд, так что в среднем это дало примерно 6 наносекунд на вызов, чаще — в пользу версии с дополнительными вызовами методов.
Это плохой тест, потому что то, что нас интересует, — стоимость вызовов методов — ничтожно мала по сравнению со стоимостью операций ввода-вывода. Тестируемость — в данном случае тестируемость производительности — снова поможет добиться лучшего результата. Мы обсудим это более подробно в следующей главе.
Столько всего скрыто от глаз, что даже экспертам трудно предсказать исход. Каков ответ? Если вас действительно интересует производительность кода, не гадайте, что работает быстро, а что медленно, — измеряйте!
Отсылка к связанности
Если мы хотим сохранить свободу исследования и иногда совершать ошибки, нам нужно побеспокоиться об издержках связанности, или сцепления (coupling).
Связанность: для двух строк кода, A и B, связанность означает, что B должна изменить поведение только потому, что изменилась A, — строки сцеплены.
Связность: строки являются связными, когда изменение А позволяет измениться В, так что обе добавляют новую ценность.
Связанность — слишком общий термин. Различные виды связанности мы более подробно рассмотрим в главе 13.
Несвязанная система — это забавно. Если мы хотим, чтобы две части системы взаимодействовали, они должны быть в какой-то степени сцеплены. Так что, как и связность, связанность — это вопрос степени, а не какой-то абсолютной меры. Однако цена несоответствующих уровней связанности чрезвычайно высока, поэтому при проектировании важно учитывать ее влияние.
Связанность в некотором смысле является платой за связность. Области системы, которые обладают связностью, вероятно, будут иметь и большее сцепление.
Обеспечение высокой связности с помощью TDD
Использование автоматических тестов, в частности TDD, для управления дизайном дает много преимуществ. Стремление создать проверяемый дизайн и достаточно абстрагированные поведенческие тесты помогут сделать код связным.
Прежде чем создавать код, описывающий поведение, которого мы ожидаем от системы, мы создаем тестовый сценарий. Это позволяет сосредоточиться на разработке внешнего API/интерфейса для кода. Теперь напишем реализацию, которая будет соответствовать созданной нами небольшой исполняемой спецификации. Если кода слишком много, больше, чем нужно для соответствия спецификации, процесс нарушится и связность реализации снизится. Если кода слишком мало, не удастся реализовать желаемое поведение. Методология TDD стимулирует добиваться связности.
Как всегда, никаких гарантий. Это не механический процесс, и результат по-прежнему зависит от опыта и навыков программиста, но этот подход позволяет добиться прогресса, о котором раньше и помыслить было невозможно, и развивает навыки и опыт.
Как добиться связности
Ключевой метрикой связности служит степень, или стоимость, изменений. Если вам приходится изменять кодовую базу во многих местах, это не очень связная система. Связность — мера функционального соответствия. Это измерение соответствия цели. И очень зыбкое понятие!
Давайте рассмотрим простой пример.
Класс с двумя методами, каждый из которых ассоциирован с переменной-членом (листинг 10.3), — пример плохой связности, потому что переменные здесь никак не соотносятся. Они принадлежат разным методам, но хранятся вместе на уровне класса, даже если не соотносятся между собой.
Листинг 10.3. Плохая связность
class PoorCohesion:
def __init__(self):
self.a = 0
self.b = 0
def process_a(x):
a = a + x
def process_b(x):
b = b * x
В листинге 10.4 представлено гораздо более красивое и связное решение. Обратите внимание, что эта версия не только более связная, но и более модульная, с лучшим разделением функций. Очевидно, что эти концепции взаимосвязаны.
Листинг 10.4. Улучшенная связность
class BetterCohesionA:
def __init__(self):
self.a = 0
def process_a(x):
a = a + x
class BetterCohesionB:
def __init__(self):
self.b = 0
def process_b(x):
b = b * x
В сочетании с остальными принципами управления сложностью стремление создать тестируемый дизайн помогает улучшить связность решений. Хороший пример — внимание к разделению ответственности, особенно когда речь идет об отделении случайной сложности от необходимой.
В листинге 10.5 показаны три простых примера улучшения связности кода за счет сознательного разделения необходимой и случайной сложности. В каждом примере мы добавляем товар в корзину, сохраняем его в базе данных и вычисляем стоимость корзины.
Листинг 10.5. Три примера связности
def add_to_cart1(self, item):
self.cart.add(item)
conn = sqlite3.connect('my_db.sqlite')
cur = conn.cursor()
cur.execute('INSERT INTO cart (name, price)
values (item.name, item.price)')
conn.commit()
conn.close()
return self.calculate_cart_total();
def add_to_cart2(self, item):
self.cart.add(item)
self.store.store_item(item)
return self.calculate_cart_total();
def add_to_cart3(self, item, listener):
self.cart.add(item)
listener.on_item_added(self, item)
Первая функция — очевидно несвязный код. В нем смешано множество понятий и переменных — необходимой и случайной сложности. Я бы сказал, что это очень плохой код, даже в таком масштабе. Я бы не стал писать такой код, потому что из него трудно понять, что происходит, хотя сценарий чрезвычайно прост.
Второй пример немного лучше. Связность в нем выше. Понятия в этой функции соотносятся друг с другом и представляют более последовательный уровень абстракции, поскольку они в основном связаны с необходимой сложностью задачи. Инструкция «сохранить» выглядит спорно, но по крайней мере этим мы скрыли случайную сложность.
Последний пример интересен. Я бы сказал, что, он, безусловно, связный. Чтобы выполнить полезную работу, следует добавить товар в корзину и сообщить об этом другим потенциально заинтересованным сторонам. Мы полностью разделили задачи хранения и подсчета общей стоимости товаров в корзине. Программа может отреагировать в ответ на уведомление о добавлении, а может и не отреагировать, если соответствующие фрагменты кода не зафиксируют интерес к событию «элемент добавлен».
Код можно считать либо более связным, если в нем заключена вся необходимая сложность задачи, а другие варианты поведения являются побочными эффектами, либо менее связным, если считать события «store» и «total» частями задачи. В конечном счете выбор дизайна зависит от контекста задачи, которую вы решаете.
Цена плохой связности
Связность, пожалуй, наименее поддается количественной оценке как атрибут «инструментов управления сложностью», но она важна. Проблема в том, что когда связность плохая, код и системы становятся менее гибкими, их сложнее тестировать и с ними сложнее работать.
В простом примере в листинге 10.5 влияние связного кода очевидно. Если в коде пересекается функционал, ему не хватает ясности и удобочитаемости, как показано в add_to_cart1. Если функционал широкий, труднее увидеть, что происходит, как в add_to_cart3. Располагая взаимо-связанные идеи рядом, мы добиваемся максимальной удобочитаемости, как в add_to_cart2.
На самом деле у способа проектирования add_to_cart3 есть некоторые преимущества, и такой код, безусловно, удобнее для работы, чем версия 1.
Я считаю, что это оптимальный вариант связности. Если вы смешаете слишком много концепций, вы потеряете связность на уровне деталей. В примере 1 можно утверждать, что вся работа делается внутри одного метода, но это только наивная связность.
На самом деле добавление товара в корзину (основной бизнес-функционал) смешано с другими функционалами, что делает общую картину неясной. Даже в этом простом примере непонятно, как работает код, пока вы не углубитесь в него.
Другой альтернативе, add_to_cart3, хотя она и более гибкая, все еще не хватает ясности. В этом экстремальном случае функционал может быть настолько рассредоточен, что вы не сможете понять общую картину, не прочитав множество строк кода и не разобравшись в нем. Это, наверное, и неплохо, но я считаю, что ограничение в ясности — стоимость слабой связанности и некоторых других преимуществ.
Оба этих недостатка очень часто встречаются в уже готовых системах. На самом деле настолько часто, что в больших сложных системах становятся даже своего рода нормой.
Это ошибка дизайна, и она дорого обходится. Вы с ней наверняка хорошо знакомы, если когда-нибудь работали с «унаследованным кодом».
Существует простой субъективный способ определить плохую связность. Если вы читаете код и думаете: «Я не знаю, что он кодирует», — вероятно, причиной тому плохая связность.
Связность в человеческих системах
Как и в других случаях, проблемы связности не ограничиваются только кодом, который мы пишем, и системами, которые мы создаем. Связность работает на уровне информации, так что она также важна для построения эффективной структуры в организациях, где мы работаем. Самый очевидный пример — организация работы команд. Выводы из отчета State of DevOps свидетельствуют, что способность принимать собственные решения — без их утверждения у кого-либо вне команды — один из главных признаков высокой производительности, метриками которой служат пропускная способность и стабильность. То есть информация и навыки команды обладают связностью в том смысле, что внутри команды есть все, что требуется, чтобы принимать решения и добиваться успеха.
Итоги
Связность, вероятно, самый отвлеченный из принципов управления сложностью. Разработчики могут утверждать (и иногда утверждают), что связность — это когда весь код находится в одном месте, одном файле и даже одной функции, но это слишком упрощенное представление.
Код, который таким образом случайно объединяет несколько концепций, не является связным; в нем просто нет структуры. Это плохо, поскольку не позволяет увидеть, что делает код и как его безопасно изменить.
Связность заключается в объединении соотносящихся концепций, которые изменяются в коде совместно. Если они оказались вместе случайно, на самом деле связности между ними нет.
Связность — это детектор модульности и в целом имеет смысл, если рассматривать ее в сочетании с модульностью. Одним из наиболее эффективных инструментов, помогающих найти рабочий баланс между связностью и модульностью, является разделение ответственности.
Об авторе
Дэвид Фарли — пионер в области непрерывной доставки, лидер мнения и эксперт-практик в сфере непрерывной доставки, автоматизации технологических процессов, разработки через тестирование и общей разработки ПО.
Начав карьеру на заре эры современных компьютерных вычислений, Дэйв многие годы работал программистом, инженером ПО, системным архитектором, а также руководил работой успешных команд. Он использует фундаментальные принципы работы компьютеров и программного обеспечения и прорывные инновационные подходы, трансформирующие современную разработку. Он меняет традиционный подход к мышлению, и под его руководством команды создают продукты мирового класса.
Дэвид — соавтор книги «Continuous Delivery», получившей премию Jolt Award, постоянный участник конференций и автор успешного и популярного YouTube-канала Continuous Delivery, посвященного программной инженерии. Он — разработчик одной из самых быстрых в мире финансовых бирж, пионер разработки через поведение, автор Манифеста реактивных систем и лауреат премии Duke Award за создание открытого продукта с использованием LMAX Disruptor.
На своем канале, на курсах и консультациях Дэйв увлеченно делится опытом и дает советы командам разработчиков из разных стран мира, помогая им улучшать дизайн и качество и повышать надежность своих продуктов.
Twitter: @davefarley77
YouTube-канал: https://bit.ly/CDonYT
Блог: http://www.davefarley.net
Сайт компании: https://www.continuous-delivery.co.uk
Начав карьеру на заре эры современных компьютерных вычислений, Дэйв многие годы работал программистом, инженером ПО, системным архитектором, а также руководил работой успешных команд. Он использует фундаментальные принципы работы компьютеров и программного обеспечения и прорывные инновационные подходы, трансформирующие современную разработку. Он меняет традиционный подход к мышлению, и под его руководством команды создают продукты мирового класса.
Дэвид — соавтор книги «Continuous Delivery», получившей премию Jolt Award, постоянный участник конференций и автор успешного и популярного YouTube-канала Continuous Delivery, посвященного программной инженерии. Он — разработчик одной из самых быстрых в мире финансовых бирж, пионер разработки через поведение, автор Манифеста реактивных систем и лауреат премии Duke Award за создание открытого продукта с использованием LMAX Disruptor.
На своем канале, на курсах и консультациях Дэйв увлеченно делится опытом и дает советы командам разработчиков из разных стран мира, помогая им улучшать дизайн и качество и повышать надежность своих продуктов.
Twitter: @davefarley77
YouTube-канал: https://bit.ly/CDonYT
Блог: http://www.davefarley.net
Сайт компании: https://www.continuous-delivery.co.uk
Более подробно с книгой можно ознакомиться на сайте издательства:
» Оглавление
» Отрывок
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Инженерия