Псіхалёгія коду - чаму важна паважаць уласныя межы кампанентаў

Псіхалёгія коду - чаму важна паважаць уласныя межы кампанентаў

Быў выпадак літаральна на днях. Зьвярнуўся распрацоўшчык - абмеркаваць архітэктуру праекту. Пра сам праект я ведаў толькі ў агульных рысах: думаю, на месцы ўсё высьвятлю. Заўважыў дзіўнасьць: яшчэ да сустрэчы ён актыўна нешта рабіў з кодам і настойваў, каб я гэтыя зьмены абавязкова паглядзеў. Мы ж не на код-рэв'ю дамаўляліся, здаецца.

Сустрэча пачалася зь струменю "дзе якія тэчкі, дзе сэрвісы, дзе мадэлі". Я пачаў задаваць пытаньні па сутнасьці — функцыял, залежнасьці, межы кампанентаў. І тут канчаткова высьвятлілася: сабраліся мы не для гэтага. А каб паглядзець, у якіх тэчках ляжыць код і ў якія яго яшчэ можна перакласьці.

Нічога нечаканага. Чарговы праект, які рэфактарыўся, не даехаўшы да прадакшану. А пад "праектаваньнем архітэктуры" мелася на ўвазе напісаньне коду як мага раней — каб "не губляць часу". І ізноў шукалася чароўная пілюля, каб гэты раз код раскласьці правільна, бо па-адчуваньню нешта ляжыць ня так.

Усё, што далей — набор маіх думак і падыходаў, якія рэгулярна выкарыстоўваю. Усе супадзеньні не супадзеньні: калі хто сябе пазнаў — гэта ён і ёсьць 😄 Прыемнага чытаньня.

Ці можа быць распрацоўшчык дызайнерам?

Насамрэч блытаніна пачынаецца ад неразуменьня, што архітэктура ляжыць на іньшым узроўні абстракцыі і жыцьцёвага цыклу. А тое да чаго хутчэй і звычней дацягнуцца - гэта дызайн коду. І празь дызайн коду спрабуюць залатаць дзіркі, што застаюцца пасьля паверхневага, альбо зусім ніякага, праектаваньня архітэктуры.

Дызайн коду — гэта пра тое, як арганізаваны код знутры аднаго кампаненту: прынцыпы SOLID, dependency injection, паттэрны праектаваньня (Strategy, Factory, Repository), як названы клас, у якой ён тэчцы. Усё гэта адказвае на пытаньне "як рэалізаваць вось гэтую частку".

Архітэктура — гэта пра самі часткі і межы паміж імі, і яна павінна ўжо існаваць да таго, як будзе напісаны хоць адзін радок коду. Тэчкі services/, controllers/, repositories/ апісваюць, як арганізаваны код знутры нечага. Але архітэктура пытаецца: а што гэтае "нешта"? З якіх дамэнных частак складаецца праект? Тэчка services/ можа змяшчаць пятнаццаць сэрвісаў, якія на самой справе належаць пяці розным кампанентам з рознымі адказнасьцямі — і праз структуру файлаў гэтага не відаць.

Розьніца добра відаць на простым тэсьце: SOLID, DI і паттэрны застаюцца дакладна такімі самымі незалежна ад таго, які канкрэтны дамэн рэалізуецца. Dependency Inversion аднолькава прымяняецца і ў сэрвісе аплаты, і ў сэрвісе апавяшчэньняў. А вось дзе межы паміж кампанентам "Аплата" і кампанентам "Апавяшчэньні", якія дадзеныя ў каго жывуць і хто каму што павінен паведамляць - гэта і ёсьць архітэктура, і яе нельга вывесьці з веданьня SOLID. Гэтыя межы залежаць ад канкрэтнага дамэну, канкрэтнага праекту, канкрэтных адказнасьцяў.

