}
21 |
22 |
23 | // Отформатированный код помогает упростить чтение
24 | // и быстрее перейти к смыслу:
25 |
26 | function ProductList({ products }) {
27 | return (
28 |
29 | {products.map((product) => (
30 |
31 |
32 |
33 | ))}
34 |
35 | );
36 | }
37 | ```
38 |
39 | Форматирование лучше автоматизировать. В примере выше я использовал Prettier,[^prettier] но конкретный инструмент здесь не так важен, как подход в целом. Если команду не устраивает Prettier, можно выбрать другой форматер и использовать его. Суть в _автоматизации процесса_.
40 |
41 | Бывает, что форматер ломает работу кода, например, при неосторожном переносе фрагмента на новую строку:
42 |
43 | ```
44 | // До форматирования:
45 |
46 | function setDiscount(discount) {
47 | if (user.isVip) order.discount = discount; order.total -= discount
48 | }
49 |
50 | // После:
51 |
52 | function setDiscount(discount) {
53 | if (user.isVip) {
54 | order.discount = discount;
55 | }
56 |
57 | order.total -= discount;
58 | }
59 | ```
60 |
61 | Чтобы не пропускать такие ошибки, нам нужны тесты. Если тесты открыты в интерактивном режиме рядом с редактором, мы будем видеть, какие ошибки появились после применения форматирования.
62 |
63 | Форматирование можно считать отдельной техникой рефакторинга, поэтому результат можно оформить в виде коммита или даже отдельного PR. Наша задача здесь как можно раньше интегрироваться в основной веткой, чтобы не приходилось разруливать сложные конфликты между форматированием и смысловыми изменениями кода от других разработчиков.
64 |
65 | ## Линтинг кода
66 |
67 | Включив линтер и переведя «предупреждения» в «ошибки», мы можем получить список таких ошибок. Этот список можно использовать как список задач для текущей итерации рефакторинга.
68 |
69 | Мне нравится оформлять работу над каждым из правил линтера как отдельный коммит или PR. Например, можно удалить весь неиспользуемый код, оформить это как коммит и перейти к следующей проблеме из списка.
70 |
71 |
72 |
73 | Линтер подсвечивает неиспользуемый код, который можно удалить
74 |
75 |
76 | Если ошибок после включения линтера очень много, то можно включать не все правила сразу, а по одному. Чем мельче будут шаги, тем проще распилить задачу на несколько и решить каждую отдельно.
77 |
78 | После исправления каждого правила потребуется проверить, не сломались ли тесты. В будущем я перестану акцентировать внимание на проверке тестов, чтобы сократить текст. Просто договоримся держать в голове, что мы проверяем, не сломались ли тесты, после _каждого_ изменения.
79 |
80 | ## Возможности языка
81 |
82 | Современные языки программирования развиваются и получают обновления. Особенно это применимо к JavaScript, так как спецификация ES обновляется каждый год.[^proposals]
83 |
84 | Иногда в новой версии языка появляются фичи, которыми можно заменить старые самописные функции. Как правило, встроенные конструкции языка компактнее, быстрее, надёжнее и понятнее. Мы можем внедрять новые возможности языка, оглядываясь на требуемую поддержку с помощью, к примеру, Caniuse.[^caniuse]
85 |
86 | ```js
87 | // Самодельный хелпер для проверки начала строки:
88 | const startsWith = (str, chunk) => str.indexOf(chunk) === 0;
89 | const yup = startsWith("Some String", "So");
90 |
91 | // ...Можно заменить нативным методом:
92 | const yup = "Some String".startsWith("So");
93 | ```
94 |
95 | | К слову 💡 |
96 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
97 | | Если самописная реализация отличается от нативной, и мы не можем заменить её, то я предпочитаю отметить это в документации. Так будет понятно, почему мы используем свои наработки вместо возможностей языка. |
98 |
99 | Удалять код выгодно: чем меньше кода, тем меньше потенциальных точек отказа в работе приложения. В целом, при прочих равных я предпочту отдать большую часть работы языку или окружению, чтобы не писать код самостоятельно. Так обычно получается надёжнее.
100 |
101 | ## Возможности среды разработки
102 |
103 | Вместе с возможностями языка ещё хочется выделить возможности редактора или IDE, с которыми мы работаем. Если в них есть автоматизированные средства рефакторинга — стоит научиться ими пользоваться.
104 |
105 | “Rename Symbol”, “Extract into Function” и другие инструменты ускоряют работу и снижают когнитивную нагрузку. Например, в VS Code можно изменить имя функции или переменной во всех местах использования сочетанием горячих клавиш:[^vscode]
106 |
107 |
108 |
109 | “Rename Symbol” обновляет название сразу и везде
110 |
111 |
112 | Однако, результат применения этих инструментов стоит перепроверять. Например, Rename Symbol может «не заметить» какое-то имя или добавить лишнее переименование:
113 |
114 | ```tsx
115 | // Например, мы хотим заменить поле `name`
116 | // в типе `AccountProps` на `firstName`:
117 |
118 | type AccountProps = { name: string };
119 | const Account = ({ name }: AccountProps) => <>{name}>;
120 |
121 | // После применения Rename Symbol
122 | // может остаться «лишнее переименование»:
123 |
124 | type AccountProps = { firstName: string };
125 | const Account = ({ firstName: name }: AccountProps) => <>{name}>;
126 | ```
127 |
128 | Чтобы этого избежать я пробегаюсь по диффу изменений с последнего коммита и проверяю, что именно переименовалось и как.
129 |
130 |
131 |
132 | Гит показывает, что именно поменялось с последнего коммита
133 |
134 |
135 | Упростить и максимизировать пользу от такого сравнения помогает стратегия маленьких шагов, о которой мы говорили ранее. Если применять лишь одну технику рефакторинга за коммит, в диффах не будет шума и будет лучше видно, как именно изменения повлияют на код
136 |
137 | Линтеры и тесты при этом помогут избежать конфликтов имён и других ошибок. Например, мы можем настроить правила, которые запретят одинаковые имена переменных, и тогда при конфликте имён линтер будет падать с ошибкой. Если он запущен параллельно с редактором, то мы это сразу же увидим и сможем исправить.
138 |
139 | [^prettier]: Prettier, an opinionated code formatter, https://prettier.io
140 | [^proposals]: List of EcmaScript Proposals, https://proposals.es
141 | [^caniuse]: Can I Use, support tables for web, https://caniuse.com
142 | [^vscode]: Refactoring Source Code in VSCode, https://code.visualstudio.com/docs/editor/refactoring
143 |
--------------------------------------------------------------------------------
/manuscript-ru/07-duplication.md:
--------------------------------------------------------------------------------
1 | # Дублирование кода
2 |
3 | Главная цель рефакторинга — сделать код более читабельным. Один из способов этого достичь — уменьшить в нём количество шума.
4 |
5 | Дублирование кода шумит, когда не несёт в себе полезной информации. Однако, далеко не всякое дублирование — зло. Оно может быть инструментом разработки и проектирования, поэтому при рефакторинге нам стоит понимать, с каким именно дублированием мы имеем дело.
6 |
7 | В этой главе мы обсудим, на что обращать внимание при выявлении дублирования и как понимать, когда от него пора избавляться.
8 |
9 | ## Не любое дублирование — зло
10 |
11 | Если у двух кусков кода одинаковая цель, они содержат одинаковый набор действий и работают с одинаковыми данными — это _прямое_ дублирование. От него можно смело избавляться, например, выделив повторяющийся код в переменную, функцию или модуль.
12 |
13 | Но бывает, что две части кода «вроде похожи», а спустя время оказываются совершенно разными. Если поспешить и объединить их слишком рано, то распиливать такой код будет сложнее, чем объединять действительно одинаковый код позже.
14 |
15 | Когда мы не уверены, что перед нами два _действительно_ одинаковых куска кода, мы можем отметить эти места специальными метками и добавить предположение о том, что в них дублируется.
16 |
17 | ```js
18 | /** @duplicate Применяет купон на скидку к заказу. */
19 | function applyCoupon(order, coupon) {}
20 |
21 | /** @duplicate Применяет купон на скидку к заказу. */
22 | function applyDiscount(order, discount) {}
23 | ```
24 |
25 | Такие метки принесут пользу, только если проводить их регулярные аудиты. Во время аудитов нам следует проверять, что нам стало известно нового о возможных дубликатах.
26 |
27 | | К слову ⏰ |
28 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29 | | Регулярные аудиты в своих проектах я воспринимаю как часть выплаты технического долга. Для подобных периодических задач я завожу списки дел. Внутри списка я указываю, что надо сделать в рамках той или иной задачи. Техника регулярных аудитов и её польза хорошо описана у Максима Дорофеева в «Джедайских техниках».[^jeditechnics] |
30 |
31 | Если во время аудита метки стало ясно, что описанное в ней дублирование _прямое_, мы можем провести рефакторинг кода с этой меткой. Если же код оказался разным, метку можно удалить. Это помогает не спешить с обобщением кода, но при этом не терять места вероятного дублирования.
32 |
33 | ## Переменные для данных
34 |
35 | Дублирование статических данных или результатов вычислений удобно выносить в переменные или наборы переменных. Это, например, помогает в распутывании сложных условий или выделении этапов преобразований данных.
36 |
37 | ```js
38 | // Если условия расположены близко,
39 | // или используются рядом:
40 |
41 | if (user.age < 18) toggleParentControl();
42 | // ...
43 | if (user.age < 18) askParents();
44 |
45 | // Мы можем вынести выражение в переменную:
46 |
47 | const isChild = user.age < 18;
48 |
49 | if (isChild) toggleParentControl();
50 | // ...
51 | if (isChild) askParents();
52 | ```
53 |
54 | Иногда дублирование может быть менее очевидным, и заметить его сложнее:
55 |
56 | ```js
57 | // Второе условие «вывернуто»
58 | // и использует переменную `years`,
59 | // а не поле объекта напрямую.
60 |
61 | if (user.age < 18) askParents();
62 | // ...
63 | const { age: years } = user;
64 | if (years >= 18) askDocuments();
65 |
66 | // Но мы всё ещё можем избавиться от него,
67 | // вынеся выражение в переменную:
68 |
69 | const isChild = user.age < 18;
70 |
71 | if (isChild) askParents();
72 | // ...
73 | if (!isChild) askDocuments();
74 | ```
75 |
76 | | Подробнее 🔬 |
77 | | :-------------------------------------------------------------------------------------------- |
78 | | Об упрощении и распутывании сложных условий мы детальнее поговорим в одной из следующих глав. |
79 |
80 | ## Функции для действий
81 |
82 | Повторяющиеся действия или преобразования данных удобно выносить в функции и методы. Определять дубликаты помогает «проверка на одинаковость»:
83 |
84 | - Перед нами прямое дублирование, если у действий одинаковая цель — то есть желаемый результат;
85 | - Одинаковая область действия — часть приложения, на которую они влияют;
86 | - Одинаковые прямые входные данные — аргументы и параметры;
87 | - Одинаковые непрямые входные данные — зависимости и импортируемые модули.
88 |
89 | В примере ниже фрагменты кода такую проверку проходят:
90 |
91 | ```js
92 | // - Цель: добавить поле со абсолютным значением скидки к заказу;
93 | // - Область: объект заказа;
94 | // - Прямые данные: заказ без скидки, относительное значение скидки;
95 | // - Непрямые данные: конвертер из процентов в абсолютное значение.
96 |
97 | // a)
98 | const fromPercent = (amount, percent) => (amount * percent) / 100;
99 |
100 | const order = {};
101 | order.discount = fromPercent(order.total, 50);
102 |
103 | // b)
104 | const order = {};
105 | const discount = (order.total * percent) / 100;
106 | const discounted = { ...order, discount };
107 |
108 | // Действия одинаковые, мы можем вынести их в отдельную функцию:
109 |
110 | function applyDiscount(order, percent) {
111 | const discount = (order.total * percent) / 100;
112 | return { ...order, discount };
113 | }
114 | ```
115 |
116 | В другом примере цель и прямые входные данные фрагментов кода одинаковые, а вот зависимости отличаются:
117 |
118 | ```js
119 | // Первый фрагмент считает скидку в процентах,
120 | // а второй использует «скидку дня» — `todayDiscount`.
121 |
122 | // a)
123 | const order = {};
124 | const discount = (order.total * percent) / 100;
125 | const discounted = { ...order, discount };
126 |
127 | // b)
128 | const todayDiscount = () => {
129 | // ...Подбор скидки к сегодняшнему дню.
130 | };
131 |
132 | const order = {};
133 | const discount = todayDiscount();
134 | const discounted = { ...order, discount };
135 | ```
136 |
137 | В примере выше у фрагмента “b” среди зависимостей есть функция `todayDiscount`. Из-за неё наборы действий отличаются достаточно, чтобы считать их «похожими», но не «одинаковыми».
138 |
139 | Мы можем использовать `@duplicate`-метки и проследить за развитием событий, чтобы получить больше информации о том, как работает предметная область. Когда мы точно знаем и уверены, как должны работать эти фрагменты, мы можем действия «обобщить»:
140 |
141 | ```js
142 | // Обобщённая функция будет принимать
143 | // абсолютное значение скидки:
144 |
145 | function applyDiscount(order, discount) {
146 | return {...order, discount}
147 | }
148 |
149 | // Отличия в подсчёте (процент от суммы, «скидка дня» и т.д.)
150 | // соберём в виде отдельного набора функций:
151 |
152 | const discountOptions = {
153 | percent: (order, percent) => order.total * percent / 100
154 | daily: daysDiscount()
155 | }
156 |
157 | // В результате получим обобщённую функцию
158 | // и словарь со скидками разных видов.
159 | // Тогда применение любой скидки станет единообразным:
160 |
161 | const a = applyDiscount(order, discountOptions.daily)
162 | const b = applyDiscount(order, discountOptions.percent(order, 40))
163 | ```
164 |
165 | | Подробнее 🔬 |
166 | | :--------------------------------------------------------------------------------------------------- |
167 | | Детально об обобщённых алгоритмах, их использовании и параметризации мы поговорим в отдельной главе. |
168 |
169 | [^jeditechnics]: «Джедайские техники» Максим Дорофеев, https://www.goodreads.com/book/show/34656521
170 |
--------------------------------------------------------------------------------
/manuscript-en/05-low-hanging-fruit.md:
--------------------------------------------------------------------------------
1 | # Low-Hanging Fruit
2 |
3 | Refactoring a piece of code can require various changes, and it can be challenging to choose how to start. To solve this, we can rank the changes from simple to complex and begin with the simplest ones. It helps to “blow the dust off the code” and start to see more severe problems in it.
4 |
5 | By simple changes, we mean code formatting, linter errors, and replacing self-written code with features of the language or environment. In this chapter, we'll discuss these improvements and see why they're helpful at the beginning of refactoring.
6 |
7 | ## Code Formatting
8 |
9 | Code formatting is a matter of taste, but it has one useful function. If the code in the project is _consistent_, it takes less time for readers to understand it. That's how habits work: the familiar “shapes” of code help us focus on the meaning instead of characters and words.
10 |
11 | ```
12 | // Unformatted code.
13 | // We have to focus harder to see its meaning:
14 |
15 | function ProductList({ products }) {
16 | return
{products.map((product) =>
17 |
)}
}
18 |
19 |
20 | // Formatted code makes it easier to read it:
21 |
22 | function ProductList({ products }) {
23 | return (
24 |
25 | {products.map((product) => (
26 |
27 |
28 |
29 | ))}
30 |
31 | );
32 | }
33 | ```
34 |
35 | It's better to automate code formatting. In the example above, I used automatic code formatted called Prettier,[^prettier] but the particular tool is not as important here as the overall approach. If the team is not satisfied with Prettier, we can choose another formatter and use it. The point is to _automate the process_.
36 |
37 | Sometimes the formatter might break the code, for example, when it carelessly moves a fragment to a new line:
38 |
39 | ```
40 | // Before applying formatter:
41 |
42 | function setDiscount(discount) {
43 | if (user.isVip) order.discount = discount; order.total -= discount
44 | }
45 |
46 | // After:
47 |
48 | function setDiscount(discount) {
49 | if (user.isVip) {
50 | order.discount = discount;
51 | }
52 |
53 | order.total -= discount;
54 | }
55 | ```
56 |
57 | To notice such errors quicker, we need tests. If the tests run beside the editor, we'll instantly see what exactly was broken by the formatter.
58 |
59 | Formatting can be a separate refactoring technique, so the result can be a commit or even an independent PR. The main goal is to integrate into the main branch as early as possible so we don't have to handle complex merge conflicts between formatting and other code changes made by other developers.
60 |
61 | ## Code Linting
62 |
63 | After turning linter “warnings” to “errors,” we might have a list of such errors. This list can be a task list for the current refactoring iteration.
64 |
65 | I prefer to save the work on each linter rule as a separate commit or PR. For example, we could remove all unused code, make it a commit, and move on to the next problem on the list.
66 |
67 |
68 |
69 | Linter highlights unused code that can be removed
70 |
71 |
72 | If the linter shows many errors, we can turn the rules one by one rather than all simultaneously. The smaller the steps, the easier it is to break the problem into several and solve each separately.
73 |
74 | After fixing each rule, we'll need to check if the tests pass. In the future, I will stop emphasizing test-checking to shorten the text. Let's keep in mind that we check the tests after _each_ change.
75 |
76 | ## Language Features
77 |
78 | Modern languages evolve and get new features. It is especially true for JavaScript since the ES specification is updated yearly.[^proposals]
79 |
80 | Sometimes, a new language version feature can replace old self-written helper functions. Built-in language features are faster, more reliable, and easier to work with. So if there's a chance for replacement we can use it.
81 |
82 | | By the way 🥫 |
83 | | :------------------------------------------------------------------------------------------------------------------------------ |
84 | | On the frontend, we might need to ensure the feature has the necessary browser support. We can check it with Caniuse.[^caniuse] |
85 |
86 | ```js
87 | // Self-written helper for checking the beginning of a string:
88 | const startsWith = (str, chunk) => str.indexOf(chunk) === 0;
89 | const yup = startsWith("Some String", "So");
90 |
91 | // ...Can be replaced with the native string method:
92 | const yup = "Some String".startsWith("So");
93 | ```
94 |
95 | | However 💡 |
96 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
97 | | If the self-written implementation differs from the native one and we can't replace it, I'd prefer to mention the difference in the documentation. That way, it will be clear why we're using a self-written function instead of the language feature. |
98 |
99 | Removing code is beneficial: the less code there is, the fewer potential points of failure in the application. We can use the rule “give most of the work to the language or environment than write ourselves.” It's usually more reliable.
100 |
101 | ## Environment Features
102 |
103 | Along with the language features, we should also highlight the features of the text editor or IDE we're working with. If they have automated refactoring tools, it's worth learning them.
104 |
105 | “Rename Symbol,” “Extract into Function,” and other tools speed up the work and reduce the cognitive load. For example, in VS Code, we can change the name of a function or variable everywhere by using the hotkeys:[^vscode]
106 |
107 |
108 |
109 | “Rename Symbol” updates the variable name everywhere
110 |
111 |
112 | However, we should double-check the result of applying these tools. For example, Rename Symbol may “miss” some name or add unnecessary renaming:
113 |
114 | ```tsx
115 | // For example, we want to replace `name`
116 | // with `firstName` in the `AccountProps`:
117 |
118 | type AccountProps = { name: string };
119 | const Account = ({ name }: AccountProps) => <>{name}>;
120 |
121 | // After applying “Rename Symbol,”
122 | // there might appear an “extra renaming”:
123 |
124 | type AccountProps = { firstName: string };
125 | const Account = ({ firstName: name }: AccountProps) => <>{name}>;
126 | ```
127 |
128 | To avoid this, we should study the diff from the latest commit and check what was renamed and how.
129 |
130 |
131 |
132 | Git shows exactly what has changed since the latest commit
133 |
134 |
135 | A strategy of small steps helps simplify and maximize such checks' benefits. If we apply only one refactoring technique per commit, there's no noise in the diffs, and we can better see how the changes affect the code.
136 |
137 | Linters and tests help us avoid name conflicts and other bugs. For example, we can set up rules that forbid identical variable names, so the linter will error if there's a name conflict. If the linter is running in the background with refactoring, we will see the error immediately and be able to fix it.
138 |
139 | [^prettier]: Prettier, an opinionated code formatter, https://prettier.io
140 | [^proposals]: List of EcmaScript Proposals, https://proposals.es
141 | [^caniuse]: Can I use, support tables for the web, https://caniuse.com
142 | [^vscode]: Refactoring Source Code in VSCode, https://code.visualstudio.com/docs/editor/refactoring
143 |
--------------------------------------------------------------------------------
/manuscript-en/07-duplication.md:
--------------------------------------------------------------------------------
1 | # Code Duplication
2 |
3 | The main goal of refactoring is to make the code more readable. One way to achieve this is to reduce the amount of noise in it.
4 |
5 | Code duplication makes the code noisy because it contains no useful information. However, not all duplication is evil, and it even can be a development tool. So when refactoring, we should understand what kind of duplication we're dealing with.
6 |
7 | In this chapter, we'll discuss what to look for when searching for duplication and how to know when it's time to eliminate it.
8 |
9 | ## Not All Duplication is Evil
10 |
11 | If two pieces of code have the same purpose, contain the same set of actions, and process the same data, that is _direct_ duplication. We can safely get rid of it by extracting the repeating code into a variable, function, or module.
12 |
13 | But there are cases when two pieces of code seem “similar” but later turn out different. If we merge them too early, it'll be harder to split such code later. In general, it's much easier to combine identical code than to break modules merged earlier.
14 |
15 | When we're not sure that we're facing two _really_ identical pieces of code, we can mark these places with unique labels. We can write an assumption about what's duplicated there in these labels.
16 |
17 | ```js
18 | /** @duplicate Applies a discount coupon to the order. */
19 | function applyCoupon(order, coupon) {}
20 |
21 | /** @duplicate Applies a discount coupon to the order. */
22 | function applyDiscount(order, discount) {}
23 | ```
24 |
25 | Such labels will only be helpful if we conduct their regular reviews. During the review, we should check if we have learned something new about possible duplicates, which either confirms that they're identical or disproves it by showing the difference between them.
26 |
27 | | By the way ⏰ |
28 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
29 | | I consider regular audits in my projects as part of paying the technical debt. I make to-do lists for such periodic tasks. Within the list, I specify what needs to be done within the task. The technique of regular audits and its benefits are well described in “Jedi Techniques” by Maxim Dorofeev.[^jeditechnics] |
30 |
31 | If it becomes apparent during an audit of a label that the duplication described in it is _direct_, we can proceed with refactoring code with that label. If the code turns out to be different, we can remove the label. This approach helps not to rush with code generalization but also not to forget places where possible duplication exists.
32 |
33 | ## Variables for Data
34 |
35 | Duplicated data or calculation results are convenient to put into variables. For example, it helps unravel complex conditions or highlight the steps of data transformations.
36 |
37 | ```js
38 | // If conditions are relatively close to each other:
39 |
40 | if (user.age < 18) toggleParentControl();
41 | // ...
42 | if (user.age < 18) askParents();
43 |
44 | // ...We can extract the expression into a variable:
45 |
46 | const isChild = user.age < 18;
47 |
48 | if (isChild) toggleParentControl();
49 | // ...
50 | if (isChild) askParents();
51 | ```
52 |
53 | Sometimes the data duplication can be less obvious and more difficult to spot:
54 |
55 | ```js
56 | // The second condition is turned “inside out”
57 | // and uses the `years` variable instead of the object field.
58 |
59 | if (user.age < 18) askParents();
60 | // ...
61 | const { age: years } = user;
62 | if (years >= 18) askDocuments();
63 |
64 | // But we can still get rid of it,
65 | // by extracting the expression into a variable:
66 |
67 | const isChild = user.age < 18;
68 |
69 | if (isChild) askParents();
70 | // ...
71 | if (!isChild) askDocuments();
72 | ```
73 |
74 | | More info 🔬 |
75 | | :-------------------------------------------------------------------------------------------------------------- |
76 | | We'll talk about simplifying and unraveling complex conditions in more detail in one of the following chapters. |
77 |
78 | ## Functions for Actions
79 |
80 | We can extract duplicated actions and data transformations into functions and methods. To detect them, we can use the “sameness check”:
81 |
82 | - Actions are duplicated if they have the same goal—the desired result;
83 | - Have the same scope—the part of the application they affect;
84 | - Have the same direct input—arguments and parameters;
85 | - Have the same indirect input—dependencies and imported modules.
86 |
87 | In the example below, the code snippets pass this test:
88 |
89 | ```js
90 | // - Goal: to add a field with the absolute discount value to the order;
91 | // - Scope: the order object;
92 | // - Direct input: order object, relative discount value;
93 | // - Indirect input: function that converts percents to absolute value.
94 |
95 | // a)
96 | const fromPercent = (amount, percent) => (amount * percent) / 100;
97 |
98 | const order = {};
99 | order.discount = fromPercent(order.total, 50);
100 |
101 | // b)
102 | const order = {};
103 | const discount = (order.total * percent) / 100;
104 | const discounted = { ...order, discount };
105 |
106 | // Actions in “a” and “b” are the same,
107 | // so we can extract them into a function:
108 |
109 | function applyDiscount(order, percent) {
110 | const discount = (order.total * percent) / 100;
111 | return { ...order, discount };
112 | }
113 | ```
114 |
115 | In the other example, the goal and direct input are the same, but the dependencies are different:
116 |
117 | ```js
118 | // The first snippet calculates discount in percent,
119 | // the second one applies the “discount of the day”
120 | // by using the `todayDiscount` function.
121 |
122 | // a)
123 | const order = {};
124 | const discount = (order.total * percent) / 100;
125 | const discounted = { ...order, discount };
126 |
127 | // b)
128 | const todayDiscount = () => {
129 | // ...Match the discount to today's date.
130 | };
131 |
132 | const order = {};
133 | const discount = todayDiscount();
134 | const discounted = { ...order, discount };
135 | ```
136 |
137 | In the example above, fragment “b” has the `todayDiscount` function among its dependencies. Because of it, the action sets differ enough to be considered “similar” but not “the same.”
138 |
139 | We can use `@duplicate` labels and wait a bit to get more information about how they should work. When we know exactly how these functions should work, we can “generalize” the actions:
140 |
141 | ```js
142 | // The generalized `applyDiscount` function will take
143 | // the absolute discount value:
144 |
145 | function applyDiscount(order, discount) {
146 | return {...order, discount}
147 | }
148 |
149 | // Differences in the calculation (percentages, “discount of the day,” etc.)
150 | // are collected as a separate set of functions:
151 |
152 | const discountOptions = {
153 | percent: (order, percent) => order.total * percent / 100
154 | daily: daysDiscount()
155 | }
156 |
157 | // As a result, we get a generalized action for applying a discount
158 | // and a dictionary with discounts of different kinds.
159 | // Then, the application of any discount will now become uniform:
160 |
161 | const a = applyDiscount(order, discountOptions.daily)
162 | const b = applyDiscount(order, discountOptions.percent(order, 40))
163 | ```
164 |
165 | | More info 🔬 |
166 | | :--------------------------------------------------------------------------------------------- |
167 | | We will discuss generalized algorithms, their use, and parameterization in a separate chapter. |
168 |
169 | [^jeditechnics]: “Jedi Technics” by Maxim Dorofeev, Translated summary, https://bespoyasov.me/blog/jedi-technics/
170 |
--------------------------------------------------------------------------------
/manuscript-en/02-introduction.md:
--------------------------------------------------------------------------------
1 | # Introduction
2 |
3 | Refactoring requires resources. The amount of these resources depends on the size of the project and its code quality. The larger the project and the worse the code, the more difficult it is to clean it up and the more resources it may require.
4 |
5 | To justify the investment of resources and find a balance between costs and benefits, we need to understand the benefits and limitations of refactoring.
6 |
7 | ## Benefits for Developers
8 |
9 | Code quality is an investment in developers' free time in the future. The simpler and cleaner the code is, the less time we'll spend fixing bugs and developing new features.
10 |
11 | Developers may care about different properties of the code. For example, we might want to:
12 |
13 | - Find code related to specific parts of the application faster.
14 | - Eliminate misunderstanding about how the code works and avoid miscommunication and conflicts in the team.
15 | - Make it easier to review code and check it against business requirements.
16 | - Painlessly add, change, and delete code without regressions.
17 | - Reduce the time to find and fix bugs and make debugging process more convenient.
18 | - Simplify project exploration for new developers.
19 |
20 | This list is incomplete. Other properties may be necessary to a particular team, varying from project to project.
21 |
22 | Regular refactoring helps pay attention to code properties before problems appear. It makes daily work more efficient, gives developers extra time and resources, and prevents “big refactorings” in the future.
23 |
24 | ## Benefits for Business
25 |
26 | In a perfectly organized development process, there's no need to “sell” refactoring to the business. In such projects, regular code improvement is at the core of the development, and bad code does not accumulate—no need to “explain the benefits to the business” in this case.
27 |
28 | However, there are projects where development is organized differently for various reasons. In such projects, as a rule, legacy code tends to accumulate.
29 |
30 | We may feel the need to improve the code, but we may not have enough resources to do that. A proposal to “take a week to refactor” might cause a conflict of interests because, to the business, it sounds like “we'll do nothing useful for a week.” These are the cases where we may need to “sell” the ideas of the code improvement.
31 |
32 | The benefits of refactoring aren't evident to the business because they aren't immediate. We may see them in the future, but it's difficult to predict when.
33 |
34 | Usually, to sell the idea of refactoring to business, we should speak the business language, and _sell the result, not the process_. Discuss what exactly we'll get as a result of the time spent:
35 |
36 | - We'll spend less time fixing bugs, so the number of unhappy users will drop.
37 | - We'll start implementing new features before our competitors, so they generate new users and profit.
38 | - We'll better understand the requirements and constraints, so we react to unpredicted problems faster.
39 | - We'll make onboarding easier for new developers to make significant changes sooner.
40 | - We'll decrease staff turnover because developers don't run away from good code, only from the bad one.
41 |
42 | We can use various metrics to measure code quality. It'll be much easier to determine the necessity of refactoring by relying on the numbers. For example, the costs statistics might help to incorporate regular refactoring into the development process smoothly.
43 |
44 | ## “Good” Code, “Bad” Code
45 |
46 | It isn't easy to name a list of _universal_ characteristics of a good code. There are a few, but they have limits in applicability, too.
47 |
48 | | For example 💡 |
49 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
50 | | I think of cyclomatic complexity and the number of dependencies as more or less universal characteristics. But we'll talk about them separately in future chapters. |
51 |
52 | Most of the books I've read also describe good code subjectively.[^workingeffectively][^readablecode][^cleancode] Different authors use different words, but they always emphasize “readability.”
53 |
54 | Some studies have tried to determine this “readability.”[^evaluatingstudies][^readability][^howreadable] However, their samples are either small or skewed, so it's difficult to conclude the universal rules of the “good” code.
55 |
56 | In practice, we can try to look for a “bad” code rather than a “good” one. It's easier because we can use the help of heuristics and “cognitive alarms” when searching for it.
57 |
58 | Cognitive alarms are the feelings we get when reading bad code. I believe that a code needs refactoring if one of these thoughts arises while reading it:
59 |
60 | #### Hard to Read
61 |
62 | - It's hard for us to read code if it's unformatted, intertwined, or noisy.
63 | - If there are a lot of unnecessary details in the code, there is no clear entry point.
64 | - If it's hard to follow the code execution, if we need to jump between screens, files, or lines constantly.
65 | - If the code is inconsistent, if it doesn't follow the project rules.
66 |
67 | #### Hard to Change
68 |
69 | - Code is hard to change if we need to update many files or double-check the entire application when adding a feature.
70 | - If we aren't sure, we can painlessly remove a particular piece of code.
71 | - If there's no clear entry point or we can't relate a feature with a specific module.
72 | - If there's too much boilerplate code or copypaste.
73 |
74 | #### Hard to Test
75 |
76 | - Code is hard to test if we need a “complex infrastructure” for tests or need to mock a lot of functionality.
77 | - If we must emulate the whole app running to check a single function.
78 | - If tests for a task require data irrelevant to the task.
79 |
80 | #### Hard to “Fit in the Head”
81 |
82 | - Code doesn't fit in our heads if it's hard to keep track of what's going on in it.
83 | - If by the middle of the module, it's hard to remember what happened at the beginning.
84 | - If the code is “too complicated” and drawing diagrams doesn't help to understand it.
85 |
86 | #### Code Smells
87 |
88 | Some of those problems have already been shaped in the form of code smells. _Code smells_ are antipatterns that lead to problems.[^smells]
89 |
90 | There are solutions for most of the code smells. Sometimes it's enough to look at the code, find the smell, and apply a specific solution to it.
91 |
92 | | About smells 🦨 |
93 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
94 | | Most often, examples of code smells are given in code written in OOP style, which may not be as valuable in the JavaScript world. Nevertheless, some of the smells are universal and applicable to OOP and multi-paradigm code. |
95 |
96 | [^workingeffectively]: “Working Effectively with Legacy Code” by Michael C. Feathers, https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code
97 | [^readablecode]: “The Art of Readable Code” by Dustin Boswell, Trevor Foucher, https://www.goodreads.com/book/show/8677004-the-art-of-readable-code
98 | [^cleancode]: “Clean Code” by Robert C. Martin, https://www.goodreads.com/book/show/3735293-clean-code
99 | [^evaluatingstudies]: Evaluating Code Readability and Legibility: An Examination of Human-centric Studies, https://github.com/reydne/code-comprehension-review/blob/master/list-papers/AllPhasesMergedPapers-Part1.md
100 | [^readability]: Code Readability Testing, an Empirical Study, https://www.researchgate.net/publication/299412540_Code_Readability_Testing_an_Empirical_Study
101 | [^howreadable]: How Readable Code Is, a Readability Experiment https://howreadable.com
102 | [^smells]: Code Smells, Refactoring Guru, https://refactoring.guru/refactoring/smells
103 |
--------------------------------------------------------------------------------
/manuscript-ru/02-introduction.md:
--------------------------------------------------------------------------------
1 | # Введение
2 |
3 | Рефакторинг требует ресурсов. Количество этих ресурсов зависит от размера проекта и качества кода в нём. Чем больше проект и хуже код, тем сложнее наводить в нём порядок и больше ресурсов может для этого потребоваться.
4 |
5 | Чтобы обосновать вложение ресурсов и найти баланс между затратами и выгодой, нам надо понять пользу и ограничения рефакторинга.
6 |
7 | ## Польза для разработчиков
8 |
9 | Порядок в кодовой базе — это инвестиция в свободное время разработчиков в будущем. Чем проще и понятнее код, тем меньше времени будет уходить на исправление багов и новые фичи.
10 |
11 | Разработчикам могут быть важны разные свойства кода. Например, нам может быть важно:
12 |
13 | - Быстрее находить куски кода, отвечающие за конкретные части приложения.
14 | - Исключить разночтения о работе кода, недопонимание и конфликты в команде.
15 | - Легче проводить код-ревью и сверять код на соответствие бизнес-требованиям.
16 | - Добавлять, изменять и удалять код без регрессий и лишних усилий.
17 | - Уменьшить время на поиск и исправление багов, сделать процесс отладки удобнее.
18 | - Упростить исследование проекта для новых разработчиков.
19 |
20 | Это неполный список. Конкретной команде могут быть важны и другие свойства, они могут варьироваться от проекта в проекту.
21 |
22 | Регулярный рефакторинг помогает уделять внимание характеристикам кода заранее, до появления проблем с ними. Это делает ежедневную работу удобнее и предотвращает «большие рефакторинги» в будущем. Такой процесс даёт разработчикам больше свободного времени и ресурсов.
23 |
24 | ## Польза для бизнеса
25 |
26 | В идеально организованной разработке необходимости «продавать» рефакторинг бизнесу нет. В таких проектах регулярное улучшение кода «вшито» в процесс разработки и плохой код не накапливается. Отдельно «объяснять пользу бизнесу» в этом случае не нужно.
27 |
28 | Но есть проекты, где разработка по разным причинам организована иначе. В таких проектах, как правило, копится легаси.
29 |
30 | Мы можем чувствовать необходимость улучшить код, но у нас может не хватать на это ресурсов. Предложение «взять недельку на рефакторинг» вызовет конфликт интересов, потому что для бизнеса оно звучит, будто «целую неделю не будет происходить ничего полезного». В этих случаях нам и может понадобиться «продать» идею улучшения кода.
31 |
32 | Польза рефакторинга для бизнеса неочевидна, потому что она не мгновенна. Польза проявляется «через какое-то время», какое именно — предсказать трудно.
33 |
34 | Обычно, чтобы продать идею рефакторинга бизнесу, я стараюсь говорить на языке бизнеса и продавать _не процесс, а результат_. Что именно мы получим в результате потраченного времени:
35 |
36 | - Сможем быстрее находить и исправлять ошибки, это уменьшит количество разочарованных пользователей.
37 | - Начнём реализовывать новые фичи раньше конкурентов, это будет генерировать новых пользователей и прибыль.
38 | - Будем лучше понимать требования, это позволит раньше реагировать на непредвиденные проблемы.
39 | - Избавимся от текучки кадров, потому что от приятного кода разработчики не бегут.
40 | - Сделаем онбординг быстрее и понятнее для новых разработчиков, это позволит им приносить пользу раньше.
41 |
42 | Мы можем использовать различные метрики для измерения качества кода. Опираясь на эти цифры будет проще обусловить необходимость рефакторинга. А снижение затрат при регулярном рефакторинге поможет плавно внедрить его в процесс разработки.
43 |
44 | ## «Плохой» и «хороший» код
45 |
46 | Мне сложно назвать список _объективных_ характеристик хорошего кода. Таких характеристик мало, и у них есть ограничения в трактовке и применимости.
47 |
48 | | Например 💡 |
49 | | :--------------------------------------------------------------------------------------------------------------------------------------------- |
50 | | Среди таких характеристик можно выделить цикломатическую сложность и количество зависимостей. Но мы поговорим о них отдельно в будущих главах. |
51 |
52 | Это не новая проблема. В большей части книг, что я прочёл, хороший код описывают субъективно:
53 |
54 | - У Физерса хороший код «читаемый, поддерживаемый и приятный»;[^workingeffectively]
55 | - У Фаучера — «читаемый, лаконичный и простой»;[^readablecode]
56 | - У Мартина — «элегантный, простой и читаемый».[^cleancode]
57 |
58 | Разные авторы используют разные слова, но можно заметить, что все делают упор на «читаемость». Есть исследования, которые пытались определить, что такое эта «читаемость».[^evaluatingstudies][^readability][^howreadable] Однако их проблема в маленькой или искажённой выборке, поэтому делать выводы об универсальных правилах «хорошего» кода сложно.
59 |
60 | На практике я стараюсь искать не «хороший», а «плохой» код. Это проще, потому что в его поиске мне помогают эвристики и «когнитивные костыли».
61 |
62 | Когнитивными костылями я называю ощущения, которые появляются при чтении плохого кода. Я считаю, что коду нужен рефакторинг, если при чтении возникает одна из этих мыслей:
63 |
64 | ### Тяжело читать
65 |
66 | - Нам тяжело читать код, если в нём беспорядочное форматирование, он «грузный», запутанный, шумный.
67 | - В коде много лишних деталей, нет явной точки входа.
68 | - Сложно проследить последовательность выполнения, нужно прыгать между экранами, файлами, строками.
69 | - Код непоследовательный, не отвечает правилам, принятым в проекте.
70 |
71 | ### Тяжело менять
72 |
73 | - Код тяжело менять, если при добавлении новой фичи нужно изменить много файлов или перепроверить всё приложение.
74 | - Нет уверенности, что можно беспроблемно удалить конкретный кусок кода.
75 | - Нет явной точки входа, нельзя соотнести фичу приложения и конкретный модуль.
76 | - Слишком много бойлерплейта или копипасты.
77 |
78 | ### Тяжело тестировать
79 |
80 | - Код тяжело тестировать, если для тестов нужна «навороченная инфраструктура» или нужно мо́кать много функциональности.
81 | - Приходится имитировать работу всей программы, чтобы проверить одну функцию.
82 | - Для теста нужны тестовые данные, которые не относятся к задаче.
83 |
84 | ### «Не помещается в голову»
85 |
86 | - Код не помещается в голову, если сложно уследить за всем, что в нём происходит.
87 | - К середине модуля сложно вспомнить, что было в начале.
88 | - При чтении «кипит» голова, схемы работы на бумажке не помогают.
89 |
90 | ### Запахи кода
91 |
92 | Часть описанных проблем умные люди уже оформили в виде запахов кода. _Запахи_ — это антипаттерны, которые приводят к проблемам.[^smells]
93 |
94 | Против запахов уже разработаны решения. Иногда нам достаточно посмотреть на код, найти в нём запах и применить конкретное решение против него.
95 |
96 | | О запахах 🦨 |
97 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
98 | | Чаще всего примеры запахов приводят в коде, написанном в ООП-стиле, что может быть не так полезно в JavaScript-мире. Тем не менее часть запахов достаточно универсальна и применима не только к ООП, но и к мультипарадигменному коду. |
99 |
100 | [^workingeffectively]: “Working Effectively with Legacy Code” by Michael C. Feathers, https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code
101 | [^readablecode]: “The Art of Readable Code” by Dustin Boswell, Trevor Foucher, https://www.goodreads.com/book/show/8677004-the-art-of-readable-code
102 | [^cleancode]: “Clean Code” by Robert C. Martin, https://www.goodreads.com/book/show/3735293-clean-code
103 | [^evaluatingstudies]: Evaluating Code Readability and Legibility: An Examination of Human-centric Studies, https://github.com/reydne/code-comprehension-review/blob/master/list-papers/AllPhasesMergedPapers-Part1.md
104 | [^readability]: Code Readability Testing, an Empirical Study, https://www.researchgate.net/publication/299412540_Code_Readability_Testing_an_Empirical_Study
105 | [^howreadable]: How Readable Code Is, a Readability Experiment https://howreadable.com
106 | [^smells]: Code Smells, Refactoring Guru, https://refactoring.guru/refactoring/smells
107 |
--------------------------------------------------------------------------------
/manuscript-en/04-during-refactoring.md:
--------------------------------------------------------------------------------
1 | # During Refactoring
2 |
3 | After we've defined clear boundaries, examined the code, and covered it with tests, we can start refactoring.
4 |
5 | As we work, we want the changes to be as helpful as possible while staying within our boundaries. In this chapter, we'll discuss heuristics that help us do this.
6 |
7 | ## Small Steps
8 |
9 | A small step is a minimal change that makes sense. An excellent example of a small step is an _atomic commit_.[^atomic] Such a commit contains a meaningful functionality change and evolves the code from one working state to another. We can develop and change the code base with atomic commits while keeping it valid at every point.
10 |
11 | | By the way 💡 |
12 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13 | | Atomic commits allow us to bisect the repository in the future while searching for bugs.[^bisect] When code is valid and can compile in each of the commits, we can “time travel” through the repository by switching between different commits. |
14 |
15 | The size of commits depends on the habits and preferences of the team. Personally, my rule of thumb is “the smaller the commit, the better.” For example, I commit separately even function and variable renames on my projects.
16 |
17 | Smaller steps encourage us to decompose tasks into simpler ones. This way, extensive features turn into sets of compact tasks, which are not so scary to merge into the main repository branch more often. Without decomposition, such a task could turn into a blocker that drags the whole team down.
18 |
19 | | About CI 🔬 |
20 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21 | | The point of _continuous integration, CI_ is for the team to synchronize code changes with each other as often as possible. This approach and its benefits are described well in “Code That Fits in Your Head” by Mark Seemann and “Beyond Legacy Code” by Scott Bernstein.[^codethatfits][^beyond] |
22 |
23 | Blocking tasks should be avoided in general, but especially when refactoring code. If the whole team is busy with refactoring, it can become expensive. The precedent of being expensive can deprive the project of resources for refactoring at all in the future.
24 |
25 | Except for that, small steps allow you to “postpone” refactoring at any time and switch to another task. If we're working with git, we can use “stash” to save unfinished work via `git stash`.
26 |
27 | And lastly, small changes are easier to describe in commit messages. The scope of such changes is less, so it's easier to explain their meaning in a short sentence.
28 |
29 | ## Small but Detailed Pull Requests
30 |
31 | This section follows the previous one, but I want to focus on it separately. The problem with big pull requests is that:
32 |
33 | ---
34 |
35 | **❗️ No one reviews big pull requests**
36 |
37 | ---
38 |
39 | If we want to _improve_ the code, we need the pull request to _be reviewed_. So our job is to make the reviewer's job easier. To do this, we can:
40 |
41 | - Limit the size of changes. So it's easier to find time for a review and understand the PR's goals and meaning. This way, the code review won't look like a big chunk of new work.
42 | - Describe the context of the task in the message to the PR. The reasons, goals, and constraints of the task will help share the understanding of the task with the reviewer. This way, we can anticipate likely questions and answer them in advance. It will speed up the review.
43 |
44 | The desire for small but detailed PR also helps break down large tasks into smaller ones and refactor them in small steps.
45 |
46 | ## Tests for Every Change
47 |
48 | For the code to evolve through valid states, we will test _every_ change, no matter how small.
49 |
50 | When using unit tests, we can keep them running next to the editor. When using longer-running tests (like E2E), we can run them before each commit (e.g., on a pre-commit hook) so that invalid code doesn't get into the repository.
51 |
52 | The tests should drive us to _commit only valid code_. Each commit will contain a set of _complete and meaningful_ changes.
53 |
54 | | By the way 🧪 |
55 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56 | | It will only work if tests are trustworthy. That's why we emphasized edge cases and detailed specifications of the result in the latest chapter. That help make tests more reliable. |
57 |
58 | ## One Technique at a Time
59 |
60 | Frequent commits are anchor points in code evolution. The more frequent such points are, the more compact the changes in between. It is useful, for example, when examining changes from the latest commit we just made. Small changes are faster to study, easier to understand, and contain less work, so it's emotionally easier to roll them back in case we need to.
61 |
62 | During refactoring, though, it's not always obvious how to make changes smaller and more often. I prefer to follow this rule:
63 |
64 | ---
65 |
66 | **❗️ Don't mix different refactoring techniques in the same commit**
67 |
68 | ---
69 |
70 | This rule makes us commit code more often: we renamed a function—made a commit, extracted a variable—made a commit, added code for a future replacement—made a commit, and so on.
71 |
72 | As long as we don't mix different techniques in one commit, it's easier for us to track code changes by diffs and find errors like name conflicts.
73 |
74 | Complex techniques that are too big for a single commit can be broken up into steps. We can commit each of these steps separately. But when splitting the task, we should remember that each step must leave the code in a valid state.
75 |
76 | ## No Features, No Bug Fixes
77 |
78 | During refactoring, we may find an idea for a feature or a piece of code that doesn't work correctly. We may want to “fix it along the way,” but it's better not to mix bug fixes and new features with refactoring.
79 |
80 | Refactoring _should not_ change the code functionality. If, during refactoring, we add a feature, and later it needs to be rolled back, we'll have to cherry-pick specific commits or even lines of code into the repo manually.
81 |
82 | Instead, it's better to put all the found feature ideas into a separate list and return to them after refactoring. If we find a bug, we should postpone refactoring (`git stash`) and return to it after the fix. Again, working in small steps is more convenient for this maneuverability.
83 |
84 | ## Transformation Priority Premise
85 |
86 | _Transformation Priority Premise, TPP_ is a list of actions that help develop code from the naive, most straightforward implementation to a more complex one that meets all the project requirements.[^tpp]
87 |
88 | TPP helps _avoid doing everything at once_. It encourages us to gradually update a piece of code, recording each step in the version control system, and integrating changes into the main repository branch more often.
89 |
90 | For me, TPP helps me not overload my head with details while writing code. All improvement ideas that come up along the way, I put in a separate list. I then compare this list with TPP and choose what to implement next.
91 |
92 | | Why bother 🧠 |
93 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94 | | “Offloaded” details free up the brain's “working memory.”[^shorttermmemory] The freed resources can be spent on the task details. They will improve attentiveness and focus. |
95 |
96 | ## Refactoring Tests Separately
97 |
98 | Tests and the application code cover each other. Tests verify that we haven't made mistakes in the application code and vice versa. If we refactor them simultaneously, the probability of missing a bug becomes higher.
99 |
100 | It's better to refactor the application code and test code one at a time. If, while refactoring an application, we notice that we need to refactor a test, we should:
101 |
102 | - Stash the application code changes;
103 | - Refactor the desired test;
104 | - Verify that the test breaks for a reason it should;
105 | - Get the last changes from the stash and continue working on them.
106 |
107 | | In detail 🔬 |
108 | | :------------------------------------------------------------------------------------ |
109 | | We'll talk a little more about this technique in the “Refactoring Test Code” chapter. |
110 |
111 | [^atomic]: Atomic Commit, Wikipedia https://en.wikipedia.org/wiki/Atomic_commit
112 | [^bisect]: git-bisect, Use binary search to find the commit that introduced a bug, https://git-scm.com/docs/git-bisect
113 | [^codethatfits]: “Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head
114 | [^beyond]: “Beyond Legacy Code” by David Scott Bernstein, https://www.goodreads.com/book/show/26088456-beyond-legacy-code
115 | [^tpp]: Transformation Priority Premise, Wikipedia https://en.wikipedia.org/wiki/Transformation_Priority_Premise
116 | [^shorttermmemory]: Working memory, Capacity, Wikipedia, https://en.wikipedia.org/wiki/Working_memory#Capacity
117 |
--------------------------------------------------------------------------------
/manuscript-ru/20-refactoring-process.md:
--------------------------------------------------------------------------------
1 | # Рефакторинг как процесс
2 |
3 | В прошлых главах мы уделяли основное внимание техническим деталям рефакторинга, но это только часть процесса улучшения кода в проекте.
4 |
5 | В этой главе поговорим, как находить на рефакторинг время и заниматься им регулярно. Обсудим, как преодолевать трение в проекте и приступать к рефакторингу, даже если кодовая база большая и в ней много легаси.
6 |
7 | ## Рефакторить или переписывать
8 |
9 | Если мы работаем с легаси-кодом, то первая мысль при взгляде на него — «да проще с нуля переписать». Иногда это действительно так, но адекватно оценить ситуацию с первого взгляда можно не всегда. Перед тем как решить, рефакторить или переписывать, нам стоит оценить 3 вещи: ресурсы команды, выгоды и риски нашего решения.
10 |
11 | Эти параметры не дадут прямого ответа на вопрос «Что делать?», но дадут более объективную оценку состояния проекта. Иногда одной такой оценки бывает достаточно, чтобы принять решение. Но даже если её не достаточно, она подскажет, каких знаний о проекте нам не хватает для принятия окончательного решения.
12 |
13 | ## Ресурсы
14 |
15 | Под _ресурсами_ мы будем понимать время, знания и опыт, которые есть в распоряжении команды. Это то, что можно «тратить» на рефакторинг или переписывание кода, чтобы улучшить его.
16 |
17 | ### Время
18 |
19 | Оценивать свободное время разработчиков можно ретроспективно, смотря на прошлый опыт работы над проектом. Нам потребуется посчитать сколько времени команда уделяла улучшениям в прошлом, как часто появлялись свободные «окна» в расписании проекта.
20 |
21 | Прошлый опыт покажет, как расходовалось время на разработку в прошлом и какие паттерны прослеживались во время неё. Это поможет спрогнозировать, сколько свободного времени у нас будет в ближайшем будущем.
22 |
23 | Такой прогноз может показаться заниженным, потому что мы можем хотеть уделять улучшениям кода больше времени, но он отражает привычные паттерны разработки. Их полезно знать, потому что даже если мы договорились уделять больше времени рефакторингу и тех. долгу, без перемен в рабочих процессах разработка вернётся к привычным паттернам.
24 |
25 | К тому же нам всегда стоит помнить о «непредвиденных проблемах», которые точно появятся при работе с легаси и будут отнимать на решение дополнительное время.
26 |
27 | ### Накопленные знания
28 |
29 | Накопленные знания о проекте можно оценить по количеству документации, полезных комментариев в коде, качеству истории комитов, доступности участников, которые писали код, и выразительности самого кода.
30 |
31 | Знания сложно оценить количественно, поэтому можно использовать качественную оценку. Чем больше противоречий между разными источниками информации мы находим, тем хуже можем считать качество накопленных знаний. И наоборот, чем стройнее выглядит модель проекта и проще найти людей, которые занимались им с самого начала, тем качество знаний выше.
32 |
33 | ### Опыт
34 |
35 | Под опытом будем иметь в виду знакомство разработчиков с текущим и _различными другими_ проектами, языками и парадигмами. Чем разностороннее опыт, чем больше разработчики «повидали», тем меньше времени мы будем тратить на разработку нерабочих решений.
36 |
37 | | К слову 🧑🏫 |
38 | | :--------------------------------------------------------------------------------------------------------------------------------------------- |
39 | | Стоит также учитывать и опыт сторонних консультантов и экспертов, если мы допускаем возможность их участия, но это сложнее сделать объективно. |
40 |
41 | ## Выгоды и риски
42 |
43 | Также нам стоит определить выгоды и риски рефакторинга и переписывания кода. Они будут напрямую зависеть от рабочих процессов и «кухни» проекта, поэтому понадобится какое-то внутреннее исследование. Удобнее всего оценивать выгоды и риски параллельно, потому что стремление к любой выгоде имеет под собой какой-то риск.
44 |
45 | ## Мета-информация о проекте
46 |
47 | «Мета-информация» рассказывает о процессе работы над проектом и том, какие части кода в нём наиболее важные. Если команда использует систему контроля версий, то вся необходимая «мета-информация» уже есть там. Например, история комитов может нам рассказать:
48 |
49 | - что является ключевой частью проекта — в каких файлах дописывают фичи и правят баги чаще всего;
50 | - какое неявное зацепление есть между частями проекта — какие файлы менялись вместе с другими файлами;
51 | - в каких частях проекта было больше всего багов — какие комиты относились к баг-фиксам и т.д.
52 |
53 | Анализ мета-информации о проекте может рассказать, какие части кода самые сложные или самые полезные с точки зрения бизнеса.
54 |
55 | | Подробнее 📚 |
56 | | :-------------------------------------------------------------------------------------- |
57 | | Хорошо об этом написал Адам Торнхил в своей книге “Your Code as a Crime Scene”.[^scene] |
58 |
59 | Если системы контроля версий нет, то можно попробовать собрать косвенные показатели (релиз-ноуты, базу данных поддержки и т.д.), но делать выводы по ним будет сложнее.
60 |
61 | ## Эстимейты
62 |
63 | Если мы всё же решили отрефакторить конкретный кусок кода, то следующий шаг — спланировать итерацию.
64 |
65 | Чтобы понять, какое количество времени может уйти на рефакторинг куска кода, сперва стоит выделить в нём места, которые вызывают вопросы. Чтобы определиться, с какими проблемами мы имеем дело, мы можем разложить проблемные места в коде по такой таблице:
66 |
67 | | | Просто | Сложно |
68 | | --------- | --------------------------------------------------------------- | ------------------------------------------------------------ |
69 | | Понятно | Ресёрч не нужен, ясно как решать, не займёт много времени | Ясно как решать, но точно замёт много времени |
70 | | Непонятно | Задача маленькая и изолированная, но может потребоваться ресёрч | Точно нужен ресёрч, много скрытых связей, незнакомая область |
71 |
72 | Количество требуемого времени будет напрямую зависеть от пропорции кода в этой таблице. Чем больше сложного и непонятного, тем сложнее дать точный эстимейт.
73 |
74 | Задачи из последней ячейки спланировать сложнее всего. Для таких задач можно предложить команде использовать метод «Итерация-гипотеза». В этом методе каждое предположение о проблеме будет отдельной итерацией разработки. Опровержение или подтверждение этого предположения — цель итерации.
75 |
76 | Проверка одной гипотезы занимает не так много времени, как исследование всей проблемы целиком. Итерации с такими проверками проще планировать и проводить. При этом с каждой проверенной гипотезой мы понимаем о проблеме больше и рано или поздно она перейдёт из правой нижней ячейки в какую-то другую — тогда мы сможем дать более точный эстимейт.
77 |
78 | ## Рефакторинг больших кусков кода
79 |
80 | Если кусок кода не отрефакторить разом, то полезно следовать методологии «Рядом, а не вместо».
81 |
82 | Суть подхода в том, чтобы _не заменять_ кусок кода под рефакторингом, а создавать аналогичную функциональность _рядом_ с этим кодом. Когда аналог достаточно проработан, чтобы заменить собой проблемную часть, — мы заменяем старый код на свежесозданный.
83 |
84 | | К слову 🌳 |
85 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86 | | Мне нравится аналогия этого подхода с фикусом-душителем.[^strangler] Фикус обвевает дерево, твердеет, а когда дерево умирает, фикус остаётся в форме этого дерева. Рефакторинг «рядом» — это такой вот фикус: мы сперва «оплетаем снаружи» фичу новым кодом, и когда он готов, убираем старый код внутри. |
87 |
88 | При работе по подходу «Рядом, а не вместо» важно часто сливать результаты в главную ветку проекта. Это поможет предотвратить конфликты в системе контроля версий и не даст процессу рефакторинга затянуться на долгое время.
89 |
90 | ## Частота и гигиена
91 |
92 | Рефакторить лучше как можно чаще. Удобнее всего рефакторить код сразу после внесения в него изменений. Идеально, если мы можем вернуться к этому коду ещё раз после отдыха, чтобы глянуть на него свежей головой. Такое ревью помогает раньше выявить слабые решения.
93 |
94 | При работе с легаси стоит рефакторить код _до_ того, как фиксить в нём баги или добавлять новые фичи. После рефакторинга код будет более понятен, покрыт тестами и в целом приятнее для работы. Так мы уменьшим необходимое время на починку бага или новую фичу.
95 |
96 | Также при разработке стоит помнить о правиле бойскаута:[^opportunistic][^codethatfits]
97 |
98 | ---
99 |
100 | **❗️ Оставлять после себя код чище, чем он был до**
101 |
102 | ---
103 |
104 | ## Метрики
105 |
106 | Хоть рефакторинг и опирается на субъективные показатели типа красоты и читаемости, мы всё же можем использовать метрики для оценки качества внесённых изменений.
107 |
108 | За основу мы возьмём метрики из доклада “Where does bad code come from?” и немного расширим список.[^wherefrom] У нас получится список из 7 метрик, каждая из которых отвечает на вопрос «Сколько времени нужно, чтобы сделать XXX?»:
109 |
110 | - **S**earch — сколько времени нужно, чтобы найти требуемое место в коде.
111 | - **W**rite — чтобы написать новую фичу и покрыть её тестами.
112 | - **A**gree — чтобы разработчики согласились, что «код хороший».
113 | - **R**ead — чтобы прочесть и понять, что кусок кода делает.
114 | - **M**odify — чтобы изменить код под новые требования.
115 | - **E**xecute — чтобы выполнить/собрать/задеплоить кусок кода.
116 | - **D**ebug — чтобы найти и отладить баг.
117 |
118 | Если мы проведём замеры эти метрик до рефакторинга и после, то сможем сравнить качество внесённых изменений. Понятно, что некоторые метрики всё ещё довольно субъективны, но их тем не менее можно выразить в цифрах. Количественное описание характеристик поможет нам заметить тренд на улучшение или ухудшение при оценке качества кода.
119 |
120 | [^scene]: “Your Code As a Crime Scene” by Adam Tornhill, https://www.goodreads.com/book/show/23627482-your-code-as-a-crime-scene
121 | [^strangler]: “Strangler Fig Application” by Martin Fowler https://martinfowler.com/bliki/StranglerFigApplication.html
122 | [^opportunistic]: “Opportunistic Refactoring” by Martin Fowler https://martinfowler.com/bliki/OpportunisticRefactoring.html
123 | [^codethatfits]: “Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head
124 | [^wherefrom]: “Where Does Bad Code Come From?” https://youtu.be/7YpFGkG-u1w
125 |
--------------------------------------------------------------------------------
/manuscript-ru/04-during-refactoring.md:
--------------------------------------------------------------------------------
1 | # Во время рефакторинга
2 |
3 | После того, как мы обозначили границы изменений, исследовали код и покрыли его тестами, мы можем приступить к рефакторингу.
4 |
5 | Во время работы нам хочется, чтобы изменения кода были максимально полезными и при этом находились внутри обозначенных границ. В этой главе обсудим эвристики, которые помогают это делать.
6 |
7 | ## Двигаться маленькими шагами
8 |
9 | Маленький шаг — это минимальное изменение, несущее смысл. Хороший пример маленького шага — это _атомарный коммит (Atomic Commit)_. Такой коммит содержит осмысленное изменение функциональности и переводит кодовую базу из одного рабочего состояния в другое рабочее состояние.[^atomic] С их помощью мы можем развивать и менять кодовую базу, держа её при этом валидной в каждый момент времени.
10 |
11 | | К слову 💡 |
12 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13 | | Атомарные коммиты позволяют в будущем проводить бисекцию репозитория во время поиска багов.[^bisect] Когда код валиден в каждом из коммитов, мы можем «путешествовать во времени» по репозиторию, переключаясь между разными коммитами. |
14 |
15 | Размер коммитов зависит от привычек и предпочтений команды. Лично я придерживаюсь правила «чем меньше коммит — тем лучше». Например, в своих проектах я коммичу отдельно даже переименование методов и переменных.
16 |
17 | Маленькие шаги побуждают декомпозировать задачи на более простые. Так неподъёмные фичи превращаются в наборы компактных задач, которые не так страшно чаще сливать в основную ветку репозитория. Без декомпозиции такая задача может превратиться в долгострой, блокирующий всю команду.
18 |
19 | | Подробнее 🔬 |
20 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
21 | | Суть _частой интеграции в основную ветку (Continuous Integration)_ в том, чтобы команда как можно чаще синхронизировала изменения в коде между собой. О пользе подхода хорошо написали Марк Симанн в “Code That Fits in Your Head” и Скотт Бернштайн в “Beyond Legacy Code”.[^codethatfits][^beyond] |
22 |
23 | Блокирующих задач лучше избегать в принципе, а при рефакторинге кода особенно. Если вся команда занята рефакторингом, это может дорого стоить. Прецедент дороговизны может в будущем лишить проект ресурсов на рефакторинг вообще.
24 |
25 | Кроме этого маленькие шаги позволяют в любой момент «отложить» рефакторинг и переключиться на другую задачу. Если мы работаем с git, то можем использовать «полочки», чтобы сохранять недоделанную работу через `git stash`.
26 |
27 | И напоследок, маленькие изменения проще описывать в сообщениях к коммитам. Скоуп таких изменений укладывается в одно-два предложения, поэтому проще описать их смысл в коротком предложении.
28 |
29 | ## Создавать маленькие, но подробные пул-реквесты
30 |
31 | Этот раздел вытекает из предыдущего, но я хочу заострить на нём внимание отдельно. Проблема больших пул-реквестов в том, что...
32 |
33 | ---
34 |
35 | **❗️ Никто не проверяет большие и непонятные пул-реквесты**
36 |
37 | ---
38 |
39 | Если мы хотим _улучшить_ код, то нам нужно, чтобы пул-реквест _проверили_ на ревью, тогда наша задача — облегчить работу ревьюерам. Для этого мы можем:
40 |
41 | - Ограничить размер изменений — на компактные пул-реквесты проще выделить время и вникнуть в их суть. Ревью не будет выглядеть большой внезапной работой.
42 | - Описать контекст задачи в сообщении к PR. Причины, цель и ограничения задачи помогут поделиться с ревьюерами тем, что известно нам, но ещё не известно им. Так мы сможем предугадать вероятные вопросы и заранее ответить на них — это ускорит ревью.
43 |
44 | | К слову 📚 |
45 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46 | | Пул-реквест — это часть коммуникации в команде. Подробнее о том, как упростить коммуникацию писал Максим Ильяхов в «Новых правилах деловой переписки».[^communicationrules] |
47 |
48 | Стремление к маленьким, но подробным PR помогает дробить большие задачи на задачи поменьше и продвигать рефакторинг маленькими шагами.
49 |
50 | ## Проверять каждое изменение
51 |
52 | Чтобы код эволюционировал через валидные состояния, мы будем проверять тестами _каждое_ изменение, каким бы маленьким оно ни было.
53 |
54 | При использовании юнит-тестов можно держать окно с запущенными тестами рядом с редактором. При использовании более долгих тестов (например, E2E) можно запускать их перед коммитом (например, на pre-commit хуке), чтобы в репозиторий не попадал невалидный код.
55 |
56 | Тесты должны побуждать нас коммитить в репозиторий только валидный код. Тогда в каждом коммите будет находиться набор _законченных и осмысленных_ изменений.
57 |
58 | | К слову 🧪 |
59 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
60 | | Это может работать только в том случае, когда мы доверяем тестам. Поэтому в прошлой главе мы и делали акцент на эдж-кейсах и конкретизации результата — они помогают сделать тесты более надёжными. |
61 |
62 | ## Применять по одной технике за раз
63 |
64 | Частые коммиты фиксируют опорные точки в эволюции кода. Чем такие точки чаще, тем компактнее изменения между ними. Это, например, полезно при осмотре изменений с последнего коммита, которые мы только что внесли. Компактные изменения быстрее изучить, проще понять и не так жалко откатывать.
65 |
66 | Во время рефакторинга не всегда очевидно, как делать изменения компактнее и чаще. Мне в этом помогает правило:
67 |
68 | ---
69 |
70 | **❗️ Не смешивать разные техники рефакторинга в одном коммите**
71 |
72 | ---
73 |
74 | Это правило помогает коммитить ритмичнее и чаще: переименовали функцию — коммит, вынесли переменную — коммит, добавили код для будущей замены — коммит, и так далее.
75 |
76 | Пока мы не смешиваем разные техники в одном коммите, нам проще отслеживать изменения кода по диффам и находить ошибки типа конфликтов имён.
77 |
78 | Сложные техники можно разбивать на отдельные этапы, каждый из которых оформлять в виде коммита. В этом случае этапы важно выделять так, чтобы каждый из них тоже оставлял код в валидном состоянии.
79 |
80 | ## Не добавлять фич, не чинить багов
81 |
82 | Во время рефакторинга мы можем найти кусок кода, который работает неправильно. Может возникнуть желание «исправить это по пути», но багфиксы и новые фичи к рефакторингу лучше не примешивать.
83 |
84 | Рефакторинг _не должен менять функциональность_ кода. Если мы добавили фичу во время рефакторинга, и её потребуется откатить, то нам придётся переносить конкретные коммиты или даже строчки кода руками.
85 |
86 | Вместо этого все найденные идеи для фич лучше положить в отдельный список и вернуться к ним после рефакторинга. Если мы нашли баг, то рефакторинг стоит отложить (`git stash`) и вернуться к нему после фикса. Для такой манёвренности опять же удобнее работать маленькими шагами.
87 |
88 | ## Соблюдать приоритет преобразований
89 |
90 | _Приоритет преобразований (Transformation Priority Premise, TPP)_ — это список действий, которые помогают развивать код от наивной простейшей реализации до более сложной, которая отвечает всем требованиям проекта.[^tpp]
91 |
92 | TPP помогает _не пытаться сделать всё сразу_. Он побуждает обновлять кусок кода постепенно, записывая каждый шаг в системе контроля версий, и чаще интегрировать изменения в основную ветку репозитория.
93 |
94 | Мне лично TPP помогает не перегружать голову деталями, пока я пишу код. Все идеи для улучшений, которые возникают по ходу работы, я складываю в отдельный список. Этот список я потом сравниваю с TPP и выбираю, что реализовать следующим шагом.
95 |
96 | | Зачем 🧠 |
97 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
98 | | «Выгруженные» из головы детали освобождают «оперативную память» мозга.[^shorttermmemory] Высвобожденные ресурсы можно потратить на детали задачи — это улучшает внимательность. |
99 |
100 | ## Не смешивать рефакторинг тестов и кода приложения
101 |
102 | Тесты и продуктовый код страхуют друг друга. Тесты проверяют, что мы не допустили ошибок в коде приложения, и наоборот. Если рефакторить их одновременно, вероятность пропустить ошибку становится выше.
103 |
104 | Рефакторить код приложения и тесты лучше по очереди. Если во время рефакторинга приложения мы заметили, что нужно отрефакторить тест, то стоит:
105 |
106 | - «Положить на полочку» изменения в коде приложения;
107 | - Отрефакторить нужный тест;
108 | - Проверить, что тест ломается по той причине, по которой должен;
109 | - «Достать с полочки» предыдущие изменения и продолжить работать над ними.
110 |
111 | | Подробнее 🔬 |
112 | | :--------------------------------------------------------------------------------- |
113 | | Чуть подробнее об этой технике мы поговорим в главе о рефакторинге тестового кода. |
114 |
115 | [^atomic]: Atomic Commit, Wikipedia https://en.wikipedia.org/wiki/Atomic_commit
116 | [^bisect]: git-bisect, Use binary search to find the commit that introduced a bug, https://git-scm.com/docs/git-bisect
117 | [^codethatfits]: “Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head
118 | [^beyond]: “Beyond Legacy Code” by David Scott Bernstein, https://www.goodreads.com/book/show/26088456-beyond-legacy-code
119 | [^tpp]: Transformation Priority Premise, Wikipedia https://en.wikipedia.org/wiki/Transformation_Priority_Premise
120 | [^communicationrules]: «Новые правила деловой переписки» М. Ильяхов, Л. Сарычева, https://www.goodreads.com/book/show/41070833
121 | [^shorttermmemory]: Оценка емкости рабочей памяти, Википедия, https://ru.wikipedia.org/wiki/Рабочая_память#Оценка_емкости_рабочей_памяти
122 |
--------------------------------------------------------------------------------
/manuscript-en/20-refactoring-process.md:
--------------------------------------------------------------------------------
1 | # Refactoring as a Process
2 |
3 | In the previous chapters, we primarily focused on the technical details of refactoring. However, this is only a part of improving the code in a project.
4 |
5 | In this chapter, we'll talk about how to find time for refactoring and perform it regularly. We'll discuss how to overcome “friction” in the project and start refactoring even if the code base is large and has a lot of legacy code.
6 |
7 | ## Refactor or Rewrite
8 |
9 | When we look at a big chunk of legacy code, the first thought is, “it's easier just to rewrite it from scratch.” Sometimes it's true, but it isn't always possible to adequately assess the state of the code at a glance.
10 |
11 | Before we decide whether to refactor or rewrite, we should evaluate three things: team resources, the benefits of our solution, and its risks.
12 |
13 | These parameters won't give us a direct answer to the question “What to do?” but often, this assessment alone is enough to decide. However, even if it isn't enough, it'll tell us what to investigate before we make a final decision.
14 |
15 | ## Resources
16 |
17 | By _resources_, we'll mean the time, knowledge, and experience the team has at its disposal. These resources can be “spent” on refactoring or rewriting code to improve it.
18 |
19 | ### Available Time
20 |
21 | To estimate the developers' free time, we can look back at the past experience with the project. We need to assess how much time the team spent on code improvements in the past and how often there were “empty holes” in the project schedule.
22 |
23 | This assessment shows how the team spent time on development in the past. It helps predict how much time we'll have in the future.
24 |
25 | This prediction may seem an understatement because we may want to spend more time improving the code. However, it reflects the development patterns that the team is used to. These patterns are helpful to consider because even if we agree to spend more time on refactoring, the development will fall back to familiar patterns without a change in the process.
26 |
27 | In addition, we should always be aware of “unpredicted problems” with legacy code that will take extra time to solve.
28 |
29 | ### Accumulated Knowledge
30 |
31 | Accumulated knowledge about the project can be assessed by the amount of documentation, valuable comments in the code, the quality of the commit history, the availability of the developers who wrote the code, and the clarity of the code itself.
32 |
33 | Knowledge is difficult to quantify, so we can use a qualitative assessment. The more contradictions we find between different sources of information, the worse we can consider the quality of the accumulated knowledge. Conversely, the clearer the code and easier it is to find the developers who wrote it, the higher the quality of expertise.
34 |
35 | ### Experience
36 |
37 | By experience, we mean the developers' familiarity with this project and proficiency in other projects, languages, and paradigms—the more varied the experience, the less time we'll spend developing poor non-working solutions.
38 |
39 | | By the way 🧑🏫 |
40 | | :----------------------------------------------------------------------------------------------------------------------------------------------- |
41 | | It's also worth considering the experience of outside consultants and experts if we want them to participate, but it's harder to do objectively. |
42 |
43 | ## Benefits and Risks
44 |
45 | We should also define the benefits and risks of refactoring or rewriting the code. They will directly depend on the work processes and the project's inner structure, so we might need internal research.
46 |
47 | ## Project Meta Information
48 |
49 | The project's meta information reflects the process of working on the project and what parts of the code are the most important. All the necessary meta information is already there if the team uses a version control system. For example, the commit history can show us:
50 |
51 | - What is a crucial part of the project—what files were updated with the new features and bug fixes the most;
52 | - What implicit coupling exists between project parts—what files were modified along with other files;
53 | - What parts of the project had the most bugs—what commits were related to bug fixes, etc.
54 |
55 | Meta information analysis can tell us which parts of the project are the most complex or beneficial from a business perspective.
56 |
57 | | Read more 📚 |
58 | | :----------------------------------------------------------------------------------------------- |
59 | | Adam Tornhill wrote more about this assessment in the book “Your Code as a Crime Scene.”[^scene] |
60 |
61 | If we don't have a version control system, we can try to collect indirect indicators: release notes, tech support logs, etc. However, it'll be much harder to conclude from them.
62 |
63 | ## Estimates
64 |
65 | If we decide to refactor a particular piece of code, we'll need to estimate the required time.
66 |
67 | To understand how much time a piece of code can take to refactor, we should first point out the places that raise questions. To find out what problems we're dealing with, we can lay out those questions in the following table:
68 |
69 | | | Simple | Difficult |
70 | | ------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------- |
71 | | Clear | Research is unnecessary, clear how to solve, and takes minimal time. | Clear how to solve but definitely takes a long time. |
72 | | Unclear | The problem is small and isolated but might need research. | Definitely need research, lots of hidden connections, unfamiliar area. |
73 |
74 | The amount of time needed directly depends on the proportion of topics in this table. The more complex and unclear questions we have, the harder it is to give an exact estimate.
75 |
76 | Tasks from the last table cell are the most difficult to plan. For such problems, we can suggest the team use the “Hypothesis per Iteration” method. Each assumption about the problem would be a separate development iteration in this method. Disproving or confirming that assumption is the goal of the iteration.
77 |
78 | Testing one assumption doesn't take as much time as examining the whole problem. Iterations with such checks are easier to plan and conduct. At the same time, with each tested assumption, we understand more about the problem, and sooner or later, it'll move from the lower right cell to another cell in the table. Then we can give a more accurate estimate.
79 |
80 | ## Refactoring Large Chunks of Code
81 |
82 | If a piece of code is too big and cannot be refactored all at once, it's helpful to follow the “Strangler Fig” method.
83 |
84 | The goal of the approach is _not to replace_ a piece of code under refactoring but to implement similar functionality _beside_ that code. We can develop this copy considering all the knowledge we now have and all the problems we want to avoid. When the copy is done, we replace the old code with it.
85 |
86 | | By the way 🌳 |
87 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88 | | The name of this approach comes from the analogy to the strangler fig plant.[^strangler] This plant winds around a tree, hardens, and when the tree dies, it stays in the shape of that tree. This method of refactoring does the same. We first “cover” a feature with new code and remove the old code inside after it's ready. |
89 |
90 | When working with the “Strangler Fig” approach, it's important to frequently merge the results into the main branch of the repo. This technique will help prevent merge conflicts in the version control system and keep the refactoring process from taking too much time.
91 |
92 | ## Frequency and Hygiene
93 |
94 | In general, it's better to refactor as often as possible. It's most convenient to refactor the code right after making changes to it. The best option is if we can come back to the code after a short rest to take a fresh look at it. This kind of review helps us identify poor solutions in the code sooner.
95 |
96 | When working with legacy code, we should refactor the code _before_ fixing bugs in it or adding new features. After refactoring, the code will be clearer, better covered by tests, and more pleasant to work with. Thus, we'll reduce the time needed to fix a bug or add a feature.
97 |
98 | Finally, when touching any code, it's worth remembering the “Boy-Scout Rule”:[^opportunistic][^codethatfits]
99 |
100 | ---
101 |
102 | **❗️ Leave the code behind in a better state than we found it**
103 |
104 | ---
105 |
106 | ## Metrics
107 |
108 | Although refactoring relies on subjective measures such as beauty and readability, we can still use quantitative metrics to assess the changes' quality.
109 |
110 | As the basis, we'll take the metrics from the talk “Where does bad code come from?” and expand the list.[^wherefrom] As a result, we'll get a list of 7 metrics, each of which answers the question, “How long does it take to XXX?”
111 |
112 | - **S**earch, how long it takes to find the required place in the code.
113 | - **W**rite, to write a new feature and cover it with tests.
114 | - **A**gree, to get developers to agree that “code is good enough.”
115 | - **R**ead, to read and understand what a piece of code does.
116 | - **M**odify, to modify the code to fit the new requirements.
117 | - **E**xecute, to execute/build/deploy a piece of code.
118 | - **D**ebug, to find and fix a bug.
119 |
120 | If we measure these metrics before and after refactoring, we can compare the quality of the changes we've made. Some metrics are still quite subjective but can be expressed in numbers. A quantitative description of characteristics will help us to notice a trend of improvement or deterioration when estimating code quality.
121 |
122 | [^scene]: “Your Code As a Crime Scene” by Adam Tornhill, https://www.goodreads.com/book/show/23627482-your-code-as-a-crime-scene
123 | [^strangler]: “Strangler Fig Application” by Martin Fowler https://martinfowler.com/bliki/StranglerFigApplication.html
124 | [^opportunistic]: “Opportunistic Refactoring” by Martin Fowler https://martinfowler.com/bliki/OpportunisticRefactoring.html
125 | [^codethatfits]: “Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head
126 | [^wherefrom]: “Where Does Bad Code Come From?” https://youtu.be/7YpFGkG-u1w
127 |
--------------------------------------------------------------------------------
/manuscript-ru/19-comments-and-docs.md:
--------------------------------------------------------------------------------
1 | # Рядом с кодом
2 |
3 | Обычно рефакторинг затрагивает только код приложения и тесты. Но иногда во время рефакторинга бывает полезно коснуться комментариев в коде, проектной документации и рабочих процессов типа код-ревью.
4 |
5 | В этой главе мы поговорим о том, как и зачем рефакторить комментарии и документацию. Обсудим, как искать противоречащие друг другу источники информации о проекте и на что обращать внимание в процессах разработки.
6 |
7 | ## Источники правды
8 |
9 | Работа над проектом строится из различных задач: написания и тестирования кода, обсуждения ограничений, уточнения требований. В процессе работы над ними мы производим код, документацию, проектный план, бэклог и другое.
10 |
11 | Для работы _приложения_ из всего перечисленного реально значим только код. Он непосредственно влияет на выполнение программы, а всё остальное — нет.
12 |
13 | | К слову 💬 |
14 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15 | | Вообще, в JavaScript есть инструменты, которые используют комментарии JSDoc как сигнатуры,[^jsdocts] но это уже скорее _типы_, чем комментарии. Мы под комментариями будем иметь в виду те, что не влияют на исполнение программы. |
16 |
17 | _Разработчики_ же учитывают не только код, но и всё, что находится «рядом с ним». Мы читаем комментарии, документацию и сообщения из код-ревью, чтобы составить более полное представление о задаче и проекте.
18 |
19 | Обычно то, что «рядом с кодом», устаревает быстрее него. Из-за этого информация из разных источников может противоречить, а нам может быть сложно понять, чему доверять. Например, во фрагменте ниже комментарий конфликтует с сигнатурой функции:
20 |
21 | ```js
22 | /**
23 | * @param {User} Current user object.
24 | * @param {Product[]} List of products for the order.
25 | * @return {Order}
26 | */
27 | function createOrder(userId, products) {
28 | /**
29 | * В аннотации User, а в реализации UserId...
30 | * Такое противоречие может вызвать вопрос,
31 | * как должно быть на самом деле и где ошибка:
32 | * - А как правильно: как работает код, или как написано в аннотации?
33 | * - А что из этого менялось последним? А почему приняли такое решение?
34 | * - А как написано в документации? А что скажет продукт-оунер?
35 | */
36 | }
37 | ```
38 |
39 | Фрагмент выше предлагает нам два противоречащих друг другу утверждения. Такие противоречия затрудняют чтение кода и замедляют разработку. Мы не можем быть уверены, что правильно поняли код, пока не разрешим их, а это может требовать усилий и времени.
40 |
41 | Чтобы избежать подобных проблем, полезно проводить ревью комментариев, документации и других источников информации о проекте, во время которых выявлять и разрешать противоречия.
42 |
43 | ## Комментарии
44 |
45 | Для «рефакторинга» комментариев мы можем воспользоваться несколькими инструментами и эвристиками:
46 |
47 | ### Лживые комментарии исправить или удалить
48 |
49 | Комментарии могут врать — то есть сообщать читателю неправильную информацию. Иногда ложь может быть откровенной:
50 |
51 | ```js
52 | /** @obsolete Not used anymore across the code base. */
53 | function isEmpty(cart) {}
54 |
55 | // ...
56 |
57 | // Комментарий врёт, функция-то используется...
58 | if (!isEmpty(cart)) {
59 | }
60 | ```
61 |
62 | ...А иногда ложь может быть менее очевидной. Во втором случае, чтобы убедиться в лживости подозрительного комментария, мы можем попробовать опровергнуть утверждение из него.
63 |
64 | За опровержением мы можем обратиться к другим разработчикам или документации. Например, мы можем найти коллег, которые точно знают, как функция должна работать, и спросить о подозрительном комментарии у них.
65 |
66 | Когда возможности обратиться к команде и документации нет, мы можем провести серию экспериментов над противоречивым кодом. Нам потребуется покрыть этот код тестами и понаблюдать, как они себя ведут при изменении работы функции.
67 |
68 | Если мы убедились, что комментарий лживый, его стоит удалить или переписать так, чтобы в нём не осталось противоречий. Этот шаг важно отметить в сообщении к комиту в репозитории. В нём полезно написать, почему мы заподозрили, что комментарий врёт, и что именно помогло выявить ложь.
69 |
70 | | К слову 💡 |
71 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72 | | Изменения комментариев тоже лучше оформлять в виде отдельных комитов, как и другие техники рефакторинга. Это помогает отражать в сообщениях к комитам контекст задачи и цель изменений. |
73 |
74 | ### Расплывчатые комментарии уточнить
75 |
76 | Расплывчатым, неточным или двусмысленным комментариям стоит добавить деталей: примеров вызова функции, ссылок на конкретные PR, задачи или баги в трекере. Например, такой комментарий не очень полезен:
77 |
78 | ```js
79 | // Custom sorting function:
80 | function sort(a, b) {}
81 | ```
82 |
83 | То, что функция `sort` отвечает за сортировку, понятно из её названия. Вместо этого лучше описать, какой алгоритм сортировки она реализует, добавить примеров работы и ссылок на детальное описание алгоритма:
84 |
85 | ```js
86 | /**
87 | * Implements the most efficient sorting algorithm
88 | * by comparing electron movement in the circuitry.
89 | * @see https://wiki.our-project.app/sorting/electron-movement-sort/
90 | * @example ...
91 | *
92 | * @param {Sortable} a
93 | * @param {Sortable} b
94 | * @return {CompareResult}
95 | */
96 | function superFastSort(a, b) {}
97 | ```
98 |
99 | ### Мелкие уточняющие комментарии перенести в код
100 |
101 | Небольшие уточнения из комментариев можно перенести прямо в имена и сигнатуры переменных, функций или методов:
102 |
103 | ```ts
104 | // Fetches post contents by the author's ID.
105 | async function getPost(user) {}
106 |
107 | // ↓
108 | async function fetchPostContents(authorId) {}
109 |
110 | // ↓
111 | async function fetchPost(authorId: UserId): Promise {}
112 | ```
113 |
114 | Это срабатывает не всегда. Детали из комментария могут сделать имя переменной или функции слишком длинным. В таких случаях лучше откатить изменения.
115 |
116 | ### «Пересказам названий» добавить контекста
117 |
118 | Некоторые комментарии не несут пользы и лишь пересказывают другими словами имя сущности, которую комментируют:
119 |
120 | ```js
121 | // Compares strings.
122 | function compareString(a, b) {}
123 | ```
124 |
125 | В таких случаях нам, опять же, будет полезнее описать в комментарии контекст задачи. Например, для функции `compareString` лучше указать, _почему_ мы используем собственную реализацию функции сравнения. Причина появления функции передаст больше деталей задачи и проекта в целом, чем просто пересказ имени:
126 |
127 | ```js
128 | /**
129 | * Implements compare for our limited alphabet with custom diacritic rules.
130 | * - Required for correct handling of ...
131 | * - Justified as a part of R&D in ...
132 | * @see https://wiki.our-project.app/sorting/custom-diacritic-comparer/
133 | *
134 | * @example compareString('a', 'ä') === -1
135 | * @example compareString('a', 't') === -1
136 | */
137 | function compareString(a, b) {}
138 | ```
139 |
140 | ### TODO и FIXME превратить в задачи
141 |
142 | Иногда комментарии содержат `TODO` и `FIXME` теги с описанием ошибок или недостатков в коде. Содержимое таких комментариев полезно превращать в тикеты в трекере задач проекта.
143 |
144 | В отличие от комментариев задачи в трекере видны другим разработчикам и остальной команде. По количеству таких задач можно судить о состоянии проекта и накопившемся техническом долге.
145 |
146 | В описании создаваемых задач стоит указать, как и почему код работает сейчас и что мы хотим изменить. Ссылки на созданные задачи можно оставить в комментарии рядом с кодом. Тогда такой комментарий:
147 |
148 | ```js
149 | // TODO: improve post-conditions.
150 | function ensureMessageSent() {}
151 | ```
152 |
153 | ...Превратится в задачу и ссылку на неё:
154 |
155 | ```js
156 | /**
157 | * For now, it's not clear what criteria to use for the verification.
158 | * Update post-conditions when the criteria are known:
159 | * @see https://our-team.tasktracker.com/our-product/task-42
160 | */
161 | function ensureMessageSent() {}
162 | ```
163 |
164 | Наиболее эффективная стратегия для этого — проводить регулярные ревью с поиском тегов и превращением их в задачи. Тогда разработчики также смогут создавать `TODO` и `FIXME` теги в коде, чтобы «не отрываться» от работы во время написания кода. Но далее во время регулярных ревью эти комментарии будут конвертироваться в задачи, что не даст выйти количеству тегов из-под контроля.
165 |
166 | ## Документация
167 |
168 | Проектная документация хранит историю развития приложения и причины принятых технических решений. Она отвечает на вопросы разработчиков, но её проблема в том, что она устаревает быстрее кода.
169 |
170 | Обычно документация не интегрирована в код и существует отдельно. Из-за этого обновлять её нужно _дополнительно_ к изменениям в коде, а значит вероятность забыть об этом выше, и расхождений между ней и кодом будет становиться больше.
171 |
172 | Обновление документации вслед за изменениями кода требует дисциплины или _процесса_. Чтобы документация не устаревала, стоит завести регулярную задачу на её ревью. Раз в определённый период времени (скажем, в месяц) мы будем сравнивать отличия между документацией и кодом и исправлять их.
173 |
174 | Регулярные задачи ограничивают размер расхождений, потому что они существуют недолго и не успевают накопиться. Когда разработчики периодически их «подчищают», негативный эффект от расхождений будет меньше.
175 |
176 | | К слову 🎥 |
177 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
178 | | Чтобы регулярные аудиты не превращались в чрезмерно объёмные задачи, в документации стоит хранить в основном code-agnostic информацию, которая скорее относится к предметной области и вряд ли будет меняться так же быстро, как код. |
179 | | А вот архитектурно-важные решения может быть полезно хранить ближе к коду, чем остальную документацию. Если эти решения влияют на организацию кода или принципы работы приложения, разработчикам должно быть удобно к ним обращаться, не тратя лишнего времени. |
180 | | Один из подходов к работе с такими решениями известен как _Architectural Decision Records, ADR_.[^adr] |
181 |
182 | ## Доступность знаний
183 |
184 | Также полезно постараться сделать знания о проекте и предметной области как можно _более доступными команде разработки_. Под доступностью знаний мы будем понимать, насколько быстро и удобно разработчики могут их найти.
185 |
186 | Чем больше разработчики знают о предметной области, тем меньше ошибок они допустят в коде приложения и больше противоречий выявят на ранних этапах жизни проекта. Чем доступнее информация в проекте, тем меньше времени будет занимать её поиск и меньше будет замедляться разработка.
187 |
188 | Знания можно хранить по-разному: с помощью документации, метаданных репозитория, комментариев в коде или самого кода. Доступность разных вариантов отличается. Например, код доступнее всего — он всегда под рукой разработчика. Метаданные из репозитория или документация менее доступны, потому что к ним нужно обращаться отдельно.
189 |
190 | Чтобы сделать информацию доступнее, мы можем во время регулярных ревью переносить знания «ближе к коду»:
191 |
192 | - Детали из комментариев переносить в переменные, функции и типы;
193 | - Замечания из код-ревью — в код, документацию или сообщения комитов;
194 | - Инсайты из «разговоров у кулера» — в документацию или трекер задач.
195 |
196 | Эта техника может не сработать, если в проекте уже есть процесс, который регламентирует работу с документацией или репозиторием. Но в проектах «без процесса» помнить о повышении доступности информации может быть полезно.
197 |
198 | [^jsdocts]: JSDoc Reference, TypeScript Documentation https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
199 | [^adr]: Architectural Decision Records, ADR, https://adr.github.io
200 |
--------------------------------------------------------------------------------
/manuscript-en/19-comments-and-docs.md:
--------------------------------------------------------------------------------
1 | # Comments and Documentation
2 |
3 | Usually, refactoring only affects the application code and its tests. But sometimes, we'll need to improve comments in the code, project documentation, and development process during refactoring.
4 |
5 | In this chapter, we'll talk about how and why to refactor these things. We'll discuss how to look for conflicting sources of information about the project and what to pay attention to in the development process.
6 |
7 | ## Sources of Truth
8 |
9 | Work on a project consists of various tasks. We write code and test it, discuss constraints and the domain, clarify the project requirements, etc. In the process, we produce code, documentation, project plans, backlogs, and more.
10 |
11 | From the _application_ point of view, the only important part is the code. The code directly affects the program execution, and everything else doesn't.
12 |
13 | | By the way 💬 |
14 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15 | | In JavaScript, some tools use JSDoc comments as signatures,[^jsdocts] but these are more _types_ than comments. By comments, we'll mean those that don't affect program execution. |
16 |
17 | On the other hand, developers consider the code and everything “around it.” We pay attention to comments, documentation, and code reviews to better understand a particular task and the whole project.
18 |
19 | Usually, everything “around the code” gets out of date faster than the code itself. Because of this, information from different sources can be contradictory, and we might doubt what to trust. For example, in the snippet below, the comment conflicts with the function signature:
20 |
21 | ```js
22 | /**
23 | * @param {User} Current user object.
24 | * @param {Product[]} List of products for the order.
25 | * @return {Order}
26 | */
27 | function createOrder(userId, products) {
28 | /**
29 | * In the annotation, the first argument is typed as `User`
30 | * but the implementation seems to use `UserId`.
31 | * This contradiction questions how the code should really work:
32 | * - What's correct: the code or the annotation?
33 | * - What has changed last? Why has that decision been made?
34 | * - What does the documentation say? What do the product owners say?
35 | */
36 | }
37 | ```
38 |
39 | The snippet above offers us two contradictory statements. Such contradictions make it hard to read the code and slow down development. We can't be sure we understand the code correctly until we resolve those contradictions. Such a resolution can take effort and time.
40 |
41 | To avoid such problems, we can conduct reviews of comments, documentation, and other sources of information about the project. During these reviews, we should identify the contradictions and resolve them.
42 |
43 | ## Comments
44 |
45 | We can use several tools and heuristics to “refactor” comments:
46 |
47 | ### Correct or Delete False Comments
48 |
49 | Comments can lie and tell the reader incorrect information. Sometimes the lies can be blatant:
50 |
51 | ```js
52 | /** @obsolete Not used anymore across the code base. */
53 | function isEmpty(cart) {}
54 |
55 | // ...
56 |
57 | // The comment above lies the function _is_ used.
58 | if (!isEmpty(cart)) {
59 | }
60 | ```
61 |
62 | However, sometimes the lie can be less noticeable. In this case, to see if a suspicious comment is false, we should try to disprove its statement.
63 |
64 | To do so, we can ask for help from other developers or documentation. For example, we can find developers who know exactly how the function should work and ask them about the suspicious comment.
65 |
66 | When there's no way to contact the team or use documentation, we can conduct a series of experiments on the suspicious code. We'll need to cover this code with tests and observe how they behave when we change the function.
67 |
68 | If we're convinced that the comment is false, we should delete it or rewrite it to remove all contradictions. It's important to note this step in the commit message. It's helpful to describe why we suspected the comment was false, and what exactly helped reveal the lie.
69 |
70 | | By the way 💡 |
71 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
72 | | Just like other refactoring techniques, comment refactoring is better done in small steps and separate commits. It helps to describe the context of the problem and the purpose of the changes in the commit message. |
73 |
74 | ### Clarify Vague Comments
75 |
76 | For vague, inaccurate, or ambiguous comments, we should add details. We can add examples of function calls, links to specific pull requests, tasks, or issues in the task tracker. For example, such a comment isn't constructive:
77 |
78 | ```js
79 | // Custom sorting function:
80 | function sort(a, b) {}
81 | ```
82 |
83 | The fact that the `sort` function “sorts stuff” is clear from its name. Instead, it'd be better to describe what sorting algorithm is used, add examples of how it works, and links to a detailed description of the algorithm:
84 |
85 | ```js
86 | /**
87 | * Implements the most efficient sorting algorithm
88 | * by comparing electron movement in the circuitry.
89 | * @see https://wiki.our-project.app/sorting/electron-movement-sort/
90 | * @example ...
91 | *
92 | * @param {Sortable} a
93 | * @param {Sortable} b
94 | * @return {CompareResult}
95 | */
96 | function superFastSort(a, b) {}
97 | ```
98 |
99 | ### Inline Small Details in the Code
100 |
101 | Comments containing small details can be “inlined” in the types and names of variables, functions, or methods:
102 |
103 | ```ts
104 | // Fetches post contents by the author's ID.
105 | async function getPost(user) {}
106 |
107 | // We can express it like this ↓
108 | async function fetchPostContents(authorId) {}
109 |
110 | // Or even like this using types ↓
111 | async function fetchPost(authorId: UserId): Promise {}
112 | ```
113 |
114 | This technique doesn't always work. It can make the variable or function name too long. In such cases, it's better not to apply the changes.
115 |
116 | ### Describe Context Instead of “Rephrasing Names”
117 |
118 | Some comments are unhelpful because they only rephrase the name of the commented entity with different words:
119 |
120 | ```js
121 | // Compares strings.
122 | function compareString(a, b) {}
123 | ```
124 |
125 | In such cases, again, it'd be more beneficial to put what isn't known to the developers in the comment and describe the task's context.
126 |
127 | For example, for the `compareString` function, it's better to indicate _why_ we use our own implementation of the comparison function. The reason behind the function will convey more details about the problem and the project as a whole:
128 |
129 | ```js
130 | /**
131 | * Implements compare for our limited alphabet with custom diacritic rules.
132 | * - Required for the correct handling of ...
133 | * - Justified as a part of R&D in ...
134 | * @see https://wiki.our-project.app/sorting/custom-diacritic-comparer/
135 | *
136 | * @example compareString('a', 'ä') === -1
137 | * @example compareString('a', 't') === -1
138 | */
139 | function compareString(a, b) {}
140 | ```
141 |
142 | ### Turn TODOs and FIXMEs into Tasks
143 |
144 | Sometimes comments contain `TODO` and `FIXME` tags describing bugs or flaws in the code. It's useful to turn the contents of such comments into tickets in the project's task tracker.
145 |
146 | Unlike comments, tasks in the tracker are visible to other developers and the rest of the team. By the number of such tasks, we can evaluate the state of the project and estimate the accumulated technical debt.
147 |
148 | The description of the created tasks is worth specifying how and why the code works now and what we want to change. Links to the created tasks can be left in a comment next to the code. Then a comment like this:
149 |
150 | ```js
151 | // TODO: improve post-conditions.
152 | function ensureMessageSent() {}
153 | ```
154 |
155 | ...Will turn into:
156 |
157 | ```js
158 | /**
159 | * For now, it's unclear what criteria to use for the verification.
160 | * Update post-conditions when the criteria are known:
161 | * @see https://our-team.tasktracker.com/our-product/task-42
162 | */
163 | function ensureMessageSent() {}
164 | ```
165 |
166 | The most effective strategy for this is to conduct regular reviews, look for tags, and turn them into tasks. Then developers can still create `TODO` and `FIXME` tags in the code while writing code to “stay focused on a task.” But then, during regular reviews, these comments will be converted to tickets, keeping the number of such tags under control.
167 |
168 | ## Documentation
169 |
170 | The project documentation keeps the history of the application development and the reasons for the technical decisions made. It's useful because it answers developers' questions, but its problem is that it becomes obsolete faster than the code.
171 |
172 | The documentation isn't usually integrated into the code but exists separately. Because of this, we have to update it _in addition to changes in the code_. This separation makes it harder to remember to update documentation after the code. It increases the number of discrepancies between the docs and the code.
173 |
174 | Updating documentation and code requires a _process_. To keep the documentation from becoming obsolete, we should have a regular task to review it. Once every certain period of time (say, a month), we compare differences between documentation and code and fix them.
175 |
176 | Regular tasks limit the size of discrepancies because they don't last long and don't have time to accumulate. When developers periodically “clean up” discrepancies, their negative effect is less.
177 |
178 | | By the way 🎥 |
179 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
180 | | To make regular reviews less excessive, we should store in the docs mostly code-agnostic information, which is more related to the domain and the business and is unlikely to change as quickly as the code. |
181 | | On the other hand, storing architecture-important decisions closer to the code can be more useful. If these decisions affect the organization of the code or the way the application works, it should be easy for developers to refer to them without wasting time. |
182 | | One approach to dealing with such decisions is known as _Architectural Decision Records, ADR_.[^adr] |
183 |
184 | ## Knowledge Accessibility
185 |
186 | It's also helpful to make knowledge about the project and the subject area as _available to the development team_ as possible. The easier and quicker developers can find and refer to a piece of knowledge, the more accessible it is.
187 |
188 | When developers know about the subject area, they make fewer errors in the application code and find inconsistencies in the project earlier. Accessible knowledge makes development more convenient and thus speeds it up.
189 |
190 | Knowledge can be stored in different ways: in documentation, repository metadata, code comments, or the code itself. The accessibility of different options varies. For example, information in the code is most accessible because the code is always at the developer's fingertips. Repository metadata or documentation is less accessible because it exists separately and has to be accessed differently.
191 |
192 | To make information more accessible, we can move it “closer to the code” during regular reviews:
193 |
194 | - Turn details from comments into variables, functions, and types;
195 | - Turn notes from code reviews into code, documentation, or commit messages;
196 | - Turn insights from “conversations at the cooler” into documentation or issues in the repo or task tracker.
197 |
198 | This technique may not work if the project already has a process governing documentation or repository working strategies. But in projects “without a process,” the idea of knowledge accessibility can be helpful.
199 |
200 | [^jsdocts]: JSDoc Reference, TypeScript Documentation https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html
201 | [^adr]: Architectural Decision Records, ADR, https://adr.github.io
202 |
--------------------------------------------------------------------------------
/manuscript-en/18-test-code.md:
--------------------------------------------------------------------------------
1 | # Refactoring Test Code
2 |
3 | Tests help us refactor application code by indicating errors we may have made. But the tests are code, too, so we must refactor them from time to time.
4 |
5 | In this chapter, we'll talk about how not to break tests during refactoring, how to keep their reliability, and what to pay attention to when searching for problems with the test code.
6 |
7 | ## “Tests” for Tests
8 |
9 | A reliable test fails when the code doesn't work as expected. Tests “cover our back” during the refactoring of application code because they will catch an error in the app behavior. The application code, on the other hand, covers the tests because it helps check if they fail for the right reasons.
10 |
11 | When tests change along with the application code, we can't check if everything works _as before_. The updated tests may contain bugs or check something _different_ from the original functionality, and we might not even notice it.
12 |
13 | As a result, we might start trusting tests that don't work or work incorrectly. To avoid this, while refactoring test code, we should follow the rule:
14 |
15 | ---
16 |
17 | **❗️ Alternate refactoring tests and the application code. Avoid doing it at the same time**.
18 |
19 | ---
20 |
21 | If, during refactoring, we realize that we need to refactor the test code, we should:
22 |
23 | - Stash the application code changes from the last commit (using `git stash`)
24 | - Refactor the tests
25 | - Check that they fail for the specified reasons
26 | - Commit test changes
27 | - Unstash the application code changes
28 | - Continue refactoring
29 |
30 | This way, we turn the refactoring into “ping-ponging” between refactoring the tests and the application code. They support and cover each other, ensuring the overall program behavior stays the same.
31 |
32 |
33 |
34 | When we change the code, the behavior is captured by the tests. When we change the tests, the application code captures the behavior
35 |
36 |
37 | This technique doesn't guarantee that we won't make any mistakes but reduces their probability. With it, there's always at least one part (either tests or code) that _hasn't changed since the last moment everything worked_. So we're more confident that everything works as before.
38 |
39 | | By the way 📚 |
40 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41 | | In “Code That Fits in Your Head,”[^codethatfits] Mark Seemann recommends doing the same thing. Besides the technique itself in the book, he also describes how to avoid weakening tests during refactoring. |
42 |
43 | ## “Brittle” Tests
44 |
45 | Sometimes tests feel “brittle” and unreliable. Most of the time, this happens because of _mocks_. Mocks are demanding about their internal structure, the order and manner in which they're called, and the structure of results they return. In some cases, an application can become “over-mocked”—when mocks replace almost all modules in tests.
46 |
47 | In such cases, any changes to the application code, even the smallest ones, result in many updates to the test code. The tests require more resources to support and slow down the development. This effect is called _test-induced damage_.[^testinduceddamage]
48 |
49 | Unlike mocks, _stubs and simple test data_ help write more change-resistant tests. They're more straightforward in use and forgive far more than they demand. They help us avoid test-induced damage and spend less time updating test code.
50 |
51 | | In detail 🥸 |
52 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------- |
53 | | The difference between stubs, mocks, and other fake objects, is well described by Microsoft.[^unittestsdotnet] We'll use their terminology in this chapter. |
54 |
55 | To make the tests less brittle, we can use this heuristic:
56 |
57 | ---
58 |
59 | **❗️ Use fewer mocks. Make stubs and test data simpler**
60 |
61 | ---
62 |
63 | For example, to reduce the number of mocks, we can organize business logic to test it without mocks. It isn't easy to achieve when the logic is mixed with various effects. So it's better to keep effects separated and describe the logic in the form of pure functions.
64 |
65 | Pure functions are intrinsically testable. They don't require a fancy test infrastructure and only need the test data and the expected result to be tested.
66 |
67 | Let's look at the difference between code where logic and effects are mixed and code where they're separated. Notice how brittle their tests seem:
68 |
69 | ```js
70 | // In this function, the logic and effects are mixed:
71 |
72 | function fetchPostList(kind) {
73 | const directory = path.resolve("content", kind);
74 | const onlyMdx = fs.readDirSync(directory).filter((f) => f.endsWith(".mdx"));
75 | const postNames = onlyMdx.map((f) => f.replace(/\.mdx$/, ""));
76 | return postNames;
77 | }
78 |
79 | // For a unit test of such a function, we need to mock `fs`.
80 | // We need to describe the work of the used method,
81 | // specify the results of that method,
82 | // reset the mock after the test:
83 |
84 | it("should return a list of post names with the given kind", () => {
85 | jest.spyOn(fs, "readDirSync").mockImplementation(() => testFileList);
86 | const result = fetchPostList("blogPost");
87 | expect(result).toEqual(expected);
88 | jest.restoreAllMocks();
89 | });
90 | ```
91 |
92 | In the second case, the logic and the effects are separated. The data transformation can be tested using only stubs and test data:
93 |
94 | ```ts
95 | function namesFromFiles(fileList) {
96 | return fileList
97 | .filter((f) => f.endsWith(".mdx"))
98 | .map((f) => f.replace(/\.mdx$/, ""));
99 | }
100 |
101 | // To test the function, it's enough
102 | // to only have test data and the expected result:
103 |
104 | it("should convert file list into a list of post names", () => {
105 | const result = namesFromFiles(testList);
106 | expect(result).toEqual(expected);
107 | });
108 | ```
109 |
110 | The test structure becomes simpler, and updating the test data doesn't take a lot of resources. With this code organization, we can completely abandon static test data and generate it automatically based on predefined properties. Such testing is sometimes called _property-based_.
111 |
112 | | By the way 🧪 |
113 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
114 | | It's usually convenient to use additional tools to generate test data in property-based tests.[^codethatfits] In the JavaScript ecosystem, libraries like faker.js,[^faker] create objects with random data according to predefined properties. |
115 |
116 | The effects we separated earlier can be tested by integration tests or E2E tests. Depending on how we organize the work with dependencies, it may be enough to test only adapters to them. In most cases, the complexity of mocks in such tests will be lower.
117 |
118 | For example, in tests of such an adapter for `fs`, it's enough to check that the correct method has been called with the required argument:
119 |
120 | ```ts
121 | function postsByType(kind) {
122 | const directory = path.resolve("content", kind);
123 | const fileList = fs.readDirSync(directory);
124 | return fileList;
125 | }
126 |
127 | // We don't need to mock the whole service implementation anymore,
128 | // it's enough just to expose the API similar to the service interface.
129 | // This kind of mock is much more resistant to changes in application code
130 | // and causes less test-induced damage.
131 |
132 | describe("when called with a post kind", () => {
133 | it("should read file list from the correct directory", () => {
134 | const spy = jest.spyOn(fs, "readDirSync");
135 | postsByType("blogPost");
136 | expect(spy).toHaveBeenCalledWith("/content/blogPost/");
137 | });
138 | });
139 | ```
140 |
141 | | In detail 🧩 |
142 | | :---------------------------------------------------------------------------------------------------------------------- |
143 | | More about dependency and effect organization strategies we discussed in the chapters on architecture and side effects. |
144 |
145 | Then the `fetchPostList` function now becomes a “composition” of logic with effects:
146 |
147 | ```ts
148 | function fetchPostList(kind) {
149 | // Read Effect:
150 | const fileList = postsByType(kind);
151 |
152 | // Pure Logic:
153 | return namesFromFiles(fileList);
154 | }
155 | ```
156 |
157 | Such a function may no longer need to be tested by unit tests. Since it combines the functionality of different modules (units), we can think about integration or E2E testing.
158 |
159 | ### Test Duplicates
160 |
161 | The test-induced damage slows down development because, after each code change, we have to update the tests. One reason for this slowdown can be tests that check the same functionality multiple times.
162 |
163 | Ideally, we want only _one_ test to be responsible for a particular part of the code. When there're more, we start spending unnecessary time updating them. The more duplicates, the greater the “time tax.”
164 |
165 | For example, if we wrote an additional unit test for the `fetchPostList` function in the example above, it would most likely be redundant and duplicate the tests of the `postsByType` and `namesFromFiles` functions. Then for every change to those functions, we would need to update not one but two tests.
166 |
167 | The duplicate tests could hint at one of several problems:
168 |
169 | 1. There may indeed be duplication in the application code. It's a reason to perform a review and reduce the duplication in the code.
170 | 1. The modules' responsibilities aren't clearly divided, so tests of one module check something already checked by others. It's a signal to define the concerns of different modules more clearly or reconsider the testing strategy to avoid overlapping.
171 |
172 | | Clarification 🧪 |
173 | | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174 | | Tests of _different kinds_ can sometimes overlap for reliability. An integration test may take over some of the functionality tested by unit tests if it's more convenient to test the application. |
175 | | I try to keep the number of such overlaps to a minimum, but testing strategies may differ from project to project, so it's difficult to give general recommendations here. |
176 |
177 | ### Never-Failing Tests
178 |
179 | A test should be responsible for a specific problem and _must fail_ when it occurs. If the test never fails, it's harmful: it has no value but takes resources for support. Such a test should be removed or rewritten to fail when the specified problem occurs.
180 |
181 | | By the way 🙃 |
182 | | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
183 | | Most often, I've encountered never-failing tests in over-mocked systems, where the infrastructure and test arrangement consisted almost entirely of calling mocks. Such tests often pass the result of one mock to another and end up testing nothing. |
184 |
185 | ### Tests for Simple Functions
186 |
187 | When choosing what and how to test, we should compare the benefits of the test and its costs. For example, we can pay attention to the cyclomatic complexity of the function this test covers.
188 |
189 | If the complexity of the function equals one and the test brings more additional work than real benefit, we can abandon the test. For example, a separate unit test for the `fullName` function may be unnecessary:
190 |
191 | ```js
192 | const fullName = (user) => `${user.firstName} ${user.lastName}`;
193 | ```
194 |
195 | | Clarification 🚧 |
196 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
197 | | We're not saying that simple functions don't need tests. The decision whether to test or not depends on the specific situation. The main idea is that if a test brings more costs than benefits, we should consider its necessity. |
198 |
199 | ### Regressions
200 |
201 | There are cases when simple functions still _need_ to be tested though, for example, if a function once had a regression. Regressions pay attention not to potential but actual bugs in the code, which _could and once did happen_.
202 |
203 | Anything that comes up in a regression should be covered with tests. If the test seems too simple, and someone might find it useless and delete it, we can add a note in the comment with a link to the regression:
204 |
205 | ```js
206 | /**
207 | * @regression JIRA-420: Users had full names in an incorrect format where the last name came before the first.
208 | * @see https://some-project.atlassian.com/...
209 | */
210 | describe("when called with a user object", () => {
211 | it("should return a full name representation with first name at start", () => {
212 | const name = fullName(42);
213 | expect(name).toEqual(expected);
214 | });
215 | });
216 | ```
217 |
218 | [^codethatfits]: “Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head
219 | [^testinduceddamage]: “Test-Induced Design Damage” by David Heinemeier Hansson, https://dhh.dk/2014/test-induced-design-damage.html
220 | [^unittestsdotnet]: Unit testing best practices with .NET Core and .NET Standard, Microsoft Docs, https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
221 | [^faker]: Faker, Generate fake (but realistic) data for testing and development, https://fakerjs.dev
222 |
--------------------------------------------------------------------------------
/manuscript-ru/18-test-code.md:
--------------------------------------------------------------------------------
1 | # Рефакторинг тестового кода
2 |
3 | Тесты помогают рефакторить код приложения, указывая на ошибки, которые мы могли допустить. Но сами тесты — это тоже код, поэтому их также нужно держать в порядке и время от времени рефакторить.
4 |
5 | В этой главе поговорим о том, как не поломать тесты и не снизить их надёжность во время рефакторинга и на что обращать внимание при поиске проблем с тестовым кодом.
6 |
7 | ## «Тесты» для тестов
8 |
9 | Надёжный тест падает, когда код работает не так, как ожидается. Тесты будто «прикрывают нам спину» во время изменения кода приложения, потому что ошибка в поведении отразится в тестах. Самим же тестам «прикрывает спину» продакшен-код, потому что именно в нём мы проверяем, что тесты падают по указанным причинах.
10 |
11 | Когда тесты меняются вместе с кодом приложения, у нас не остаётся способов проверить, что всё работает _как раньше_. Обновлённые тесты могут содержать ошибку или проверять что-то _отличное_ от оригинальной функциональности. Если код, по которому мы раньше могли проверить работу теста, тоже изменился, эта ошибка может остаться незамеченной.
12 |
13 | В итоге в проекте могут появиться тесты, которым мы доверяем, но которые не работают или работают неправильно. Чтобы этого не допустить, во время рефакторинга тестового кода нам стоит следовать правилу:
14 |
15 | ---
16 |
17 | **❗️ Не рефакторить тесты одновременно с кодом приложения. Лучше делать это по очереди**
18 |
19 | ---
20 |
21 | Если во время рефакторинга мы понимаем, что хотим отрефакторить тестовый код, то нам стоит:
22 |
23 | - Положить на полочку изменения кода с последнего комита (с помощью `git stash`);
24 | - Оытрефакторить код теста;
25 | - Проверить, что он падает по указанной причине;
26 | - Закомитить изменения тестов;
27 | - Достать с полочки изменения кода приложения;
28 | - Продолжить рефакторинг.
29 |
30 | Это правило превращает рефакторинг в «пинг-понг» между тестами и кодом приложения. Когда мы меняем код, поведение фиксируют и проверяют тесты. Когда мы меняем тесты, поведение фиксирует код приложения.
31 |
32 |
33 |
34 | Когда мы меняем код, поведение фиксируют и проверяют тесты. Когда мы меняем тесты, поведение фиксирует код приложения
35 |
36 |
37 | Эта техника не гарантирует, что мы не допустим никаких ошибок, но уменьшает их вероятность. При её использовании на каждом этапе есть как минимум одна часть, которая _не изменилась с последнего рабочего состояния_. Поэтому мы более уверены, что всё работает как раньше.
38 |
39 | | К слову 📚 |
40 | | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
41 | | Таким же образом рекомендует поступать и Марк Симанн в “Code That Fits in Your Head”.[^codethatfits] Кроме самой техники в книге он также рассказывает, как не допустить ослабления тестовых критериев во время рефакторинга. |
42 |
43 | ## «Хрупкие» тесты
44 |
45 | Во время работы с тестами стоит обращать внимание на свои ощущения. Если в тестах чувствуется «хрупкость» и ненадёжность, вероятно, их можно улучшить. Ощущение «дунешь, и всё развалится» — отличный индикатор хрупких тестов.
46 |
47 | Чаще всего хрупкими тесты делают _моки_. Они требовательны к внутренней структуре, порядку и способу их вызова и результату, который они должны вернуть. В запущенных случаях приложение может стать «сверх-замоканным» — когда работа почти всех модулей в тестах имитируется моками.
48 |
49 | В таких случаях любые изменения кода приложения, даже самые незначительные, приводят к большому количеству правок в тестах. Тесты требуют больше ресурсов на поддержку, и разработка приложения замедляется. Такой эффект называют _уроном от тестов (test-induced damage)_.[^testinduceddamage]
50 |
51 | В отличие от моков _стабы и простые тестовые данные_ помогают писать более устойчивые к изменениям тесты. Стабы и тестовые данные прощают нам при их использовании гораздо больше, чем требуют. Они помогают избегать урона от тестов и тратить меньше времени на их обновление.
52 |
53 | | Подробнее 🥸 |
54 | | :------------------------------------------------------------------------------------------------------------ |
55 | | О разнице между стабами, моками и другими фейковыми объектами, хорошо написано у Microsoft.[^unittestsdotnet] |
56 |
57 | Для уменьшения хрупкости тестов мы можем руководствоваться эвристикой:
58 |
59 | ---
60 |
61 | **❗️ Меньше моков; проще стабы и тестовые данные**
62 |
63 | ---
64 |
65 | Например, бизнес-логику мы можем тестировать без моков в принципе. Это сложно, когда логика перемешана с эффектами; но проще, если она описана чистыми функциями.
66 |
67 | Чистые функции тестируемы по своей природе. Они не требуют замороченной тестовой инфраструктуры, тесты для них могут быть запущены параллельно с разработкой. (Такие функции можно протестировать даже без тест-раннеров, сравнивая настоящий результат с желаемым — они настолько просты в тестировании.)
68 |
69 | Посмотрим на разницу между кодом, где логика и эффекты перемешаны, и кодом, где они разделены. Обратим внимание, насколько «хрупкими» кажутся их тесты. В первом случае логика и эффекты смешаны:
70 |
71 | ```js
72 | function fetchPostList(kind) {
73 | const directory = path.resolve("content", kind);
74 | const onlyMdx = fs.readDirSync(directory).filter((f) => f.endsWith(".mdx"));
75 | const postNames = onlyMdx.map((f) => f.replace(/\.mdx$/, ""));
76 | return postNames;
77 | }
78 |
79 | // Для юнит-теста такой функции нам потребуется замокать `fs`,
80 | // описать работу нужного метода,
81 | // указать, что метод должен вернуть,
82 | // сбросить мок после окончания теста:
83 |
84 | it("should return a list of post names with the given kind", () => {
85 | jest.spyOn(fs, "readDirSync").mockImplementation(() => testFileList);
86 | const result = fetchPostList("blogPost");
87 | expect(result).toEqual(expected);
88 | jest.restoreAllMocks();
89 | });
90 | ```
91 |
92 | Во втором случае логика отделена от эффектов. Преобразование можно протестировать используя только стабы и тестовые данные:
93 |
94 | ```ts
95 | function namesFromFiles(fileList) {
96 | return fileList
97 | .filter((f) => f.endsWith(".mdx"))
98 | .map((f) => f.replace(/\.mdx$/, ""));
99 | }
100 |
101 | // Для сравнения достаточно тестовых данных
102 | // и желаемого результата работы функции:
103 |
104 | it("should convert file list into a list of post names", () => {
105 | const result = namesFromFiles(testList);
106 | expect(result).toEqual(expected);
107 | });
108 | ```
109 |
110 | Структура теста становится проще, а обновление тестовых данных не отнимает большого количества ресурсов. С такой организацией кода мы даже можем полностью отказаться от статических тестовых данных и генерировать их автоматически по заранее определённым свойствам. Такое тестирование будет называться _атрибутным (property-based)_.
111 |
112 | | К слову 🧪 |
113 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
114 | | Для генерации тестовых данных в property-based тестах обычно удобно использовать дополнительные инструменты.[^codethatfits] Например, в JS-экосистеме существуют библиотеки типа faker.js,[^faker] которые создают объекты со случайными данными по заранее описанным шаблонам. |
115 |
116 | Выделенные эффекты можно протестировать отдельно интеграционными или E2E тестами. В зависимости оттого, как в нашем проекте устроена работа зависимостями типа `fs`, нам может быть достаточно протестировать только адаптеры к ним. Как правило, сложность и требовательность моков в таком случае будет ниже.
117 |
118 | Например, в тестах подобного адаптера для `fs` нам достаточно будет проверить, что был вызван правильный метод с нужным аргументом:
119 |
120 | ```ts
121 | function postsByType(kind) {
122 | const directory = path.resolve("content", kind);
123 | const fileList = fs.readDirSync(directory);
124 | return fileList;
125 | }
126 |
127 | // Нам уже не нужно мокать реализацию «сервиса»,
128 | // достаточно предоставить нужное публичное API.
129 | // Такой мок гораздо более устойчив к изменениям
130 | // кода приложения и наносит меньше «урона от тестов».
131 |
132 | describe("when called with a post kind", () => {
133 | it("should read file list from the correct directory", () => {
134 | const spy = jest.spyOn(fs, "readDirSync");
135 | postsByType("blogPost");
136 | expect(spy).toHaveBeenCalledWith("/content/blogPost/");
137 | });
138 | });
139 | ```
140 |
141 | Тогда сама функция `fetchPostList` превратится в «композицию» логики с эффектами:
142 |
143 | ```ts
144 | function fetchPostList(kind) {
145 | // Чтение данных, эффект:
146 | const fileList = postsByType(kind);
147 |
148 | // Логика, чистые функции:
149 | return namesFromFiles(fileList);
150 | }
151 | ```
152 |
153 | Такую функцию проверять юнит-тестами уже может оказаться не нужно. Она объединяет функциональность разных модулей (юнитов), поэтому мы можем подумать об интеграционном или E2E-тестировании.
154 |
155 | | Подробнее 🧩 |
156 | | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
157 | | Более подробно о том, какие бывают стратегии работы с зависимостями и организации эффектов, мы говорили ранее в главах об архитектуре и сайд-эффектах. |
158 |
159 | ### Тесты-дубликаты
160 |
161 | Урон от тестов замедляет разработку, потому что после каждого изменения код приходится тратить много ресурсов на исправление тестов. Одной из причин такого замедления могут быть тесты, которые тестируют одну и ту же функциональность несколько раз.
162 |
163 | В идеале мы хотим, чтобы за одну часть кода отвечал _один_ тест. Когда тестов становится больше, мы начинаем тратить лишнее время на их обновление. Чем больше дубликатов, тем больше временной «налог».
164 |
165 | Например, если бы в примере выше мы написали дополнительный юнит-тест для функции `fetchPostList`, скорее всего, он бы оказался лишним и дублировал тесты функций `postsByType` и `namesFromFiles`. Тогда на каждое изменение `postsByType` или `namesFromFiles` нам бы пришлось обновлять не один тест, а два.
166 |
167 | Тесты-дубликаты могут намекнуть на одну из нескольких проблем:
168 |
169 | 1. В коде приложения действительно может быть дублирование. Это повод провести ревью и устранить повторяющуюся функциональность. (Подробнее о том, как разделять дублирование и недостаток информации о системе мы говорили в одной из предыдущих глав.)
170 | 1. В коде нечётко разделена ответственность между модулями; тесты одного модуля частично перекрывают функциональность другого. Например, один тест может проверять то, что уже проверено другими. Это может быть поводом пересмотреть стратегию тестирования и чётче определить границы модулей.
171 |
172 | | Уточнение 🧪 |
173 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
174 | | Тесты _разных видов_ для надёжности могут перекрывать друг друга. Например, интеграционный тест может захватить часть функциональности, проверенной юнит-тестами, если так удобнее тестировать приложение. |
175 | | Лично я стараюсь держать количество и таких перекрываний минимальным, но в разных проектах стратегия тестирования может отличаться, поэтому дать общие рекомендации здесь сложно. |
176 |
177 | ### Тесты, которые никогда не ломаются
178 |
179 | Тест должен отвечать за конкретную проблему, при появлении которой обязан упасть. Если тест никогда не падает, он вреден: пользы не приносит, но отнимает ресурсы на поддержку. Такой тест стоит удалить или переписать так, чтобы он начал падать в описанных обстоятельствах.
180 |
181 | | К слову 🙃 |
182 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
183 | | Чаще всего никогда-не-падающие тесты я встречал в сверх-замоканных системах, где инфраструктура и подготовка к тесту почти полностью состояли из вызова моков. Такие тесты часто передают результат работы одного мока в другой — и в итоге не проверяют ничего. |
184 |
185 | ### Тесты простых функций
186 |
187 | При выборе что и как тестировать, нам стоит сравнивать пользу от теста и его издержки. Например, можно обратить внимание на цикломатическую сложность функции, которую этот тест проверяет.
188 |
189 | Если сложность функции равна единице, а тест приносит больше дополнительной работы, чем реальной пользы, то от теста можно отказаться. Например, отдельный юнит-тест для функции `fullName` может быть лишним:
190 |
191 | ```js
192 | const fullName = (user) => `${user.firstName} ${user.lastName}`;
193 | ```
194 |
195 | | Уточнение 🚧 |
196 | | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
197 | | Мы здесь не утверждаем, что несложным функциям тесты не нужны вовсе. Решение, тестировать или нет, зависит от конкретной ситуации. Главная идея в том, что если тест приносит больше издержек, чем пользы, стоит подумать о его необходимости. |
198 |
199 | ### Регрессии
200 |
201 | Иногда простые функции всё же _надо_ тестировать: например, если в функции когда-то была регрессия. Регрессии обращают внимание не на потенциальные, а на настоящие баги в коде, которые _действительно могут случиться и однажды случились_.
202 |
203 | Всё, что всплыло во время регрессий, надо закрыть тестами. Если кажется, что тест слишком простой, и кто-то посчитает его бесполезным и удалит, то можно добавить аннотацию в комментарий со ссылкой на регрессию.
204 |
205 | ```js
206 | /**
207 | * @regression JIRA-420: Users had full names in an incorrect format where last name came before first.
208 | * @see https://some-project.atlassian.com/...
209 | */
210 | describe("when called with a user object", () => {
211 | it("should return a full name representation with first name at start", () => {
212 | const name = fullName(42);
213 | expect(name).toEqual(expected);
214 | });
215 | });
216 | ```
217 |
218 | [^codethatfits]: “Code That Fits in Your Head” by Mark Seemann, https://www.goodreads.com/book/show/57345272-code-that-fits-in-your-head
219 | [^testinduceddamage]: “Test-Induced Design Damage” by David Heinemeier Hansson, https://dhh.dk/2014/test-induced-design-damage.html
220 | [^unittestsdotnet]: Unit testing best practices with .NET Core and .NET Standard, Microsoft Docs, https://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices
221 | [^faker]: Faker, Generate fake (but realistic) data for testing and development, https://fakerjs.dev
222 |
--------------------------------------------------------------------------------