35 | );
36 | }
37 |
38 | function Coupon({ onEnter }) {
39 | const [coupon, setCoupon] = useState("");
40 |
41 | return (
42 | setCoupon(e.target.value)}
47 | />
48 | );
49 | }
50 |
51 | const safeMakePurchase = robustAsync(makePurchase);
52 |
53 | function useMakePurchaseExecutor() {
54 | const [status, setStatus] = useState("idle");
55 | const [error, setError] = useState(null);
56 |
57 | async function execute(command) {
58 | setStatus("loading");
59 | const result = await safeMakePurchase({ ...command, service });
60 |
61 | if (result.value) alert(`Your order ID is ${result.value}!`);
62 | if (result.error) setError("Woah! Something went terribly wrong!");
63 | setStatus("finished");
64 | }
65 |
66 | return { execute, status, error };
67 | }
68 |
69 | function App() {
70 | const { user } = useUserStore();
71 | const { cart } = useCartStore();
72 | const { execute, status, error } = useMakePurchaseExecutor();
73 |
74 | if (!!error) return error;
75 | if (status === "loading") return "Loading...";
76 | if (status === "finished") return "We'll call you to confirm the order.";
77 |
78 | function handleSubmit(e) {
79 | e.preventDefault();
80 | const { coupon } = Object.fromEntries(new FormData(e.target));
81 | const makePurchaseCommand = { user, cart, coupon };
82 | execute(makePurchaseCommand);
83 | }
84 |
85 | return (
86 |
92 | );
93 | }
94 |
95 | export default App;
96 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import userEvent from "@testing-library/user-event";
3 | import App from "./App";
4 |
5 | const defaultOrder = {
6 | user: "John Doe",
7 | products: [
8 | { count: 1, icon: "🦄", name: "Unicorn", price: 50 },
9 | { count: 3, icon: "✨", name: "Magic Sparkles", price: 10 },
10 | { count: 1, icon: "🌈", name: "Rainbow", price: 150 },
11 | { count: 4, icon: "👾", name: "Space Invader", price: 15 },
12 | ],
13 | total: 290,
14 | discount: 0,
15 | };
16 |
17 | const logSpy = jest.spyOn(console, "log");
18 | const alertSpy = jest.spyOn(window, "alert");
19 |
20 | afterEach(() => jest.clearAllMocks());
21 | afterAll(() => jest.restoreAllMocks());
22 |
23 | /**
24 | * A set of tests for checking the order creation without coupons.
25 | * For brevity I kept only one test.
26 | * In reality though, we would have to test this more thoroughly,
27 | * and test this function against multiple input variants:
28 | *
29 | * - different products in the cart,
30 | * - different user data,
31 | * - erroneous requests (empty cart, no user ID),
32 | * - unsuccessful server responses, etc.
33 | */
34 |
35 | describe("When sent an order form without coupons", () => {
36 | it("should send the created order to the server", async () => {
37 | render();
38 | userEvent.click(screen.getByRole("button"));
39 |
40 | await screen.findByText("We'll call you to confirm the order.");
41 | expect(logSpy).toHaveBeenCalledWith(defaultOrder);
42 | expect(alertSpy).toHaveBeenCalledWith("Your order ID is some-order-id!");
43 | });
44 | });
45 |
46 | /**
47 | * A set of tests for checking the order creation with coupons.
48 | * Again, for brevity I kept only a few of these.
49 | * In reality, we would check against multiple coupons,
50 | * various data and under different circumstances.
51 | *
52 | * This is required to fully understand the code base.
53 | * When we start working with an unknown piece of code
54 | * tests are not only a way to ensure no regressions
55 | * but also a tool to explore the domain and how the code really works.
56 | */
57 |
58 | describe("When sent an order with a coupon", () => {
59 | it.each([
60 | { coupon: "HAPPY_MONDAY", discount: 20 },
61 | { coupon: "LAZY_FRIDAY", discount: defaultOrder.total * 0.2 },
62 | ])(
63 | "should include the according discount to the order",
64 | async ({ coupon, discount }) => {
65 | const order = { ...defaultOrder, discount };
66 |
67 | render();
68 | userEvent.type(screen.getByRole("textbox"), coupon);
69 | userEvent.click(screen.getByRole("button"));
70 |
71 | await screen.findByText("We'll call you to confirm the order.");
72 | expect(logSpy).toHaveBeenCalledWith(order);
73 | }
74 | );
75 | });
76 |
--------------------------------------------------------------------------------
/docs/ru.md:
--------------------------------------------------------------------------------
1 | # Рефакторинг на максималках
2 |
3 | Рефакторинг кода требует усилий, и не всегда на него получается выделить время. В докладе «Рефакторинг на максималках» я рассказываю, как подмечать проблемы с кодом быстрее и тратить меньше времени на их исправление.
4 |
5 | Из доклада вы узнаете:
6 |
7 | - Как продать идею рефакторинга бизнесу;
8 | - Как подмечать проблемы в коде и начать «видеть» их интуитивно;
9 | - Какие словечки из мира программирования приносят больше всего пользы в рефакторинге кода.
10 |
11 | В этом репозитории я собрал примеры кода, которые использовал на слайдах. Ссылки на слайды и дополнительные материалы вы можете найти в списке ниже:
12 |
13 | - [Запись на YouTube](https://youtu.be/G7NcuYJ-HSM)
14 | - [Слайды к докладу](https://bespoyasov.ru/slides/refactor-like-a-superhero/)
15 | - [Список литературы](https://bespoyasov.ru/slides/refactor-like-a-superhero/sources.html)
16 | - [Бесплатная онлайн-книга о рефакторинге](https://github.com/bespoyasov/refactor-like-a-superhero)
17 |
18 | ## История коммитов
19 |
20 | Каждый коммит в этом репозитории — один этап рефакторинга приложения. Вся история коммитов отражает процесс рефакторинга в целом.
21 |
22 | Сообщения коммитов описывают, _что_ было сделано. Здесь же я подробнее расскажу, _почему_ эти изменения были внесены и _в чём их польза_.
23 |
24 | ### [Добавляем «грязные» исходники](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/2c277c71e9bbd1204d1055c5eee934dd0ba79b94)
25 |
26 | Добавляем приложение с «грязным» кодом. Приложение — это корзина интернет-магазина. В корзине находится список товаров, поле ввода купона на скидку и кнопка отправки заказа.
27 |
28 | Этот коммит будет отправной точкой. Код из него мы приведём в порядок к концу истории.
29 |
30 | ### Определим рамки рефакторинга
31 |
32 | Перед началом рефакторинга стоит определить область кода и функциональности, которые мы затронем.
33 |
34 | Это нужно по двум причинам:
35 |
36 | - мы хотим оставаться в рамках временного и ресурсного бюджета, который у нас есть;
37 | - маленькими изменения проще управлять и следить за тем, что именно сломало работу кода.
38 |
39 | Мы можем рефакторить функцию, модуль, подсистему или даже всю систему в целом. Но, как правило, лучше двигаться _маленькими шагами_. Несколько небольших рефакторингов лучше, чем один большой.
40 |
41 | В нашем случае рамки рефакторинга — это юзкейс оформления заказа.
42 |
43 | ### [Покрываем юзкейс тестами](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/68877aa0fc67fd27f8ca2d432f77d78331478579)
44 |
45 | При рефакторинге важно убедиться, что мы ничего не сломали. Для этого прежде, чем делать _что-либо_, мы покрываем тестами участок кода, который собираемся рефакторить.
46 |
47 | Когда мы пишем тесты, мы исследуем код. Стоит проверить как можно больше крайних случаев и посмотреть, как код себя в них ведёт. Информация о поведении приложения с разными входными данными нам пригодится в будущем.
48 |
49 | В нашем случае юзкейс оформления заказа затрагивает срез всего приложения, поэтому нам потребуется E2E-тест. Вид тестов при рефакторинге не так важен, как их _наличие_. Можно использовать и юнит-тесты, если они покрывают весь код в рамках рефакторинга.
50 |
51 | Тестировать вручную тоже можно, но лучше всё же это автоматизировать. Проверять работу кода надо будет часто — после каждого, даже самого маленького изменения. Так мы сможем быстрее определять, что именно поломало код. Делать это руками очень быстро надоест 😃
52 |
53 | ### [Применяем Prettier](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/67dcaeeb429356518a626a9025c17d742bac5c1b)
54 |
55 | Начнём с простого — с форматирования. Чем унифицированнее кодовая база, тем проще в ней ориентироваться.
56 |
57 | Представим, что в этом проекте мы решили использовать Prettier в качестве набора правил форматирования. Применим его.
58 |
59 | Бывает, что Prettier ломает работу кода, когда, например, переносит что-то на новую строку. Чтобы такого не случилось, мы проверяем, проходят ли написанные ранее тесты.
60 |
61 | ### [Удаляем «мёртвый» код](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/803d6b54bd1528ce72efb6188cd92b524bc0e572)
62 |
63 | Для гигиены кода мы также можем использовать линтеры, например, ESLint.
64 |
65 | Линтеры укажут на недостижимый или неиспользуемый код, а также не практики, которые индустрия считает плохими.
66 |
67 | Неиспользуемый код мы можем удалить. После каждого этапа мы будем проверять, не сломались ли тесты. В будущем я перестану акцентировать на этом внимание. Будем просто держать в голове, что мы проверяем тестами _каждое_ изменение.
68 |
69 | ### [Переименовываем `d` в `discount`](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/72661edb6c7c754b5553eafa504526db49381d4e)
70 |
71 | Когда встречаются сущности с непонятными именами, нам стоит выяснить, за что они отвечают.
72 |
73 | Как правило, «непонятное имя» — это сигнал о слабом понимании предметной области или проблемах с разделением кода и выделением слоёв абстракции (об этом позже).
74 |
75 | Слишком короткие имена и сокращения плохи тем, что они скрывают информацию о предметной области. Рано или поздно такое имя будет прочитано неправильно, потому что все «знающие» разработчики перестали работать над проектом.
76 |
77 | Мы можем уменьшить [автобусный фактор](https://ru.wikipedia.org/wiki/Фактор_автобуса), передав всю необходимую информацию прямо в названии сущности. (На крайний случай — в документации, но она быстро устаревает, что может привести к нескольким источникам информации, среди которых неясно кому доверять.)
78 |
79 | ### [Используем термин из предметной области (`User`) для названия сущности](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/b84ba8460b6702f89b7e42735e95c18665cf4d82)
80 |
81 | Чтобы в проекте все участники понимали друг друга, можно использовать повсеместный (ubiquitous) язык.
82 |
83 | Такой язык состоит из терминов предметной области, к которой проект относится. Именно этими терминами стоит оперировать при проектировании и разработке системы.
84 |
85 | В нашем случае мы называем переменную термином `User`. Так любой участник проекта сможет правильно интерпретировать код и его назначение.
86 |
87 | ### [Объявляем переменную `_userId` ближе к месту использования](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/03f879ae9e2bb24117d150e697a14978b278d277)
88 |
89 | Когда код сильно разрежен и «раскидан» по файлу, его становится сложно читать и «сканировать» глазами во время беглого прочтения.
90 |
91 | Здесь нам приходится держать в голове всё, что происходило с переменной `_userId` до того момента, как мы начали её использовать. В этом есть две проблемы:
92 |
93 | - переменная используется _слишком_ далеко от того места, где объявлена;
94 | - её можно изменить в любом месте кода, надо держать в голове список всех таких изменений.
95 |
96 | Об иммутабельности мы поговорим ещё позже. Этим же коммитом решим первую проблему — объявим переменную ближе к месту её использования.
97 |
98 | ### [Объявляем переменную `products` ближе к месту использования](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/1bf8fb1d980ee19fa6f448c098bdf1e87ac2f6e9)
99 |
100 | По тем же причинам, что и в прошлый раз, «дефрагментируем» и эту часть кода.
101 |
102 | ### [Делаем подсчёт итоговой суммы заказа более декларативным](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/63cb8917f653eeed98b81fd72fd9170f8c35c762)
103 |
104 | Декларативный код — такой, который рассказывает, _что_ он делает. Императивный же рассказывает, _как_ он что-то делает.
105 |
106 | Декларативный код выражает _намерение_. Его проще читать, потому что он прячет лишние детали реализации под понятными названиями функций и переменных. Декларативный код помогает выражаться терминами из того уровня абстракции, на котором читатель кода в этот момент времени находится.
107 |
108 | ### [Переименовываем переменную `price`, чтобы избежать одинаковых имён для разных сущностей](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/86dea9d987af5aa0beb780efa916c6c0070b2929)
109 |
110 | Разные сущности могут называться одинаковыми именами, если они находятся в разных контекстах или взаимодействие с ними разделено во времени.
111 |
112 | В остальных случаях для разных сущностей лучше использовать разные имена. Это избавит от путаницы при чтении и ошибок при исполнении кода.
113 |
114 | (Особенно это опасно, если код мутабелен. Изменение одной переменной может случайно задеть другую с таким же именем.)
115 |
116 | ### [Используем `onSubmit`, чтобы поймать отправку формы](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/16313564ad6d9dc954230bba1a612b087b6482e0)
117 |
118 | Один из «низко висящих фруктов» при рефакторинге — это код, который можно заменить возможностями языка, среды или окружения.
119 |
120 | Мы можем, например, заменить методом `.includes()` старую функциональность, которая имитировала его работу. Или, как в нашем случае, не обрабатывать два отдельных события (клик по кнопке и нажатие Enter в поле), а использовать событие отправки формы.
121 |
122 | ### [Используем `FormData` для сериализации форм](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/62d16cb554ffe9d708b40cdafef7e957f260b4b2)
123 |
124 | Браузерная `FormData` избавляет от написания кучи лишнего кода. (И скорее всего решает задачу лучше наших костылей.)
125 |
126 | Также это структура данных, которая была специально придумана под задачу сериализации форм. Использование подходящих структур данных часто может определить, какой мы будем использовать алгоритм, и насколько он будет эффективен.
127 |
128 | ### [Группируем бизнес-логику и сайд-эффекты отдельно друг от друга](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/aca4619f4e76526ab7b467a0cb366065a75d2193)
129 |
130 | Бизнес-логика — самое главное в приложении. Чем проще она написана, тем проще её тестировать, проверять и изменять.
131 |
132 | Я предпочитаю использовать для её написания чистые функции и функциональный подход. Взаимодействие с внешним миром, однако, всегда связано с побочными эффектами.
133 |
134 | Чтобы не смешивать бизнес-логику и побочные эффекты, я пользуюсь принципом организации кода, который называется «Функциональное ядро в императивной оболочке». (Или как это называет Марк Зиманн — [Impureim Sandwich](https://blog.ploeh.dk/2020/03/02/impureim-sandwich/).)
135 |
136 | Принцип заключается в том, чтобы все побочные эффекты взаимодействия с внешним миром расставлять _вокруг_ бизнес-логики. Например:
137 |
138 | - побочный эффект для получения данных;
139 | - функциональное ядро бизнес-логики;
140 | - побочный эффект для сохранения данных.
141 |
142 | Этот коммит группирует код так, чтобы собрать всё, связанное с бизнес-логикой, в центре функции, а все побочные эффекты — вокруг. Такая группировка нам также пригодится позже.
143 |
144 | ### [Расцепляем UI и функцию оформления заказа](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/5fe6e1f3c459f98ff633ae04fdeb12b17c6f7ad6)
145 |
146 | Применяем принцип разделения ответственности и отделяем функцию юзкейса от рендера компонента. Это две разные задачи, поэтому и заниматься ими должны разные сущности.
147 |
148 | Также мы уменьшим зацепление между функциональностью «оформления заказа» и «вывода информации на экран». Это нам поможет сделать юзкейс независимым от фреймворка и библиотек. Нам будет проще его тестировать и проверять на соответствие требованиям.
149 |
150 | ### [Проясняем название провайдера данных](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/cbb7a5033af28fd04b54bd01d2a6d9d2077c41c5)
151 |
152 | Уточняем название провайдера данных о пользователе, добавляя отсутствующие детали, необходимые для этого уровня абстракции.
153 |
154 | Так как мы в компоненте используем провайдеры данных для разных сущностей, стоит указать, к какой сущности относится каждый. Слишком абстрактное название может стать проблемой при чтении: придётся держать в голове пропущенные детали.
155 |
156 | ### [Укорачиваем название обработчика отправки формы](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/80ebc00bb9bfbf7e0a2cc3e8051ec037a99f0e07)
157 |
158 | Мы абстрагировали оформление заказа в функцию. Теперь мы можем использовать название этой функции, как термин, чтобы объясняться на текущем уровне абстракции.
159 |
160 | Детали, которые содержатся в названии функции оформления заказа, теперь можно не дублировать в названии обработчика отправки формы.
161 |
162 | ### [Выносим работу с API в функцию](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/7475ae2e4acfbe5ee6b2bd2735dacacfd759c90e)
163 |
164 | Отделяем «служебный» код от остального.
165 |
166 | Работа с API — утилитарная задача, которая не относится напрямую к оформлению заказа. Мы можем назвать это «сервисным» кодом, а сущность, которая этим будет заниматься, — сервисом.
167 |
168 | Функции оформления заказа не нужно знать детали того, как «сервис отправки данных» будет отправлять данные. Единственное, что ей _надо_ знать — что при вызове функции `makePurchase` данные отправятся.
169 |
170 | Так мы абстрагируем детали, разделяем ответственность между сущностями и в дальнейшем — снизим зацепление между модулями.
171 |
172 | ### [Уменьшаем дублирование](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/832b7d83e4b3cf66115298e4c1c2aebcefb25756)
173 |
174 | Набор одинаковых действий, которые _преследуют одинаковую цель_ — это дублирование кода. Нам стоит обращать внимание на такие повторяющиеся действия и выносить их в функции, называя понятными именами.
175 |
176 | Не любое дублирование — это зло. Иногда, особенно на ранних этапах проектирования, нам может просто не хватать данных о предметной области. Тогда лучше отметить дубликаты особыми метками в коде и вернуться к ним позже — когда информации о домене будет больше.
177 |
178 | Часто бывает, что две «казалось бы одинаковые» сущности ведут себя «почти одинаково», но деле они абсолютно разные. Лучше сперва понаблюдать за ними и объединить их позже, если потребуется.
179 |
180 | В этом же случае мы видим два набора одинаковых операций с одинаковой целью, которые можно назвать одним именем. Именно эти операции мы вынесем в функцию.
181 |
182 | ### [Выносим определение скидки в функцию](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/822a36732d9d07a62b788c60ec4bea3d0e147bf1)
183 |
184 | Абстрагируем детали, давая понятное имя. Это имя будет объясняться в терминах, понятных для уровня абстракции, где функция будет использоваться.
185 |
186 | ### [Заменяем тернарный оператор на `Math.min`](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/17e20dfc2c02823e9f97f4dba4737bdac1e09766)
187 |
188 | Улучшаем декларативность, используя `Math.min`.
189 |
190 | Нам неважно знать, как именно мы определим минимальное значение из перечисленных. Но считывать намерение из названия функции проще, чем из тела тернарного оператора.
191 |
192 | ### [Выносим проверку на пустоту корзины в функцию](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/403504bfd9e645b6d548f5b83c0e1e92784d21a2)
193 |
194 | Абстрагируем детали, давая понятное имя. Это имя будет объясняться в терминах, понятных для уровня абстракции, где функция будет использоваться.
195 |
196 | ### [Выносим проверку на наличие достаточного количества денег в функцию](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/2612d459bf32c8080d127e1b974f3a492ae5d19f)
197 |
198 | Абстрагируем детали, давая понятное имя. Это имя будет объясняться в терминах, понятных для уровня абстракции, где функция будет использоваться.
199 |
200 | ### [Выделяем создание объекта заказа в функцию](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/cbdda6146cf7738b07bc16e43fc47bf078b574d0)
201 |
202 | Заказ — это доменная сущность. У каждой доменной сущности есть жизненный цикл из одного или нескольких состояний. Вынося создание заказа в функцию мы фокусируемся на состояниях жизненного цикла доменной сущности и её дальнейших преобразованиях.
203 |
204 | Состояния и преобразования сущностей продиктованы бизнес-процессами и событиями в них. Работая с такими функциями нам проще соотнести преобразования в реальном мире и в коде.
205 |
206 | Также мы избавляем создание заказа ото всех сайд-эффектов и делаем так, чтобы функция `createOrder` только возвращала новый заказ. Так мы делим функции, которые _возвращают значения_, от функций, которые _производят сайд-эффекты_. Это называется разделением на команды и запросы, Command-Query Separation. CQS помогает предупредить нежелательные изменения данных и сделать ожидания от функций прозрачнее и надёжнее.
207 |
208 | ### [Исправляем «лгущие» имена](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/6aa493589e8287418b7debd14eb0758db5e9960c)
209 |
210 | Когда у нас достаточно информации о бизнес-процессах и приложении в целом, мы можем делать выводы о неправильных именах переменных.
211 |
212 | Иногда бывает так, что название неточное или откровенно врёт. В этом случае переменная содержит имя пользователя, но названа `_userId`. Может быть в этом проекте когда-то имена и использовались как идентификаторы, но не сейчас.
213 |
214 | Имена переменных должны быть правдивыми, иначе они будут сильно путать разработчиков. При наличии документации неправильные имена могут стать вторым «источником правды», откуда разработчики будут брать неверную информацию.
215 |
216 | ### [Выносим подсчёт цены списка товаров в отдельный модуль](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/138ccd221aca453b60b242552efd89288e4aa390)
217 |
218 | Расцепляем функциональность «оформления заказа» и «товаров». (А если не расцепляем, то как минимум делаем зацепление заметнее с помощью прямых импортов.)
219 |
220 | Теперь нам не нужно обращаться в модуль `Order`, чтобы посчитать итоговую сумму по списку, например, товаров из рекламной рассылки.
221 |
222 | ### [Выносим проверку пустоты корзины в отдельный модуль](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/e3b760c8c2dc3d2b3890667512ff86b8b562a60a)
223 |
224 | Снова расцепляем функциональность разных модулей: «корзины» и заказа».
225 |
226 | ### [Упрощаем название вынесенной функции](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/1e368520a9bb204132d44048f588ad915cfbc7b5)
227 |
228 | Так как теперь информация о «корзине» доступна из контекста модуля, мы можем убрать лишний префикс из названия функции.
229 |
230 | ### [Выносим применение купона скидки в отдельную функцию](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/8e4858eddbb2bae523c5c99ed77d7dfe353cf659)
231 |
232 | Прорабатываем жизненный цикл сущности «заказа».
233 |
234 | Заказ может находиться в разных состояниях: «создан», «подготовлен», «отправлен» и т.д. Одно из таких состояний в нашем случае – это «применена скидка». В бизнес-процессах такое состояние может фигурировать не только после создания заказа, но и в других случаях.
235 |
236 | Если мы имеем дело с отдельным состоянием, преобразование к нему лучше сделать отдельной функцией. Тогда мы можем протестировать это состояние изолировано, а также пользоваться композицией для применения скидки к абсолютно разным заказам.
237 |
238 | В этом случае все функции преобразования данных — это _запросы_ из CQS. Мы не _меняем_ созданный объект заказа `order`, чтобы избежать нежелательных или неконтролируемых сайд-эффектов. Вместо этого мы _преобразуем_ данные в новое состояние («заказ со скидкой») и возвращаем новый объект.
239 |
240 | Такой подход не даёт данным «случайно попасть» в невалидное состояние и вызвать из-за этого ошибку.
241 |
242 | ### [Делаем выбор скидки декларативным](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/9a06c8e4e9ef3cd20d629f00bfe99dda199ae983)
243 |
244 | Вместо `switch` мы используем словарь, в котором ключ — это купон, а значение — величина скидки.
245 |
246 | При таком написании мы передаём больше информации о предметной области в именах: имени словаря, имени фолбек-значения. Расширяемость кода не страдает, потому что новый купон можно добавить, дописав новую пару «ключ-значение» в словарь.
247 |
248 | (Этот код ещё и надёжнее, потому что в словаре невозможно сделать ошибку с пропущенным `break`.)
249 |
250 | ### [Выносим повторяющиеся условия в переменные](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/83087e073fb5197b84db429190b266eb10e4277c)
251 |
252 | При проверке статуса мы пару раз сравниваем его с `idle` и пару раз — с `loading`. Нам будет проще увидеть паттерны в условиях, если мы вынесем эти проверки в переменные и понятно назовём их.
253 |
254 | ### [Используем ранний `return` в рендере при наличии ошибки](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/77698649c7b24e6756ec86dc53899786b99bcf68)
255 |
256 | Упрощённое условие помогло увидеть, что вместо запутанной проверки и `else`-ветки можно «вывернуть» условие и сперва обработать эту самую `else`-ветку.
257 |
258 | Чтобы не держать в голове чрезмерно много условий, мы можем использовать ранний `return` и «отсеивать» ненужные проверенные ветки условия.
259 |
260 | Особенно хорошо это подходит для функции рендера: мы можем проверить все «проблемные» случаи, а потом работать с основной разметкой компонента.
261 |
262 | ### [Используем ранний `return` в рендере в состоянии загрузки](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/562d6251aaaad5b339da2cb8abebf2072e1954e6)
263 |
264 | Предыдущий ранний `return` помог «вытащить» вложенное условие на верхний уровень. Теперь мы видим, что его можно применить снова, на этот раз — к обработке состояния загрузки.
265 |
266 | ### [Используем прямое сравнение статуса](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/7bc70ea6f8a0cec8bd10d39e6ee9e1ff856fc56c)
267 |
268 | Когда мы распутали условие, стало видно, что оставшуюся проверку можно заменить на единственный непроверенный статус. Сверяемся со списком возможных состояний этого компонента и убеждаемся, что всё так и есть.
269 |
270 | ### [Меняем порядок проверки, чтобы сделать условие компактнее](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/576fe79895dac904055d0cbdfeb010f5ad4e66d5)
271 |
272 | Таким образом распутываем условие до конца и делаем его плоским.
273 |
274 | ### [Абстрагируем «служебный» код отлова ошибок](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/3ad5cf8a5e71a65dbc1e6405f61002b9699b6faa)
275 |
276 | Обработка ошибок — тоже функциональность, детали реализации которой бизнес-логике не важны. Мы можем вынести отлов ошибок в «сервис». Это сделает проверку декларативной, расцепит функциональность и уменьшит возможное дублирование.
277 |
278 | Также мы можем позаботиться о том, чтобы использование этого кода было максимально удобно внутри функционального пайплайна. Так мы сделаем код плоским, что в свою очередь сделает его проще для понимания.
279 |
280 | Используем result-контейнер для упрощения работы с отказными случаями. Теперь мы точно будем знать, в каком формате ожидать ответ от «небезопасных» функций.
281 |
282 | Пример контейнера в этом репо — игрушечный и не «канонический». Я рекомендую изучить ваш проект и посмотреть, не используется ли уже какая-либо реализация контейнера. Также стоит поглядеть на уже существующие решения, такие как [fp/ts](https://github.com/gcanti/fp-ts), перед написанием контейнеров с нуля.
283 |
284 | ### [Прячем работу с API за антикоррозионным слоем](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/37e22983809bfd6ac9e7aadc09614d791a7eee3e)
285 |
286 | Используем инверсию зависимостей, чтобы юзкейс зависел не от конкретной реализации сервиса API, а от интерфейса — контракта на поведение такого сервиса.
287 |
288 | Это ещё сильнее расцепляет код и позволяет заменять сервис как на другой сервис в продакшене, так и на моки во время тестов.
289 |
290 | ### [Разделяем UI-логику и вызов юзкейса](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/adf63312b71d1df9877a6050be4952fce1d928ec)
291 |
292 | При обработке и отправке формы нам нужно не только вызвать функцию юзкейса, но ещё и выполнить несколько операций, связанных с UI. Последние — это UI-логика, которая по-хорошему не должна быть смешана с первой.
293 |
294 | Этим коммитом мы расцепляем UI и вызов юзкейса, добавляя команду и её обработчик. Так мы делим ответственность между компонентом (отвечает за UI) и обработчиком команды (отвечает за работу с юзкейсом и предоставление ему всех необходимых данных).
295 |
296 | ### [Проверяем, насколько проще стало тестировать юзкейс](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/c1d1e766594695c6b9652a2b7ba277618845cde7)
297 |
298 | Пишем тесты, не зависящие от React и UI. Функция юзкейса теперь может быть проверена, как обычная асинхронная функция. Нам не требуется дополнительных технологий или специальной инфраструктуры для этого.
299 |
300 | Также мы можем подменить сервис отправки данных без использования глобальных моков или других костылей.
301 |
302 | ## Заключение
303 |
304 | В этом примере я постарался подобрать примеры на большую часть техник, которые использую сам в каждодневной работе. Но это далеко не всё, что бывает полезно во время рефакторинга.
305 |
306 | Полный список всех техник, полезных книг, постов и других докладов я собрал по ссылкам ниже:
307 |
308 | - [Основные эвристики](https://bespoyasov.ru/slides/refactor-like-a-superhero/?full#takes)
309 | - [Техники и рецепты](https://bespoyasov.ru/slides/refactor-like-a-superhero/?full#conclusion)
310 | - [Список литературы](https://bespoyasov.ru/slides/refactor-like-a-superhero/sources.html)
311 |
312 | ## Об авторе
313 |
314 | Саша Беспоясов, консультант в [0+X](https://0x.se). Пишу код больше 10 лет. Веду [технический блог](bespoyasov.ru), менторю начинающих разработчиков.
315 |
316 | - [0x.se](https://0x.se)
317 | - [bespoyasov.ru](https://bespoyasov.ru)
318 | - [Твитер](https://twitter.com/bespoyasov)
319 | - [Телеграм](https://t.me/bespoyasov)
320 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > Other languages: [Russian](./docs/ru.md).
2 |
3 | # Refactor Like a Superhero
4 |
5 | Refactoring code takes effort and resources. It isn't always possible to find time for it. In this talk, I want to share technics I use that help me find time for refactoring and search for problems in the code.
6 |
7 | From this talk you will learn:
8 |
9 | - How to sell the idea of refactoring to the business;
10 | - How to spot problems in the code and start “feeling” them intuitively;
11 | - What buzzwords from the development world are most useful in refactoring code.
12 |
13 | In this repository, I've collected code examples that I've used in the slides. You can find links to the slides and additional materials in the list below:
14 |
15 | - [Talk on YouTube](https://youtu.be/G7NcuYJ-HSM)
16 | - [Slides from the talk](https://bespoyasov.me/slides/refactor-like-a-superhero/)
17 | - [Sources and useful links](https://bespoyasov.me/slides/refactor-like-a-superhero/sources-en.html)
18 | - [Free online book about refactoring](https://github.com/bespoyasov/refactor-like-a-superhero)
19 |
20 | ## Commit History
21 |
22 | Each commit in this repository is one step in the refactoring of an application. The entire commit history reflects the refactoring process as a whole.
23 |
24 | The commit messages describe _what_ was done. Here I'll go into more detail about _why_ these changes were made and _what their benefits_ are.
25 |
26 | ### [Add “Dirty” Source Code](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/2c277c71e9bbd1204d1055c5eee934dd0ba79b94)
27 |
28 | Add an application with “dirty” source code. Our application is the cart of an online store. The cart contains a list of products, a field for entering a discount coupon and a button for sending the order.
29 |
30 | This commit will be the starting point. We will clean up the code starting from here.
31 |
32 | ### Define Refactoring Boundaries
33 |
34 | Before we start refactoring, it's worth determining the scope of code and functionality that we're going to refactor.
35 |
36 | This is necessary for two reasons:
37 |
38 | - we want to stay within the time and resource budget that we have;
39 | - with small changes, it's easier to manage them and keep track of what exactly broke the code.
40 |
41 | We can refactor a function, a module, a subsystem, or even the whole system. But as a rule of thumb, it's better to move in _small steps_. Several small refactoring sessions are better than a single big one.
42 |
43 | In our case, the refactoring scope is the checkout case.
44 |
45 | ### [Cover Use Case with Tests](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/68877aa0fc67fd27f8ca2d432f77d78331478579)
46 |
47 | When refactoring, it's important to make sure that we haven't broken anything. To do this, before doing _anything_, we cover with tests the part of the code we're going to refactor.
48 |
49 | When we write tests, we examine the code. It's worth testing as many edge cases as possible and seeing how the code behaves in them. The information on how the application behaves with different inputs will be useful in the future.
50 |
51 | In our case, the checkout use case is a slice of the entire application, so we need an end-to-end test. The type of tests isn't as important during refactoring as their _existence_. We can use unit tests too, if they cover the whole refactoring scope.
52 |
53 | We can also test the app manually, but we'd better automate it. We should check the code's work often—after each, even the smallest, change. In this way, we'll be able to find out more quickly what exactly has broken the code. We'll get tired of testing everything manually very quickly 😃
54 |
55 | ### [Apply Prettier](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/67dcaeeb429356518a626a9025c17d742bac5c1b)
56 |
57 | Let's start with a simple one: formatting. The more unified the code base is, the easier it's to navigate on it.
58 |
59 | In this project, let's say we decided to use Prettier as a set of formatting rules. Let's apply it.
60 |
61 | Sometimes Prettier breaks code when it wraps something onto a new line, for example. To make sure this doesn't happen, we check to see if the tests we wrote earlier pass.
62 |
63 | ### [Remove “Dead” Code](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/803d6b54bd1528ce72efb6188cd92b524bc0e572)
64 |
65 | We can also use linters, such as ESLint, for code hygiene.
66 |
67 | Linters will point out unreachable or unused code, as well as practices that the industry considers bad.
68 |
69 | Unused code we can remove. After each step, we'll check to see if the tests pass. In the future, I will stop emphasizing this. We'll just keep in mind that we test _every_ change.
70 |
71 | ### [Rename `d` to `discount`](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/72661edb6c7c754b5553eafa504526db49381d4e)
72 |
73 | For entities with unclear names, we should find out what they are responsible for.
74 |
75 | As a rule, an “obscure name” is a signal of a poor understanding of the domain, or problems with code separation and abstraction layers (more on this later).
76 |
77 | Too short names and abbreviations are bad because they hide information about the domain. Sooner or later such a name will be misread because all the “knowledgeable” developers have left the project.
78 |
79 | We can reduce the [bus factor](https://en.wikipedia.org/wiki/Bus_factor) by passing all the necessary information directly in the entity name. (As a last resort, in the documentation, but it gets outdated quickly, which can lead to multiple sources of information. It can become unclear which to trust: docs or the code.)
80 |
81 | ### [Use Domain Term (`User`) as Entity Name](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/b84ba8460b6702f89b7e42735e95c18665cf4d82)
82 |
83 | To make sure that everyone in the project understands each other, you can use ubiquitous language.
84 |
85 | This language consists of domain terms. The domain is an area of knowledge that the project is modelling. These terms should be used everywhere in the design and development of the system.
86 |
87 | In our case, we call the variable `User`. This way anyone involved in the project will be able to interpret the code and its purpose correctly.
88 |
89 | ### [Declare `_userId` Closer to Where It's Used](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/03f879ae9e2bb24117d150e697a14978b278d277)
90 |
91 | When the code is “scattered” throughout the file, it becomes difficult to read and “scan” with your eyes during an eye-scanning.
92 |
93 | In this case, we have to keep in mind everything that happened to the `_userId` variable before we started using it. There are two problems with this:
94 |
95 | - The variable is used _too far_ from where it's declared;
96 | - It can be changed anywhere in the code, we have to keep in mind all such changes.
97 |
98 | We'll talk more about immutability later. With this commit we will solve the first problem by declaring the variable closer to where it is used.
99 |
100 | ### [Declare `products` Closer to Where It's Used](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/1bf8fb1d980ee19fa6f448c098bdf1e87ac2f6e9)
101 |
102 | For the same reasons as last time, we “defragment” this part of the code as well.
103 |
104 | ### [Make Total Amount Calculation Declarative](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/63cb8917f653eeed98b81fd72fd9170f8c35c762)
105 |
106 | A declarative code is one that tells you _what_ it does. An imperative code, on the other hand, that tells you _how_ it does something.
107 |
108 | Declarative code expresses the _intent_. It's easier to read because it hides unnecessary implementation details under clear function and variable names. Declarative code helps to express itself in terms of the level of abstraction at which the reader of the code is at that point in time.
109 |
110 | ### [Rename `price` to Avoid Identical Names for Different Entities](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/86dea9d987af5aa0beb780efa916c6c0070b2929)
111 |
112 | Different entities can be called by the same name if they're in different contexts or if the interaction with them is separated in time.
113 |
114 | In other cases, it's better to use different names for different entities. This prevents from confusion when reading and errors when executing code.
115 |
116 | (Identical names are especially dangerous if the code is mutable. Changing one variable may accidentally affect another with the same name.)
117 |
118 | ### [Use `onSubmit` to Submit the Form](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/16313564ad6d9dc954230bba1a612b087b6482e0)
119 |
120 | One of the “low-hanging fruit” in refactoring is code that can be replaced by language or environment features.
121 |
122 | We can, for example, replace the helper that imitated the `.includes()` method with the method itself. Or, as in our case, don't handle two separate events (clicking on the button and pressing Enter in the field), but use the form submission event.
123 |
124 | ### [Use `FormData` for Form Serialization](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/62d16cb554ffe9d708b40cdafef7e957f260b4b2)
125 |
126 | The standard `FormData` saves us from writing a bunch of unnecessary code. (And probably solves the problem better than us.)
127 |
128 | It's also a data structure that was specifically invented for the task of serializing forms. Using the right data structures can often determine what kind of algorithm we should use, and how effective the algorithm will be.
129 |
130 | ### [Split Business Logic and Side Effects](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/aca4619f4e76526ab7b467a0cb366065a75d2193)
131 |
132 | The business logic is the most important thing in an application. The simpler it's written, the easier it is to test, validate, and modify.
133 |
134 | I prefer to use pure functions and a functional approach to write it. Interacting with the outside world, however, is always associated with side effects.
135 |
136 | To avoid mixing business logic and side effects, I use a code organization principle called “Functional Core / Imperative Shell”. (Or as Mark Seemann calls it, [Impureim Sandwich](https://blog.ploeh.dk/2020/03/02/impureim-sandwich/).)
137 |
138 | The principle is to keep the logic pure and keep the side effects _around_ it. For example:
139 |
140 | - Side effect to get data from the outer world;
141 | - Functional core for transforming data;
142 | - Side effect to store data in the outer world.
143 |
144 | This commit groups the code so that everything related to business logic is in the center of the function and all of the side effects are around it. We will also use this grouping later on.
145 |
146 | ### [Decouple UI and Use Case](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/5fe6e1f3c459f98ff633ae04fdeb12b17c6f7ad6)
147 |
148 | We apply the “Separation of Concerns” principle and separate the use case function from the component rendering. These are two different tasks, so they should be handled by different entities.
149 |
150 | We'll also reduce the coupling between “checkout” and “displaying information on the screen” functionality. This will help us make the use case independent of the framework and libraries. It'll be easier for us to test it and check whether it meets our requirements.
151 |
152 | ### [Clarify the Data Provider Name](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/cbb7a5033af28fd04b54bd01d2a6d9d2077c41c5)
153 |
154 | Refine the name of the user data provider by adding the missing details needed for this level of abstraction.
155 |
156 | Since we use data providers for different entities in the component, it's worth specifying which entity each one refers to. A name that is too abstract can be a problem to read: we'll have to keep the missing details in mind.
157 |
158 | ### [Shorten Form Submission Handler Name](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/80ebc00bb9bfbf7e0a2cc3e8051ec037a99f0e07)
159 |
160 | We have abstracted the checkout into a function. We can now use the name of this function as a _term_ to explain ourselves at the current level of abstraction.
161 |
162 | We now can avoid duplication of the details contained in the name of the checkout function in the name of the form submit handler.
163 |
164 | ### [Extract API](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/7475ae2e4acfbe5ee6b2bd2735dacacfd759c90e)
165 |
166 | Here we separate the “service” code from the rest.
167 |
168 | Working with the API is a utility task that isn't directly related to the checkout process. We can call it “service” code, and the entity that performs it—a service.
169 |
170 | The checkout function doesn't need to know the details of how the “data sending service” sends the data. The only thing it _needs_ to know is that when the `makePurchase` function is called, the data is sent.
171 |
172 | This way we abstract the details, separate the responsibility between the entities and further—reduce coupling between the modules.
173 |
174 | ### [Reduce Duplication](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/832b7d83e4b3cf66115298e4c1c2aebcefb25756)
175 |
176 | A set of identical actions that _have the same goal_ is duplication. We should pay attention to such repetitive actions and put them in functions, calling them by understandable names.
177 |
178 | Not all the duplication is pure evil. Sometimes, especially in the early stages of the project, we simply may not have enough data about the domain. Then it's better to mark the duplicates with special labels in the code and return to them later—when there's more information about the domain.
179 |
180 | It often happens that two “seemingly identical” entities behave “almost the same”, but in fact they are completely different. It's better to observe them first and merge them later, if necessary.
181 |
182 | In this case, we see two sets of identical operations with the same goal, which can be called by the same name. These operations can be extracted into a function.
183 |
184 | ### [Extract Discount Calculation into a Function](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/822a36732d9d07a62b788c60ec4bea3d0e147bf1)
185 |
186 | Abstract the details by giving a clear name. This name is explained in terms understandable to the level of abstraction where the function is used.
187 |
188 | ### [Replace Ternary Operator with `Math.min`](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/17e20dfc2c02823e9f97f4dba4737bdac1e09766)
189 |
190 | We improve the declarativeness of the code by using `Math.min`.
191 |
192 | It isn't important for us to know _how_ exactly we define the minimum value of the listed ones. But it's easier to read the intention from the function name than from the body of the ternary operator.
193 |
194 | ### [Extract Cart Emptiness Check into a Function](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/403504bfd9e645b6d548f5b83c0e1e92784d21a2)
195 |
196 | Abstract the details by giving a clear name. This name is explained in terms understandable to the level of abstraction where the function is used.
197 |
198 | ### [Extract Money Amount Check into a Function](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/2612d459bf32c8080d127e1b974f3a492ae5d19f)
199 |
200 | Abstract the details by giving a clear name. This name is explained in terms understandable to the level of abstraction where the function is used.
201 |
202 | ### [Extract Order Creation into a Function](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/cbdda6146cf7738b07bc16e43fc47bf078b574d0)
203 |
204 | An order is a domain entity. Each domain entity has a lifecycle of one or more states. By putting the creation of an order into a function, we focus on the states of the domain entity lifecycle and its further transformations.
205 |
206 | Entity states and transformations are dictated by business processes and events in them. Working with such functions, it's easier for us to relate real-world processes and code transformations.
207 |
208 | Also, we get rid of all side effects in the order creation and make it so that the `createOrder` function now only returns a new order. Thus, we differentiate between functions that _return values_ and functions that _produce side effects_. This is called Command-Query Separation and it helps making code behave more expectedly and controllably.
209 |
210 | ### [Fix “Lying” Names](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/6aa493589e8287418b7debd14eb0758db5e9960c)
211 |
212 | When we have enough information about the business processes and the application as a whole, we can infer incorrect variable names.
213 |
214 | Sometimes it happens that the name is inaccurate or even false. In this case, the variable contains a user name, but is called `_userId`. Maybe this project once used names as identifiers, but not now.
215 |
216 | Variable names must be truthful, otherwise they will greatly confuse developers. With documentation, incorrect names can become a second “source of truth” from which developers will get the wrong information.
217 |
218 | ### [Extract Total Price Calculation into a Module](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/138ccd221aca453b60b242552efd89288e4aa390)
219 |
220 | Here we decouple the functionality of “checkout” and “products” modules. (And if we don't decouple them, we at least make the coupling more noticeable with the direct imports).
221 |
222 | Now we don't need to refer to the module `Order` to calculate the total amount for a list of, for example, products from the promotional newsletter.
223 |
224 | ### [Extract Cart Emptiness Check into a Module](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/e3b760c8c2dc3d2b3890667512ff86b8b562a60a)
225 |
226 | Again we decouple the functionality of different modules: “cart” and “order”.
227 |
228 | ### [Simplify Extracted Function Name](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/1e368520a9bb204132d44048f588ad915cfbc7b5)
229 |
230 | Since the information about the “shopping cart” is now available from the module context, we can remove the extra prefix from the function name.
231 |
232 | ### [Extract Discount Applying into a Function](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/8e4858eddbb2bae523c5c99ed77d7dfe353cf659)
233 |
234 | We work through the lifecycle of the “order” entity.
235 |
236 | An order may be in different states: “created”, “prepared”, “shipped”, etc. One of these states in our case is “discount applied”. In business processes, this state can appear not only after the order has been created, but also in other cases.
237 |
238 | If we're dealing with a _separate state_, it's better to make the transformation to it a separate function. Then we can test this state in isolation and use the composition to apply a discount to completely different orders.
239 |
240 | In this case, all our data transformation functions are made as _queries_ from CQS.
241 |
242 | We don't update the created `order` object to avoid unexpected or uncontrolled side effects on that object. Instead, we _transform_ data into the new state, “discounted order”, and return a new object. This prevents application data from going into an invalid state and causing errors.
243 |
244 | ### [Make Discount Selection Declarative](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/9a06c8e4e9ef3cd20d629f00bfe99dda199ae983)
245 |
246 | Instead of `switch` we use a dictionary in which the key is the coupon and the value is the discount value.
247 |
248 | When written this way, we pass more information about the subject area in names: the dictionary name, the name of the value fallback. The extensibility of the code doesn't suffer, because a new coupon can be added by adding a new key-value pair to the dictionary.
249 |
250 | (This code is also more reliable, because it's impossible to make a mistake with a missing `break` in the dictionary.)
251 |
252 | ### [Extract Repetitive Conditions in Variables](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/83087e073fb5197b84db429190b266eb10e4277c)
253 |
254 | When we check the status, we compare it a couple of times with `idle` and a couple of times with `loading`. It'll be easier for us to see the patterns in the conditions if we put these checks into variables and name them clearly.
255 |
256 | ### [Use Early `return` in Render in Case of Error](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/77698649c7b24e6756ec86dc53899786b99bcf68)
257 |
258 | The simplified condition helped us see that we can turn the condition inside out and handle the `else`-branch first.
259 |
260 | To avoid keeping too many conditions in mind, we can use an early `return` and “filter out” unnecessary checked condition branches.
261 |
262 | This is especially good for the render function: we can check all “problematic” cases, and then work with the main markup of the component.
263 |
264 | ### [Use Early `return` in Render for Loading State](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/562d6251aaaad5b339da2cb8abebf2072e1954e6)
265 |
266 | The previous early `return` helped “pull” the nested condition to the top level. Now we see that it can be applied again, this time to the processing of the loading condition.
267 |
268 | ### [Use Direct Status Comparison](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/7bc70ea6f8a0cec8bd10d39e6ee9e1ff856fc56c)
269 |
270 | When we unraveled the condition, it became clear that the remaining check can be replaced by a single unchecked status. We check the list of possible states of this component and make sure that this is the case.
271 |
272 | ### [Compactify Condition by Changing Check Order](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/576fe79895dac904055d0cbdfeb010f5ad4e66d5)
273 |
274 | In this way we unravel the condition to the end and make it flat.
275 |
276 | ### [Abstract “Service” Error Catching Code](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/3ad5cf8a5e71a65dbc1e6405f61002b9699b6faa)
277 |
278 | Error handling is also a functionality whose implementation details are not important to the business logic. We can bring error catcher into “service”. This will make the checking declarative, decouple functionality, and reduce possible duplication.
279 |
280 | We can also make sure that the use of this code is convenient within the functional pipelines. This way we make the code flat, which in turn makes it easier to understand.
281 |
282 | We'll use the result container to make it easier to handle failures. Now we'll know exactly in what format to expect a response from "unsafe" functions. The concrete container implementation heavily depends on the project, its style, and requirements.
283 |
284 | I don't claim this implementation to be a “canonical” one. Instead, I encourage you to investigate your project and see if some container implementation is alrady used. Also, take a look at existing solutions like [fp/ts](https://github.com/gcanti/fp-ts) before implementing containers from scratch.
285 |
286 | ### [Use Anti-Corruption Layer for API](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/37e22983809bfd6ac9e7aadc09614d791a7eee3e)
287 |
288 | We use dependency inversion, so that the use case depends not on a particular implementation of the API service, but on the interface—the contract on the behavior of such a service.
289 |
290 | This decouples the code even further and allows you to replace the service with another service in production, as well as with mocks during tests.
291 |
292 | ### [Split UI Logic and Use Case Logic](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/adf63312b71d1df9877a6050be4952fce1d928ec)
293 |
294 | When processing and submitting a form, we not only need to call the use case function, but also perform some operations related to the UI. The latter are UI logic, which should not be mixed up with the former.
295 |
296 | With this commit, we decouple the UI and the use case call by adding a command and its handler. This way we divide the responsibility between the component (responsible for the UI) and the command handler (responsible for handling the use case and providing it with all the necessary data).
297 |
298 | ### [Check if Use Case is Now Easier to Test](https://github.com/bespoyasov/refactor-like-a-superhero-talk/commit/c1d1e766594695c6b9652a2b7ba277618845cde7)
299 |
300 | Here we write tests independent of React and UI. The use case function can now be tested like a regular asynchronous function. We don't need any additional technology or special infrastructure to do this.
301 |
302 | We can also change the data sending service without using global mocks or complex testing setup.
303 |
304 | ## Conclusion
305 |
306 | In this example, I've collected examples of most of the techniques that I use in my daily work. But this is far from all that's useful in refactoring.
307 |
308 | I've gathered a complete list of all the techniques, useful books, posts, and other talks in the links below:
309 |
310 | - [Main Heuristics](https://bespoyasov.me/slides/refactor-like-a-superhero/?full#takes)
311 | - [Technics and How-Tos](https://bespoyasov.me/slides/refactor-like-a-superhero/?full#conclusion)
312 | - [Complete List of Sources](https://bespoyasov.me/slides/refactor-like-a-superhero/sources-en.html)
313 |
314 | ## About Author
315 |
316 | Alex Bespoyasov, consultant at [0+X](https://0x.se). Been writing code for more than 10 years. Have a [technical blog](bespoyasov.me), teach and mentor other developers.
317 |
318 | - [0x.se](https://0x.se)
319 | - [bespoyasov.me](https://bespoyasov.me)
320 | - [Twitter](https://twitter.com/bespoyasov_)
321 | - [Telegram](https://t.me/bespoyasov)
322 |
--------------------------------------------------------------------------------