Добры дызайн кода ўнутры дрэннай архітэктуры - гэта прыгожа арганізаваны бязладзіца: кожны асобны кавалак выглядае чыста, але разам яны не складаюцца ў нешта зразумелае. А добрая архітэктура з пасрэдным кодам унутры кампанентаў - гэта рашаемая задача: рэфактарынг аднаго кампанента не зачэпіць іньшыя, бо межы ўжо правільныя.

Падрабязней пра разьмежаваньне ўзроўняў дызайну: Simon Brown — Software Architecture vs Code і яго кніга Software Architecture for Developers. Пра тое, чаму SOLID-прынцыпы не вырашаюць архітэктурныя пытаньні: Robert C. Martin — Clean Architecture.

Калі кажуць - не ўскладняй

У чым ёсьць праца праграміста? Праграмаваць? А вось і не. Праца праграміста - думаць. Вынікам павінен быць код (але не абавязкова). Але ў большасьці невялічкіх кампаній лічацца што праграмаваньне гэта якразь механічная праца і зь чаго б ёй складацца, як не зь напісаньня коду. Таму відавочна настойваюць на тым, каб код пісаўся літаральна ў наступную хвіліну пасьля атрыманьня задачы (капаць ад вугла і да вечару, а два землякопы праграмісты выкапаюць у два разы хутчэй). Пры гэтым і самі задачы фармулююцца недастаткова дэталізавана, бо мы занадта давяраем кагнітыўнай памылцы пазнаваньня і здаецца, што вось жа пісалі нешта падобнае ўжо шмат разоў (толькі здаецца).

Чалавек, які фармулюе задачу, думае ў катэгорыях функцыянальнасьці: "каб карыстальнік мог зарэгістравацца", "каб аплата праходзіла", "каб быў асабісты кабінет". Гэта натуральна — так думаюць людзі.

Але архітэктар думае ў катэгорыях адказнасьцяў і межаў: хто валодае дадзенымі пра карыстальніка, хто ініцыюе аплату і хто яе пацьвярджае, якія кампаненты могуць жыць незалежна, а якія зьвязаныя.

Пераход паміж гэтымі двума ўзроўнямі — ёсьць задача архітэктурнага аналізу. І гэты пераход не адбываецца сам сабой: трэба яго рабіць сьвядома, крок за крокам.

Крок 0. Не кідайцеся праектаваць адразу

Самая частая памылка - ўзяць ТЗ і адразу пачаць маляваць кампанэнты. Гэта не працуе, таму што ТЗ амаль заўжды няпоўнае. Ці без "амаль". Заўжды.

Спачатку трэба зразумець, пра што ўвогуле задача. Не "якія кампаненты", а "які кантэкст".

Запытайцеся сабе:

  • Хто карыстаецца сыстэмай і якія ў іх ролі?
  • Якія сцэнары — галоўныя, а якія дадатковыя?
  • Якія вонкавыя сыстэмы ўдзельнічаюць?
  • Якія абмежаваньні ўжо вядомыя (тэхналагічны стэк, бюджэт, тэрміны, патрабаваньні да прадукцыйнасьці)?

Крытэр гатоўнасьці да наступнага кроку: вы можаце апісаць мэту сыстэмы адным сказам і назваць хаця б двух эктараў. Калі гэта яшчэ незразумела - не ідзіце далей, пытайцеся.

Крок 1. Знайдзіце эктараў і іх намеры

Далей варта вызначыць, хто узаемадзейнічае з сыстэмай і чаго яны хочуць. Гэта не "карыстальнік уводзіць лагін і пароль", а ўзровень вышэй: "карыстальнік хоча атрымаць доступ да свайго акаўнту".

Эктары - гэта не заўжды людзі. Гэта могуць быць вонкавыя сыстэмы, планавальнікі задач, іншыя сэрвісы.

Для кожнага эктара трэба сабраць яго намеры (use cases). Не апісаньні экранаў, не крокі ўзаемадзеяньня — менавіта намеры. "Аплаціць заказ". "Атрымаць сьпіс транзакцый за перыяд". "Аўтаматычна адправіць паведамленьне аб падазронным дзеяньні".

