├── images ├── 16-fsm.png ├── cover-en.png ├── cover-ru.png ├── 05-git-diff.png ├── 05-linter.png ├── 11-impureim.png ├── 05-rename-symbol.png ├── 08-app-fractal.png ├── 10-early-return.png ├── 12-prevalidation.png ├── 13-message-bus.png ├── 03-result-on-edge.png ├── 08-fractal-levels.png ├── 09-bounded-context.png ├── 15-ports-adapters.png ├── 08-app-detalization.png ├── 10-complexity-linter.png ├── 12-linear-execution.png ├── 12-result-conjunction.png ├── 13-coupling-cohesion.png ├── 08-fractal-architecture.png ├── 09-functional-pipeline.png ├── 08-has-access-detalization.png ├── 10-cyclomatic-complexity.png ├── 18-refactoring-ping-pong.png ├── 08-app-detalization-simpler.png └── 11-referential-transparency.png ├── README.md ├── manuscript-ru ├── README.md ├── 21-afterword.md ├── 01-preface.md ├── TOC.md ├── 22-cheatsheet.md ├── 03-before-start.md ├── 05-low-hanging-fruit.md ├── 07-duplication.md ├── 02-introduction.md ├── 20-refactoring-process.md ├── 04-during-refactoring.md ├── 19-comments-and-docs.md └── 18-test-code.md └── manuscript-en ├── README.md ├── 21-afterword.md ├── 01-preface.md ├── TOC.md ├── 03-before-start.md ├── 22-cheatsheet.md ├── 05-low-hanging-fruit.md ├── 07-duplication.md ├── 02-introduction.md ├── 04-during-refactoring.md ├── 20-refactoring-process.md ├── 19-comments-and-docs.md └── 18-test-code.md /images/16-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/16-fsm.png -------------------------------------------------------------------------------- /images/cover-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/cover-en.png -------------------------------------------------------------------------------- /images/cover-ru.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/cover-ru.png -------------------------------------------------------------------------------- /images/05-git-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/05-git-diff.png -------------------------------------------------------------------------------- /images/05-linter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/05-linter.png -------------------------------------------------------------------------------- /images/11-impureim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/11-impureim.png -------------------------------------------------------------------------------- /images/05-rename-symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/05-rename-symbol.png -------------------------------------------------------------------------------- /images/08-app-fractal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/08-app-fractal.png -------------------------------------------------------------------------------- /images/10-early-return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/10-early-return.png -------------------------------------------------------------------------------- /images/12-prevalidation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/12-prevalidation.png -------------------------------------------------------------------------------- /images/13-message-bus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/13-message-bus.png -------------------------------------------------------------------------------- /images/03-result-on-edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/03-result-on-edge.png -------------------------------------------------------------------------------- /images/08-fractal-levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/08-fractal-levels.png -------------------------------------------------------------------------------- /images/09-bounded-context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/09-bounded-context.png -------------------------------------------------------------------------------- /images/15-ports-adapters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/15-ports-adapters.png -------------------------------------------------------------------------------- /images/08-app-detalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/08-app-detalization.png -------------------------------------------------------------------------------- /images/10-complexity-linter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/10-complexity-linter.png -------------------------------------------------------------------------------- /images/12-linear-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/12-linear-execution.png -------------------------------------------------------------------------------- /images/12-result-conjunction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/12-result-conjunction.png -------------------------------------------------------------------------------- /images/13-coupling-cohesion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/13-coupling-cohesion.png -------------------------------------------------------------------------------- /images/08-fractal-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/08-fractal-architecture.png -------------------------------------------------------------------------------- /images/09-functional-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/09-functional-pipeline.png -------------------------------------------------------------------------------- /images/08-has-access-detalization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/08-has-access-detalization.png -------------------------------------------------------------------------------- /images/10-cyclomatic-complexity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/10-cyclomatic-complexity.png -------------------------------------------------------------------------------- /images/18-refactoring-ping-pong.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/18-refactoring-ping-pong.png -------------------------------------------------------------------------------- /images/08-app-detalization-simpler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/08-app-detalization-simpler.png -------------------------------------------------------------------------------- /images/11-referential-transparency.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Archakov06/refactor-like-a-superhero-online-book/HEAD/images/11-referential-transparency.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Refactor Like a Superhero 2 | 3 |
4 | 5 |
“Refactor Like a Superhero” book cover
6 |
7 | 8 | --- 9 | 10 | It is a book about how to efficiently refactor code. In it, we'll discuss the benefits of refactoring for developers and business, how to search for problems in your code, and how to solve them. 11 | 12 | ## Manuscript 13 | 14 | The book is currently available in 2 languages: 15 | 16 | - [In English](./manuscript-en/README.md) 17 | - [In Russian](./manuscript-ru/README.md) 18 | 19 | Currently WIP translations: 20 | 21 | - [Spanish by @DavidRamiroBarragan](https://github.com/DavidRamiroBarragan/refactor-like-a-superhero-online-book) 22 | - [Portuguese (pt-br) by @thiagodsti](https://github.com/thiagodsti/refactor-like-a-superhero-online-book) 23 | 24 | If you are interested in translating it into other languages, please, contact me. I'll be happy to discuss details! 25 | 26 | ## Errata and Feedback 27 | 28 | If you found a typo or an error, please, open an issue or a pull request in this repository. I'll also be glad to hear your ideas and code snippets that can make the examples in the book more descriptive. 29 | 30 | You can find all previous fixes and updates in the commit history of this repository. 31 | 32 | ## License & Copyright 33 | 34 | All materials are © 2022 Alex Bespoyasov. The work is licensed under the [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](http://creativecommons.org/licenses/by-nc-nd/4.0/). 35 | 36 | [![CC BY-NC-ND 4.0](https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/) 37 | -------------------------------------------------------------------------------- /manuscript-ru/README.md: -------------------------------------------------------------------------------- 1 | # Рефакторинг на максималках 2 | 3 |
4 | 5 |
Обложка книги «Рефакторинг на максималках»
6 |
7 | 8 | --- 9 | 10 | Книга о том, как эффективно рефакторить код, в чём польза рефакторинга для разработки и бизнеса, а также как искать проблемы в коде и что с ними делать. 11 | 12 | ## [Содержание](./TOC.md) 13 | 14 | - [Предисловие](./01-preface.md) 15 | - [Введение](./02-introduction.md) 16 | - [Прежде, чем начать](./03-before-start.md) 17 | - [Во время рефакторинга](./04-during-refactoring.md) 18 | - [Низко-висящие фрукты](./05-low-hanging-fruit.md) 19 | - [Имена](./06-names.md) 20 | - [Дублирование кода](./07-duplication.md) 21 | - [Абстракция](./08-abstraction.md) 22 | - [Функциональный пайплайн](./09-functional-pipeline.md) 23 | - [Условия и сложность кода](./10-conditions.md) 24 | - [Сайд-эффекты](./11-side-effects.md) 25 | - [Обработка ошибок](./12-error-handling.md) 26 | - [Интеграция модулей](./13-module-integration.md) 27 | - [Обобщения и иерархии](./14-generics.md) 28 | - [Архитектура](./15-architecture.md) 29 | - [Декларативность](./16-declarative-style.md) 30 | - [Статическая типизация](./17-static-typing.md) 31 | - [Рефакторинг тестового кода](./18-test-code.md) 32 | - [Рядом с кодом](./19-comments-and-docs.md) 33 | - [Рефакторинг как процесс](./20-refactoring-process.md) 34 | - [Заключение](./21-afterword.md) 35 | - [Шпаргалка по техникам рефакторинга](./22-cheatsheet.md) 36 | - [Список литературы](./23-sources.md) 37 | 38 | ## Об авторе 39 | 40 | Саша Беспоясов, девелопер в 0+X, в веб-разработке с 2010 года. 41 | 42 | - [bespoyasov.ru](https://bespoyasov.ru) 43 | - [twitter](https://twitter.com/bespoyasov) 44 | - [0x.se](https://0x.se) 45 | 46 | ## Лицензии и копирайт 47 | 48 | Текст и иллюстрации книги распространяются под лицензией [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](http://creativecommons.org/licenses/by-nc-nd/4.0/). 49 | 50 | [![CC BY-NC-ND 4.0](https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/) 51 | -------------------------------------------------------------------------------- /manuscript-en/README.md: -------------------------------------------------------------------------------- 1 | # Refactor Like a Superhero 2 | 3 |
4 | 5 |
“Refactor Like a Superhero” book cover
6 |
7 | 8 | --- 9 | 10 | It is a book about how to efficiently refactor code. In it, we'll discuss the benefits of refactoring for developers and business, how to search for problems in your code, and how to solve them. 11 | 12 | ## [Table of Contents](./TOC.md) 13 | 14 | - [Preface](./01-preface.md) 15 | - [Introduction](./02-introduction.md) 16 | - [Before Refactoring](./03-before-start.md) 17 | - [During Refactoring](./04-during-refactoring.md) 18 | - [Low-Hanging Fruit](./05-low-hanging-fruit.md) 19 | - [Names](./06-names.md) 20 | - [Code Duplication](./07-duplication.md) 21 | - [Abstraction](./08-abstraction.md) 22 | - [Functional Pipeline](./09-functional-pipeline.md) 23 | - [Conditions and Complexity](./10-conditions.md) 24 | - [Side Effects](./11-side-effects.md) 25 | - [Error Handling](./12-error-handling.md) 26 | - [Module Integration](./13-module-integration.md) 27 | - [Generics, Inheritance, and Composition](./14-generics.md) 28 | - [App Architecture](./15-architecture.md) 29 | - [Declarative Style](./16-declarative-style.md) 30 | - [Static Typing](./17-static-typing.md) 31 | - [Refactoring Test Code](./18-test-code.md) 32 | - [Comments and Documentation](./19-comments-and-docs.md) 33 | - [Refactoring as a Process](./20-refactoring-process.md) 34 | - [Afterword](./21-afterword.md) 35 | - [Cheat Sheet on Refactoring Techniques](./22-cheatsheet.md) 36 | - [Sources and References](./23-sources.md) 37 | 38 | ## About Author 39 | 40 | Alex Bespoyasov, software engineer at 0+X, web developer since 2010. 41 | 42 | - [bespoyasov.me](https://bespoyasov.me) 43 | - [twitter](https://twitter.com/bespoyasov_) 44 | - [0x.se](https://0x.se) 45 | 46 | ## License & Copyright 47 | 48 | All materials are © 2022 Alex Bespoyasov. The work is licensed under the [Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License](http://creativecommons.org/licenses/by-nc-nd/4.0/). 49 | 50 | [![CC BY-NC-ND 4.0](https://i.creativecommons.org/l/by-nc-nd/4.0/88x31.png)](http://creativecommons.org/licenses/by-nc-nd/4.0/) 51 | -------------------------------------------------------------------------------- /manuscript-en/21-afterword.md: -------------------------------------------------------------------------------- 1 | # Afterword 2 | 3 | Thank you for reading this book! I hope you find it helpful, and I would love to hear your feedback and ideas. 4 | 5 | ## Errata and Feedback 6 | 7 | If you found a typo or an error, please, open an issue in the project's repository or send them by email.[^repo][^email] I'll also be glad to hear your ideas and code snippets that can make the examples in the book more descriptive. 8 | 9 | You can find previous fixes and updates in the commit history of the project's repository.[^repo] 10 | 11 | ## For Publishers 12 | 13 | This book is a large draft of a book on refactoring that I would like to write in the future. 14 | 15 | In the future book, I would like to combine fragmented code examples into a single application. It would help to show how to refactor not various pieces of code but whole projects. In my opinion, it would be more demonstrative and helpful. 16 | 17 | Refactoring such a project would cover all the techniques mentioned in this book combined under the shared context of a single application. It would make it possible to see how the different refactoring techniques support each other and help work toward a common result. 18 | 19 | If you'd like to publish such a book, please, contact me by mail.[^email] I'd be happy to discuss the details! 20 | 21 | ## For Translators 22 | 23 | This book is available in 2 languages: Russian and English. If you are interested in translating it into other languages, please, contact me by mail.[^email] 24 | 25 | ## Acknowledgements 26 | 27 | Thanks to Maksim Ivanov (@satansdeer),[^satansdeer] Nick Lopin (@nlopin),[^nlopin] and Artem Samofalov (@dex157)[^dex157] for help in proofreading and fact-checking! 28 | 29 | Thanks to all project contributors for finding typos, improving phrasing, and other help with the text![^contributors] 30 | 31 | ## Extras 32 | 33 | In the appendices to this book, you'll find a list of links to books, articles, talks, and tools that I refer to in my work and can recommend. 34 | 35 | You'll also find a list of shortened recommendations for finding and fixing code problems based on the topics of the previous chapters. 36 | 37 | [^repo]: Refactor Like a Superhero, Book Repository, https://github.com/bespoyasov/refactor-like-a-superhero-online-book 38 | [^email]: Contact Email, bespoyasov@me.com 39 | [^satansdeer]: Maksim Ivanov, https://github.com/satansdeer 40 | [^nlopin]: Nikolai Lopin, https://github.com/nlopin 41 | [^dex157]: Artem Samofalov, https://github.com/dex157 42 | [^contributors]: Project Contributors, https://github.com/bespoyasov/refactor-like-a-superhero-online-book/graphs/contributors 43 | -------------------------------------------------------------------------------- /manuscript-ru/21-afterword.md: -------------------------------------------------------------------------------- 1 | # Заключение 2 | 3 | Спасибо, что дочитали до конца! 4 | 5 | Я надеюсь, техники, приёмы и ссылки из этой книжки окажутся вам полезны! Буду рад услышать ваш фидбек и дополнить книгу новыми идеями. 6 | 7 | ## Ошибки, фидбек и история изменений 8 | 9 | Опечатки и ошибки присылайте в ишью репозитория проекта или мне на почту.[^repo][^email] Я также буду рад идеям и фрагментам кода с более показательными примерами и контрпримерами описанных в книге техник. 10 | 11 | Историю изменений и обновлений текста книги вы сможете найти в истории комитов в репозитории.[^repo] 12 | 13 | ## Издателям 14 | 15 | Этот текст — большой черновик для книги о рефакторинге, которую мне бы хотелось написать в будущем. 16 | 17 | В будущей книге мне хочется объединить разрозненные примеры кода в одно приложение, чтобы показать, как можно рефакторить не кусочки кода, а целые проекты. Мне кажется, это может быть более показательно и полезно. 18 | 19 | Рефакторинг такого проекта охватил бы все упомянутые в этой книги приёмы, но был бы объединён общим контекстом одного приложения. Это бы позволило увидеть, как различные техники рефакторинга поддерживают друг друга и помогают работать на общий результат. 20 | 21 | Если вам хотелось бы издать такую книгу, свяжитесь со мной по почте.[^email] Буду рад обсудить детали! 22 | 23 | ## Переводчикам 24 | 25 | Сейчас книга доступна на 2 языках: русском и английском. Если вам интересно перевести её на другие языки, пожалуйста, свяжитесь со мной по почте.[^email] 26 | 27 | ## Благодарности 28 | 29 | Спасибо Максиму Иванову (@satansdeer),[^satansdeer] Нику Лопину (@nlopin)[^nlopin] и Артёму Самофалову (@dex157)[^dex157] за помощь в вычитке, факт-чекинге и ссылки на дополнительные материалы! 30 | 31 | Также спасибо всем контрибьюторам за помощь с опечатками, формулировками и другими правками текста![^contributors] 32 | 33 | ## Дополнительные материалы 34 | 35 | В приложениях к этой книге вы найдёте список ссылок на книги, статьи, доклады и инструменты, которые я использую в работе сам и могу порекомендовать вам. 36 | 37 | Также вы найдёте сокращённый список рекомендаций для поиска и исправления проблем с кодом, основанных на тезисах и доводах из предыдущих глав. 38 | 39 | [^repo]: Рефакторинг на максималках, https://github.com/bespoyasov/refactor-like-a-superhero-online-book 40 | [^email]: Почта для связи, bespoyasov@me.com 41 | [^satansdeer]: Максим Иванов, https://github.com/satansdeer 42 | [^nlopin]: Ник Лопин, https://github.com/nlopin 43 | [^dex157]: Артём Самофалов, https://github.com/dex157 44 | [^contributors]: Контрибьюторы проекта, https://github.com/bespoyasov/refactor-like-a-superhero-online-book/graphs/contributors 45 | -------------------------------------------------------------------------------- /manuscript-en/01-preface.md: -------------------------------------------------------------------------------- 1 | # Preface 2 | 3 | The idea for this book appeared after my talk “Refactoring Like a Superhero,”[^talk] which I was making in January 2022. For that talk, I've collected various technics and heuristics of refactoring I wanted to share. 4 | 5 | At some point, it became apparent that I couldn't fit everything I wanted to discuss into a 40-minute slot. I had to cut and trim the material for the talk to show the most valuable practices and technics. 6 | 7 | The cut-out material wasn't useless, though. It contained details and clarifications about the technics' usage and applicability. I decided it would make more sense not to throw out everything that didn't fit but to change the format and release it as an online book. That's how this project came to be. 8 | 9 | ## Who Might Want to Read This Book 10 | 11 | This book might be helpful for developers of web services and user applications who write in high-level languages and have a couple of years of experience. 12 | 13 | Library developers might also find some ideas, but I'll mainly focus on user applications since I have more experience in that area. 14 | 15 | This book doesn't consider the needs and constraints of low-level development. Some heuristics in it might contradict best practices of low-level code. If you write “closer to the hardware,” please, be careful, and proceed at your own risk 😃 16 | 17 | ## What This Book Is Not 18 | 19 | I don't claim to show “the only correct way” to refactor and write code in this book. If you have a lot of experience, you probably already know most of the techniques described and have your own opinion. 20 | 21 | Also, this is not a “step-by-step manual” universally applicable to all projects. My development experience biases my coding habits and programming style. Your experience, projects, and habits may differ from mine, so our views may not be the same. That's okay. 22 | 23 | The purpose of this book is to describe a set of practices and heuristics that helped _me_ start writing code that _looks good_ to me and the people I worked with. 24 | 25 | Not all practices have to be applied at all times. Using an idea depends significantly on the project, the context, available resources, and the purpose of refactoring. Try to choose techniques that bring more benefits at less cost. 26 | 27 | If something in the book seems helpful to you, discuss it with your colleagues and other developers. Make sure you and your team have the same opinion about the chosen idea's benefits and costs. Don't apply something controversial to your team. 28 | 29 | ## Limitations and Applicability 30 | 31 | All of the techniques described here are a compilation of the books I've read and my experience in development. I have spent most of my time developing medium and sometimes large user applications. 32 | 33 | My experience imprints on the way I see good code. Basically, the whole book is a snapshot of my perception of development in 2022. My opinion may have changed if you're reading this in the future. 34 | 35 | | By the way 🐝 | 36 | | :------------------------------------------------------------------------------------------------------------------ | 37 | | I will update the book's text as my opinion changes, but I can't guarantee I will do so promptly and without delay. | 38 | 39 | As you read the book, be aware of the author's cognitive biases. Weigh the techniques before using them and consider their applicability to your project. 40 | 41 | ## Good to Know Before Reading 42 | 43 | In the text, I assume you're already familiar with the concept of refactoring[^term] and that you have a couple of years of programming experience. I expect you've already encountered issues with task decomposition, “leaky” abstractions, separation of concerns between modules, etc. 44 | 45 | I expect you've heard about some of the “programming buzzwords” on this list: 46 | 47 | - Separation of concerns 48 | - Coupling and cohesion 49 | - Declarative style 50 | - Abstraction layers 51 | - Command-query separation 52 | - Referential transparency 53 | - Functional pipeline 54 | - Functional core in an imperative shell 55 | - Layered architecture, “Ports and Adapters” 56 | - Direction of dependencies 57 | - Immutability and statelessness 58 | - 12-factor applications 59 | 60 | You don't have to _know_ them. We'll be exploring the terms and techniques as the book progresses. But it's good if you have a rough idea about their basic meaning. 61 | 62 | ## Why Another Book 63 | 64 | There are many books on refactoring, why do we need another one? 65 | 66 | By and large, we don't. All the principles and techniques are described in other books, most likely in a much more detailed, logical, and correct way. 67 | 68 | I need this text, first of all, for myself: 69 | 70 | - To systematize what I know today; 71 | - To refer to a specific topic when discussing a problem; 72 | - To mark the level of my knowledge so that I understand where to improve and what to learn. 73 | 74 | However, I thought that maybe this text could be helpful to someone else, so here we are. I hope you'll find this book helpful! 75 | 76 | [^talk]: “Refactor Like a Superhero” Talk, https://bespoyasov.me/talks/refactor-like-a-superhero/ 77 | [^term]: Code Refactoring, Wikipedia, https://en.wikipedia.org/wiki/Code_refactoring 78 | -------------------------------------------------------------------------------- /manuscript-ru/01-preface.md: -------------------------------------------------------------------------------- 1 | # Предисловие 2 | 3 | Эта книжка выросла из моего доклада «Рефакторинг на максималках»[^talk], который я готовил в январе 2022 года. Для этого доклада я собрал различные эвристики и техники рефакторинга, которыми хотел поделиться. 4 | 5 | При подготовке стало очевидно, что в 40-минутный слот не получится затолкать всё, о чём хотелось бы рассказать. Материала оказалось много — пришлось его ужимать, фильтровать и обрезать, чтобы уместиться в отведённое время и показать полезные практики. 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 | Мой опыт накладывает отпечаток на то, как я вижу хороший код. По сути вся книжка — это такой снапшот моего восприятия разработки в 2022 году. Если вы читаете это в будущем (привет!), возможно, моё мнение поменялось, так бывает. 34 | 35 | | К слову 🐝 | 36 | | :------------------------------------------------------------------------------------------------------------------------------ | 37 | | Я буду обновлять текст книги по мере изменения мнения, но не могу гарантировать, что буду это делать оперативно и без задержек. | 38 | 39 | При чтении книги помните о когнитивных искажениях автора. Мысленно сравнивайте техники со своим проектом и думайте об их применимости. 40 | 41 | ## Что хорошо знать перед прочтением 42 | 43 | В тексте я подразумеваю, что вы уже знакомы с самим понятием рефакторинга[^term] и что у вас есть пара лет опыта программирования. Я ожидаю, что вы уже сталкивались с проблемами декомпозиции задач, «протекающих» абстракций, разграничением ответственности между модулями и т.д. 44 | 45 | Я рассчитываю, что вы слышали о какой-то части «программистских словечек» из вот этого списка: 46 | 47 | - Разделение ответственности 48 | - Зацепление и связность 49 | - Декларативность 50 | - Слои абстракции 51 | - Разделение на команды и запросы 52 | - Ссылочная прозрачность 53 | - Функциональный пайплайн 54 | - Функциональное ядро в императивной оболочке 55 | - Слои архитектуры, порты и адаптеры 56 | - Направление зависимостей 57 | - Неизменяемость и отсутствие состояния 58 | - 12-факторные приложения 59 | 60 | Вам не обязательно знать их все, потому что мы будем раскрывать смысл техник по ходу книги. Но будет хорошо, если вы имеете представление, в чём их польза и основной смысл. 61 | 62 | ## Зачем нужна ещё одна книга 63 | 64 | Книг о рефакторинге много, зачем нужна ещё одна? 65 | 66 | По большому счёту — ни зачем. Все принципы, техники и причины их появления описаны в других книгах и, скорее всего, гораздо более подробно, детально, логично и корректно. 67 | 68 | Мне этот текст нужен в первую очередь самому: 69 | 70 | - для систематизации того, что я знаю сам; 71 | - для возможности сослаться на конкретное место при дискуссии о какой-то проблеме; 72 | - для фиксации уровня знаний в конкретном моменте времени, чтобы понимать куда расти. 73 | 74 | Но я подумал, что возможно, это может оказаться полезным кому-то ещё. Так этот проект и появился. 75 | 76 | [^talk]: Доклад «Рефакторинг на максималках», https://bespoyasov.ru/talks/refactor-like-a-superhero/ 77 | [^term]: Рефакторинг, Википедия, https://ru.wikipedia.org/wiki/Рефакторинг 78 | -------------------------------------------------------------------------------- /manuscript-en/TOC.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | - Preface 4 | - Who Might Want to Read This Book 5 | - What This Book Isn't 6 | - Limits and Constraints 7 | - Good to Know Before Reading 8 | - Why Another Book 9 | - Introduction 10 | - Benefits for Developers 11 | - Benefits for Business 12 | - “Good” Code, “Bad” Code 13 | - Hard to Read 14 | - Hard to Change 15 | - Hard to Test 16 | - Hard to “Fit in the Head” 17 | - Code Smells 18 | - Before Refactoring 19 | - Define Scope and Boundaries 20 | - Cover with Tests 21 | - Find More Edge Cases 22 | - Define Explicit and Implicit Input 23 | - Specify Desired Result 24 | - Set Up Automatic Tests 25 | - Make Linter and Compiler Settings Stricter 26 | - Use “Errors” Instead of “Warnings” 27 | - Configure More Automated Checks 28 | - During Refactoring 29 | - Small Steps 30 | - Small but Detailed Pull Requests 31 | - Tests for Every Change 32 | - One Technique at a Time 33 | - No Features, No Bug Fixes 34 | - Transformation Priority Premise 35 | - Refactoring Tests Separately 36 | - Low-Hanging Fruit 37 | - Code Formatting 38 | - Code Linting 39 | - Language Features 40 | - Environment Features 41 | - Names 42 | - General Guidelines 43 | - Too Short Names 44 | - Too Long Names 45 | - Option for Naming Functions 46 | - Different Entities with Identical Names 47 | - Ubiquitous Language 48 | - Lying Names 49 | - Domain Types 50 | - Code Duplication 51 | - Not All Duplication is Evil 52 | - Variables for Data 53 | - Functions for Actions 54 | - Abstraction 55 | - Intent and Implementation 56 | - Fractal Architecture 57 | - Separation of Concerns 58 | - Task Decomposition 59 | - Single Responsibility Principle 60 | - Encapsulation 61 | - Functional Pipeline 62 | - Data Transformations 63 | - Data States 64 | - Unrepresentable Invalid States 65 | - Data Validation 66 | - Data Mapping and Selectors 67 | - Conditions and Complexity 68 | - Cyclomatic and Cognitive Complexity 69 | - Flat Over Nested 70 | - Early Return 71 | - Component Rendering 72 | - Variables, Predicates, and Boolean Algebra 73 | - De Morgan's Laws 74 | - Predicates 75 | - Primitive Pattern Matching 76 | - Strategy 77 | - Null-Object 78 | - Side Effects 79 | - Pure Functions 80 | - Referential Transparency 81 | - Immutable by Default 82 | - Functional Core in Imperative Shell 83 | - Adapters for Effects 84 | - Commands and Queries 85 | - CQS and Generated IDs 86 | - CQRS 87 | - Error Handling 88 | - Types of Errors 89 | - Handling Techniques 90 | - Throwing Panics 91 | - Unexpected Errors and Missing Context 92 | - Different Error Types 93 | - Fail Fast 94 | - Rethrow 95 | - Advantages 96 | - Problems 97 | - Result Containers 98 | - More Accurate Signature 99 | - Explicit Handling 100 | - Centralized Handling 101 | - Panics are Separated 102 | - Multiple Errors 103 | - Unpacking 104 | - Binding Results 105 | - Problems 106 | - When to Prefer Panics 107 | - Cross-Cutting Concerns 108 | - So Many Different Handlers 109 | - Error Handling Hierarchy 110 | - Wrappers for “Low-Level” Code 111 | - Use Case Handlers 112 | - Last Resort Handlers 113 | - Data Prevalidation 114 | - Module Integration 115 | - Coupling and Cohesion 116 | - Separation of Concerns 117 | - Integration Rule 118 | - Task Decomposition 119 | - Search for Cohesion 120 | - Contracts 121 | - Events and Messages 122 | - Dependencies 123 | - Object Composition 124 | - Separate Data and Behavior 125 | - Interface Segregation Principle 126 | - Functional Composition 127 | - Dependency Rejection 128 | - Other Dependency Management Options 129 | - Integrity and Consistency 130 | - Aggregates 131 | - Input Prevalidation 132 | - Generics, Inheritance, and Composition 133 | - Generic Algorithms 134 | - Generic Types 135 | - Inheritance and Composition 136 | - Inheritance 137 | - The Liskov Substitution Principle 138 | - App Architecture 139 | - Not About File Structure 140 | - Business Workflow Modeling 141 | - Ubiquitous Language 142 | - Domain Model 143 | - Dependency Direction 144 | - Interaction with the World 145 | - Ports and Adapters 146 | - All Kinds of Architecture 147 | - UI Logic 148 | - Reactivity 149 | - Testing 150 | - Declarative Style 151 | - Readability 152 | - Reliability 153 | - Extensibility 154 | - Configurability 155 | - State Machines 156 | - Drawbacks 157 | - Code Complexity 158 | - Performance 159 | - Static Typing 160 | - Ubiquitous Language 161 | - Domain Modeling 162 | - Types in TypeScript 163 | - Model and Reality 164 | - Violation of Agreements 165 | - API Design 166 | - Refactoring Test Code 167 | - “Tests” for Tests 168 | - “Brittle” Tests 169 | - Test Duplicates 170 | - Never-Failing Tests 171 | - Tests for Simple Functions 172 | - Regressions 173 | - Comments and Documentation 174 | - Sources of Truth 175 | - Comments 176 | - Correct or Delete False Comments 177 | - Clarify Vague Comments 178 | - Inline Small Details in the Code 179 | - Describe Context Instead of “Rephrasing Names” 180 | - Turn TODOs and FIXMEs into Tasks 181 | - Documentation 182 | - Knowledge Accessibility 183 | - Refactoring as a Process 184 | - Refactor or Rewrite 185 | - Resources 186 | - Available Time 187 | - Accumulated Knowledge 188 | - Experience 189 | - Benefits and Risks 190 | - Project Meta Information 191 | - Estimates 192 | - Refactoring Large Chunks of Code 193 | - Frequency and Hygiene 194 | - Metrics 195 | - Afterword 196 | - Errata and Feedback 197 | - For Publishers 198 | - For Translators 199 | - Acknowledgements 200 | - Extras 201 | - Cheat Sheet on Refactoring Techniques 202 | - Sources and References 203 | -------------------------------------------------------------------------------- /manuscript-ru/TOC.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 | - Абстракция 55 | - Намерение и реализация 56 | - Фрактальная архитектура 57 | - Разделение ответственности 58 | - Декомпозиция задач 59 | - Принцип единственной ответственности 60 | - Инкапсуляция 61 | - Функциональный пайплайн 62 | - Преобразования данных 63 | - Состояния данных 64 | - Невыразимость неправильного 65 | - Валидация данных 66 | - Селекторы и маппинг данных 67 | - Условия и сложность кода 68 | - Цикломатическая и когнитивная сложность 69 | - Плоское лучше вложенного 70 | - Ранний возврат 71 | - Рендер компонентов 72 | - Переменные, предикаты и булева алгебра 73 | - Законы де Моргана 74 | - Предикаты 75 | - Примитивный паттерн-матчинг 76 | - Стратегия 77 | - Null-объект 78 | - Сайд-эффекты 79 | - Чистые функции 80 | - Ссылочная прозрачность 81 | - Неизменяемость по умолчанию 82 | - Функциональное ядро в императивной оболочке 83 | - Адаптеры для эффектов 84 | - Команды и запросы 85 | - CQS и сгенерированные ID 86 | - CQRS 87 | - Обработка ошибок 88 | - Виды ошибок 89 | - Техники обработки 90 | - Выбрасывание паник 91 | - Неожиданные ошибки и потеря контекста 92 | - Разные типы ошибок 93 | - Fail Fast 94 | - Rethrow 95 | - Преимущества 96 | - Проблемы 97 | - Result-контейнеры 98 | - Сигнатура точнее отражает процесс 99 | - Явная обработка 100 | - Централизованная обработка 101 | - Паники отдельно 102 | - Множественные ошибки 103 | - Распаковка 104 | - Связывание результатов 105 | - Проблемы 106 | - Когда предпочесть паники 107 | - Cross-Cutting Concerns 108 | - Зоопарк обработчиков 109 | - Иерархия отлова ошибок 110 | - Обёртки над «низкоуровневым» кодом 111 | - Обработчики пользовательских сценариев 112 | - Обработчики последней надежды 113 | - Предвалидация данных 114 | - Интеграция модулей 115 | - Зацепление и связность 116 | - Разделение ответственности 117 | - Правило интеграции модулей 118 | - Разделение задач 119 | - Как определить связность 120 | - Контракты 121 | - События и сообщения 122 | - Зависимости 123 | - Объектная композиция 124 | - Разделение данных и поведения 125 | - Принцип разделения интерфейса 126 | - Функциональная композиция 127 | - Отказ от зависимостей 128 | - Другие способы управления зависимостями 129 | - Целостность и согласованность 130 | - Агрегаты 131 | - Предвалидация на входе в контекст 132 | - Обобщения и иерархии 133 | - Обобщённые алгоритмы 134 | - Обобщённые типы 135 | - Наследование и композиция 136 | - Наследование 137 | - Принцип подстановки Лисков 138 | - Архитектура 139 | - Не про «папочки» 140 | - Моделирование бизнес-процессов 141 | - Повсеместный язык 142 | - Доменная модель 143 | - Направление зависимостей 144 | - Связь с внешним миром 145 | - Порты и адаптеры 146 | - 100500 видов архитектур 147 | - UI-логика 148 | - Реактивность 149 | - Тестирование 150 | - Декларативность 151 | - Читаемость 152 | - Надёжность 153 | - Расширяемость 154 | - Конфигурируемость 155 | - Автоматное программирование 156 | - Налог на декларативность 157 | - Сложность поддержки 158 | - Производительность 159 | - Статическая типизация 160 | - Повсеместный язык 161 | - Моделирование домена 162 | - Типизация в TypeScript 163 | - Сверка модели с реальностью 164 | - Нарушения договорённостей 165 | - Дизайн API 166 | - Рефакторинг тестового кода 167 | - «Тесты» для тестов 168 | - «Хрупкие» тесты 169 | - Тесты-дубликаты 170 | - Тесты, которые никогда не ломаются 171 | - Тесты простых функций 172 | - Регрессии 173 | - Рядом с кодом 174 | - Источники правды 175 | - Комментарии 176 | - Лживые комментарии исправить или удалить 177 | - Расплывчатые комментарии уточнить 178 | - Мелкие уточняющие комментарии перенести в код 179 | - «Пересказам названий» добавить контекста 180 | - TODO и FIXME превратить в задачи 181 | - Документация 182 | - Доступность знаний 183 | - Рефакторинг как процесс 184 | - Рефакторить или переписывать 185 | - Ресурсы 186 | - Время 187 | - Накопленные знания 188 | - Опыт 189 | - Выгоды и риски 190 | - Мета-информация о проекте 191 | - Эстимейты 192 | - Рефакторинг больших кусков кода 193 | - Частота и гигиена 194 | - Метрики 195 | - Заключение 196 | - Ошибки, фидбек и история изменений 197 | - Издателям 198 | - Переводчикам 199 | - Благодарности 200 | - Дополнительные материалы 201 | - Шпаргалка по техникам рефакторинга 202 | - Список литературы 203 | -------------------------------------------------------------------------------- /manuscript-en/03-before-start.md: -------------------------------------------------------------------------------- 1 | # Before Refactoring 2 | 3 | To refactor code faster, without regressions, and without “too much extra work,” we need to first _study and prepare_ the code. In particular, we should: 4 | 5 | - Define the refactoring scope and boundaries. 6 | - Cover the code under refactoring with tests. 7 | - Configure linters and, if needed, the compiler more strictly. 8 | 9 | In this chapter, we'll discuss why these steps are helpful and how to make them simpler. 10 | 11 | ## Define Scope and Boundaries 12 | 13 | Refactoring should not take forever. Instead, it should be a little chunk of work with clear boundaries and a definition of done. It's essential for two reasons: 14 | 15 | - We want to stay within our _time and resource budget_. 16 | - Limited changes are _easier to keep track of_ to see faster if they break the app. 17 | 18 | Refactoring boundaries define the scope of work. They are the junction points between the code we're going to change and everything else. They show where our changes should stop, that is, what code _won't_ change. 19 | 20 | Defining boundaries in the code isn't always obvious, especially if the modules are tightly coupled. In such cases, we should pay attention to the code's _data and dependencies_. 21 | 22 | The more different the data two pieces of code work with, the more likely they are different, independent parts of the program. The junction of these pieces is the boundary that should prevent changes from spreading across the code base. 23 | 24 | | By the way 🪡 | 25 | | :--------------------------------------------------------------------------------------------------------------------------------------------------- | 26 | | Feathers in “Working Effectively with Legacy Code” calls these junctions “seams.”[^workingeffectively] Sometimes, I will use this term as a synonym. | 27 | 28 | Boundaries prevent refactoring from taking forever and help you integrate changes into the repository's main branch more often. 29 | 30 | | In detail 🔬 | 31 | | :------------------------------------------------------------------------------------- | 32 | | We'll talk a little more about finding and using boundaries in the following chapters. | 33 | 34 | ## Cover with Tests 35 | 36 | We need to cover the code inside the boundaries with tests. We'll use them to check if something was broken during refactoring constantly. To make more use of the tests, we should meet several conditions: 37 | 38 | ### Find More Edge Cases 39 | 40 | Edge cases help avoid regressions and ensure we haven't broken the code behavior in “exotic” circumstances. (“Exotic” bugs are much harder to fix.[^debugit]) 41 | 42 | The more diverse the edge cases, the easier it is to prepare and systematize test data for different situations. The knowledge of the application's behavior in these situations will come in handy in the future, even after refactoring. 43 | 44 | ### Define Explicit and Implicit Input 45 | 46 | The explicit input is the arguments of functions or methods. The implicit input is dependencies, shared or global state, and the context of functions and methods. Systematization of input data will simplify test case creation. 47 | 48 | ### Specify Desired Result 49 | 50 | During refactoring, we can't change the code functionality, so the desired result is the actual current behavior of the program. We can capture it as output data (e.g., the result of a function) or as a desired side effect (e.g., state change or API call). 51 | 52 | Ideally, the result should exist _on the boundary_ of code under refactoring. Checking results on the boundary is easier, while the amount of affected code is minimal. 53 | 54 |
55 | 56 |
Result on the border is usually easier to check
57 |
58 | 59 | ### Set Up Automatic Tests 60 | 61 | When refactoring, we want to test _every_ change, even a minuscule one. Manual testing can get tiresome. We might get lazy or forget to test the changes with manual testing. 62 | 63 | Automatic tests don't have this problem. They can run in parallel while refactoring and re-test the code each time we save a file. That way, the test results are always in front of our eyes, and we can notice faster what change has broken the code. 64 | 65 | What type of tests to use depends on the situation and isn't as important as their existence. If unit tests are enough, we can use only them. If we have to test the work of several modules, we may need an integration or E2E test. The main point here is _automation_. The fewer checks we do by hand, the fewer human errors appear. 66 | 67 | ## Make Linter and Compiler Settings Stricter 68 | 69 | This section is somewhat optional, but I like using it. In my opinion, more aggressive linter and compiler settings help spot occasional bugs and bad practices. “More aggressive” settings can include: 70 | 71 | ### Use “Errors” Instead of “Warnings” 72 | 73 | Linter warnings are a collective experience of the industry. They can be valuable, but they're easy to miss because they don't make the code compilation “crash with an error.” 74 | 75 | With errors, on the other hand, the code “won't compile.” Errors will force us to update the code or change our linter rules. 76 | 77 | | Keep in mind ❗️ | 78 | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | 79 | | Not all linter rules are equally helpful. We can choose rules that we think are more important and discard others. However, once we have selected a set of rules, we should follow them strictly. | 80 | 81 | ### Configure More Automated Checks 82 | 83 | It is a great time to try out new linter rules or other automated tools if the team wants to try them out. 84 | 85 | But it's worth remembering that not all rules are equally valuable and appropriate for every project. We should try not to go against the team. If most of the other developers in the team think a particular linter rule is unnecessary, we shouldn't introduce it. 86 | 87 | Teams can debate rules and practices to determine the most suitable for them. But remember that no rule is worth having a conflict in the team. 88 | 89 | [^workingeffectively]: “Working Effectively with Legacy Code” by Michael C. Feathers, https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code 90 | [^debugit]: “Debug It!: Find, Repair, and Prevent Bugs in Your Code” by Paul Butcher, https://www.goodreads.com/book/show/6770868-debug-it 91 | -------------------------------------------------------------------------------- /manuscript-en/22-cheatsheet.md: -------------------------------------------------------------------------------- 1 | # Cheat Sheet on Refactoring Techniques 2 | 3 | This appendix contains a list of short recommendations for finding and fixing problems with the code. 4 | 5 | The recommendations are based on the topics from the previous chapters. Before applying them, it's worth reading the chapters to assess whether these techniques fit your project, style, and coding habits. 6 | 7 | It isn't a list of strict rules. Instead, it's a set of tips you can consider. 8 | 9 | ## Searching for Problems 10 | 11 | - Search for code smells in the project. 12 | - Pay attention to feelings like “hard to read, change, test.” 13 | 14 | ## Before Refactoring 15 | 16 | - Define the boundaries of the changes and find the “seams” in the code. 17 | - Cover the selected part of the code with tests. 18 | - Describe as many edge cases as possible with tests. 19 | - Set up automatic test runner and code linter. 20 | - Mark the compiler and linter warnings as errors. 21 | 22 | ## During Refactoring 23 | 24 | - Use small steps and atomic commits. 25 | - Create compact but detailed pull requests. 26 | - Apply just one refactoring technique at a time. 27 | - Fix bugs and add features separately from refactoring. 28 | - Respect the transformation priority premise. 29 | - Do not mix refactoring of tests and application code. 30 | - Apply the automated refactoring tools in IDE. 31 | - Look at diffs to find inconsistencies before committing changes. 32 | - Run all changes through tests, even the smallest ones. 33 | 34 | ## Where to Start 35 | 36 | - Implement automatic code formatting. 37 | - Test the code with linters and static analyzers. 38 | - Replace self-written helpers with language and environment features. 39 | 40 | ## Improving Names 41 | 42 | - Clarify obscure names. 43 | - Decipher undocumented abbreviations. 44 | - Decompose entities with long names. 45 | - Use different names for different entities. 46 | - Use ubiquitous language. 47 | - Fix lying names. 48 | 49 | ## Dealing with Code Duplication 50 | 51 | - Separate duplication from the lack of information about the system. 52 | - Conduct regular reviews of duplicated code. 53 | - Extract repetitive data into variables. 54 | - Extract repetitive actions in functions. 55 | 56 | ## Abstraction as a Tool 57 | 58 | - Describe intent in function names. 59 | - Structure code to give information that is needed to the reader at a particular moment. 60 | - Remember the limits of humans' working memory; limit the number of entities in the function. 61 | - Split information by levels of abstraction. 62 | - Decompose tasks based on the data used in them. 63 | - Make sure the function has only one reason to change. 64 | - Ensure that the module guarantees the validity of its data itself. 65 | 66 | ## Linear Code Execution 67 | 68 | - Explicitly express the data states in the application. 69 | - Make it difficult to pass invalid data in code. 70 | - Validate the data before use. 71 | - Use selectors to “prepare” data for different use cases. 72 | - Set a limit on the cyclomatic and cognitive complexity of the code. 73 | - “Straighten” the conditions: 74 | - Use the early return; 75 | - Apply de Morgan's laws; 76 | - Use predicates for dynamic conditions; 77 | - Use design patterns (e.g. Strategy, Null-object); 78 | - Apply pattern-matching when possible. 79 | 80 | ## Working with Side Effects 81 | 82 | - Use pure functions more often. 83 | - Aim for referential transparency for easier debugging. 84 | - Treat data and code as immutable and stateless by default. 85 | - Move side effects to the edges of the module, function, or use case. 86 | - Test the simplicity of code with tests: the easier it is to write tests, the simpler the code is. 87 | - Write adapters to reduce coupling and create fewer mocks in tests. 88 | - Separate effects into commands and queries. 89 | - Identify CQS violations by type signatures and function names. 90 | - Use different models to read and write data when appropriate. 91 | 92 | ## Handling Errors 93 | 94 | - Identify different types of errors in the application. 95 | - Centralize error handling. 96 | - Get to error handling as early as possible. 97 | - Try to express possible errors in the function signature. 98 | - Check input data before using it. 99 | - Use logging and analytics in the “last resort” handlers. 100 | - Use decorators for composing cross-cutting concerns. 101 | 102 | ## Integrating Application Parts 103 | 104 | - Keep coupling low and cohesion high. 105 | - Identify cohesion by input data, output data, and code dependencies. 106 | - Designate module guarantees through its public API. 107 | - Make module communication less coupled: 108 | - Use messages or events when appropriate; 109 | - Apply patterns (e.g., Observer). 110 | - Limit the number of module dependencies. 111 | - Compose data transformations instead of side effects. 112 | - Separate logic from side effects. 113 | - Separate data from behavior. 114 | 115 | ## Working with Generics 116 | 117 | - Do not rush to generalize. 118 | - Compose complex types from simple types instead of using inheritance. 119 | - Use generics when you are sure that the type structure will not change. 120 | - Pay attention to conditions; they can indicate a violation of the Liskov substitution principle. 121 | 122 | ## Architecture and Communication with the World 123 | 124 | - Use ubiquitous language to model the domain. 125 | - Express the stages of the application data lifecycle in their states. 126 | - Try not to depend directly on third-party code. 127 | - Add an anti-corruption layer where the data format or API may change. 128 | - Separate UI logic from business logic. 129 | - Separate configuration from code. 130 | 131 | ## Static Typing as a Tool 132 | 133 | - Use types to describe the domain and the business workflows. 134 | - Make invalid data states unrepresentable in types. 135 | - Identify CQS violations by method and function signatures. 136 | - Apply the “xxxing” technique to increase the information density of signatures and function names. 137 | 138 | ## Refactoring Tests 139 | 140 | - Do not mix refactoring tests and application code. 141 | - Use more stubs and test data and fewer mocks. 142 | - Get rid of test duplicates and never-failing tests. 143 | 144 | ## Refactoring Documentation and Comments 145 | 146 | - Get rid of conflicts between the code and documentation. 147 | - Clarify vague comments: add context, examples, and reasons behind the current implementation. 148 | - Conduct regular reviews of comments, documentation, and project issues. 149 | - Evaluate resources, benefits, and risks when choosing between refactoring or rewriting from scratch. 150 | - Use project meta information from the version control system to analyze project health. 151 | - Use the “Strangler Fig” technique when refactoring large chunks of code. 152 | - Refactor code regularly. 153 | - Remember the “Boy-Scout Rule.” 154 | - Rely on measurable metrics when evaluating improvements. 155 | -------------------------------------------------------------------------------- /manuscript-ru/22-cheatsheet.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 | - Применяйте инструменты автоматизированного рефакторинга в IDE. 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 | 58 | - Описывайте намерение в названиях функций. 59 | - Структурируйте код так, чтобы выдавать информацию читателю дозировано. 60 | - Помните об ограничении рабочей памяти мозга, следите за количеством сущностей в функции. 61 | - Упорядочьте информацию и используемые термины по уровням абстракции. 62 | - Декомпозируйте задачи на основе данных, которые в них используются. 63 | - Проверяйте, чтобы у функции была только одна причина для изменения. 64 | - Следите, чтобы модуль самостоятельно обеспечивал валидность своих данных. 65 | 66 | ## Линейное выполнение кода 67 | 68 | - Выделяйте состояния, через которые проходят данные приложения. 69 | - Сделайте передачу невалидных данных в коде затруднительной. 70 | - Валидируйте данные перед использованием. 71 | - Используйте селекторы для «подготовки» данных под разные условия использования. 72 | - Установите лимит на цикломатическую и когнитивную сложность кода. 73 | - «Выпрямите» условия: 74 | - Используйте ранний возврат; 75 | - Применяйте законы де Моргана; 76 | - Используйте предикаты для динамических условий; 77 | - Используйте паттерны проектирования (Стратегия, Null-объект); 78 | - Применяйте паттерн-матчинг там, где возможно. 79 | 80 | ## Работа с сайд-эффектами 81 | 82 | - Чаще используйте чистые функции. 83 | - Стремитесь к ссылочной прозрачности для более простой отладки. 84 | - Воспринимайте данные и код по умолчанию как неизменяемые. 85 | - Отодвиньте сайд-эффекты к краям модуля / функции / юзкейса. 86 | - Проверяйте простоту кода тестами: чем проще писать тесты, тем проще код. 87 | - Пишите адаптеры, чтобы уменьшить зацепление и создавать меньше моков в тестах. 88 | - Разделяйте эффекты на команды и запросы. 89 | - Выявляйте нарушения CQS по сигнатурам и именам функций. 90 | - Используйте разные модели для чтения и записи данных, когда это уместно. 91 | 92 | ## Обработка ошибок 93 | 94 | - Выделяйте разные виды ошибок в приложении. 95 | - Обрабатывайте ошибки централизованно. 96 | - Переходите к обработке ошибок как можно раньше. 97 | - Старайтесь отобразить возможные ошибки в сигнатуре функции. 98 | - Проверяйте входные данные перед началом работы с ними. 99 | - Используйте логирование и аналитику в обработчиках последней надежды. 100 | - Используйте декораторы для cross-cutting concerns. 101 | 102 | ## Интеграция частей приложения 103 | 104 | - Держите зацепление низким, а связность высокой. 105 | - Проверяйте связность по выходным, выходным данным и зависимостям кода. 106 | - Обозначьте гарантии модулей через их публичное API. 107 | - Сделайте общение модулей менее зацепленным: 108 | - Используйте сообщения / события; 109 | - Применяйте паттерны (Наблюдатель). 110 | - Ограничивайте количество зависимостей модуля. 111 | - Компонуйте преобразования данных, а не сайд-эффекты. 112 | - Отделяйте логику от сайд-эффектов. 113 | - Отделяйте данные от поведения. 114 | 115 | ## Работа с обобщёнными алгоритмами 116 | 117 | - Не торопитесь обобщать. 118 | - Компонуйте сложные типы из простых вместо наследования. 119 | - Используйте дженерики, когда уверены, что структура типа не поменяется. 120 | - Обращайте внимание на условия, они могут указать на нарушение принципа подстановки. 121 | 122 | ## Архитектура и общение с внешним миром 123 | 124 | - Используйте повсеместный язык для моделирования домена. 125 | - Отражайте этапы жизненного цикла данных приложения в их состояниях. 126 | - Старайтесь не зависеть напрямую от стороннего кода. 127 | - Добавляйте анти-коррозионный слой там, где может поменяться формат данных или API. 128 | - Разделяйте UI-логику и бизнес-логику. 129 | - Отделяйте конфигурацию от кода. 130 | 131 | ## Статическая типизация как инструмент 132 | 133 | - Используйте типы для описания предметной области. 134 | - Сделайте невалидные состояния данных «невыразимыми» в типах. 135 | - Выявляйте нарушения CQS по сигнатурам методов и функций. 136 | - Применяйте технику «вычёркивания», чтобы определить информационную насыщенность сигнатур. 137 | 138 | ## Рефакторинг тестового кода 139 | 140 | - Рефакторьте тестовый код и код приложения по очереди. 141 | - Используйте больше простых стабов и тестовых данных, чем сложных моков. 142 | - Избавляйтесь от тестов-дубликатов и никогда-не-падающих тестов. 143 | 144 | ## Рефакторинг документации, комментариев и процессов 145 | 146 | - Избавьтесь от конфликтов между кодом и тем, что «рядом с ним». 147 | - Уточните расплывчатые комментарии: добавьте контекста, примеров и причин именно такой реализации. 148 | - Проводите регулярные аудиты комментариев, документации и задач в трекере. 149 | - Оцените ресурсы, выгоды и риски при выборе между рефакторингом и переписыванием с нуля. 150 | - Используйте мета-информацию о проекте из системы контроля версий для анализа состояния проекта. 151 | - Используйте технику «Рядом, а не вместо» при рефакторинге больших кусков кода. 152 | - Уделяйте время рефакторингу регулярно. 153 | - Помните о правиле бойскаута. 154 | - Опирайтесь на измеримые метрики при оценке улучшений. 155 | -------------------------------------------------------------------------------- /manuscript-ru/03-before-start.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 | | Физерс в «Эффективной работе с легаси» называет такие места «швами». Я иногда буду использовать этот термин как синоним.[^workingeffectively] | 27 | 28 | Границы не дадут рефакторингу модуля превратиться в «долгострой» и помогут интегрировать изменения в основную ветку репозитория чаще. 29 | 30 | | Подробнее 🔬 | 31 | | :------------------------------------------------------------------------------ | 32 | | Чуть подробнее о поиске и использовании границ мы поговорим в следующих главах. | 33 | 34 | ## Покрыть тестами 35 | 36 | Код внутри выделенных границ нужно покрыть тестами. С их помощью мы будем проверять, что ничего не сломали во время рефакторинга. Чтобы тесты приносили больше пользы, я стараюсь выполнить несколько условий: 37 | 38 | ### Выявить больше крайних случаев 39 | 40 | Крайние случаи помогут избежать регрессий и убедиться, что мы не сломали работу кода в «экзотических» обстоятельствах. («Экзотические» баги чинить сложнее.[^debugit]) 41 | 42 | Чем крайние случаи разнообразнее, тем легче подобрать и систематизировать тестовые данные для различных ситуаций. Знания о поведении приложения в этих ситуациях пригодится нам и в будущем, после рефакторинга. 43 | 44 | ### Определить явные и неявные входные данные 45 | 46 | Явные входные данные — это аргументы функций или методов. Неявные — зависимости, общее или глобальное состояние, контекст работы функций и методов. Систематизация входных данных упростит составление тест-кейсов. 47 | 48 | ### Конкретизировать желаемый результат 49 | 50 | Во время рефакторинга мы не меняем функциональность, поэтому желаемый результат — это фактическое поведение программы. Зафиксировать это поведение мы можем в виде данных (например, результата работы функции) или желаемого побочного эффекта (изменения состояния или вызова API). 51 | 52 | В идеале результат должен находиться _на границе_ части кода, которую мы рефакторим. Проверять такой результат проще, а количество затронутого кода будет минимально. 53 | 54 |
55 | 56 |
Если результат находится на границе, его проще проверять
57 |
58 | 59 | ### Настроить автотесты 60 | 61 | Нам потребуется тестировать _каждое_ изменение. Тестирование вручную будет утомлять, из-за чего мы можем начать лениться или забывать о проверке изменений. 62 | 63 | У автоматических тестов таких проблем нет. Мы можем настроить перезапуск тестов на каждое сохранение кода и запустить их перед началом рефакторинга. Так результат проверки всегда будет перед глазами, и мы раньше заметим, какое изменение сломало работу кода. 64 | 65 | Как выбрать вид тестов, зависит от ситуации и не так важно, как их наличие. Если можно обойтись юнит-тестами, то я предпочту использовать их. Если приходится тестировать работу нескольких модулей, может понадобиться интеграционный или E2E тест. 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 | | :----------------------------------------------------------------------------------------------------------------------------------- | 93 | | О полезных характеристиках кода, которые мне кажутся обязательными и которые при этом можно поймать линтером, мы поговорим отдельно. | 94 | 95 | [^workingeffectively]: “Working Effectively with Legacy Code” by Michael C. Feathers, https://www.goodreads.com/book/show/44919.Working_Effectively_with_Legacy_Code 96 | [^debugit]: “Debug It!: Find, Repair, and Prevent Bugs in Your Code” by Paul Butcher, https://www.goodreads.com/book/show/6770868-debug-it 97 | -------------------------------------------------------------------------------- /manuscript-ru/05-low-hanging-fruit.md: -------------------------------------------------------------------------------- 1 | # Низко-висящие фрукты 2 | 3 | Для рефакторинга куска кода могут потребоваться разные изменения, и бывает сложно выбрать, с чего начать. Мне нравится ранжировать изменения от простых к сложным и начинать с простых. Это помогает «сдуть с кода пыль» и начать видеть в нём более серьёзные проблемы. 4 | 5 | К простым изменениям я обычно отношу форматирование, ошибки линтера и замену самописного кода на возможности языка или окружения, в котором код будет работать. Об этом сейчас и поговорим. 6 | 7 | ## Форматирование кода 8 | 9 | Форматирование — это вкусовщина, но у него есть одна полезная функция. Если код во всём проекте написан _последовательно_, то у читателей уходит меньше времени на его восприятие. 10 | 11 | Так работают привычки: знакомый «рисунок» кода помогает сосредоточиться не на словах и буквах, а на смысле. 12 | 13 | ``` 14 | // Код без форматирования. 15 | // При чтении надо «продираться» сквозь него, 16 | // чтобы увидеть смысл за буквами: 17 | 18 | function ProductList({ products }) { 19 | return } 21 | 22 | 23 | // Отформатированный код помогает упростить чтение 24 | // и быстрее перейти к смыслу: 25 | 26 | function ProductList({ products }) { 27 | return ( 28 | 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 } 18 | 19 | 20 | // Formatted code makes it easier to read it: 21 | 22 | function ProductList({ products }) { 23 | return ( 24 | 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 | --------------------------------------------------------------------------------