👇

3.4. What you need, where you need it (WYNWYN): Контекстозависимость и Связанность кода

Основное правило, которые я взял для себя в кодинге, так это писать:
 
То, что нужно, там, где нужно (ТЧНТГН) (англ) What you need, where you need it (WYNWYN)
 
Наверное, главное вопрос, на который отвечает данный принцип: создавать или не создавать переиспользуемый код?
 
Так вот если следовать WYNWYN, то в 90% случаев надо писать код, который решает задачу там, где она у меня возникла только через какое-то время буду думать нужно ли его выносить, чтобы переиспользовать в другом месте.
 
И сейчас я докажу почему WYNWYN работает.

“Никто не понимает DRY” или “Контекстозависимость”

Во-первых, мало кто понимает “Don’t Repeat Yourself”. 
 
Большинство разработчиков трактуют это правило как “не должно быть повторяющегося кода”.
 
Поэтому, как только, неопытный разработчик видит, что какой-то код повторяется (ну, то есть, прям 3-5 строчек одинаково написаны в 2-х разных местах), он сразу пытается выделить их в отдельный метод / функцию / слой / etc.
 
Но самое важное, что они забывают:
 
Один и тот же код, в разных контекстах является разным.
 
И это полностью меняет смысл понятия DRY. Я бы даже сказал наоборот: это наталкивает нас на то, что “Repeat Yourself” может быть даже более полезным.
 
Для этого понимания давайте обсудим понятие “контекст”.

Что такое контекст

Точного определения у “контекста” нет, я бы описал это как: “принадлежность кода к бизнес-логике конкретного бизнес-процесса и доменной области кода”.
 
Чем контекст может быть на практике:
 
  1. Роль пользователя, который стригерил бизнес-логику (”заказ создает администратор / клиент / партнер”)
  1. Применяется ли этот бизнес-процесс к одной (”удалить пользователя по id”) или ко множеству сущностей (”удалить пользователей, который не появлялись более года”)
  1. Тип устройства от которого мы парсим данные (”парсим данные с кофемашины / вендингового автомата”)
  1. 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. Аксиомы

 
Все, что является аксиомой (доказанным и нерушимым правилом) может лежать в отдельной переиспользуемой области.
 
Например:
 
  1. Математические формулы и типы данных (вектор и функция его сложения, формула вычисления скорости падающего предмета, etc.)
  1. Интерфейс доступа к сторонним сервисам, то есть все, что представляет является SDK / библиотекой API.
  1. Формулы, заданные ГОСТ-ами (например, по вычислению минимально требующегося индекса качества материала, на базе анализа его содержания)
  1. Специфические правила вашей бизнес-области (например, мы не можем отправлять на наши устройства числа больше 8bit, поэтому функции создания или валидации этих чисел можно вынести в общую область)
  1. Описание структуры таблиц вашей базы данных (если у сервиса есть доступ к этой базе и к этим таблицам, значит он имеет право просто видеть какие там есть структуры данных)
 
Для еще большего упрощения: если из кода можно сделать библиотеку или SDK, положить в общем доступе или в ваш закрытый репозиторий и это будет логично, то значит этот код можно выносить в переиспользуемую область.

Заключение

 
Возможно, эта статья противоречит всему тому, чему вас учили, но в этом и вся сложность кодинга: ты начинаешь как чистый лист и просто пишешь код, потом ты думаешь “мой код слишком тупой, так быть не должно” и учишь разные “паттерны”, как сделать его “умным”, а в конце, намучавшись с этим “умным” кодом, ты понимаешь, что был прав первоначально.
 
notion image
 
 
Почему важно пройти этот путь? Потому что в начале ты пишешь тупой код, не выбирая его, а от незнания, а потом ты пишешь тупой код, выбирая его от знания.
 
А еще эта глава может быть неочень понятна, потому что это все ооочень сложно нормально написать и не думаю, что у меня могло получится с первого раза, поэтому просьба: оставьте комментик, чтобы я мог сделать ее более очевидной.
 

👈 Предыдущая глава
Следующая глава 👉