Гэты крок выяўляе першыя белыя плямы: часта аказваецца, што ТЗ апісвае функцыянальнасьць аднаго эктара, а пра іншых умоўчвае. Напрыклад, ёсьць "карыстальнік" — але ці ёсьць "адміністратар"? Ці ёсьць "аўдытар", які глядзіць логі? Ці ёсьць "знешні партнёр", які інтэгруецца праз API?

Падрабязней пра тэхніку выяўленьня эктараў і варыянтаў выкарыстаньня: Use Case Modeling — IBM Developer і клясічная праца Алістэра Кокбёрна Writing Effective Use Cases.

Крытэр гатоўнасьці: для кожнага эктара ёсьць сьпіс намераў. Аўтаматычныя эктары (планавальнікі, вонкавыя сыстэмы) таксама выяўленыя. Намеры сфармуляваныя на ўзроўні "чаго хоча эктар", а не "якія кнопкі ён націскае".

Крок 2. Намалюйце кантэкстную дыяграму

Перш чым думаць пра ўнутраную структуру — акрэсьліце вонкавыя межы сыстэмы. Кантэкстная дыяграма (C4 Context Level) паказвае: вось наша сыстэма як чорная скрынка, а вось хто з ёй узаемадзейнічае.

Гэта крыху нагадвае банальнасьць, але яна дае вельмі важную рэч: вы відавочна фіксуеце ўсе вонкавыя залежнасьці. Калі нейкая інтэграцыя не відаць на гэтым узроўні - значыць пра яе яшчэ не думалі і трэба пытацца.

Менавіта тут узьнікаюць пытаньні: "А хто адпраўляе email - мы самі ці праз якое-небудзь SaaS?" "А сапраўды патрэбны SMS, ці дастаткова email?" Гэтыя пытаньні танней за ўсё зараз, а не пасьля таго, як сэрвіс напісаны.

Падыход C4 Model: c4model.com — просты і практычны спосаб апісваць архітэктуру на розных узроўнях дэталізацыі.

Крытэр гатоўнасьці: на дыяграме ёсьць усе вонкавыя сыстэмы, якія ўпамінаюцца ў намерах эктараў. Дыяграму можна паказаць нетэхнічнаму чалавеку і ён зразумее, "пра што" сыстэма.

Крок 3. Разьбіце сыстэму на кампаненты

Самы час для ўнутранай структуры. Ёсьць некалькі рознабаковых падыходаў, як вызначаць межы кампанентаў. Я карыстаюся двума эўрыстыкамі, якія добра дапаўняюць адна адну.

Эўрыстыка 1: Адзінства прычыны зьмены

Спытайцеся: "Калі зьменяцца патрабаваньні да часткі X, якія яшчэ часткі прыйдзецца чапаць?" Калі адказ — "нічога іншага", гэта добры кандыдат на асобны кампанент. Калі — "аўтаматычна зьменіцца Y", то X і Y, хутчэй за ўсё, адзін кампанент.

Гэта прынцып Single Responsibility з SOLID — толькі не на ўзроўні класа, а на ўзроўні кампанента.

Эўрыстыка 2: Валоданьне дадзенымі

Спытайцеся: "Хто адзіны, хто мае права пісаць гэтыя дадзеныя?" Кампанент — гэта заўжды гаспадар нейкіх дадзеных. Калі два месцы ў сыстэме пішуць у адну і тую ж табліцу — у вас праблема з межамі.

Гэта аснова падыходу Domain-Driven Design: кожны Bounded Context валодае сваімі дадзенымі і не дазваляе іх змяняць ніводнаму іншаму кантэксту напрамую.

Чаго не варта рабіць

Не трэба вылучаць кампаненты паводле тэхнічных пластоў: "кампанент бізнэс-логікі", "кампанент дадзеных", "кампанент API". Гэта не архітэктурны падзел, гэта тэхнічны. У выніку атрымліваецца анэмічная мадэль: кожная зьмена патрабаваньняў зачапляе ўсе пласты адначасова.

