Основное правило, которые я взял для себя в кодинге, так это писать:
То, что нужно, там, где нужно (ТЧНТГН) (англ) What you need, where you need it (WYNWYN)
Наверное, главное вопрос, на который отвечает данный принцип: создавать или не создавать переиспользуемый код?
Так вот если следовать WYNWYN, то в 90% случаев надо писать код, который решает задачу там, где она у меня возникла только через какое-то время буду думать нужно ли его выносить, чтобы переиспользовать в другом месте.
И сейчас я докажу почему WYNWYN работает.
“Никто не понимает DRY” или “Контекстозависимость”
Во-первых, мало кто понимает “Don’t Repeat Yourself”.
Большинство разработчиков трактуют это правило как “не должно быть повторяющегося кода”.
Поэтому, как только, неопытный разработчик видит, что какой-то код повторяется (ну, то есть, прям 3-5 строчек одинаково написаны в 2-х разных местах), он сразу пытается выделить их в отдельный метод / функцию / слой / etc.
Но самое важное, что они забывают:
Один и тот же код, в разных контекстах является разным.
И это полностью меняет смысл понятия DRY. Я бы даже сказал наоборот: это наталкивает нас на то, что “Repeat Yourself” может быть даже более полезным.
Для этого понимания давайте обсудим понятие “контекст”.
Что такое контекст
Точного определения у “контекста” нет, я бы описал это как: “принадлежность кода к бизнес-логике конкретного бизнес-процесса и доменной области кода”.
Чем контекст может быть на практике:
- Роль пользователя, который стригерил бизнес-логику (”заказ создает администратор / клиент / партнер”)
- Применяется ли этот бизнес-процесс к одной (”удалить пользователя по id”) или ко множеству сущностей (”удалить пользователей, который не появлялись более года”)
- Тип устройства от которого мы парсим данные (”парсим данные с кофемашины / вендингового автомата”)
- etc.
Основная особенность контекста – он напрямую влияет на то, как этот код дальше будет развиваться. Потому что новые фичи или изменение текущих правил чаще всего связаны только с одним контекстом.
Например, появилось требование: “при создании заказа партнером, уходили письма не только ему, но и нашим админам” – если код, написанный для разных ролей один и тот же, вам придется начать его ветвить, что может привести к куче ошибок и деградации системы в целом.
Если вы хотите, чтобы ваш код был достаточно гибким , всегда думайте о том, в каком контексте вы его пишите и стоит ли переиспользовать его в другом контексте.
Связанность кода
Второй аргумент в пользу WYNWYN: каждый раз, когда вы выделяете куда-то обобщенный код, вы повышаете связанность кода.
Это значит, что 2 куска кода, начинают оба зависеть от какого-то одного, а значит при его изменении, у нас всегда есть шанс сломать зависимые кодовые базы.
Поменяли что-то, чтобы добавить фичу, сломали что-то старое:
// common/index.ts // Вот эта переиспользуемая функция, которая повышает связанность кода const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } // first/index.ts import {someCommonFunction} from "common/index.ts" const first = () => { // ... someCommonFunction(foo, bar) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
А теперь сделаем изменение:
// common/index.ts // Например, нам теперь в первом случае надо еще добавлять postfix. // Сделаем это самым хреновым способом: добавим флаг const someCommonFunction = (foo: string, bar: Record<any, any>, postfixNeeded: boolean = false) => { // ... } // first/index.ts import {someCommonFunction} from "common/index.ts" const first = () => { // ... someCommonFunction(foo, bar, true) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
Это простое изменение, но даже оно может случайно сломать логику приложения.
Чуть лучше, было бы сделать 2 разные функции, это чуть больше увеличит надежность использования измененной функции:
// common/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } const someCommonLogicWithPrefix = (foo: string, bar: Record<any, any>) => { // ... someCommonFunction(foo, bar) // ... } // first/index.ts import {someCommonLogicWithPrefix} from "common/index.ts" const first = () => { // ... someCommonLogicWithPrefix(foo, bar) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
Это еще часто называют “не меняйте существующее, а создавайте новое” и тут я полностью согласен, только с поправкой: “не меняйте существующее, а создавайте новое там, где вы это будете использовать”:
// common/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } // first/index.ts import {someCommonLogic} from "common/index.ts" const someCommonLogicWithPrefix = (foo: string, bar: Record<any, any>) => { // ... someCommonFunction(foo, bar) // ... } const first = () => { // ... someCommonLogicWithPrefix(foo, bar) // ... } // second/index.ts import {someCommonFunction} from "common/index.ts" const second = () => { // ... someCommonFunction(foo, bar) // ... }
И самый прекрасный вариант, если в реальности
someCommonFunction
слишком сильно меняется в зависимости от контекста, просто оставить по одной нужный версии в каждом месте кода:// first/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } const first = () => { // ... someCommonLogic(foo, bar) // ... } // second/index.ts const someCommonFunction = (foo: string, bar: Record<any, any>) => { // ... } const second = () => { // ... someCommonFunction(foo, bar) // ... }
Если у них разные контексты исполнения, то тогда подобный подход позволит вам независимо друг от друга развивать их и не боятся, что изменение в одном месте сломает другое.
ООП поощеряет нарушение WYNWYN
Это одна из моих самых больших претензий к ООП.
Проблема в том, что он мотивирует разработчиков к тому, чтобы выделять код в какие-то обобщенные места, нарушая как контекстозависимость, так и добавляя связанность кода:
class First { someLogic() { // ... // И вот по-середине кода какая-то переиспользуемая логика // ... } } class Second { someLogic() { // ... // И вот по-середине кода какая-то переиспользуемая логика // ... } }
Я видел как люди старались вынести эту логику в отдельный класс, чтобы “придерживаться SOLID”:
class CommonLogicService { commonLogic() { // ... } } class First { constructor(private commonLogicService: CommonLogicService) {} someLogic() { // ... this.commonLogicService.commonLogic() // ... } } class Second { constructor(private commonLogicService: CommonLogicService) {} someLogic() { // ... this.commonLogicService.commonLogic() // ... } }
Этот кейс исправляется достаточно просто, но люди должны понять, что в этом нет проблемы в том, чтобы дублировать код:
class First { // Опять же, вместо выноса в отдельный класс, мы просто создадим // приватный метод и сделаем его дубликат в классе Second private commonLogic() { // ... } someLogic() { // ... this.commonLogic() // ... } } class Second { private commonLogic() { // ... } someLogic() { // ... this.commonLogic() // ... } }
И так мы решим вопрос с контекстозаваимостью и связанностью.
Но гораааздо хуже в ООП то, что является его основным преимуществом: абсолютно все методы, относящиеся к классу будут находится на этом классе.
// Например class User { private id: string private email: string private age: number changeEmail() { // ... } changeAge() { // ... } }
Так а в чем проблема?
А теперь представьте, что мы хотим сделать возможность изменения email но для админа (то есть, с измененной логикой) и следуя best-pracrise сверху мы сделаем отдельный метод:
// Например class User { private id: string private email: string private age: number changeEmail() { // ... } changeEmailByAdmin() { // ... } changeAge() { // ... } }
А потом тоже самое для
age
и причем разными ролями и выделением общей логики:// Например class User { private id: string private email: string private age: number private changeEmailCommonLogic() { // ... } changeEmail() { // ... } changeEmailByPartner() { // ... } changeEmailByAdmin() { // ... } private changeAgeCommonLogic() { // ... } changeAge() { // ... } changeAgeByAdmin() { // ... } changeAgeByPartner() { // ... } }
Так 2 метода с развитием приложения и появления большей разницы в правилах контекстов превращаются в 8. Но самое ужасное то, что я готов поспорить, что практически все эти методы вызываются всего-лишь в 1-м конкретном месте… и при этом они засоряют файл с кодовой базой User…
И когда я об этом думаю, у меня на полужопиях волосы встают дыбом…
Еще прикольнее, когда другой разработчик не понял, что это контекстозависимая функция, решил ее использовать, а потом, когда его контексте перестал подходить под текущий, надобавлял туда кучу флагов…
Да, преимущество этого подхода – мы знаем все операции над сущностью пользователя, но на мой взгляд недостаток гораздо больше – когда ВСЕ операции над пользователем, которые вызываются всего лишь 1 раз за код, засоряют 1 общий файл.
И это одна из моих главных претензий к ООП наравне с излишней атомарностью из главы 2.3. Неисправимая проблема ООП.
Лайфхаки
Приведу пару лайфхаков, которые помогают мне принимать решение о переиспользовании кода:
i. Правило 3-х
Для себя я выбрал одно очень просто правило, по которому я понимаю стоит ли выносить что-то или нет:
Я выношу код для переиспользования только в том случае, если повторил его 3 и более раз
Опять же! Все будет зависеть от контекста, но есть большой шанс, что в 2-х из 3-х случаев контекст будет повторяться.
ii. Аксиомы
Все, что является аксиомой (доказанным и нерушимым правилом) может лежать в отдельной переиспользуемой области.
Например:
- Математические формулы и типы данных (вектор и функция его сложения, формула вычисления скорости падающего предмета, etc.)
- Интерфейс доступа к сторонним сервисам, то есть все, что представляет является SDK / библиотекой API.
- Формулы, заданные ГОСТ-ами (например, по вычислению минимально требующегося индекса качества материала, на базе анализа его содержания)
- Специфические правила вашей бизнес-области (например, мы не можем отправлять на наши устройства числа больше 8bit, поэтому функции создания или валидации этих чисел можно вынести в общую область)
- Описание структуры таблиц вашей базы данных (если у сервиса есть доступ к этой базе и к этим таблицам, значит он имеет право просто видеть какие там есть структуры данных)
Для еще большего упрощения: если из кода можно сделать библиотеку или SDK, положить в общем доступе или в ваш закрытый репозиторий и это будет логично, то значит этот код можно выносить в переиспользуемую область.
Заключение
Возможно, эта статья противоречит всему тому, чему вас учили, но в этом и вся сложность кодинга: ты начинаешь как чистый лист и просто пишешь код, потом ты думаешь “мой код слишком тупой, так быть не должно” и учишь разные “паттерны”, как сделать его “умным”, а в конце, намучавшись с этим “умным” кодом, ты понимаешь, что был прав первоначально.
Почему важно пройти этот путь? Потому что в начале ты пишешь тупой код, не выбирая его, а от незнания, а потом ты пишешь тупой код, выбирая его от знания.
А еще эта глава может быть неочень понятна, потому что это все ооочень сложно нормально написать и не думаю, что у меня могло получится с первого раза, поэтому просьба: оставьте комментик, чтобы я мог сделать ее более очевидной.
👈 Предыдущая глава
Следующая глава 👉