[{"data":1,"prerenderedAt":2750},["ShallowReactive",2],{"content:\u002F06-architecture\u002F04-clean-architecture":3},{"title":4,"description":5,"path":6,"body":7},"Clean Architecture","","\u002F06-architecture\u002F04-clean-architecture",{"type":8,"value":9,"toc":2717},"minimark",[10,15,24,28,33,41,232,235,251,255,262,589,592,596,604,607,611,615,619,632,850,853,857,860,864,881,885,888,892,955,965,969,972,1137,1140,1154,1158,1161,1164,1226,1366,1369,1373,1384,1629,1632,1636,1639,1642,1648,1651,1657,1660,1664,1667,1673,1676,1680,1687,1708,1711,1717,1723,1727,1730,1794,1797,1801,1804,1818,1822,1825,2043,2054,2058,2072,2076,2090,2094,2157,2161,2237,2241,2303,2306,2310,2340,2548,2713],[11,12,14],"h2",{"id":13},"суть","Суть",[16,17,18,19,23],"p",{},"Onion + каждый бизнес-сценарий — отдельный Use Case с явным входом и выходом. Главный закон — ",[20,21,22],"strong",{},"Dependency Rule",": код снаружи зависит от кода внутри, никогда наоборот.",[11,25,27],{"id":26},"задача-и-подход","Задача и подход",[29,30,32],"h3",{"id":31},"проблема-которую-решает","Проблема, которую решает",[16,34,35,36,40],{},"В Onion есть ",[37,38,39],"code",{},"Application Service"," — точка входа сценариев. Со временем он превращается в мешок методов:",[42,43,47],"pre",{"className":44,"code":45,"language":46,"meta":5,"style":5},"language-go shiki shiki-themes github-dark","type OrderService struct { repo OrderRepo; mailer Mailer; payments PaymentGateway }\n\nfunc (s *OrderService) Create(...)  {}\nfunc (s *OrderService) Cancel(...)  {}\nfunc (s *OrderService) Refund(...)  {}\nfunc (s *OrderService) Ship(...)    {}\nfunc (s *OrderService) Reopen(...)  {}\n\u002F\u002F ... и так до 30 методов и 12 зависимостей в конструкторе\n","go",[37,48,49,87,94,128,152,176,201,225],{"__ignoreMap":5},[50,51,54,58,62,65,69,72,75,78,81,84],"span",{"class":52,"line":53},"line",1,[50,55,57],{"class":56},"snl16","type",[50,59,61],{"class":60},"svObZ"," OrderService",[50,63,64],{"class":56}," struct",[50,66,68],{"class":67},"s95oV"," { repo ",[50,70,71],{"class":60},"OrderRepo",[50,73,74],{"class":67},"; mailer ",[50,76,77],{"class":60},"Mailer",[50,79,80],{"class":67},"; payments ",[50,82,83],{"class":60},"PaymentGateway",[50,85,86],{"class":67}," }\n",[50,88,90],{"class":52,"line":89},2,[50,91,93],{"emptyLinePlaceholder":92},true,"\n",[50,95,97,100,103,107,110,113,116,119,122,125],{"class":52,"line":96},3,[50,98,99],{"class":56},"func",[50,101,102],{"class":67}," (",[50,104,106],{"class":105},"s9osk","s ",[50,108,109],{"class":56},"*",[50,111,112],{"class":60},"OrderService",[50,114,115],{"class":67},") ",[50,117,118],{"class":60},"Create",[50,120,121],{"class":67},"(",[50,123,124],{"class":56},"...",[50,126,127],{"class":67},")  {}\n",[50,129,131,133,135,137,139,141,143,146,148,150],{"class":52,"line":130},4,[50,132,99],{"class":56},[50,134,102],{"class":67},[50,136,106],{"class":105},[50,138,109],{"class":56},[50,140,112],{"class":60},[50,142,115],{"class":67},[50,144,145],{"class":60},"Cancel",[50,147,121],{"class":67},[50,149,124],{"class":56},[50,151,127],{"class":67},[50,153,155,157,159,161,163,165,167,170,172,174],{"class":52,"line":154},5,[50,156,99],{"class":56},[50,158,102],{"class":67},[50,160,106],{"class":105},[50,162,109],{"class":56},[50,164,112],{"class":60},[50,166,115],{"class":67},[50,168,169],{"class":60},"Refund",[50,171,121],{"class":67},[50,173,124],{"class":56},[50,175,127],{"class":67},[50,177,179,181,183,185,187,189,191,194,196,198],{"class":52,"line":178},6,[50,180,99],{"class":56},[50,182,102],{"class":67},[50,184,106],{"class":105},[50,186,109],{"class":56},[50,188,112],{"class":60},[50,190,115],{"class":67},[50,192,193],{"class":60},"Ship",[50,195,121],{"class":67},[50,197,124],{"class":56},[50,199,200],{"class":67},")    {}\n",[50,202,204,206,208,210,212,214,216,219,221,223],{"class":52,"line":203},7,[50,205,99],{"class":56},[50,207,102],{"class":67},[50,209,106],{"class":105},[50,211,109],{"class":56},[50,213,112],{"class":60},[50,215,115],{"class":67},[50,217,218],{"class":60},"Reopen",[50,220,121],{"class":67},[50,222,124],{"class":56},[50,224,127],{"class":67},[50,226,228],{"class":52,"line":227},8,[50,229,231],{"class":230},"sAwPA","\u002F\u002F ... и так до 30 методов и 12 зависимостей в конструкторе\n",[16,233,234],{},"Симптомы god-object:",[236,237,238,242,245,248],"ul",{},[239,240,241],"li",{},"В конструкторе 10+ зависимостей, хотя каждый метод использует 2–3",[239,243,244],{},"Любое изменение требует тащить весь сервис в тест",[239,246,247],{},"Невозможно понять «что вообще делает этот класс»",[239,249,250],{},"Merge-конфликты — все правят один файл",[29,252,254],{"id":253},"решение-use-case-как-первый-класс","Решение: Use Case как первый класс",[16,256,257,258,261],{},"Каждый бизнес-сценарий — отдельная структура с одной публичной точкой входа ",[37,259,260],{},"Execute",":",[42,263,265],{"className":44,"code":264,"language":46,"meta":5,"style":5},"type CreateOrderInput struct {\n    UserID string\n    Items  []Item\n}\n\ntype CreateOrderOutput struct {\n    OrderID   string\n    CreatedAt time.Time\n}\n\ntype CreateOrder struct {\n    repo    OrderRepo\n    pricing PricingService\n    clock   Clock\n}\n\nfunc (uc *CreateOrder) Execute(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) {\n    order, err := domain.NewOrder(in.UserID, in.Items)\n    if err != nil {\n        return CreateOrderOutput{}, err\n    }\n    order.SetPrice(uc.pricing.Calc(order))\n    if err := uc.repo.Save(ctx, order); err != nil {\n        return CreateOrderOutput{}, err\n    }\n    return CreateOrderOutput{OrderID: order.ID(), CreatedAt: uc.clock.Now()}, nil\n}\n",[37,266,267,279,287,295,300,304,315,322,336,341,346,358,367,376,385,390,395,449,467,485,496,502,520,544,553,558,584],{"__ignoreMap":5},[50,268,269,271,274,276],{"class":52,"line":53},[50,270,57],{"class":56},[50,272,273],{"class":60}," CreateOrderInput",[50,275,64],{"class":56},[50,277,278],{"class":67}," {\n",[50,280,281,284],{"class":52,"line":89},[50,282,283],{"class":67},"    UserID ",[50,285,286],{"class":56},"string\n",[50,288,289,292],{"class":52,"line":96},[50,290,291],{"class":67},"    Items  []",[50,293,294],{"class":60},"Item\n",[50,296,297],{"class":52,"line":130},[50,298,299],{"class":67},"}\n",[50,301,302],{"class":52,"line":154},[50,303,93],{"emptyLinePlaceholder":92},[50,305,306,308,311,313],{"class":52,"line":178},[50,307,57],{"class":56},[50,309,310],{"class":60}," CreateOrderOutput",[50,312,64],{"class":56},[50,314,278],{"class":67},[50,316,317,320],{"class":52,"line":203},[50,318,319],{"class":67},"    OrderID   ",[50,321,286],{"class":56},[50,323,324,327,330,333],{"class":52,"line":227},[50,325,326],{"class":67},"    CreatedAt ",[50,328,329],{"class":60},"time",[50,331,332],{"class":67},".",[50,334,335],{"class":60},"Time\n",[50,337,339],{"class":52,"line":338},9,[50,340,299],{"class":67},[50,342,344],{"class":52,"line":343},10,[50,345,93],{"emptyLinePlaceholder":92},[50,347,349,351,354,356],{"class":52,"line":348},11,[50,350,57],{"class":56},[50,352,353],{"class":60}," CreateOrder",[50,355,64],{"class":56},[50,357,278],{"class":67},[50,359,361,364],{"class":52,"line":360},12,[50,362,363],{"class":67},"    repo    ",[50,365,366],{"class":60},"OrderRepo\n",[50,368,370,373],{"class":52,"line":369},13,[50,371,372],{"class":67},"    pricing ",[50,374,375],{"class":60},"PricingService\n",[50,377,379,382],{"class":52,"line":378},14,[50,380,381],{"class":67},"    clock   ",[50,383,384],{"class":60},"Clock\n",[50,386,388],{"class":52,"line":387},15,[50,389,299],{"class":67},[50,391,393],{"class":52,"line":392},16,[50,394,93],{"emptyLinePlaceholder":92},[50,396,398,400,402,405,407,410,412,414,416,419,422,424,427,430,433,435,438,441,443,446],{"class":52,"line":397},17,[50,399,99],{"class":56},[50,401,102],{"class":67},[50,403,404],{"class":105},"uc ",[50,406,109],{"class":56},[50,408,409],{"class":60},"CreateOrder",[50,411,115],{"class":67},[50,413,260],{"class":60},[50,415,121],{"class":67},[50,417,418],{"class":105},"ctx",[50,420,421],{"class":60}," context",[50,423,332],{"class":67},[50,425,426],{"class":60},"Context",[50,428,429],{"class":67},", ",[50,431,432],{"class":105},"in",[50,434,273],{"class":60},[50,436,437],{"class":67},") (",[50,439,440],{"class":60},"CreateOrderOutput",[50,442,429],{"class":67},[50,444,445],{"class":56},"error",[50,447,448],{"class":67},") {\n",[50,450,452,455,458,461,464],{"class":52,"line":451},18,[50,453,454],{"class":67},"    order, err ",[50,456,457],{"class":56},":=",[50,459,460],{"class":67}," domain.",[50,462,463],{"class":60},"NewOrder",[50,465,466],{"class":67},"(in.UserID, in.Items)\n",[50,468,470,473,476,479,483],{"class":52,"line":469},19,[50,471,472],{"class":56},"    if",[50,474,475],{"class":67}," err ",[50,477,478],{"class":56},"!=",[50,480,482],{"class":481},"sDLfK"," nil",[50,484,278],{"class":67},[50,486,488,491,493],{"class":52,"line":487},20,[50,489,490],{"class":56},"        return",[50,492,310],{"class":60},[50,494,495],{"class":67},"{}, err\n",[50,497,499],{"class":52,"line":498},21,[50,500,501],{"class":67},"    }\n",[50,503,505,508,511,514,517],{"class":52,"line":504},22,[50,506,507],{"class":67},"    order.",[50,509,510],{"class":60},"SetPrice",[50,512,513],{"class":67},"(uc.pricing.",[50,515,516],{"class":60},"Calc",[50,518,519],{"class":67},"(order))\n",[50,521,523,525,527,529,532,535,538,540,542],{"class":52,"line":522},23,[50,524,472],{"class":56},[50,526,475],{"class":67},[50,528,457],{"class":56},[50,530,531],{"class":67}," uc.repo.",[50,533,534],{"class":60},"Save",[50,536,537],{"class":67},"(ctx, order); err ",[50,539,478],{"class":56},[50,541,482],{"class":481},[50,543,278],{"class":67},[50,545,547,549,551],{"class":52,"line":546},24,[50,548,490],{"class":56},[50,550,310],{"class":60},[50,552,495],{"class":67},[50,554,556],{"class":52,"line":555},25,[50,557,501],{"class":67},[50,559,561,564,566,569,572,575,578,581],{"class":52,"line":560},26,[50,562,563],{"class":56},"    return",[50,565,310],{"class":60},[50,567,568],{"class":67},"{OrderID: order.",[50,570,571],{"class":60},"ID",[50,573,574],{"class":67},"(), CreatedAt: uc.clock.",[50,576,577],{"class":60},"Now",[50,579,580],{"class":67},"()}, ",[50,582,583],{"class":481},"nil\n",[50,585,587],{"class":52,"line":586},27,[50,588,299],{"class":67},[16,590,591],{},"Один файл = один сценарий = один Use Case = одна точка ответственности.",[11,593,595],{"id":594},"dependency-rule-главное-правило","Dependency Rule — главное правило",[42,597,602],{"className":598,"code":600,"language":601,"meta":5},[599],"language-text","       +---------------------------------------+\n       |  Frameworks & Drivers (HTTP, DB, MQ)  |\n       |   +-------------------------------+   |\n       |   |  Interface Adapters           |   |\n       |   |   (handlers, repo impls)      |   |\n       |   |   +-----------------------+   |   |\n       |   |   |   Use Cases           |   |   |\n       |   |   |   +---------------+   |   |   |\n       |   |   |   |   Entities    |   |   |   |\n       |   |   |   +---------------+   |   |   |\n       |   |   +-----------------------+   |   |\n       |   +-------------------------------+   |\n       +---------------------------------------+\n\n       Зависимости направлены ТОЛЬКО внутрь: ──→\n","text",[37,603,600],{"__ignoreMap":5},[16,605,606],{},"Формулировка от Мартина: «Source code dependencies must point only inward, toward higher-level policies». Внутренний слой ничего не знает о внешнем. Если домену нужен внешний мир — он объявляет интерфейс, а внешний слой его реализует.",[11,608,610],{"id":609},"слои-роли-и-границы","Слои, роли и границы",[29,612,614],{"id":613},"четыре-слоя","Четыре слоя",[29,616,618],{"id":617},"entities","Entities",[16,620,621,622,429,625,429,628,631],{},"Корпоративные правила, существующие и без приложения. ",[37,623,624],{},"Order",[37,626,627],{},"Money",[37,629,630],{},"Customer"," со своими инвариантами.",[42,633,635],{"className":44,"code":634,"language":46,"meta":5,"style":5},"type Order struct {\n    id     string\n    items  []Item\n    status Status\n}\n\nfunc NewOrder(userID string, items []Item) (*Order, error) {\n    if len(items) == 0 {\n        return nil, ErrEmptyOrder\n    }\n    return &Order{id: uuid.New().String(), items: items, status: StatusDraft}, nil\n}\n\nfunc (o *Order) Cancel() error {\n    if o.status == StatusShipped {\n        return ErrAlreadyShipped\n    }\n    o.status = StatusCancelled\n    return nil\n}\n",[37,636,637,648,655,662,670,674,678,716,734,743,747,773,777,781,805,817,824,828,839,846],{"__ignoreMap":5},[50,638,639,641,644,646],{"class":52,"line":53},[50,640,57],{"class":56},[50,642,643],{"class":60}," Order",[50,645,64],{"class":56},[50,647,278],{"class":67},[50,649,650,653],{"class":52,"line":89},[50,651,652],{"class":67},"    id     ",[50,654,286],{"class":56},[50,656,657,660],{"class":52,"line":96},[50,658,659],{"class":67},"    items  []",[50,661,294],{"class":60},[50,663,664,667],{"class":52,"line":130},[50,665,666],{"class":67},"    status ",[50,668,669],{"class":60},"Status\n",[50,671,672],{"class":52,"line":154},[50,673,299],{"class":67},[50,675,676],{"class":52,"line":178},[50,677,93],{"emptyLinePlaceholder":92},[50,679,680,682,685,687,690,693,695,698,701,704,706,708,710,712,714],{"class":52,"line":203},[50,681,99],{"class":56},[50,683,684],{"class":60}," NewOrder",[50,686,121],{"class":67},[50,688,689],{"class":105},"userID",[50,691,692],{"class":56}," string",[50,694,429],{"class":67},[50,696,697],{"class":105},"items",[50,699,700],{"class":67}," []",[50,702,703],{"class":60},"Item",[50,705,437],{"class":67},[50,707,109],{"class":56},[50,709,624],{"class":60},[50,711,429],{"class":67},[50,713,445],{"class":56},[50,715,448],{"class":67},[50,717,718,720,723,726,729,732],{"class":52,"line":227},[50,719,472],{"class":56},[50,721,722],{"class":60}," len",[50,724,725],{"class":67},"(items) ",[50,727,728],{"class":56},"==",[50,730,731],{"class":481}," 0",[50,733,278],{"class":67},[50,735,736,738,740],{"class":52,"line":338},[50,737,490],{"class":56},[50,739,482],{"class":481},[50,741,742],{"class":67},", ErrEmptyOrder\n",[50,744,745],{"class":52,"line":343},[50,746,501],{"class":67},[50,748,749,751,754,756,759,762,765,768,771],{"class":52,"line":348},[50,750,563],{"class":56},[50,752,753],{"class":56}," &",[50,755,624],{"class":60},[50,757,758],{"class":67},"{id: uuid.",[50,760,761],{"class":60},"New",[50,763,764],{"class":67},"().",[50,766,767],{"class":60},"String",[50,769,770],{"class":67},"(), items: items, status: StatusDraft}, ",[50,772,583],{"class":481},[50,774,775],{"class":52,"line":360},[50,776,299],{"class":67},[50,778,779],{"class":52,"line":369},[50,780,93],{"emptyLinePlaceholder":92},[50,782,783,785,787,790,792,794,796,798,801,803],{"class":52,"line":378},[50,784,99],{"class":56},[50,786,102],{"class":67},[50,788,789],{"class":105},"o ",[50,791,109],{"class":56},[50,793,624],{"class":60},[50,795,115],{"class":67},[50,797,145],{"class":60},[50,799,800],{"class":67},"() ",[50,802,445],{"class":56},[50,804,278],{"class":67},[50,806,807,809,812,814],{"class":52,"line":387},[50,808,472],{"class":56},[50,810,811],{"class":67}," o.status ",[50,813,728],{"class":56},[50,815,816],{"class":67}," StatusShipped {\n",[50,818,819,821],{"class":52,"line":392},[50,820,490],{"class":56},[50,822,823],{"class":67}," ErrAlreadyShipped\n",[50,825,826],{"class":52,"line":397},[50,827,501],{"class":67},[50,829,830,833,836],{"class":52,"line":451},[50,831,832],{"class":67},"    o.status ",[50,834,835],{"class":56},"=",[50,837,838],{"class":67}," StatusCancelled\n",[50,840,841,843],{"class":52,"line":469},[50,842,563],{"class":56},[50,844,845],{"class":481}," nil\n",[50,847,848],{"class":52,"line":487},[50,849,299],{"class":67},[16,851,852],{},"Никаких импортов кроме stdlib. Никаких ORM-тегов, JSON-тегов, БД.",[29,854,856],{"id":855},"use-cases-application-business-rules","Use Cases (Application Business Rules)",[16,858,859],{},"Сценарий приложения. Знает про Entities, оркестрирует их и зовёт порты (интерфейсы инфраструктуры).",[29,861,863],{"id":862},"interface-adapters","Interface Adapters",[16,865,866,867,870,871,874,875,870,878,332],{},"Переводчики. HTTP-handler конвертирует ",[37,868,869],{},"*http.Request"," → ",[37,872,873],{},"CreateOrderInput",". Postgres-репозиторий конвертирует ",[37,876,877],{},"*Order",[37,879,880],{},"INSERT INTO orders",[29,882,884],{"id":883},"frameworks-drivers","Frameworks & Drivers",[16,886,887],{},"Echo, Gin, pgx, Kafka, Redis. Самый внешний и самый «грязный» слой. Меняется чаще всего, поэтому он снаружи.",[29,889,891],{"id":890},"entity-vs-use-case-vs-interactor","Entity vs Use Case vs Interactor",[893,894,895,911],"table",{},[896,897,898],"thead",{},[899,900,901,905,908],"tr",{},[902,903,904],"th",{},"Понятие",[902,906,907],{},"Что это",[902,909,910],{},"Пример",[912,913,914,928,941],"tbody",{},[899,915,916,920,923],{},[917,918,919],"td",{},"Entity",[917,921,922],{},"Объект с инвариантами, живёт без приложения",[917,924,925,927],{},[37,926,624],{}," с правилом «нельзя отменить отгруженный»",[899,929,930,933,936],{},[917,931,932],{},"Use Case (Interactor)",[917,934,935],{},"Сценарий приложения, оркестрация Entity и портов",[917,937,938],{},[37,939,940],{},"CreateOrder.Execute",[899,942,943,946,949],{},[917,944,945],{},"Boundary",[917,947,948],{},"Интерфейс на границе слоя — Input\u002FOutput",[917,950,951,429,953],{},[37,952,873],{},[37,954,71],{},[16,956,957,958,961,962,332],{},"В оригинальной книге Мартина Use Case = Interactor (термины-синонимы). В Go комьюнити прижился ",[37,959,960],{},"UseCase"," или просто пакет ",[37,963,964],{},"usecase",[29,966,968],{"id":967},"inputoutput-dto-зачем","Input\u002FOutput DTO — зачем",[16,970,971],{},"Use Case принимает свой собственный тип, не HTTP-запрос:",[42,973,975],{"className":44,"code":974,"language":46,"meta":5,"style":5},"\u002F\u002F Плохо: usecase зависит от echo\nfunc (uc *CreateOrder) Execute(c echo.Context) error { ... }\n\n\u002F\u002F Плохо: usecase принимает domain entity (теряем валидацию входа)\nfunc (uc *CreateOrder) Execute(ctx context.Context, order *domain.Order) error { ... }\n\n\u002F\u002F Хорошо: явный input DTO\nfunc (uc *CreateOrder) Execute(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) { ... }\n",[37,976,977,982,1021,1025,1030,1081,1085,1090],{"__ignoreMap":5},[50,978,979],{"class":52,"line":53},[50,980,981],{"class":230},"\u002F\u002F Плохо: usecase зависит от echo\n",[50,983,984,986,988,990,992,994,996,998,1000,1003,1006,1008,1010,1012,1014,1017,1019],{"class":52,"line":89},[50,985,99],{"class":56},[50,987,102],{"class":67},[50,989,404],{"class":105},[50,991,109],{"class":56},[50,993,409],{"class":60},[50,995,115],{"class":67},[50,997,260],{"class":60},[50,999,121],{"class":67},[50,1001,1002],{"class":105},"c",[50,1004,1005],{"class":60}," echo",[50,1007,332],{"class":67},[50,1009,426],{"class":60},[50,1011,115],{"class":67},[50,1013,445],{"class":56},[50,1015,1016],{"class":67}," { ",[50,1018,124],{"class":56},[50,1020,86],{"class":67},[50,1022,1023],{"class":52,"line":96},[50,1024,93],{"emptyLinePlaceholder":92},[50,1026,1027],{"class":52,"line":130},[50,1028,1029],{"class":230},"\u002F\u002F Плохо: usecase принимает domain entity (теряем валидацию входа)\n",[50,1031,1032,1034,1036,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1058,1061,1064,1067,1069,1071,1073,1075,1077,1079],{"class":52,"line":154},[50,1033,99],{"class":56},[50,1035,102],{"class":67},[50,1037,404],{"class":105},[50,1039,109],{"class":56},[50,1041,409],{"class":60},[50,1043,115],{"class":67},[50,1045,260],{"class":60},[50,1047,121],{"class":67},[50,1049,418],{"class":105},[50,1051,421],{"class":60},[50,1053,332],{"class":67},[50,1055,426],{"class":60},[50,1057,429],{"class":67},[50,1059,1060],{"class":105},"order",[50,1062,1063],{"class":56}," *",[50,1065,1066],{"class":60},"domain",[50,1068,332],{"class":67},[50,1070,624],{"class":60},[50,1072,115],{"class":67},[50,1074,445],{"class":56},[50,1076,1016],{"class":67},[50,1078,124],{"class":56},[50,1080,86],{"class":67},[50,1082,1083],{"class":52,"line":178},[50,1084,93],{"emptyLinePlaceholder":92},[50,1086,1087],{"class":52,"line":203},[50,1088,1089],{"class":230},"\u002F\u002F Хорошо: явный input DTO\n",[50,1091,1092,1094,1096,1098,1100,1102,1104,1106,1108,1110,1112,1114,1116,1118,1120,1122,1124,1126,1128,1130,1133,1135],{"class":52,"line":227},[50,1093,99],{"class":56},[50,1095,102],{"class":67},[50,1097,404],{"class":105},[50,1099,109],{"class":56},[50,1101,409],{"class":60},[50,1103,115],{"class":67},[50,1105,260],{"class":60},[50,1107,121],{"class":67},[50,1109,418],{"class":105},[50,1111,421],{"class":60},[50,1113,332],{"class":67},[50,1115,426],{"class":60},[50,1117,429],{"class":67},[50,1119,432],{"class":105},[50,1121,273],{"class":60},[50,1123,437],{"class":67},[50,1125,440],{"class":60},[50,1127,429],{"class":67},[50,1129,445],{"class":56},[50,1131,1132],{"class":67},") { ",[50,1134,124],{"class":56},[50,1136,86],{"class":67},[16,1138,1139],{},"Что даёт явный DTO:",[236,1141,1142,1145,1148,1151],{},[239,1143,1144],{},"Use Case вызывают и из HTTP, и из gRPC, и из CLI — без переписывания",[239,1146,1147],{},"Входная валидация на границе очевидна",[239,1149,1150],{},"Тесты пишутся без mock-ов фреймворка",[239,1152,1153],{},"Domain entity не «протекает» наружу с её приватными полями",[29,1155,1157],{"id":1156},"почему-отдельный-use-case-а-не-метод-сервиса","Почему отдельный Use Case, а не метод сервиса",[16,1159,1160],{},"Часто спорят: «Зачем структура с одним методом? Это же просто функция».",[16,1162,1163],{},"Аргументы за отдельный тип:",[1165,1166,1167,1193,1199,1211,1220],"ol",{},[239,1168,1169,1172,1173,1175,1176,1179,1180,429,1183,1186,1187,1179,1189,1192],{},[20,1170,1171],{},"Конструктор объявляет ровно те зависимости, что нужны сценарию",". ",[37,1174,409],{}," нужен ",[37,1177,1178],{},"repo"," и ",[37,1181,1182],{},"pricing",[37,1184,1185],{},"CancelOrder"," — ",[37,1188,1178],{},[37,1190,1191],{},"notifier",". Не общий god-object с 10 полями.",[239,1194,1195,1198],{},[20,1196,1197],{},"Тестируется изолированно",". Поднимаешь моки только для нужных портов.",[239,1200,1201,1172,1204,1206,1207,1210],{},[20,1202,1203],{},"Найти код легко",[37,1205,409],{}," — это и тип, и файл, и use case. Нет «где-то в ",[37,1208,1209],{},"OrderService.Create","».",[239,1212,1213,1172,1216,1219],{},[20,1214,1215],{},"Метрики\u002Fлоги\u002Fтрейсы по сценариям",[37,1217,1218],{},"metric(\"usecase.create_order.duration\")"," — естественно. По методам god-object — гадание.",[239,1221,1222,1225],{},[20,1223,1224],{},"Decorator pattern бесплатно",". Логгирование, транзакции, ретраи оборачиваются вокруг конкретного use case без затрагивания других.",[42,1227,1229],{"className":44,"code":1228,"language":46,"meta":5,"style":5},"type WithTx struct { uc CreateOrder; db *sql.DB }\nfunc (w *WithTx) Execute(ctx context.Context, in CreateOrderInput) (CreateOrderOutput, error) {\n    return runInTx(w.db, func(tx *sql.Tx) (CreateOrderOutput, error) {\n        return w.uc.Execute(injectTx(ctx, tx), in)\n    })\n}\n",[37,1230,1231,1260,1304,1340,1357,1362],{"__ignoreMap":5},[50,1232,1233,1235,1238,1240,1243,1245,1248,1250,1253,1255,1258],{"class":52,"line":53},[50,1234,57],{"class":56},[50,1236,1237],{"class":60}," WithTx",[50,1239,64],{"class":56},[50,1241,1242],{"class":67}," { uc ",[50,1244,409],{"class":60},[50,1246,1247],{"class":67},"; db ",[50,1249,109],{"class":56},[50,1251,1252],{"class":60},"sql",[50,1254,332],{"class":67},[50,1256,1257],{"class":60},"DB",[50,1259,86],{"class":67},[50,1261,1262,1264,1266,1269,1271,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294,1296,1298,1300,1302],{"class":52,"line":89},[50,1263,99],{"class":56},[50,1265,102],{"class":67},[50,1267,1268],{"class":105},"w ",[50,1270,109],{"class":56},[50,1272,1273],{"class":60},"WithTx",[50,1275,115],{"class":67},[50,1277,260],{"class":60},[50,1279,121],{"class":67},[50,1281,418],{"class":105},[50,1283,421],{"class":60},[50,1285,332],{"class":67},[50,1287,426],{"class":60},[50,1289,429],{"class":67},[50,1291,432],{"class":105},[50,1293,273],{"class":60},[50,1295,437],{"class":67},[50,1297,440],{"class":60},[50,1299,429],{"class":67},[50,1301,445],{"class":56},[50,1303,448],{"class":67},[50,1305,1306,1308,1311,1314,1316,1318,1321,1323,1325,1327,1330,1332,1334,1336,1338],{"class":52,"line":96},[50,1307,563],{"class":56},[50,1309,1310],{"class":60}," runInTx",[50,1312,1313],{"class":67},"(w.db, ",[50,1315,99],{"class":56},[50,1317,121],{"class":67},[50,1319,1320],{"class":105},"tx",[50,1322,1063],{"class":56},[50,1324,1252],{"class":60},[50,1326,332],{"class":67},[50,1328,1329],{"class":60},"Tx",[50,1331,437],{"class":67},[50,1333,440],{"class":60},[50,1335,429],{"class":67},[50,1337,445],{"class":56},[50,1339,448],{"class":67},[50,1341,1342,1344,1347,1349,1351,1354],{"class":52,"line":130},[50,1343,490],{"class":56},[50,1345,1346],{"class":67}," w.uc.",[50,1348,260],{"class":60},[50,1350,121],{"class":67},[50,1352,1353],{"class":60},"injectTx",[50,1355,1356],{"class":67},"(ctx, tx), in)\n",[50,1358,1359],{"class":52,"line":154},[50,1360,1361],{"class":67},"    })\n",[50,1363,1364],{"class":52,"line":178},[50,1365,299],{"class":67},[16,1367,1368],{},"Минус — больше файлов. Это нормальная плата за изоляцию.",[29,1370,1372],{"id":1371},"frameworks-drivers-как-не-зависеть-от-echo","Frameworks & Drivers — как не зависеть от Echo",[16,1374,1375,1376,1379,1380,1383],{},"Use Case не должен импортировать ",[37,1377,1378],{},"echo"," или ",[37,1381,1382],{},"net\u002Fhttp",". Иначе смена транспорта = переписать сценарий.",[42,1385,1387],{"className":44,"code":1386,"language":46,"meta":5,"style":5},"\u002F\u002F internal\u002Fadapter\u002Fhandler\u002Fhttp\u002Forder.go\ntype OrderHandler struct {\n    create *usecase.CreateOrder\n}\n\nfunc (h *OrderHandler) Create(c echo.Context) error {\n    var req CreateOrderRequest\n    if err := c.Bind(&req); err != nil {\n        return c.JSON(400, ErrorResp{\"invalid body\"})\n    }\n    out, err := h.create.Execute(c.Request().Context(), CreateOrderInput{\n        UserID: req.UserID, Items: mapItems(req.Items),\n    })\n    if err != nil {\n        return mapError(c, err)\n    }\n    return c.JSON(201, OrderResp{ID: out.OrderID})\n}\n",[37,1388,1389,1394,1405,1419,1423,1427,1461,1472,1500,1529,1533,1563,1574,1578,1590,1600,1604,1625],{"__ignoreMap":5},[50,1390,1391],{"class":52,"line":53},[50,1392,1393],{"class":230},"\u002F\u002F internal\u002Fadapter\u002Fhandler\u002Fhttp\u002Forder.go\n",[50,1395,1396,1398,1401,1403],{"class":52,"line":89},[50,1397,57],{"class":56},[50,1399,1400],{"class":60}," OrderHandler",[50,1402,64],{"class":56},[50,1404,278],{"class":67},[50,1406,1407,1410,1412,1414,1416],{"class":52,"line":96},[50,1408,1409],{"class":67},"    create ",[50,1411,109],{"class":56},[50,1413,964],{"class":60},[50,1415,332],{"class":67},[50,1417,1418],{"class":60},"CreateOrder\n",[50,1420,1421],{"class":52,"line":130},[50,1422,299],{"class":67},[50,1424,1425],{"class":52,"line":154},[50,1426,93],{"emptyLinePlaceholder":92},[50,1428,1429,1431,1433,1436,1438,1441,1443,1445,1447,1449,1451,1453,1455,1457,1459],{"class":52,"line":178},[50,1430,99],{"class":56},[50,1432,102],{"class":67},[50,1434,1435],{"class":105},"h ",[50,1437,109],{"class":56},[50,1439,1440],{"class":60},"OrderHandler",[50,1442,115],{"class":67},[50,1444,118],{"class":60},[50,1446,121],{"class":67},[50,1448,1002],{"class":105},[50,1450,1005],{"class":60},[50,1452,332],{"class":67},[50,1454,426],{"class":60},[50,1456,115],{"class":67},[50,1458,445],{"class":56},[50,1460,278],{"class":67},[50,1462,1463,1466,1469],{"class":52,"line":203},[50,1464,1465],{"class":56},"    var",[50,1467,1468],{"class":67}," req ",[50,1470,1471],{"class":60},"CreateOrderRequest\n",[50,1473,1474,1476,1478,1480,1483,1486,1488,1491,1494,1496,1498],{"class":52,"line":227},[50,1475,472],{"class":56},[50,1477,475],{"class":67},[50,1479,457],{"class":56},[50,1481,1482],{"class":67}," c.",[50,1484,1485],{"class":60},"Bind",[50,1487,121],{"class":67},[50,1489,1490],{"class":56},"&",[50,1492,1493],{"class":67},"req); err ",[50,1495,478],{"class":56},[50,1497,482],{"class":481},[50,1499,278],{"class":67},[50,1501,1502,1504,1506,1509,1511,1514,1516,1519,1522,1526],{"class":52,"line":338},[50,1503,490],{"class":56},[50,1505,1482],{"class":67},[50,1507,1508],{"class":60},"JSON",[50,1510,121],{"class":67},[50,1512,1513],{"class":481},"400",[50,1515,429],{"class":67},[50,1517,1518],{"class":60},"ErrorResp",[50,1520,1521],{"class":67},"{",[50,1523,1525],{"class":1524},"sU2Wk","\"invalid body\"",[50,1527,1528],{"class":67},"})\n",[50,1530,1531],{"class":52,"line":343},[50,1532,501],{"class":67},[50,1534,1535,1538,1540,1543,1545,1548,1551,1553,1555,1558,1560],{"class":52,"line":348},[50,1536,1537],{"class":67},"    out, err ",[50,1539,457],{"class":56},[50,1541,1542],{"class":67}," h.create.",[50,1544,260],{"class":60},[50,1546,1547],{"class":67},"(c.",[50,1549,1550],{"class":60},"Request",[50,1552,764],{"class":67},[50,1554,426],{"class":60},[50,1556,1557],{"class":67},"(), ",[50,1559,873],{"class":60},[50,1561,1562],{"class":67},"{\n",[50,1564,1565,1568,1571],{"class":52,"line":360},[50,1566,1567],{"class":67},"        UserID: req.UserID, Items: ",[50,1569,1570],{"class":60},"mapItems",[50,1572,1573],{"class":67},"(req.Items),\n",[50,1575,1576],{"class":52,"line":369},[50,1577,1361],{"class":67},[50,1579,1580,1582,1584,1586,1588],{"class":52,"line":378},[50,1581,472],{"class":56},[50,1583,475],{"class":67},[50,1585,478],{"class":56},[50,1587,482],{"class":481},[50,1589,278],{"class":67},[50,1591,1592,1594,1597],{"class":52,"line":387},[50,1593,490],{"class":56},[50,1595,1596],{"class":60}," mapError",[50,1598,1599],{"class":67},"(c, err)\n",[50,1601,1602],{"class":52,"line":392},[50,1603,501],{"class":67},[50,1605,1606,1608,1610,1612,1614,1617,1619,1622],{"class":52,"line":397},[50,1607,563],{"class":56},[50,1609,1482],{"class":67},[50,1611,1508],{"class":60},[50,1613,121],{"class":67},[50,1615,1616],{"class":481},"201",[50,1618,429],{"class":67},[50,1620,1621],{"class":60},"OrderResp",[50,1623,1624],{"class":67},"{ID: out.OrderID})\n",[50,1626,1627],{"class":52,"line":451},[50,1628,299],{"class":67},[16,1630,1631],{},"Завтра gRPC — пишется новый handler, use case не трогается.",[29,1633,1635],{"id":1634},"screaming-architecture","Screaming Architecture",[16,1637,1638],{},"Структура проекта должна «кричать» о бизнесе, а не о фреймворке.",[16,1640,1641],{},"Плохо (кричит «я Echo-приложение»):",[42,1643,1646],{"className":1644,"code":1645,"language":601,"meta":5},[599],"internal\u002F\n  controllers\u002F\n  models\u002F\n  routes\u002F\n  middlewares\u002F\n",[37,1647,1645],{"__ignoreMap":5},[16,1649,1650],{},"Хорошо (кричит «я обрабатываю заказы»):",[42,1652,1655],{"className":1653,"code":1654,"language":601,"meta":5},[599],"internal\u002F\n  domain\u002F\n    order\u002F\n    customer\u002F\n  usecase\u002F\n    create_order.go\n    cancel_order.go\n    refund_order.go\n  adapter\u002F\n    http\u002F\n    postgres\u002F\n",[37,1656,1654],{"__ignoreMap":5},[16,1658,1659],{},"Когда новый разработчик открывает репозиторий — он видит, чем сервис занимается, а не на каком фреймворке написан.",[29,1661,1663],{"id":1662},"boundaries-и-interface-adapters","Boundaries и Interface Adapters",[16,1665,1666],{},"Boundary = граница слоя, выраженная интерфейсом. На каждой границе — два DTO (вход\u002Fвыход) или порт-интерфейс.",[42,1668,1671],{"className":1669,"code":1670,"language":601,"meta":5},[599],"HTTP Request ─→ [Handler] ─→ Input DTO ─→ [UseCase] ─→ Output DTO ─→ [Handler] ─→ HTTP Response\n                                              │\n                                              ↓ (через port-interface)\n                                          [Repository Adapter] ─→ SQL\n",[37,1672,1670],{"__ignoreMap":5},[16,1674,1675],{},"Каждая стрелка пересекает boundary с маппингом. Маппинг — цена изоляции.",[11,1677,1679],{"id":1678},"clean-без-фанатизма","Clean без фанатизма",[16,1681,1682,1683,1686],{},"Clean Architecture в Go работает, когда она уменьшает стоимость изменений, а не когда заставляет на каждый чих заводить ",[37,1684,1685],{},"InteractorFactoryBuilder",". Практичный критерий:",[236,1688,1689,1692,1695,1698],{},[239,1690,1691],{},"если сценарий содержит бизнесовое решение, транзакцию, несколько зависимостей или важный error contract — делайте отдельный use case;",[239,1693,1694],{},"если это один CRUD-endpoint без правил — не обязательно размазывать его на 8 файлов;",[239,1696,1697],{},"если интерфейс нужен только одной реализации и не помогает тесту — возможно, он лишний;",[239,1699,1700,1701,429,1704,1707],{},"если use case начал знать про ",[37,1702,1703],{},"pgx",[37,1705,1706],{},"echo.Context",", Kafka topic или Redis key format — граница уже сломана.",[16,1709,1710],{},"В продакшене Clean чаще выглядит как прагматичный микс:",[42,1712,1715],{"className":1713,"code":1714,"language":601,"meta":5},[599],"internal\u002Fdomain       # entities, value objects, domain services\ninternal\u002Fusecase      # application scenarios + ports they consume\ninternal\u002Fadapter      # postgres, redis, providers, broker, http\u002Fgrpc mapping\ninternal\u002Fplatform     # logger, metrics, config, tracing helpers\ncmd\u002Fserver            # composition root\n",[37,1716,1714],{"__ignoreMap":5},[16,1718,1719,1722],{},[37,1720,1721],{},"platform"," не является «магическим слоем, который можно импортировать откуда угодно». Domain всё ещё не должен зависеть от logger\u002Ftracer\u002Fconfig. Use case может принимать интерфейсы для clock, transaction runner или event sink, но не должен знать формат Prometheus labels или Kafka headers.",[29,1724,1726],{"id":1725},"read-side-и-query-models","Read side и query models",[16,1728,1729],{},"Clean не запрещает отдельные модели чтения. Если API отдаёт историю операций, админский список или отчёт, не обязательно тащить полный агрегат и доменную модель.",[42,1731,1733],{"className":44,"code":1732,"language":46,"meta":5,"style":5},"type ConversionHistoryReader interface {\n    ListForClient(ctx context.Context, clientID string, limit int) ([]ConversionHistoryRow, error)\n}\n",[37,1734,1735,1747,1790],{"__ignoreMap":5},[50,1736,1737,1739,1742,1745],{"class":52,"line":53},[50,1738,57],{"class":56},[50,1740,1741],{"class":60}," ConversionHistoryReader",[50,1743,1744],{"class":56}," interface",[50,1746,278],{"class":67},[50,1748,1749,1752,1754,1756,1758,1760,1762,1764,1767,1769,1771,1774,1777,1780,1783,1785,1787],{"class":52,"line":89},[50,1750,1751],{"class":60},"    ListForClient",[50,1753,121],{"class":67},[50,1755,418],{"class":105},[50,1757,421],{"class":60},[50,1759,332],{"class":67},[50,1761,426],{"class":60},[50,1763,429],{"class":67},[50,1765,1766],{"class":105},"clientID",[50,1768,692],{"class":56},[50,1770,429],{"class":67},[50,1772,1773],{"class":105},"limit",[50,1775,1776],{"class":56}," int",[50,1778,1779],{"class":67},") ([]",[50,1781,1782],{"class":60},"ConversionHistoryRow",[50,1784,429],{"class":67},[50,1786,445],{"class":56},[50,1788,1789],{"class":67},")\n",[50,1791,1792],{"class":52,"line":96},[50,1793,299],{"class":67},[16,1795,1796],{},"Это read-side port. Он не обязан возвращать aggregate root, если сценарий ничего не меняет и не проверяет инварианты. Главное — честно назвать это query model и не использовать её для записи бизнесовых изменений.",[29,1798,1800],{"id":1799},"observability-placement","Observability placement",[16,1802,1803],{},"Observability живёт на границах:",[236,1805,1806,1809,1812,1815],{},[239,1807,1808],{},"middleware добавляет request id, trace id, latency, status;",[239,1810,1811],{},"adapter измеряет latency\u002Ferrors внешней зависимости;",[239,1813,1814],{},"use case может логировать бизнесовый outcome через абстракцию, если это действительно часть application flow;",[239,1816,1817],{},"domain entity не импортирует logger, tracer и metrics.",[11,1819,1821],{"id":1820},"тестирование","Тестирование",[16,1823,1824],{},"Каждый use case тестируется в вакууме:",[42,1826,1828],{"className":44,"code":1827,"language":46,"meta":5,"style":5},"func TestCreateOrder_OK(t *testing.T) {\n    repo := &mockOrderRepo{}\n    pricing := &stubPricing{price: money.From(100)}\n    uc := &CreateOrder{repo: repo, pricing: pricing, clock: stubClock{}}\n\n    out, err := uc.Execute(ctx, CreateOrderInput{\n        UserID: \"u-1\", Items: []Item{{SKU: \"A\", Qty: 2}},\n    })\n\n    require.NoError(t, err)\n    require.NotEmpty(t, out.OrderID)\n    require.Len(t, repo.saved, 1)\n    require.Equal(t, money.From(100), repo.saved[0].TotalPrice())\n}\n",[37,1829,1830,1854,1869,1894,1914,1918,1936,1964,1968,1972,1983,1993,2008,2039],{"__ignoreMap":5},[50,1831,1832,1834,1837,1839,1842,1844,1847,1849,1852],{"class":52,"line":53},[50,1833,99],{"class":56},[50,1835,1836],{"class":60}," TestCreateOrder_OK",[50,1838,121],{"class":67},[50,1840,1841],{"class":105},"t",[50,1843,1063],{"class":56},[50,1845,1846],{"class":60},"testing",[50,1848,332],{"class":67},[50,1850,1851],{"class":60},"T",[50,1853,448],{"class":67},[50,1855,1856,1859,1861,1863,1866],{"class":52,"line":89},[50,1857,1858],{"class":67},"    repo ",[50,1860,457],{"class":56},[50,1862,753],{"class":56},[50,1864,1865],{"class":60},"mockOrderRepo",[50,1867,1868],{"class":67},"{}\n",[50,1870,1871,1873,1875,1877,1880,1883,1886,1888,1891],{"class":52,"line":96},[50,1872,372],{"class":67},[50,1874,457],{"class":56},[50,1876,753],{"class":56},[50,1878,1879],{"class":60},"stubPricing",[50,1881,1882],{"class":67},"{price: money.",[50,1884,1885],{"class":60},"From",[50,1887,121],{"class":67},[50,1889,1890],{"class":481},"100",[50,1892,1893],{"class":67},")}\n",[50,1895,1896,1899,1901,1903,1905,1908,1911],{"class":52,"line":130},[50,1897,1898],{"class":67},"    uc ",[50,1900,457],{"class":56},[50,1902,753],{"class":56},[50,1904,409],{"class":60},[50,1906,1907],{"class":67},"{repo: repo, pricing: pricing, clock: ",[50,1909,1910],{"class":60},"stubClock",[50,1912,1913],{"class":67},"{}}\n",[50,1915,1916],{"class":52,"line":154},[50,1917,93],{"emptyLinePlaceholder":92},[50,1919,1920,1922,1924,1927,1929,1932,1934],{"class":52,"line":178},[50,1921,1537],{"class":67},[50,1923,457],{"class":56},[50,1925,1926],{"class":67}," uc.",[50,1928,260],{"class":60},[50,1930,1931],{"class":67},"(ctx, ",[50,1933,873],{"class":60},[50,1935,1562],{"class":67},[50,1937,1938,1941,1944,1947,1949,1952,1955,1958,1961],{"class":52,"line":203},[50,1939,1940],{"class":67},"        UserID: ",[50,1942,1943],{"class":1524},"\"u-1\"",[50,1945,1946],{"class":67},", Items: []",[50,1948,703],{"class":60},[50,1950,1951],{"class":67},"{{SKU: ",[50,1953,1954],{"class":1524},"\"A\"",[50,1956,1957],{"class":67},", Qty: ",[50,1959,1960],{"class":481},"2",[50,1962,1963],{"class":67},"}},\n",[50,1965,1966],{"class":52,"line":227},[50,1967,1361],{"class":67},[50,1969,1970],{"class":52,"line":338},[50,1971,93],{"emptyLinePlaceholder":92},[50,1973,1974,1977,1980],{"class":52,"line":343},[50,1975,1976],{"class":67},"    require.",[50,1978,1979],{"class":60},"NoError",[50,1981,1982],{"class":67},"(t, err)\n",[50,1984,1985,1987,1990],{"class":52,"line":348},[50,1986,1976],{"class":67},[50,1988,1989],{"class":60},"NotEmpty",[50,1991,1992],{"class":67},"(t, out.OrderID)\n",[50,1994,1995,1997,2000,2003,2006],{"class":52,"line":360},[50,1996,1976],{"class":67},[50,1998,1999],{"class":60},"Len",[50,2001,2002],{"class":67},"(t, repo.saved, ",[50,2004,2005],{"class":481},"1",[50,2007,1789],{"class":67},[50,2009,2010,2012,2015,2018,2020,2022,2024,2027,2030,2033,2036],{"class":52,"line":369},[50,2011,1976],{"class":67},[50,2013,2014],{"class":60},"Equal",[50,2016,2017],{"class":67},"(t, money.",[50,2019,1885],{"class":60},[50,2021,121],{"class":67},[50,2023,1890],{"class":481},[50,2025,2026],{"class":67},"), repo.saved[",[50,2028,2029],{"class":481},"0",[50,2031,2032],{"class":67},"].",[50,2034,2035],{"class":60},"TotalPrice",[50,2037,2038],{"class":67},"())\n",[50,2040,2041],{"class":52,"line":378},[50,2042,299],{"class":67},[16,2044,2045,2046,2049,2050,2053],{},"Без ",[37,2047,2048],{},"httptest",", без ",[37,2051,2052],{},"pgxmock",", без поднятой инфраструктуры. Только бизнес-правило.",[11,2055,2057],{"id":2056},"когда-применять","Когда применять",[236,2059,2060,2063,2066,2069],{},[239,2061,2062],{},"Сложная доменная логика, много сценариев и инвариантов",[239,2064,2065],{},"Несколько транспортов (HTTP + gRPC + CLI + cron)",[239,2067,2068],{},"Долгоживущий сервис, который будут переписывать частями",[239,2070,2071],{},"Команда 5+ человек — слои дают чёткие границы ответственности",[29,2073,2075],{"id":2074},"когда-не-применять","Когда НЕ применять",[236,2077,2078,2081,2084,2087],{},[239,2079,2080],{},"CRUD-микросервис на 5 эндпоинтов — 90% кода будет маппинг",[239,2082,2083],{},"Прототип, MVP — Clean заморозит вас в маппингах вместо проверки гипотезы",[239,2085,2086],{},"Один разработчик на проекте — выгода от чётких границ ниже накладных",[239,2088,2089],{},"ETL\u002Fскрипт — нет «бизнес-правил», есть pipeline",[29,2091,2093],{"id":2092},"подводные-камни","Подводные камни",[236,2095,2096,2102,2108,2114,2123,2129,2145],{},[239,2097,2098,2101],{},[20,2099,2100],{},"Анемичный домен",". Entity без поведения, всё в use case → получили обычный «service + struct», а не Clean.",[239,2103,2104,2107],{},[20,2105,2106],{},"Use case вызывает use case",". Признак, что один из них — на самом деле domain service. Выноси общую логику в домен.",[239,2109,2110,2113],{},[20,2111,2112],{},"Repository возвращает DTO с JSON-тегами",". Протечка. Repository должен возвращать domain entity.",[239,2115,2116,2122],{},[20,2117,2118,2119],{},"Domain импортирует ",[37,2120,2121],{},"database\u002Fsql",". Сразу красная тряпка. Domain — только stdlib + другие пакеты domain.",[239,2124,2125,2128],{},[20,2126,2127],{},"Один Input DTO на 5 полей-флагов",". Use case делает несколько вещей. Разбивай.",[239,2130,2131,1172,2134,2137,2138,2137,2141,2144],{},[20,2132,2133],{},"Logger в domain",[37,2135,2136],{},"fmt","\u002F",[37,2139,2140],{},"log",[37,2142,2143],{},"zap"," в Entity = боль. Логгирование — забота use case или handler.",[239,2146,2147,2156],{},[20,2148,2149,2137,2152,2155],{},[37,2150,2151],{},"interface{}",[37,2153,2154],{},"any"," в Input\u002FOutput",". Уничтожает смысл boundary. Всегда конкретные типы.",[29,2158,2160],{"id":2159},"чек-лист-ревью","Чек-лист ревью",[236,2162,2165,2174,2189,2198,2206,2212,2218,2224],{"className":2163},[2164],"contains-task-list",[239,2166,2169,2173],{"className":2167},[2168],"task-list-item",[2170,2171],"input",{"disabled":92,"type":2172},"checkbox"," Domain не импортирует ничего, кроме stdlib и других пакетов domain",[239,2175,2177,2179,2180,429,2182,429,2184,429,2187],{"className":2176},[2168],[2170,2178],{"disabled":92,"type":2172}," Use Case не импортирует ",[37,2181,1382],{},[37,2183,1378],{},[37,2185,2186],{},"gin",[37,2188,1703],{},[239,2190,2192,2194,2195,2197],{"className":2191},[2168],[2170,2193],{"disabled":92,"type":2172}," У каждого use case свой Input\u002FOutput (или ",[37,2196,445],{}," без output)",[239,2199,2201,2203,2204],{"className":2200},[2168],[2170,2202],{"disabled":92,"type":2172}," Один use case = один публичный метод ",[37,2205,260],{},[239,2207,2209,2211],{"className":2208},[2168],[2170,2210],{"disabled":92,"type":2172}," Интерфейсы (порты) объявлены рядом с use case, а не рядом с реализацией",[239,2213,2215,2217],{"className":2214},[2168],[2170,2216],{"disabled":92,"type":2172}," Handler не содержит бизнес-логики (только маппинг + вызов use case)",[239,2219,2221,2223],{"className":2220},[2168],[2170,2222],{"disabled":92,"type":2172}," Repository возвращает domain entity, а не raw row",[239,2225,2227,2229,2230,2233,2234],{"className":2226},[2168],[2170,2228],{"disabled":92,"type":2172}," DI собирается в ",[37,2231,2232],{},"cmd\u002Fserver\u002Fmain.go",", а не разбросана по ",[37,2235,2236],{},"init()",[11,2238,2240],{"id":2239},"вопросы-со-сборов","Вопросы со сборов",[1165,2242,2243,2249,2255,2261,2267,2273,2279,2285,2291,2297],{},[239,2244,2245,2248],{},[20,2246,2247],{},"Что такое Dependency Rule и куда направлены зависимости?"," — Снаружи внутрь. Внутренний слой не знает о внешнем.",[239,2250,2251,2254],{},[20,2252,2253],{},"Какие слои в Clean Architecture?"," — Entities, Use Cases, Interface Adapters, Frameworks & Drivers.",[239,2256,2257,2260],{},[20,2258,2259],{},"Чем Use Case отличается от метода сервиса?"," — Отдельный тип на сценарий с явными Input\u002FOutput, изолированный набор зависимостей, тестируется в вакууме.",[239,2262,2263,2266],{},[20,2264,2265],{},"Зачем Input\u002FOutput DTO, если можно передать domain entity?"," — Изоляция домена от транспорта, возможность переиспользовать use case из gRPC\u002FCLI, явная валидация на границе.",[239,2268,2269,2272],{},[20,2270,2271],{},"Где в Clean живут интерфейсы репозиториев?"," — В слое use case (или в domain), там где они используются. Реализация — снаружи.",[239,2274,2275,2278],{},[20,2276,2277],{},"Что такое Screaming Architecture?"," — Структура проекта отражает бизнес, а не фреймворк.",[239,2280,2281,2284],{},[20,2282,2283],{},"Когда Clean — оверкилл?"," — Простой CRUD, MVP, ETL без бизнес-правил.",[239,2286,2287,2290],{},[20,2288,2289],{},"Чем Clean отличается от Onion?"," — В Onion сценарии живут в Application Service (god-object risk). В Clean — каждый сценарий отдельный Use Case.",[239,2292,2293,2296],{},[20,2294,2295],{},"Можно ли вызвать один use case из другого?"," — Нежелательно. Признак пропущенного domain service.",[239,2298,2299,2302],{},[20,2300,2301],{},"Куда положить транзакцию?"," — Use case оркестрирует, но абстракция UnitOfWork\u002FTxManager — это порт. Реализация — adapter.",[2304,2305],"hr",{},[11,2307,2309],{"id":2308},"практика","Практика",[2311,2312,2315,2318,2335],"quiz",{"answer":2005,"id":2313,"xp":2314},"arch-clean-q1","10",[16,2316,2317],{},"Что точнее всего описывает Dependency Rule?",[2319,2320,2321],"template",{"v-slot:options":5},[236,2322,2323,2326,2329,2332],{},[239,2324,2325],{},"Код снаружи может зависеть от кода внутри, но не наоборот",[239,2327,2328],{},"Любой слой может импортировать любой другой, если есть интерфейс",[239,2330,2331],{},"Use case должен импортировать HTTP handler",[239,2333,2334],{},"Entity должна знать, как сохранить себя в PostgreSQL",[2319,2336,2337],{"v-slot:explanation":5},[16,2338,2339],{},"Clean Architecture держится на направлении зависимостей внутрь: frameworks и adapters знают use case\u002Fdomain, но use case\u002Fdomain не знают frameworks.",[2341,2342,2346,2349,2543],"predict",{"answer":2343,"id":2344,"xp":2345},"entity\\nusecase\\nadapter","arch-clean-p1","15",[16,2347,2348],{},"Что выведет программа?",[2319,2350,2351],{"v-slot:code":5},[42,2352,2354],{"className":44,"code":2353,"language":46,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc layer(name string) string {\n    switch name {\n    case \"Order\":\n        return \"entity\"\n    case \"CreateOrder\":\n        return \"usecase\"\n    default:\n        return \"adapter\"\n    }\n}\n\nfunc main() {\n    fmt.Println(layer(\"Order\"))\n    fmt.Println(layer(\"CreateOrder\"))\n    fmt.Println(layer(\"EchoHandler\"))\n}\n",[37,2355,2356,2364,2368,2381,2385,2406,2414,2425,2432,2441,2448,2455,2462,2466,2470,2474,2484,2505,2522,2539],{"__ignoreMap":5},[50,2357,2358,2361],{"class":52,"line":53},[50,2359,2360],{"class":56},"package",[50,2362,2363],{"class":60}," main\n",[50,2365,2366],{"class":52,"line":89},[50,2367,93],{"emptyLinePlaceholder":92},[50,2369,2370,2373,2376,2378],{"class":52,"line":96},[50,2371,2372],{"class":56},"import",[50,2374,2375],{"class":1524}," \"",[50,2377,2136],{"class":60},[50,2379,2380],{"class":1524},"\"\n",[50,2382,2383],{"class":52,"line":130},[50,2384,93],{"emptyLinePlaceholder":92},[50,2386,2387,2389,2392,2394,2397,2399,2401,2404],{"class":52,"line":154},[50,2388,99],{"class":56},[50,2390,2391],{"class":60}," layer",[50,2393,121],{"class":67},[50,2395,2396],{"class":105},"name",[50,2398,692],{"class":56},[50,2400,115],{"class":67},[50,2402,2403],{"class":56},"string",[50,2405,278],{"class":67},[50,2407,2408,2411],{"class":52,"line":178},[50,2409,2410],{"class":56},"    switch",[50,2412,2413],{"class":67}," name {\n",[50,2415,2416,2419,2422],{"class":52,"line":203},[50,2417,2418],{"class":56},"    case",[50,2420,2421],{"class":1524}," \"Order\"",[50,2423,2424],{"class":67},":\n",[50,2426,2427,2429],{"class":52,"line":227},[50,2428,490],{"class":56},[50,2430,2431],{"class":1524}," \"entity\"\n",[50,2433,2434,2436,2439],{"class":52,"line":338},[50,2435,2418],{"class":56},[50,2437,2438],{"class":1524}," \"CreateOrder\"",[50,2440,2424],{"class":67},[50,2442,2443,2445],{"class":52,"line":343},[50,2444,490],{"class":56},[50,2446,2447],{"class":1524}," \"usecase\"\n",[50,2449,2450,2453],{"class":52,"line":348},[50,2451,2452],{"class":56},"    default",[50,2454,2424],{"class":67},[50,2456,2457,2459],{"class":52,"line":360},[50,2458,490],{"class":56},[50,2460,2461],{"class":1524}," \"adapter\"\n",[50,2463,2464],{"class":52,"line":369},[50,2465,501],{"class":67},[50,2467,2468],{"class":52,"line":378},[50,2469,299],{"class":67},[50,2471,2472],{"class":52,"line":387},[50,2473,93],{"emptyLinePlaceholder":92},[50,2475,2476,2478,2481],{"class":52,"line":392},[50,2477,99],{"class":56},[50,2479,2480],{"class":60}," main",[50,2482,2483],{"class":67},"() {\n",[50,2485,2486,2489,2492,2494,2497,2499,2502],{"class":52,"line":397},[50,2487,2488],{"class":67},"    fmt.",[50,2490,2491],{"class":60},"Println",[50,2493,121],{"class":67},[50,2495,2496],{"class":60},"layer",[50,2498,121],{"class":67},[50,2500,2501],{"class":1524},"\"Order\"",[50,2503,2504],{"class":67},"))\n",[50,2506,2507,2509,2511,2513,2515,2517,2520],{"class":52,"line":451},[50,2508,2488],{"class":67},[50,2510,2491],{"class":60},[50,2512,121],{"class":67},[50,2514,2496],{"class":60},[50,2516,121],{"class":67},[50,2518,2519],{"class":1524},"\"CreateOrder\"",[50,2521,2504],{"class":67},[50,2523,2524,2526,2528,2530,2532,2534,2537],{"class":52,"line":469},[50,2525,2488],{"class":67},[50,2527,2491],{"class":60},[50,2529,121],{"class":67},[50,2531,2496],{"class":60},[50,2533,121],{"class":67},[50,2535,2536],{"class":1524},"\"EchoHandler\"",[50,2538,2504],{"class":67},[50,2540,2541],{"class":52,"line":487},[50,2542,299],{"class":67},[2319,2544,2545],{"v-slot:hint":5},[16,2546,2547],{},"Entity — бизнесовая модель, use case — сценарий, handler — внешний адаптер.",[2549,2550,2554,2565,2694],"code-task",{"expected":2551,"id":2552,"xp":2553},"ok\\nbad\\nbad","arch-clean-ct1","20",[16,2555,2556,2557,2560,2561,2564],{},"Реализуй ",[37,2558,2559],{},"UseCaseImportReview",": use case может импортировать ",[37,2562,2563],{},"context",", но не должен импортировать HTTP-фреймворк или DB-driver.",[2319,2566,2567],{"v-slot:template":5},[42,2568,2570],{"className":44,"code":2569,"language":46,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc UseCaseImportReview(pkg string) string {\n    return \"ok\"\n}\n\nfunc main() {\n    fmt.Println(UseCaseImportReview(\"context\"))\n    fmt.Println(UseCaseImportReview(\"github.com\u002Flabstack\u002Fecho\u002Fv4\"))\n    fmt.Println(UseCaseImportReview(\"github.com\u002Fjackc\u002Fpgx\u002Fv5\"))\n}\n",[37,2571,2572,2578,2582,2592,2596,2616,2623,2627,2631,2639,2656,2673,2690],{"__ignoreMap":5},[50,2573,2574,2576],{"class":52,"line":53},[50,2575,2360],{"class":56},[50,2577,2363],{"class":60},[50,2579,2580],{"class":52,"line":89},[50,2581,93],{"emptyLinePlaceholder":92},[50,2583,2584,2586,2588,2590],{"class":52,"line":96},[50,2585,2372],{"class":56},[50,2587,2375],{"class":1524},[50,2589,2136],{"class":60},[50,2591,2380],{"class":1524},[50,2593,2594],{"class":52,"line":130},[50,2595,93],{"emptyLinePlaceholder":92},[50,2597,2598,2600,2603,2605,2608,2610,2612,2614],{"class":52,"line":154},[50,2599,99],{"class":56},[50,2601,2602],{"class":60}," UseCaseImportReview",[50,2604,121],{"class":67},[50,2606,2607],{"class":105},"pkg",[50,2609,692],{"class":56},[50,2611,115],{"class":67},[50,2613,2403],{"class":56},[50,2615,278],{"class":67},[50,2617,2618,2620],{"class":52,"line":178},[50,2619,563],{"class":56},[50,2621,2622],{"class":1524}," \"ok\"\n",[50,2624,2625],{"class":52,"line":203},[50,2626,299],{"class":67},[50,2628,2629],{"class":52,"line":227},[50,2630,93],{"emptyLinePlaceholder":92},[50,2632,2633,2635,2637],{"class":52,"line":338},[50,2634,99],{"class":56},[50,2636,2480],{"class":60},[50,2638,2483],{"class":67},[50,2640,2641,2643,2645,2647,2649,2651,2654],{"class":52,"line":343},[50,2642,2488],{"class":67},[50,2644,2491],{"class":60},[50,2646,121],{"class":67},[50,2648,2559],{"class":60},[50,2650,121],{"class":67},[50,2652,2653],{"class":1524},"\"context\"",[50,2655,2504],{"class":67},[50,2657,2658,2660,2662,2664,2666,2668,2671],{"class":52,"line":348},[50,2659,2488],{"class":67},[50,2661,2491],{"class":60},[50,2663,121],{"class":67},[50,2665,2559],{"class":60},[50,2667,121],{"class":67},[50,2669,2670],{"class":1524},"\"github.com\u002Flabstack\u002Fecho\u002Fv4\"",[50,2672,2504],{"class":67},[50,2674,2675,2677,2679,2681,2683,2685,2688],{"class":52,"line":360},[50,2676,2488],{"class":67},[50,2678,2491],{"class":60},[50,2680,121],{"class":67},[50,2682,2559],{"class":60},[50,2684,121],{"class":67},[50,2686,2687],{"class":1524},"\"github.com\u002Fjackc\u002Fpgx\u002Fv5\"",[50,2689,2504],{"class":67},[50,2691,2692],{"class":52,"line":369},[50,2693,299],{"class":67},[2319,2695,2696],{"v-slot:hints":5},[236,2697,2698,2703,2708],{},[239,2699,2700,2702],{},[37,2701,2563],{}," допустим как часть контракта операции",[239,2704,2705,2707],{},[37,2706,1378],{}," относится к transport adapter",[239,2709,2710,2712],{},[37,2711,1703],{}," относится к persistence adapter",[2714,2715,2716],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}",{"title":5,"searchDepth":89,"depth":89,"links":2718},[2719,2720,2724,2725,2738,2742,2743,2748,2749],{"id":13,"depth":89,"text":14},{"id":26,"depth":89,"text":27,"children":2721},[2722,2723],{"id":31,"depth":96,"text":32},{"id":253,"depth":96,"text":254},{"id":594,"depth":89,"text":595},{"id":609,"depth":89,"text":610,"children":2726},[2727,2728,2729,2730,2731,2732,2733,2734,2735,2736,2737],{"id":613,"depth":96,"text":614},{"id":617,"depth":96,"text":618},{"id":855,"depth":96,"text":856},{"id":862,"depth":96,"text":863},{"id":883,"depth":96,"text":884},{"id":890,"depth":96,"text":891},{"id":967,"depth":96,"text":968},{"id":1156,"depth":96,"text":1157},{"id":1371,"depth":96,"text":1372},{"id":1634,"depth":96,"text":1635},{"id":1662,"depth":96,"text":1663},{"id":1678,"depth":89,"text":1679,"children":2739},[2740,2741],{"id":1725,"depth":96,"text":1726},{"id":1799,"depth":96,"text":1800},{"id":1820,"depth":89,"text":1821},{"id":2056,"depth":89,"text":2057,"children":2744},[2745,2746,2747],{"id":2074,"depth":96,"text":2075},{"id":2092,"depth":96,"text":2093},{"id":2159,"depth":96,"text":2160},{"id":2239,"depth":89,"text":2240},{"id":2308,"depth":89,"text":2309},1781022065763]