Кампаненты павінны адпавядаць дамэнным кампетэнцыям, а не тэхнічным ролям.

Пра Bounded Context і DDD: Martin Fowler — BoundedContext. Пра анэмічную мадэль і чаму яна шкодная: Martin Fowler — AnemicDomainModel.

Крытэр гатоўнасьці: для кожнага кампанента можна адным сказам апісаць яго адказнасьць і назваць дадзеныя, якімі ён валодае. Няма кампанентаў, якія "ведаюць усё". Няма дадзеных, якімі валодаюць два кампаненты адначасова.

Крок 4. Намалюйце дыяграму кампанентаў і ўзаемадзеяньняў

Калі кампаненты вызначаныя - намалюйце, як яны размаўляюць між сабой. Гэта не дыяграма класаў і не схема базы дадзеных. Гэта больш высокі ўзровень: кропкі (кампаненты) і стрэлкі (каналы ўзаемадзеяньня) з падпісамі чаго хоча адзін ад другога.

На гэтым узроўні ўжо можна бачыць:

  • Цыклічныя залежнасьці: A залежыць ад B, B залежыць ад A - гэта сігнал, што межы вызначаны няправільна.
  • God-кампаненты: адзін кампанент, з якім размаўляюць усе астатнія - кандыдат на разбіцьцё.
  • Выспы: кампанент, які ні з кім не злучаны - альбо залішні, альбо яго сувязі яшчэ не апісаны.

Крытэр гатоўнасьці: дыяграма не мае цыклічных залежнасьцяў. Кожная стрэлка мае подпіс - зразумела, пра што камунікацыя. Кожны кампанент мае хаця б адну сувязь.

Крок 5. Пракруціце сцэнары

Дыяграма - гэта статыка. Сыстэма жыве ў дынаміцы. Возьміце галоўныя use cases і "прайдзіцеся" па дыяграме: як кожны з іх рэалізуецца праз вашы кампаненты?

