Падвойкаванне проты суязу: як адначыць, калі трэба існаваць спяльны код
Дазнайце, чаму адзінаванне сэродніх компонентаў React або служб NestJS можа коштаць больш, чым ўдвойненне, і выкорыстайце трохі пытанняў, каб адначытаць, калі абстракцыя заслуговае на свое месца.
Большасць разработчыкаў навучаныя спрыймаць павтарэнне коду як дыфект: знаходзяць два падобныя компоненты, выделяючы спакульнае элемента і праходзячы далей. Што часта ёсць правильна, але гэта маскіруе выкарыстоўваны кашт, який стае зрозумелым толькі чераз калькі месцаў, калі спакульны элемент павінен служыць корыстнікам, чыя трэбавання зменіліся. Акцэнтаваўшы увагу на выборе между двума разнымі каштамі — дуплікацыяй і зв’язкам — вы можетэ задаць сабе калькі пытанняў, каб адначасова вярнуцца ў React, React Native і коды бэкенду.
Основная тэза простая, але лёгкая да неправильнага разумевання: павтарэнне не ёсць чыстай добродзея, але ў дазначальных ситуацыях дуплікацыя є дешавейшая за зв’язак. Далей прадстаўленыя інструкцыі, якія дапамагаюць распазнаць такія ситуацыі.
Як адна чыстая спакульная компанента ператвараецца на язык канфігурацыі
Пачніце з двух компанентаў, якія атрыбутуюць зображэнне чалавека. Адна з іх падходзіць для звычных корыстнікаў:
<UserAvatar user={user} />
Іншы варыянт прызначан для трэйнераў:
<TrainerAvatar trainer={trainer} />
У першы дзень яны практычна неразлічныя. Кожны з яных адрасавае:
- образ прафіля
- заменны элемент, калі абраза няма
- ідэнтычныя размеры
- стан завантажэння
Вочыма зрозумелы вывар — адна складовая павінна виканаць оба завадзены, таму практыкуецца універсальны аватар:
<Avatar
imageUrl={...}
fallback={...}
size="medium"
/>
Здаецца, гэта явны ўспех: менш коду, адна пазіцыя для вылечэння багоў. Адночаса продукт развіваецца. Карыстальнікам патрэбны заменны элементы, якія лячуць правілы прыватнасці. Трэйнерам патрэбны бейджы паўтарання. Карыстальнікі могу выкарыстоўваць толькі ініцыялы. Трэйнеры паказваюць, чы робяць яны наразе доступныя. Кожны запит падае на спакульную складовую як ўтварэнне prop:
<Avatar
imageUrl={...}
fallback={...}
size="medium"
showInitials={...}
showVerification={...}
showAvailability={...}
privacyMode={...}
/>
Працюе ўсё больша колькасць трэбаванняў, і кожна з яных стае адзінам з паказначыкаў. У канцэ на “загальны” аватар ведае пра корыстнікаў, трэнероў, правіла прыватнасці, процесы верыфікацыі, доступнасць і розныя бізнес-логікі, якія не маюць нічынага з адрасаванням кола з карткай. Тэхнічна ён заўсёды можа быць перадузначаны, але ён вялікай меры не ўжо просты. Складнасць ніколі не была усунутая; яна была зборана ў аднам файле, дзе кожны корыстнік тепер залежыць ад усіх яго элементаў. Якщо гэты патерн “выбуху пропаў” здаецца вам знайомым, стаття на коли перадузначальныя компоненты React пагубна дзейнуюць дэтальней розглядае процес рефакторавання такіх компонентаў.
Раздзеленне коду значыць раздзеленне будучыні
Тое, пра чым рэдкая час обсужваецца, — гэта тое, што выкарыстоўванне спяльнага коду не ўсё толькі яе рашэнне па атрыбутамаў імплементацыі. Ён з’яеднае будучыню викорыстоўчаў. Калі компонент А і компонент Б аба залежыць ад однай абстракцыі, будзь-яя змена, якая адбуваецца ў А, можа зламаць чы прыняць новую форму Б. Павторны выкарыстоўвання стварае зв’язак, а зв’язкі ўособліваюцься як куплінг.
Гэта зменяе пытанне, якое варта задаць. На запитанне «Чы могуць гэтыя два элементы выкарыстоўваць спяльны код?» практычна завжды можна даць адпаведзенне «так». Кращым пытаннем є: чы магуць гэтыя два элементы раздзеляць адну будучыню?
Два блакіты коду можу сьогодні быць абсалютнаўна ідэнтычныя за тэкстам, пры чым яшчэ належаць да несвязаных частак домэна. Їх реалізацыя збігаецца; прычыны іх змены можу не збігацца. Гэтае разлікаванне набагато краща прыбліжна вказвае на вартасць тэкстаўнага адтрымання, чым лік дуплікаваных ліней. Гэта тычыцца да прынцыпа адной адпаведнасці, які зазвычай формулюецца як «модуль павінен маты адную прычыну для змены», дзе прычына змены — цэлая група людзей, чыя запиты яе спрыяюць.
Калі дуплікаванне маркапаў дае незалежнасць
Разглядзім карточку корыстніка:
<UserCard />
і карточку платежа:
<PaymentCard />
Зараз обе адображаюць тую ж структуру:
<div className="card">
<h3>{title}</h3>
<p>{description}</p>
</div>
Спяльная карточка — гэта наступны логічны крок:
<Card />
Дзеяўна, якшо карточка, якаю паводзіцца корыстувач, часта перадзяляеся, тады як карточка для платежа абмежваецца савершанна іншымі тэхнічнымі та прававымі трэбованнямі. Адпаведныя маркапы ўзаемна не спраўязаны; іх бізнес-значэнне таксама не спадзяляецца. Храненне ўсіх двух варыянтаў коштуе калькі падражненых ліній JSX. У замяну кожны элемент мае можлівасць развівацца без неабходнасці пераговораў. Такое падражненне дае конкрэтную перавагу – можлівасць змяніць адны элемент без неабходнасці сумаважвання з тымі, хто керуе іншым. Якща на гэта паглядзець, два аддзельныя компоненты – гэта не недбалая архітектура. Це можа быць свядомы межа.
Для React існуе важлівая нюанс. Чыста візуальная структура, кантэйнер з дизайном без практычнага значэння, частаца можа быць безпечна для адналогічнага викорыстоўвання, таму што прычына яе змены — цэлкам дизайнавае рашэнне, а не функцыя продукту. Апасцярожнасць выклікае ситуацыя, калі такая частка почынае прымыкати рашэння, якія належаць конкрэтнаму корыстніку.
Задавайцеся пытаннем, чаму два элементы адналогічныя, а не як іх з’едыніць
Пад час сталкавання з парадоксамі мы часта спачатку задаёмся пытаннем, як іх абстрагаваць. Спачатку інстынкт ведае: «Як мне гэта зробіць?» Але болей корыстны падход — спачатку задацься пытаннямі:
- Чаму гэтыя два элементы зараз адналогічныя?
- Чакуце лі яны застануцца адналогічнымі, і з той самый прычыной?
Другое пытанне ўскладненэ, таму што яно змушвае нас перастаць порываяць сінтаксіс і пачаць аналізаваць мету.
Ідэнтычныя реалізацыі можу прыкладваты разныя концэпцыі
Взяце два абсалютнарніка для форматавання: адзін для корыстуначаў:
formatUserName(user)
і адзін для трэйнераў:
formatTrainerName(trainer)
Падазроўваюце, што якія-гэтыя ўсё час маюць аднаковы тэкст:
return `${firstName} ${lastName}`;
Тады іх з’еднаюць у аднаго абсалютнарника:
formatFullName(person)
Гэта здаецца безнебяжным, пакуль правілы не стануць разнымі. Імена корыстуначаў можаць патрабаваць урахоўвання:
- перадпачатковых імен
- настройкаў прыватнасці
- правіла локалізацыі
Імена трэйнераў можаць патрабаваць:
- професійных тытулоў
- сертыфікацый
- спецыфічных правілаў показу імена
Две первісныя функцыі моглі бы развивацца па гэтых разных шляхах без прыгожданняў. Узамен гэты абсалютнарнік прытварае, што корыстуначы і трэйнеры — гэта той самы тип „асобы“ для цоў падбору імена, чаго больш не ёсць. Гэта менш зрозумелы рызык абстракцыі:
Абстракцыя не проста ўзаемна выкарыстоўвае код; яна зашыфрувае тэзу пра тое, як структуравана домэна.
Калі гэтая тэза некальканая, абстракцыя актыўна вводзіць ў глухое кало наступнага разработчыка, які правядома прыпускае, што ўсё, што называецца formatFullName, паслужыць для кожнай аб’екта ў системе.
Цена прытэрпелай абстракцыі падае пазней
Прытэрпелая абстракцыя ёсць прываблівай, таму што кожны візуальны показнік павышаецца ў дзень яго введэння. Файл зменшваецца з чагосьць на кшталт:
100 lines
до:
60 lines
Дуплікацыя зникае, запрос на адчынэнне відгалужэння выглядае чыстейша, а сама абстракцыя здаецца элегантной. Косць адкладаецца. Чырэх месцаў пазней каму-то трэба змяніць працэсаванне для адного корыстувальніка, і ён адкрывае, што спяльны компонент викорыстоўваецца ўсьмома іншых месцах. Замест таго, каб рызыкаваць паспеўнай змінай для іх, ён стварае новае відгалужэння:
if (variant === "A") ...
else if (variant === "B") ...
else if (variant === "C") ...
Потым з’яўляецца ўсё новыя параметры, яшчэ адны флагі, шляхі сумаспаднасці для старэйшых экранаў. Абстракцыя застаецца, але яе канцэптуальная простата — ні. Самэй так кодавыя базы запоўняюцца компанентамі, чыё інтэрфейс выглядае так:
<UniversalThing
mode="..."
variant="..."
type="..."
compact
showHeader
showFooter
enableSomething
disableSomethingElse
/>
У такі момент компанента перестае быць абстракцыяй і становіцца маленькім языкам канфігурацыі для калька незв’язанных сцэнарыяў викорыстоўвання. Кожная комбінацыя гэтых параметраў — это стан, пра які хтось должен размышляць, а большасць такіх комбінацый ніколі не тэставалася.
Повторны выкарыстанне можа ускладніць змены, а не спростыць іх
Іронія заключаецца у тым, што спакульнаваны код ствараецца для таго, каб змены былі дышэўнейшыя, пры тым часам чрэзмернае спакульнаванне часта робіць іх дышэўнейшымі. Прычына — радыус удару: сяродзець рэчаў, на якія можа парадкаць адна змена.
До створэння абстракцыі кожная функцыя мела свойю сабстычную компаненту:
Feature A → Component A
Feature B → Component B
Пасля цього кожна функцыя праходзіць через адну спакаваную частку:
Feature A ─┐
Feature B ─┼→ Shared Component
Feature C ─┘
У другім дыяграме ёсць менша колькасць дуплікатаванага коду, але болей сложная сетка залежнасцяў. Таму корыстным параднікам не ёсць „яка версія мае менш дуплікацый?“ а „яка версія робіць кост будучых змян болей прыгадным?“ Калі змена ў функцыі B павінна працаваць пасля тэставання на зворотныя эфекты ў спалучанні з функцыямі A і C, спакаваная складова робіць змены менш прыгаднымі, нават якщо ёй удаёцца скорачыць код.
Чаму React спрыяе прэкансаванаму спакаванню
React робіць выдзеўленне коду практычна без працы. Праявляецца кнопка:
<Button />
і яна стае можлівай да павторнага выкарыстоўвання. Потым карточка:
<Card />
Потым модальное вікно:
Modal />
Потым поле формы:
FormField />
Потым спецыяльны хук:
useSomething()
Чырз некалькі часоў ствараецца внутранняя бібліятэка компанентаў, якая викорыстоўваецца ў всім прыкладзе практычнасці. Большая частка яе ўпрыметна ценная. Є рэчі, якія явна трэба аддаляць у спадчыну:
- кнопка, якая адпаведна відображае систему дизайну прыкладза практычнасці
- прымітівы нізкага рэвэля, такія як кантроль фокусу чыста доступнае пазначэнне
- канцэпты домэна, якія ўпрыметна стабільныя
Сама можлівасць павторнага викорыстоўвання не ёсць проблемай. Проблема ў тым, калі рэчі викорыстоўваюцца толькі таму, што яны выглядаюць аднакова. Спадчына UI-прымітіваў работае добра, калі яны не маюць належнасці да бізнес-правілаў, што таксама розглядаецца ў дызайне компанентаў на адной засаде адпаведнасці, а не павторнага викорыстоўвання.
React Native падваіць статусы
Мобільныя даплы прыносяць яшчэ адзін вімер. Два экраны можу выглядаць аднакова, хоць у іх зусім разныя патрабаванні па весь перыяд існавання. Компонент, які добра працуе на аднам экране, пазней можа змагчыцца з:
- перанесеннем даплу ў фон
- працэю клавіатуры
- перывамі з’ўязку па сети
- разніцэю між платформамі iOS і Android
- дазволамі
- глыбокімі лінкамі
- разнымі размерамі прыстроў
- станом навігацыі
- рэжымам без інтэрнету
Якщо ўсё заздалегідь зроблецца універсальным, то спяльны компонент ў канцы канцоў будзе знаты пра кожную среду, у якой ён можа запрацаваць. Ён становіцца „гнучкым“, але гнучкасць мае свою цену: кожны новы варыянт падвайнае колькісць можлівых станоў. Кожны з гэтых станоў — гэта тое, што чалавек у канцы канцоў должен зрозумець, працаваць і падтрымваць. Два варыянты ствараюць чатыры комбінацыі; пяць — трыдзіцять два.
Сэрвісы бэкенду таксама падаюць у тую ж пастку
Это не проблема толькі фронтэнду. Уявіце два сэрвісы NestJS:
UserService
TrainerService
Спачатку яны можаюць выканаць тыя ж операцыі:
create()
findById()
update()
delete()
Генерычны базовы клас выглядае прывабліва:
BaseService<T>
Інодзе таке працюе і дапамага захаваць рэальны код. Але калі бізнес-правіла для корыстувачаў і трэйнераў становяцца разнымі, базовы клас пачынае скупляць выключэння. Спачатку перакрычанне типу:
if (entityType === "user") ...
потым хук, які можна перакрыць перед адчыненням:
protected beforeUpdate(...)
потым ўсё іншае пасля стварэння:
protected afterCreate(...)
І паступова ўтварэнне цэлага набору тыпоў расшырэння, якія служаць толькі для таго, каб адна універсальная служба велася інача ў розных доменах. Гэта заменяе дублюванне коду умовную складнасцю, якая зазвычай ўскладнюе розуміўць, таму што для разумення працы адной ентытэўскай структуры тепер трэба чытаць базовы клас, яго хукі і всі перэзапісаны методы разам. Якщо гэта здаецца вам знайомым, адгук пра структуруванне доменаў NestJS за правіламі DDD прыносіць дапаможны погляд на тое, дзе трэба ставіць межы.
Гэта не ўзгоднасць з практыкай копіювання-выклейвання
Вывод пра тое, што дублюванне є добрым, быў бы такі ж спрасцаваны. Дублюванне мае рэальныя наследкі:
- Якщо дзесять элементаў самастоятельна рэалізуюць тое ж правило бізнесу, для выправлення адной бага можа знадобіцца дзесять змян, а ў разе прыхіленьства хоча бы однага з іх выклікаецца неоднаковая працэздатнась.
- Якщо двадцать компанентаў кожны рэалізуе тое ж поваджэнь у частыне доступнась, падтрымка ўніверсальнась іх стае вельмі складной.
- Якщо калькі прыкладнасцей выкарыстоўваюць той самы стабільны контракт, аднародная выкарыстоўка гэтага контракта мае вельмі вялікую цэннась.
Асалодны момант ўскладнейшы: дуплікацыя і зв’язанась — гэта два разныя відзнаки косту. Хорашая інжынерная практыка заключаецца у выборы таго косту, які падходзіць для задачы, а не у постаўленні мінімуму толькі аднаго з іх.
Тры пытанья перад стварэнням абстракцыі
Калі два фрагменты коду выглядаюць аднакова, зупініцеся і рассмотрзіце іх перш чым з’еднаваць.
Чы вы карбуюцца з таго жа разу?
Эта пункт які самыя важлівыя. Якщо трэбаванні да продукту маюць тендэнцыю змяняцца адночасна для A і B, тады спільна ўтрата часу можа быць разумной. Якщо A змяніцца з-за аднаго бізнесовага фактора, а B — з-за іншага, то сучасная ідэнтычная реалізацыя ўсё ж не ёсць пераконлівым доказам таго, што яны належаць да адной групы.
Чы розумеюць яны адно і тое ж?
Код можа быць ідэнтычным, але яго семантыка можа разліцвацца, і самэўсе семантыка ёст тая, якая эвалююць. Цена і запас на рахунку могу быть простымі числамі, але гэта не ўзначае, што іх трэба об’еднаваць у адны псевдонім у всіх месцах:
type Amount = number;
Формат выражэння збігаецца; значэнне — няўсё. Цена можа абавязкова включаць правіла валюты і податкаў, тады як запас на рахунку може мяніцца за лімітамі переказаў. Храненне іх як разлічных паняй, нават якшо сёньня і ўсе яны ёсць числамі, дапамагае утримваць гэтыя разлікі простымі.
Чы гэта значыць усуненне дуплікацыі, чы проста удаленне рэядкав?
Это разныя наследкі. Хорашая абстракцыя усувае дублюванне канцэпцый. Паслабная жа проста скарацьвае файлы. Выявленне 30 ліній з двух компанентаў і перакладзенне іх у дапаможную функцию з 40 лініямі і восьмю параметрамі не павінна обовязкова чаго-небудзь паспрабавіць падняць; сложнасць проста перасунулася і стала вымагаць дадзення інтэрфейсу для керавання.
Свяжанне дублювання — гэта легітымны выбар
Разумна ідея заліцьце два фрагменты коду окремымі, калі яны:
- маленькія
- простыя
- можаць развівацца незалежна
- не захоўваюць критычную інваріянтнасць
- не ўскладненыя спакойным канцэптам
Прычына не трогаць іх — не недастатак спэктакулярных навыкаў; гэта разуменне таго, калі будзе выкарыстоўвацца абстракцыя. Спроможнасць разглядаць павтарэння і прымець рашэнне „яшчо не зараз“ — гэта знак зреласці, а не ленства. Не кожная павтарэнне ёсць тэхнічны борг. Інодзе гэта проста павтарэнне.
Калі дуплікацыя стае сігналам апавярожнення
Іншая сторона таксама мае важлівасць. Дзеяная дуплікацыя ёсць чысты сігнал да кансалідацыі:
- тая ж сложная бізнес-правіла копіююцца ў калькі местах
- тры прыкладнікі, кожны з яых рэалізуе той самы процес аутантыкацыі
- калькі каманд, якія должны дапэўніць адпаведнасць да однаго API-контракту
- змена правіла, якая вымагае запам’ятавання дзесяці разных месцаў
У такіх случаях правым пытаннем яе: якія знання копіююцца? Гэта мае набагато большое значэнне, чым колькі рядкоў павтараецца, адтуды ўсё, чаго трэба ухіліцца ад копіювання, — це знання, а не текст.
DRY завжды стосаваўся знанняў
Фразу „Не павтарай сябе“ часта тлумачаюць як „ніколы не пісай той самы код два разы“. Аднак первісная формулія з кніги The Pragmatic Programmer стосуецца знанняў: кожны элемент знанняў павінен маты ў системе адны, автаномны варыянт. Гэта разныя правілы.
Два компаненты можу мячаць падобны JSX без дуплікацыі якіх-небудзь бізнес-знаёмасцей. У той жытак две функцыі, якія здаюцца абсалютна рознымі, кожная можа кодаваць тую ж бізнес-правілу, напрыклад, прагу знижкі, якая жорстка-закодавана і ў компаненте практыкі платежа, і ў валідаторы на бэкенде. Другі случай яўляецца небяпечным, таму што ён будзе таяцься без звукіў. Таму замест таго, каб спытацца, чы можна ўсё гэта зробіць перадаўальным, трэба спытацца дзе гэтыя знаёмасці павінны жыць.
Дазвольце абстракцыі здобыць сваё месца
Няма прычыны дизайнаваць абстракцыю як толькі з’являецца дуплікацыя. Дазволіць павтарэнню існаваць калькі час, а таксама старанна спазірваць, як этыя копіі развіваюцца, — гэта правядзамая стратэгія. Якщо другі і трэці кейсы викорыстоўвання продовжаюць рухацца ў тым жа напрамку, правильная форма становіцца явной. Тады спяльнае паведанне выкрываецца, а не выдумваецца.
Эта разліка ў значэнні. Абстракцыя, выведзена з трох сапраўдна падобных кейсаў выкарыстоўвання, зазвычай ў дзесяткі разоў моцнейшая, чым тая, якая была створана на аднам кейсе з метай падтрымкі двух гіпотэтычных будучых кейсаў. Гэтае ёсць падстава вядомай гістэрычнай методыкі „правіла трох“. Іншымі словамі:
Абстракцыя, якая базуецца на тым, камі вы зараз розумееце код, а не на тым, камі яго можаце сабе уявіць у будучыні.
Мета — гнеклівы дизайн, а не код, які можна перываць
Перывачэнне сама по сабе не ёсць корыстным показнікам якасці архітектуры. Кращыя показнікі — гэта:
- насколькі лёгка зрозумець код
- насколькі ізолаваны ўсередзіні сябе тыповы змянення
- насколькі прагнозаваныя ў наследках змянення
- чы бізнес-канцэпцыі застаюцца чыста роздзеленымі
- чы межы знаходзяцца ў разумных месцах
Іноды гэтыя крэтарыя прыводзяць да элегантнай спольнай абстракцыі. Іноды ж прыводзяць да двух практычна ідэнтычных компанентаў, якія знаходзяцца падыльні, і гэта можа быць лепейшым дизайнам, таму што гэтыя два элементы ніколі не былі адной і той самай рэчы. Яны проста выглядаюць аднакова сёння.
Ключавыя выводы
- Аднароўка коду стварае залежнасць між корыстнікамі; кожную выявленую залежнасць трэба спрыймать як рашэнне пра тое, што их будучыняя з’ѐднаная.
- Оцэнюйце кандыдатаў на абстракцыю па прычынах іх змены і значэнні, а не па текстовай садоўжы.
- Флагі Prop, галузі варіянтавання і хукі, якія можна перазначыць, — гэта знакі таго, што спольны элемент служыць некалякім домэнам.
- Можна вольна копіюваць маленькі, просты код, який развіваецца незалежна; аднак трэба кансолідаваць дублюючыяся знання, такія як правілы бізнесу, контракты і процесы безпекі.
- Лепшае выбаранне — абстракцыі, якія былі выявлены на адзінных рэальных прыкладах выкарыстоўвання, чым тые, якія былі спроектаваны для уявленых сцэнарыёў.
- Перш чым з’едыніць два падобныя элементы, запытайце ся, чы не ствараеце вы можлівасць павторнага выкарыстоўвання чы ствараеце зв’язак, і чы гэты зв’язак павінен перазісце пасля таго, як вы зберэце данні.
Спаднёючая літэратура
- Калі AI піша ваш React-дзеўайс, але ігнаруе прынцыпы чыстага коду — Дазвольце пазнаць ся з семью прыкметамі чыстага коду — DRY, адна адпаведальнасць, клазы захисту і іншыя — якія часта нараджаны AI-код у React не дотрымваецца, і як іх выправіць.
- Калі чырвоныя React-компаненты: выбух пропаў і спосаб яго адраджэння — Дазнаецеся, як прычасны павторны выкарыстоўванне ператварае простую React-компаненту на абавязак, наполнены пропамі, і як дуплікацыя, складныя компаненты і правіло трох прымаўляюць гэта.