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

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

Насамрэч блытаніна пачынаецца ад неразуменьня, што архітэктура ляжыць на іньшым узроўні абстракцыі і жыцьцёвага цыклу. А тое да чаго хутчэй і звычней дацягнуцца - гэта дызайн коду. І празь дызайн коду спрабуюць залатаць дзіркі, што застаюцца пасьля паверхневага, альбо зусім ніякага, праектаваньня архітэктуры.
Дызайн коду — гэта пра тое, як арганізаваны код знутры аднаго кампаненту: прынцыпы 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?"
- "Ці можа адзін карыстальнік бачыць дадзеныя другога?"
Пытаньні пра краявыя выпадкі:
- "Што адбываецца, калі [вонкавы сэрвіс] недаступны?"
- "Што адбываецца з [аб'ект], калі [карыстальнік] выдаляецца?"
Пытаньні пра маштаб:
- "Гэта для аднаго кліента ці для многіх?"
- "Якая чаканая нагрузка?"
Пытаньні пра неадназначнасьць:
- "Гэты тэрмін ужываецца ў двух розных сэнсах у апісаньні. Якое правільнае?"
- "Тут сказана 'паведамленьне', а крыху вышэй 'апавяшчэньне' — гэта адно і тое ж?"
Мэта гэтых пытаньняў — не дадаць сабе работы, а зафіксаваць рашэньні, якія ўсё адно прыйдзецца прымаць. Лепш прыняць іх усьвядомлена на этапе праектаваньня, чым выпадкова падчас напісаньня кода.
Калі спыніцца
Частая памылка ў другі бок - залішняе праектаваньне. Кожны кампанент разбіваецца на падкампаненты, тыя - на яшчэ меншыя, і ў выніку атрымліваецца архітэктура з дваццаці мікрасэрвісаў для задачы, якую мог бы рашыць моналіт за тыдзень.
Добры прынцып: вылучайце кампанент тады, калі можаце назваць яго адказнасьць адным сказам і яна відавочна адрозьніваецца ад суседніх. Калі назва кампанента "сэрвіс, які робіць усё астатняе" - значыць час спыніцца і перагледзець.
Таксама варта памятаць: архітэктура - гэта жывы дакумент. Першая версія не будзе ідэальнай. Важна мець дастаткова прапрацаваную базу, каб пачаць рухацца, і дастаткова ясную, каб разумець, куды рухацца далей.
Вынік: што лічыцца "збалансаванай спецыфікацыяй"
Спецыфікацыя гатовая, калі:
- Усе эктары вызначаны — вядома, хто ўзаемадзейнічае з сыстэмай, уключаючы аўтаматычных.
- Кантэкст акрэсьлены — вонкавыя межы і інтэграцыі зафіксаваныя на кантэкстнай дыяграме.
- Кампаненты маюць адназначную адказнасьць — кожны кампанент "валодае" нечым канкрэтным і робіць адно.
- Узаемадзеяньні апісаныя — ясна, хто, каго і пра што просіць, без цыклічных залежнасьцяў.
- Галоўныя сцэнары пракручаныя — і яны леглі на архітэктуру без выкручваньняў.
- Белыя плямы закрытыя — няма пытаньняў, адказ на якія "разьбяромся потым".
Шосты пункт самы важны. "Разьбяромся потым" — гэта не план, гэта адкладзены доўг. І ён абавязкова вернецца ў самы непадыходзячы момант. А яшчэ можа заафекціць тое, што ўжо зроблена, і тады...
Архітэктура — гэта не пра тое, як назваць тэчкі. Гэта пра тое, як разабрацца ў задачы настолькі добра, каб потым яе можна было растлумачыць іньшаму распрацоўшчыку, не трымаючы ўсю складанасьць у галаве адначасова. Гэта і ёсьць яе галоўная каштоўнасьць.
Карысныя крыніцы
- C4 Model — просты падыход да архітэктурных дыяграмаў на 4 узроўнях дэталізацыі
- Martin Fowler — Bounded Context — пра межы кампанентаў у DDD
- Martin Fowler — Anemic Domain Model — пра тое, чаму "сэрвіс-рэпазіторый-кантролер" — не архітэктура
- Alistair Cockburn — Writing Effective Use Cases — класіка пра выяўленьне намераў актораў
- arc42 — практычны шаблон для дакументаваньня архітэктуры