Гэта выяўляе памылкі, якія не відаць статычна: кампанент, які не ведае, у каго запытваць патрэбную яму інфармацыю; крок, які апынуўся "паміж" кампанентамі і нікому не належыць; сцэнар, які для свайго завяршэньня мусіць напрамую зьвертацца да пяці-шасьці розных кампанентаў (прыкмета адсутнасьці аб'яднальнага кампанента, які б узяў каардынацыю на сябе).

Калі сцэнар добра "лёг" на дыяграму - кампаненты правільныя. Калі давялося выкручвацца — межы варта перагледзець.

Крытэр гатоўнасьці: усе намеры эктараў з кроку 1 паспяхова праходзяць праз дыяграму без "белых плямаў". Для кожнага краявога сцэнара вядома, хто за яго адказвае.

Трэніруемся на кошках

Крыху сінтэтычы, але досыць верагодны прыклад як гэтым карыстацца.

Недзе апоўдні ў пятніцу раптам (а як інакш) зьяўляецца тэрміновая задача:

"Трэба зрабіць сэрвіс для кіраваньня задачамі каманды. Каб можна было ствараць задачы, прызначаць іх на людзей, каб яны атрымлівалі апавяшчэньні. І каб было бачна, хто чым займаецца. Ну і каб быў аналітычны раздзел — каб кіраўнік мог бачыць, колькі задач у кожнага. Яшчэ каб быў пошук. Ну і каб можна было расстаўляць прыярытэты і тэрміны. Загалоўкі таксама."

Звычайная справа для вечару пятніцы. Трэба каб было ўчора, часу няма, адчыняем IDE і... стоп. Успамінаем колькі раз празь гэта праходзілі і чым гэта сканчвалася. Зачыняем IDE і пачынаем удумліва праводзіць тое, што атрымалі, празь усе этапы праектаваньня.

Крок 0: Уцямім кантэкст

Перш чым штосьці рабіць — задаём пытаньні:

  • Гэта ўнутраны інструмент для адной каманды ці SaaS для многіх кліентаў? (ад гэтага залежыць, ці патрэбны функцыянал падпісак)
  • Колькі людзей у камандзе? (маштаб)
  • Якія апавяшчэньні — email, push, Slack? (вонкавыя інтэграцыі)
  • "Загалоўкі" — гэта тэгі? Катэгорыі? (неадназначны тэрмін)

Напрыклад, атрымалі адказы: унутраны інструмент, каманда 10–50 чалавек, апавяшчэньні праз email і ў браўзэры, "загалоўкі" — гэта тэгі.

Крок 1: Эктары і намеры

Эктар Намеры
Супрацоўнік стварыць задачу
прызначыць задачу на сябе ці іньшага
адзначыць задачу выкананай
прагледзець свае задачы
знайсьці задачу па тэксьце
Кіраўнік прызначаць задачы на любога
бачыць нагрузку на каманду
глядзець аналітыку
Сыстэма (аўтамат) адпраўляць апавяшчэньні пры зьменах
правяраць пратэрмінаваньне

Ужо тут відаць першая белая пляма: апісаньне казала пра "карыстальніка" ў адзіночным ліку, але насамрэч ёсьць дзьве розныя ролі — і яны маюць розныя правы.

Крок 2: Кантэкстная дыяграма

Яскрава бачна: дзьве вонкавыя сыстэмы апавяшчэньняў. Калі пра push нічога не было ў ТЗ — пытаньне патрэбна было задаць. І мы яго задалі на кроку 0.

Крок 3: Кампаненты

Паглядзім на намеры і зададзім сабе пытаньне: якія дадзеныя тут жывуць, хто іх "уладальнік"?

  • Задачы — CRUD, статусы, прыярытэты, тэрміны, тэгі. Уласнік: кампанент Задач.
  • Прызначэньні — хто над чым працуе, гісторыя прызначэньняў. Можа жыць у кампаненце Задач, але аналітыка нагрузкі патрабуе лічыць прызначэньні асобна. Кандыдат на асобны кампанент.
  • Апавяшчэньні — логіка "пры якой падзеі каму і як паведамляць". Гэта не дадзеныя задачы — гэта рэакцыя на падзеі. Асобны кампанент.
  • Аналітыка — агрэгацыя па камандзе. Чытае зь іншых кампанентаў, але нічога не піша. Асобны кампанент.
  • Аўтэнтыфікацыя і ролі — хто мае права на што. Перакрыжоўваецца з усімі кампанентамі, але не павінна быць размазана па ўсіх. Асобны кампанент.
  • Пошук — ці гэта асобны кампанент? Пакуль база невялікая - проста запыт у базу дадзеных. Не трэба выдзяляць у кампанент тое, чаго яшчэ не патрэбна.

Крок 4: Дыяграма ўзаемадзеяньняў

Цыклічных залежнасьцяў няма. Кожны кампанент мае зразумелую адказнасьць. Аналітыка - толькі чытае, нічога не мяняе.

Крок 5: Пракрутка сцэнараў

Сцэнар: Супрацоўнік стварае задачу і прызначае на калегу

Усё лягічна, кожны крок ведае, куды ісьці.

Сцэнар: Кіраўнік глядзіць нагрузку каманды

Чыста. Ніякіх дадатковых залежнасьцяў.

Сцэнар, які выявіў праблему: Задача пратэрмінавана

Хто гэта адсочвае? Па нашай схеме - ніхто. Мы казалі пра аўтаматычнага эктара "Сыстэма", але не прадумалі, дзе жыве гэтая логіка.

Варыянты: планіроўшчык знутры кампаненту Tasks (правярае тэрміны па раскладзе), альбо асобны кампанент Scheduler. Для маленькай сыстэмы - досыць першага варыянту. Але гэтае рашэньне трэба прыняць сьвядома, а не прапусьціць.

Калі пракрутка сцэнароў будзе даведзена да аўтаматызму - белыя плямы ў функцыянале будуць заўважныя адразу, як і спрэчныя моманты, што варта ўдакладніць.

Як пытацца, каб атрымаць патрэбнае

Калі вы разбіраецеся з чужым апісаньнем і адчуваеце пустэчу — ёсьць некалькі катэгорый пытаньняў, якія цалкам пакрываюць слабыя месцы.

Пытаньні пра межы:

  • "Хто яшчэ, апрача карыстальніка, узаемадзейнічае з сыстэмай?"
  • "Якія вонкавыя сыстэмы ўжо ёсьць і да якіх трэба падключыцца?"

Пытаньні пра дадзеныя:

  • "Хто мае права зьмяняць X?"
  • "Ці можа адзін карыстальнік бачыць дадзеныя другога?"

Пытаньні пра краявыя выпадкі:

  • "Што адбываецца, калі [вонкавы сэрвіс] недаступны?"
  • "Што адбываецца з [аб'ект], калі [карыстальнік] выдаляецца?"

Пытаньні пра маштаб:

  • "Гэта для аднаго кліента ці для многіх?"
  • "Якая чаканая нагрузка?"

Пытаньні пра неадназначнасьць:

  • "Гэты тэрмін ужываецца ў двух розных сэнсах у апісаньні. Якое правільнае?"
  • "Тут сказана 'паведамленьне', а крыху вышэй 'апавяшчэньне' — гэта адно і тое ж?"

Мэта гэтых пытаньняў — не дадаць сабе работы, а зафіксаваць рашэньні, якія ўсё адно прыйдзецца прымаць. Лепш прыняць іх усьвядомлена на этапе праектаваньня, чым выпадкова падчас напісаньня кода.

Калі спыніцца

Частая памылка ў другі бок - залішняе праектаваньне. Кожны кампанент разбіваецца на падкампаненты, тыя - на яшчэ меншыя, і ў выніку атрымліваецца архітэктура з дваццаці мікрасэрвісаў для задачы, якую мог бы рашыць моналіт за тыдзень.

Добры прынцып: вылучайце кампанент тады, калі можаце назваць яго адказнасьць адным сказам і яна відавочна адрозьніваецца ад суседніх. Калі назва кампанента "сэрвіс, які робіць усё астатняе" - значыць час спыніцца і перагледзець.

Таксама варта памятаць: архітэктура - гэта жывы дакумент. Першая версія не будзе ідэальнай. Важна мець дастаткова прапрацаваную базу, каб пачаць рухацца, і дастаткова ясную, каб разумець, куды рухацца далей.

Вынік: што лічыцца "збалансаванай спецыфікацыяй"

Спецыфікацыя гатовая, калі:

  1. Усе эктары вызначаны — вядома, хто ўзаемадзейнічае з сыстэмай, уключаючы аўтаматычных.
  2. Кантэкст акрэсьлены — вонкавыя межы і інтэграцыі зафіксаваныя на кантэкстнай дыяграме.
  3. Кампаненты маюць адназначную адказнасьць — кожны кампанент "валодае" нечым канкрэтным і робіць адно.
  4. Узаемадзеяньні апісаныя — ясна, хто, каго і пра што просіць, без цыклічных залежнасьцяў.
  5. Галоўныя сцэнары пракручаныя — і яны леглі на архітэктуру без выкручваньняў.
  6. Белыя плямы закрытыя — няма пытаньняў, адказ на якія "разьбяромся потым".

Шосты пункт самы важны. "Разьбяромся потым" — гэта не план, гэта адкладзены доўг. І ён абавязкова вернецца ў самы непадыходзячы момант. А яшчэ можа заафекціць тое, што ўжо зроблена, і тады...

Архітэктура — гэта не пра тое, як назваць тэчкі. Гэта пра тое, як разабрацца ў задачы настолькі добра, каб потым яе можна было растлумачыць іньшаму распрацоўшчыку, не трымаючы ўсю складанасьць у галаве адначасова. Гэта і ёсьць яе галоўная каштоўнасьць.

Карысныя крыніцы