[{"data":1,"prerenderedAt":2085},["ShallowReactive",2],{"content:\u002F06-architecture\u002F01-layered":3},{"title":4,"description":5,"path":6,"body":7},"Layered Architecture (N-Layer)","","\u002F06-architecture\u002F01-layered",{"type":8,"value":9,"toc":2065},"minimark",[10,15,19,29,32,36,57,62,76,79,83,88,94,99,105,108,113,119,122,126,140,143,147,150,156,337,548,794,806,810,813,831,835,840,862,867,899,902,908,914,920,924,931,945,954,958,961,967,1054,1059,1156,1165,1252,1266,1270,1273,1290,1293,1310,1314,1328,1343,1352,1362,1368,1378,1382,1387,1436,1439,1444,1480,1483,1489,1493,1542,1546,1552,1558,1564,1570,1576,1582,1588,1594,1597,1601,1632,1879,2061],[11,12,14],"h2",{"id":13},"суть","Суть",[16,17,18],"p",{},"Код разделён на горизонтальные слои по ответственности. Каждый слой зависит только от слоя ниже. Самый старый и самый интуитивный способ структурировать backend.",[20,21,26],"pre",{"className":22,"code":24,"language":25,"meta":5},[23],"language-text","┌─────────────────────────────────────┐\n│  Presentation (HTTP handlers, UI)   │  ← знает Business\n├─────────────────────────────────────┤\n│  Business Logic (services, rules)   │  ← знает Data Access\n├─────────────────────────────────────┤\n│  Data Access (repos, ORM, SQL)      │  ← знает Database\n├─────────────────────────────────────┤\n│  Database                           │\n└─────────────────────────────────────┘\n","text",[27,28,24],"code",{"__ignoreMap":5},[16,30,31],{},"Поток вызова: HTTP-запрос → handler → service → repository → SQL → ответ обратно вверх.",[11,33,35],{"id":34},"ключевые-правила","Ключевые правила",[37,38,39,48,51,54],"ul",{},[40,41,42,43,47],"li",{},"Зависимости ",[44,45,46],"strong",{},"только сверху вниз"," — presentation вызывает business, business вызывает data access",[40,49,50],{},"Слой не знает о слоях выше себя",[40,52,53],{},"Внутри слоя — свободная организация",[40,55,56],{},"Изменения локализованы в одном слое (в идеале)",[58,59,61],"h3",{"id":60},"closed-vs-open-layers","Closed vs Open layers",[37,63,64,70],{},[40,65,66,69],{},[44,67,68],{},"Closed layer"," — вызов разрешён только в соседний слой ниже. Presentation не имеет права лезть напрямую в Data Access, только через Business",[40,71,72,75],{},[44,73,74],{},"Open layer"," — можно прыгать через слой. Чаще используют для cross-cutting concerns: логирование, кэш, метрики",[16,77,78],{},"Closed строже, но провоцирует \"дырявые\" сервисы, которые просто пробрасывают вызов в repo. Open даёт гибкость, но размывает границы.",[11,80,82],{"id":81},"variations-3-4-n-слоёв","Variations: 3, 4, N слоёв",[16,84,85],{},[44,86,87],{},"3-Layer (классика):",[20,89,92],{"className":90,"code":91,"language":25,"meta":5},[23],"Presentation → Business → Data Access\n",[27,93,91],{"__ignoreMap":5},[16,95,96],{},[44,97,98],{},"4-Layer (с DTO\u002FApplication слоем):",[20,100,103],{"className":101,"code":102,"language":25,"meta":5},[23],"Presentation → Application → Domain → Data Access\n",[27,104,102],{"__ignoreMap":5},[16,106,107],{},"Application здесь — оркестрация сценариев (use case), Domain — чистые правила.",[16,109,110],{},[44,111,112],{},"N-Layer (enterprise):",[20,114,117],{"className":115,"code":116,"language":25,"meta":5},[23],"UI → API Gateway → Application → Domain → Persistence → Database\n",[27,118,116],{"__ignoreMap":5},[16,120,121],{},"Чем больше слоёв, тем больше boilerplate. В Go обычно хватает 3-4.",[11,123,125],{"id":124},"layer-vs-tier","Layer vs Tier",[37,127,128,134],{},[40,129,130,133],{},[44,131,132],{},"Layer"," — логическое разделение кода в одном процессе",[40,135,136,139],{},[44,137,138],{},"Tier"," — физическое разделение (отдельный сервер\u002Fпроцесс)",[16,141,142],{},"3 layers на одном сервере — нормально. 3 tiers = 3 разных сервера. Не путать.",[11,144,146],{"id":145},"пример-layered-в-go","Пример: Layered в Go",[16,148,149],{},"Структура:",[20,151,154],{"className":152,"code":153,"language":25,"meta":5},[23],"internal\u002F\n  handler\u002F      ← Presentation\n    order.go\n  service\u002F      ← Business\n    order.go\n  repository\u002F   ← Data Access\n    order.go\n",[27,155,153],{"__ignoreMap":5},[20,157,161],{"className":158,"code":159,"language":160,"meta":5,"style":5},"language-go shiki shiki-themes github-dark","\u002F\u002F repository\u002Forder.go\npackage repository\n\ntype OrderRepo struct {\n    db *pgxpool.Pool\n}\n\nfunc (r *OrderRepo) Save(ctx context.Context, o Order) error {\n    _, err := r.db.Exec(ctx,\n        \"INSERT INTO orders (id, user_id, total) VALUES ($1, $2, $3)\",\n        o.ID, o.UserID, o.Total)\n    return err\n}\n","go",[27,162,163,172,183,190,206,224,230,235,289,307,317,323,332],{"__ignoreMap":5},[164,165,168],"span",{"class":166,"line":167},"line",1,[164,169,171],{"class":170},"sAwPA","\u002F\u002F repository\u002Forder.go\n",[164,173,175,179],{"class":166,"line":174},2,[164,176,178],{"class":177},"snl16","package",[164,180,182],{"class":181},"svObZ"," repository\n",[164,184,186],{"class":166,"line":185},3,[164,187,189],{"emptyLinePlaceholder":188},true,"\n",[164,191,193,196,199,202],{"class":166,"line":192},4,[164,194,195],{"class":177},"type",[164,197,198],{"class":181}," OrderRepo",[164,200,201],{"class":177}," struct",[164,203,205],{"class":204},"s95oV"," {\n",[164,207,209,212,215,218,221],{"class":166,"line":208},5,[164,210,211],{"class":204},"    db ",[164,213,214],{"class":177},"*",[164,216,217],{"class":181},"pgxpool",[164,219,220],{"class":204},".",[164,222,223],{"class":181},"Pool\n",[164,225,227],{"class":166,"line":226},6,[164,228,229],{"class":204},"}\n",[164,231,233],{"class":166,"line":232},7,[164,234,189],{"emptyLinePlaceholder":188},[164,236,238,241,244,248,250,253,256,259,262,265,268,270,273,276,279,282,284,287],{"class":166,"line":237},8,[164,239,240],{"class":177},"func",[164,242,243],{"class":204}," (",[164,245,247],{"class":246},"s9osk","r ",[164,249,214],{"class":177},[164,251,252],{"class":181},"OrderRepo",[164,254,255],{"class":204},") ",[164,257,258],{"class":181},"Save",[164,260,261],{"class":204},"(",[164,263,264],{"class":246},"ctx",[164,266,267],{"class":181}," context",[164,269,220],{"class":204},[164,271,272],{"class":181},"Context",[164,274,275],{"class":204},", ",[164,277,278],{"class":246},"o",[164,280,281],{"class":181}," Order",[164,283,255],{"class":204},[164,285,286],{"class":177},"error",[164,288,205],{"class":204},[164,290,292,295,298,301,304],{"class":166,"line":291},9,[164,293,294],{"class":204},"    _, err ",[164,296,297],{"class":177},":=",[164,299,300],{"class":204}," r.db.",[164,302,303],{"class":181},"Exec",[164,305,306],{"class":204},"(ctx,\n",[164,308,310,314],{"class":166,"line":309},10,[164,311,313],{"class":312},"sU2Wk","        \"INSERT INTO orders (id, user_id, total) VALUES ($1, $2, $3)\"",[164,315,316],{"class":204},",\n",[164,318,320],{"class":166,"line":319},11,[164,321,322],{"class":204},"        o.ID, o.UserID, o.Total)\n",[164,324,326,329],{"class":166,"line":325},12,[164,327,328],{"class":177},"    return",[164,330,331],{"class":204}," err\n",[164,333,335],{"class":166,"line":334},13,[164,336,229],{"class":204},[20,338,340],{"className":158,"code":339,"language":160,"meta":5,"style":5},"\u002F\u002F service\u002Forder.go\npackage service\n\nimport \"myapp\u002Finternal\u002Frepository\" \u002F\u002F зависим вниз\n\ntype OrderService struct {\n    repo *repository.OrderRepo \u002F\u002F конкретный тип, не интерфейс\n}\n\nfunc (s *OrderService) Create(ctx context.Context, userID string, items []Item) error {\n    total := calcTotal(items)\n    if total \u003C= 0 {\n        return errors.New(\"empty order\")\n    }\n    return s.repo.Save(ctx, Order{UserID: userID, Total: total})\n}\n",[27,341,342,347,354,358,375,379,390,407,411,415,469,482,499,518,524,543],{"__ignoreMap":5},[164,343,344],{"class":166,"line":167},[164,345,346],{"class":170},"\u002F\u002F service\u002Forder.go\n",[164,348,349,351],{"class":166,"line":174},[164,350,178],{"class":177},[164,352,353],{"class":181}," service\n",[164,355,356],{"class":166,"line":185},[164,357,189],{"emptyLinePlaceholder":188},[164,359,360,363,366,369,372],{"class":166,"line":192},[164,361,362],{"class":177},"import",[164,364,365],{"class":312}," \"",[164,367,368],{"class":181},"myapp\u002Finternal\u002Frepository",[164,370,371],{"class":312},"\"",[164,373,374],{"class":170}," \u002F\u002F зависим вниз\n",[164,376,377],{"class":166,"line":208},[164,378,189],{"emptyLinePlaceholder":188},[164,380,381,383,386,388],{"class":166,"line":226},[164,382,195],{"class":177},[164,384,385],{"class":181}," OrderService",[164,387,201],{"class":177},[164,389,205],{"class":204},[164,391,392,395,397,400,402,404],{"class":166,"line":232},[164,393,394],{"class":204},"    repo ",[164,396,214],{"class":177},[164,398,399],{"class":181},"repository",[164,401,220],{"class":204},[164,403,252],{"class":181},[164,405,406],{"class":170}," \u002F\u002F конкретный тип, не интерфейс\n",[164,408,409],{"class":166,"line":237},[164,410,229],{"class":204},[164,412,413],{"class":166,"line":291},[164,414,189],{"emptyLinePlaceholder":188},[164,416,417,419,421,424,426,429,431,434,436,438,440,442,444,446,449,452,454,457,460,463,465,467],{"class":166,"line":309},[164,418,240],{"class":177},[164,420,243],{"class":204},[164,422,423],{"class":246},"s ",[164,425,214],{"class":177},[164,427,428],{"class":181},"OrderService",[164,430,255],{"class":204},[164,432,433],{"class":181},"Create",[164,435,261],{"class":204},[164,437,264],{"class":246},[164,439,267],{"class":181},[164,441,220],{"class":204},[164,443,272],{"class":181},[164,445,275],{"class":204},[164,447,448],{"class":246},"userID",[164,450,451],{"class":177}," string",[164,453,275],{"class":204},[164,455,456],{"class":246},"items",[164,458,459],{"class":204}," []",[164,461,462],{"class":181},"Item",[164,464,255],{"class":204},[164,466,286],{"class":177},[164,468,205],{"class":204},[164,470,471,474,476,479],{"class":166,"line":319},[164,472,473],{"class":204},"    total ",[164,475,297],{"class":177},[164,477,478],{"class":181}," calcTotal",[164,480,481],{"class":204},"(items)\n",[164,483,484,487,490,493,497],{"class":166,"line":325},[164,485,486],{"class":177},"    if",[164,488,489],{"class":204}," total ",[164,491,492],{"class":177},"\u003C=",[164,494,496],{"class":495},"sDLfK"," 0",[164,498,205],{"class":204},[164,500,501,504,507,510,512,515],{"class":166,"line":334},[164,502,503],{"class":177},"        return",[164,505,506],{"class":204}," errors.",[164,508,509],{"class":181},"New",[164,511,261],{"class":204},[164,513,514],{"class":312},"\"empty order\"",[164,516,517],{"class":204},")\n",[164,519,521],{"class":166,"line":520},14,[164,522,523],{"class":204},"    }\n",[164,525,527,529,532,534,537,540],{"class":166,"line":526},15,[164,528,328],{"class":177},[164,530,531],{"class":204}," s.repo.",[164,533,258],{"class":181},[164,535,536],{"class":204},"(ctx, ",[164,538,539],{"class":181},"Order",[164,541,542],{"class":204},"{UserID: userID, Total: total})\n",[164,544,546],{"class":166,"line":545},16,[164,547,229],{"class":204},[20,549,551],{"className":158,"code":550,"language":160,"meta":5,"style":5},"\u002F\u002F handler\u002Forder.go\npackage handler\n\nimport \"myapp\u002Finternal\u002Fservice\" \u002F\u002F зависим вниз\n\ntype OrderHandler struct {\n    svc *service.OrderService\n}\n\nfunc (h *OrderHandler) Create(c echo.Context) error {\n    var req CreateOrderReq\n    if err := c.Bind(&req); err != nil {\n        return c.JSON(400, err)\n    }\n    if err := h.svc.Create(c.Request().Context(), req.UserID, req.Items); err != nil {\n        return c.JSON(500, err)\n    }\n    return c.NoContent(201)\n}\n",[27,552,553,558,565,569,582,586,597,612,616,620,656,667,698,715,719,752,767,772,789],{"__ignoreMap":5},[164,554,555],{"class":166,"line":167},[164,556,557],{"class":170},"\u002F\u002F handler\u002Forder.go\n",[164,559,560,562],{"class":166,"line":174},[164,561,178],{"class":177},[164,563,564],{"class":181}," handler\n",[164,566,567],{"class":166,"line":185},[164,568,189],{"emptyLinePlaceholder":188},[164,570,571,573,575,578,580],{"class":166,"line":192},[164,572,362],{"class":177},[164,574,365],{"class":312},[164,576,577],{"class":181},"myapp\u002Finternal\u002Fservice",[164,579,371],{"class":312},[164,581,374],{"class":170},[164,583,584],{"class":166,"line":208},[164,585,189],{"emptyLinePlaceholder":188},[164,587,588,590,593,595],{"class":166,"line":226},[164,589,195],{"class":177},[164,591,592],{"class":181}," OrderHandler",[164,594,201],{"class":177},[164,596,205],{"class":204},[164,598,599,602,604,607,609],{"class":166,"line":232},[164,600,601],{"class":204},"    svc ",[164,603,214],{"class":177},[164,605,606],{"class":181},"service",[164,608,220],{"class":204},[164,610,611],{"class":181},"OrderService\n",[164,613,614],{"class":166,"line":237},[164,615,229],{"class":204},[164,617,618],{"class":166,"line":291},[164,619,189],{"emptyLinePlaceholder":188},[164,621,622,624,626,629,631,634,636,638,640,643,646,648,650,652,654],{"class":166,"line":309},[164,623,240],{"class":177},[164,625,243],{"class":204},[164,627,628],{"class":246},"h ",[164,630,214],{"class":177},[164,632,633],{"class":181},"OrderHandler",[164,635,255],{"class":204},[164,637,433],{"class":181},[164,639,261],{"class":204},[164,641,642],{"class":246},"c",[164,644,645],{"class":181}," echo",[164,647,220],{"class":204},[164,649,272],{"class":181},[164,651,255],{"class":204},[164,653,286],{"class":177},[164,655,205],{"class":204},[164,657,658,661,664],{"class":166,"line":319},[164,659,660],{"class":177},"    var",[164,662,663],{"class":204}," req ",[164,665,666],{"class":181},"CreateOrderReq\n",[164,668,669,671,674,676,679,682,684,687,690,693,696],{"class":166,"line":325},[164,670,486],{"class":177},[164,672,673],{"class":204}," err ",[164,675,297],{"class":177},[164,677,678],{"class":204}," c.",[164,680,681],{"class":181},"Bind",[164,683,261],{"class":204},[164,685,686],{"class":177},"&",[164,688,689],{"class":204},"req); err ",[164,691,692],{"class":177},"!=",[164,694,695],{"class":495}," nil",[164,697,205],{"class":204},[164,699,700,702,704,707,709,712],{"class":166,"line":334},[164,701,503],{"class":177},[164,703,678],{"class":204},[164,705,706],{"class":181},"JSON",[164,708,261],{"class":204},[164,710,711],{"class":495},"400",[164,713,714],{"class":204},", err)\n",[164,716,717],{"class":166,"line":520},[164,718,523],{"class":204},[164,720,721,723,725,727,730,732,735,738,741,743,746,748,750],{"class":166,"line":526},[164,722,486],{"class":177},[164,724,673],{"class":204},[164,726,297],{"class":177},[164,728,729],{"class":204}," h.svc.",[164,731,433],{"class":181},[164,733,734],{"class":204},"(c.",[164,736,737],{"class":181},"Request",[164,739,740],{"class":204},"().",[164,742,272],{"class":181},[164,744,745],{"class":204},"(), req.UserID, req.Items); err ",[164,747,692],{"class":177},[164,749,695],{"class":495},[164,751,205],{"class":204},[164,753,754,756,758,760,762,765],{"class":166,"line":545},[164,755,503],{"class":177},[164,757,678],{"class":204},[164,759,706],{"class":181},[164,761,261],{"class":204},[164,763,764],{"class":495},"500",[164,766,714],{"class":204},[164,768,770],{"class":166,"line":769},17,[164,771,523],{"class":204},[164,773,775,777,779,782,784,787],{"class":166,"line":774},18,[164,776,328],{"class":177},[164,778,678],{"class":204},[164,780,781],{"class":181},"NoContent",[164,783,261],{"class":204},[164,785,786],{"class":495},"201",[164,788,517],{"class":204},[164,790,792],{"class":166,"line":791},19,[164,793,229],{"class":204},[16,795,796,797,799,800,802,803],{},"Выглядит чисто. Но смотри на импорты: ",[27,798,606],{}," → ",[27,801,399],{},". ",[44,804,805],{},"Бизнес-логика зависит от инфраструктуры.",[11,807,809],{"id":808},"где-layered-начинает-болеть-в-go","Где Layered начинает болеть в Go",[16,811,812],{},"Layered сам по себе не зло. Для простого CRUD, админки или прототипа это нормальная стартовая точка: структура понятна, файлов мало, новичкам легко читать поток запроса.",[16,814,815,816,275,819,822,823,826,827,830],{},"Проблема начинается там, где слой business начинает импортировать ",[27,817,818],{},"pgx",[27,820,821],{},"database\u002Fsql",", ORM-модели, Redis-клиент или конкретный пакет ",[27,824,825],{},"repository\u002Fpostgres",". В Go принято: ",[44,828,829],{},"бизнес-логика не должна знать про БД",". Антипаттерн не в названии Layered, а в направлении зависимости: важные правила зависят от деталей хранения.",[58,832,834],{"id":833},"проблемы","Проблемы",[16,836,837],{},[44,838,839],{},"1. Бизнес-логика прибита к БД",[20,841,843],{"className":158,"code":842,"language":160,"meta":5,"style":5},"\u002F\u002F service импортирует repository → меняешь postgres на mongo → переписываешь service\nimport \"myapp\u002Finternal\u002Frepository\u002Fpostgres\"\n",[27,844,845,850],{"__ignoreMap":5},[164,846,847],{"class":166,"line":167},[164,848,849],{"class":170},"\u002F\u002F service импортирует repository → меняешь postgres на mongo → переписываешь service\n",[164,851,852,854,856,859],{"class":166,"line":174},[164,853,362],{"class":177},[164,855,365],{"class":312},[164,857,858],{"class":181},"myapp\u002Finternal\u002Frepository\u002Fpostgres",[164,860,861],{"class":312},"\"\n",[16,863,864],{},[44,865,866],{},"2. Тесты требуют БД",[20,868,870],{"className":158,"code":869,"language":160,"meta":5,"style":5},"\u002F\u002F Не получится протестировать OrderService без поднятия Postgres\nsvc := service.NewOrderService(repository.NewOrderRepo(realDB))\n",[27,871,872,877],{"__ignoreMap":5},[164,873,874],{"class":166,"line":167},[164,875,876],{"class":170},"\u002F\u002F Не получится протестировать OrderService без поднятия Postgres\n",[164,878,879,882,884,887,890,893,896],{"class":166,"line":174},[164,880,881],{"class":204},"svc ",[164,883,297],{"class":177},[164,885,886],{"class":204}," service.",[164,888,889],{"class":181},"NewOrderService",[164,891,892],{"class":204},"(repository.",[164,894,895],{"class":181},"NewOrderRepo",[164,897,898],{"class":204},"(realDB))\n",[16,900,901],{},"Можно делать integration-тесты с testcontainers, но это медленно и хрупко.",[16,903,904,907],{},[44,905,906],{},"3. Сквозная связанность","\nПоменяли тип колонки в БД → поменяли модель в repository → поменяли модель в service → поменяли DTO в handler. Каскад через все слои.",[16,909,910,913],{},[44,911,912],{},"4. Размытие границ","\nБез дисциплины handler начинает напрямую звать repository (срезать угол). Через год — спагетти.",[16,915,916,919],{},[44,917,918],{},"5. Анемичная модель","\nБизнес-логика расползается по сервисам, entities превращаются в DTO с публичными полями. DDD умер.",[11,921,923],{"id":922},"связь-с-проектом-ratedesk","Связь с проектом RateDesk",[16,925,926,927,930],{},"В проектном треке RateDesk layered-подход полезен как диагностика первой версии: ",[27,928,929],{},"handler -> service -> repository"," легко читать, пока сервис только конвертирует сумму из памяти. На архитектурном этапе проекта важно увидеть момент, где эта простота начинает течь:",[37,932,933,936,939,942],{},[40,934,935],{},"use case начинает зависеть от конкретного Postgres\u002FRedis\u002Fprovider-клиента;",[40,937,938],{},"свежесть курса, fallback и idempotency размазываются между service и repository;",[40,940,941],{},"HTTP DTO, DB row и доменная модель становятся одним и тем же типом;",[40,943,944],{},"unit-тест конвертации внезапно требует БД, сети или testcontainers.",[16,946,947,948,953],{},"Детальное задание, MR-план и acceptance criteria для такого рефакторинга находятся в отдельном этапе ",[949,950,952],"a",{"href":951},"\u002Fprojects\u002Fratedesk\u002Farchitecture","RateDesk Architecture",". В этом уроке важно понять критерий: layered допустим, пока бизнесовые правила не зависят от инфраструктурных деталей.",[11,955,957],{"id":956},"рефакторинг-layered-hexagonal","Рефакторинг: Layered → Hexagonal",[16,959,960],{},"Шаги:",[16,962,963,966],{},[44,964,965],{},"1. Объявить интерфейс рядом с сервисом"," (где он используется):",[20,968,970],{"className":158,"code":969,"language":160,"meta":5,"style":5},"\u002F\u002F service\u002Forder.go\npackage service\n\ntype OrderRepo interface {\n    Save(ctx context.Context, o Order) error\n}\n\ntype OrderService struct {\n    repo OrderRepo \u002F\u002F теперь интерфейс, не конкретный тип\n}\n",[27,971,972,976,982,986,997,1023,1027,1031,1041,1050],{"__ignoreMap":5},[164,973,974],{"class":166,"line":167},[164,975,346],{"class":170},[164,977,978,980],{"class":166,"line":174},[164,979,178],{"class":177},[164,981,353],{"class":181},[164,983,984],{"class":166,"line":185},[164,985,189],{"emptyLinePlaceholder":188},[164,987,988,990,992,995],{"class":166,"line":192},[164,989,195],{"class":177},[164,991,198],{"class":181},[164,993,994],{"class":177}," interface",[164,996,205],{"class":204},[164,998,999,1002,1004,1006,1008,1010,1012,1014,1016,1018,1020],{"class":166,"line":208},[164,1000,1001],{"class":181},"    Save",[164,1003,261],{"class":204},[164,1005,264],{"class":246},[164,1007,267],{"class":181},[164,1009,220],{"class":204},[164,1011,272],{"class":181},[164,1013,275],{"class":204},[164,1015,278],{"class":246},[164,1017,281],{"class":181},[164,1019,255],{"class":204},[164,1021,1022],{"class":177},"error\n",[164,1024,1025],{"class":166,"line":226},[164,1026,229],{"class":204},[164,1028,1029],{"class":166,"line":232},[164,1030,189],{"emptyLinePlaceholder":188},[164,1032,1033,1035,1037,1039],{"class":166,"line":237},[164,1034,195],{"class":177},[164,1036,385],{"class":181},[164,1038,201],{"class":177},[164,1040,205],{"class":204},[164,1042,1043,1045,1047],{"class":166,"line":291},[164,1044,394],{"class":204},[164,1046,252],{"class":181},[164,1048,1049],{"class":170}," \u002F\u002F теперь интерфейс, не конкретный тип\n",[164,1051,1052],{"class":166,"line":309},[164,1053,229],{"class":204},[16,1055,1056],{},[44,1057,1058],{},"2. Реализация уезжает в infrastructure-пакет:",[20,1060,1062],{"className":158,"code":1061,"language":160,"meta":5,"style":5},"\u002F\u002F infrastructure\u002Fpostgres\u002Forder_repo.go\npackage postgres\n\ntype OrderRepo struct { db *pgxpool.Pool }\n\nfunc (r *OrderRepo) Save(ctx context.Context, o service.Order) error { ... }\n",[27,1063,1064,1069,1076,1080,1103,1107],{"__ignoreMap":5},[164,1065,1066],{"class":166,"line":167},[164,1067,1068],{"class":170},"\u002F\u002F infrastructure\u002Fpostgres\u002Forder_repo.go\n",[164,1070,1071,1073],{"class":166,"line":174},[164,1072,178],{"class":177},[164,1074,1075],{"class":181}," postgres\n",[164,1077,1078],{"class":166,"line":185},[164,1079,189],{"emptyLinePlaceholder":188},[164,1081,1082,1084,1086,1088,1091,1093,1095,1097,1100],{"class":166,"line":192},[164,1083,195],{"class":177},[164,1085,198],{"class":181},[164,1087,201],{"class":177},[164,1089,1090],{"class":204}," { db ",[164,1092,214],{"class":177},[164,1094,217],{"class":181},[164,1096,220],{"class":204},[164,1098,1099],{"class":181},"Pool",[164,1101,1102],{"class":204}," }\n",[164,1104,1105],{"class":166,"line":208},[164,1106,189],{"emptyLinePlaceholder":188},[164,1108,1109,1111,1113,1115,1117,1119,1121,1123,1125,1127,1129,1131,1133,1135,1137,1140,1142,1144,1146,1148,1151,1154],{"class":166,"line":226},[164,1110,240],{"class":177},[164,1112,243],{"class":204},[164,1114,247],{"class":246},[164,1116,214],{"class":177},[164,1118,252],{"class":181},[164,1120,255],{"class":204},[164,1122,258],{"class":181},[164,1124,261],{"class":204},[164,1126,264],{"class":246},[164,1128,267],{"class":181},[164,1130,220],{"class":204},[164,1132,272],{"class":181},[164,1134,275],{"class":204},[164,1136,278],{"class":246},[164,1138,1139],{"class":181}," service",[164,1141,220],{"class":204},[164,1143,539],{"class":181},[164,1145,255],{"class":204},[164,1147,286],{"class":177},[164,1149,1150],{"class":204}," { ",[164,1152,1153],{"class":177},"...",[164,1155,1102],{"class":204},[16,1157,1158],{},[44,1159,1160,1161,1164],{},"3. Сборка зависимостей в ",[27,1162,1163],{},"main.go",":",[20,1166,1168],{"className":158,"code":1167,"language":160,"meta":5,"style":5},"func main() {\n    db := postgres.Connect(...)\n    repo := postgres.NewOrderRepo(db)\n    svc := service.NewOrderService(repo) \u002F\u002F инжектим адаптер в порт\n    h := handler.NewOrderHandler(svc)\n    ...\n}\n",[27,1169,1170,1180,1198,1211,1227,1243,1248],{"__ignoreMap":5},[164,1171,1172,1174,1177],{"class":166,"line":167},[164,1173,240],{"class":177},[164,1175,1176],{"class":181}," main",[164,1178,1179],{"class":204},"() {\n",[164,1181,1182,1184,1186,1189,1192,1194,1196],{"class":166,"line":174},[164,1183,211],{"class":204},[164,1185,297],{"class":177},[164,1187,1188],{"class":204}," postgres.",[164,1190,1191],{"class":181},"Connect",[164,1193,261],{"class":204},[164,1195,1153],{"class":177},[164,1197,517],{"class":204},[164,1199,1200,1202,1204,1206,1208],{"class":166,"line":185},[164,1201,394],{"class":204},[164,1203,297],{"class":177},[164,1205,1188],{"class":204},[164,1207,895],{"class":181},[164,1209,1210],{"class":204},"(db)\n",[164,1212,1213,1215,1217,1219,1221,1224],{"class":166,"line":192},[164,1214,601],{"class":204},[164,1216,297],{"class":177},[164,1218,886],{"class":204},[164,1220,889],{"class":181},[164,1222,1223],{"class":204},"(repo) ",[164,1225,1226],{"class":170},"\u002F\u002F инжектим адаптер в порт\n",[164,1228,1229,1232,1234,1237,1240],{"class":166,"line":208},[164,1230,1231],{"class":204},"    h ",[164,1233,297],{"class":177},[164,1235,1236],{"class":204}," handler.",[164,1238,1239],{"class":181},"NewOrderHandler",[164,1241,1242],{"class":204},"(svc)\n",[164,1244,1245],{"class":166,"line":226},[164,1246,1247],{"class":177},"    ...\n",[164,1249,1250],{"class":166,"line":232},[164,1251,229],{"class":204},[16,1253,1254,1255,1258,1259,1262,1263,1265],{},"Теперь зависимость ",[44,1256,1257],{},"инвертирована",": ",[27,1260,1261],{},"postgres"," зависит от ",[27,1264,606],{}," (импортирует тип Order). Сервис ничего не знает про БД. Это уже Hexagonal.",[11,1267,1269],{"id":1268},"когда-применять-layered","Когда применять Layered",[16,1271,1272],{},"Подходит:",[37,1274,1275,1278,1281,1284,1287],{},[40,1276,1277],{},"Простой CRUD-сервис без сложной бизнес-логики",[40,1279,1280],{},"Прототип, MVP — когда скорость важнее чистоты",[40,1282,1283],{},"Команда из 1-2 человек, проект на 1-3 месяца",[40,1285,1286],{},"Почти нет тестов и не планируется",[40,1288,1289],{},"Технологии не будут меняться (БД, транспорт)",[16,1291,1292],{},"Не подходит:",[37,1294,1295,1298,1301,1304,1307],{},[40,1296,1297],{},"Сложная бизнес-логика, инварианты, правила",[40,1299,1300],{},"Несколько источников данных (postgres + mongo + cache)",[40,1302,1303],{},"Высокие требования к покрытию тестами",[40,1305,1306],{},"Команда 3+ человек, проект на годы",[40,1308,1309],{},"Микросервис, который должен жить долго",[11,1311,1313],{"id":1312},"pitfalls","Pitfalls",[16,1315,1316,1319,1320,1323,1324,1327],{},[44,1317,1318],{},"Сервис как пробрасыватель."," ",[27,1321,1322],{},"service.GetOrder()"," просто вызывает ",[27,1325,1326],{},"repo.GetOrder()",". Зачем тогда слой? Либо положи логику в сервис, либо убери его.",[16,1329,1330,1333,1334,1336,1337,275,1340,220],{},[44,1331,1332],{},"God Service."," Один ",[27,1335,428],{}," на 5000 строк, который делает всё. Разбивай по use case: ",[27,1338,1339],{},"CreateOrder",[27,1341,1342],{},"CancelOrder",[16,1344,1345,1348,1349,1351],{},[44,1346,1347],{},"Анемичный домен."," Структура ",[27,1350,539],{}," с публичными полями и без методов. Логика в сервисах. Это процедурный стиль с неймспейсами.",[16,1353,1354,1357,1358,1361],{},[44,1355,1356],{},"Repository возвращает ORM-модели."," Service импортирует ",[27,1359,1360],{},"gorm.Model"," или sqlx-теги. Теперь бизнес-логика знает про ORM. Маппи модели на границе.",[16,1363,1364,1367],{},[44,1365,1366],{},"Циклические импорты."," Service хочет дёрнуть handler (для callback) → import cycle. Признак, что слои перемешаны.",[16,1369,1370,1373,1374,1377],{},[44,1371,1372],{},"Pseudo-layered."," Папки ",[27,1375,1376],{},"handler\u002Fservice\u002Frepo"," есть, но handler напрямую дёргает sql-driver. Это не архитектура, это видимость.",[11,1379,1381],{"id":1380},"сравнение-что-если-без-слоёв","Сравнение: что если без слоёв?",[16,1383,1384],{},[44,1385,1386],{},"Все в одном файле:",[20,1388,1390],{"className":158,"code":1389,"language":160,"meta":5,"style":5},"func createOrder(c echo.Context) error {\n    db.Exec(\"INSERT ...\") \u002F\u002F SQL прямо в handler\n}\n",[27,1391,1392,1415,1432],{"__ignoreMap":5},[164,1393,1394,1396,1399,1401,1403,1405,1407,1409,1411,1413],{"class":166,"line":167},[164,1395,240],{"class":177},[164,1397,1398],{"class":181}," createOrder",[164,1400,261],{"class":204},[164,1402,642],{"class":246},[164,1404,645],{"class":181},[164,1406,220],{"class":204},[164,1408,272],{"class":181},[164,1410,255],{"class":204},[164,1412,286],{"class":177},[164,1414,205],{"class":204},[164,1416,1417,1420,1422,1424,1427,1429],{"class":166,"line":174},[164,1418,1419],{"class":204},"    db.",[164,1421,303],{"class":181},[164,1423,261],{"class":204},[164,1425,1426],{"class":312},"\"INSERT ...\"",[164,1428,255],{"class":204},[164,1430,1431],{"class":170},"\u002F\u002F SQL прямо в handler\n",[164,1433,1434],{"class":166,"line":185},[164,1435,229],{"class":204},[16,1437,1438],{},"Быстро написать, нечитаемо через месяц, нетестируемо вообще. Подходит для скрипта.",[16,1440,1441],{},[44,1442,1443],{},"Слои есть, но без DI:",[20,1445,1447],{"className":158,"code":1446,"language":160,"meta":5,"style":5},"var repo = NewOrderRepo(db)\nvar svc = NewOrderService(repo)\n",[27,1448,1449,1465],{"__ignoreMap":5},[164,1450,1451,1454,1457,1460,1463],{"class":166,"line":167},[164,1452,1453],{"class":177},"var",[164,1455,1456],{"class":204}," repo ",[164,1458,1459],{"class":177},"=",[164,1461,1462],{"class":181}," NewOrderRepo",[164,1464,1210],{"class":204},[164,1466,1467,1469,1472,1474,1477],{"class":166,"line":174},[164,1468,1453],{"class":177},[164,1470,1471],{"class":204}," svc ",[164,1473,1459],{"class":177},[164,1475,1476],{"class":181}," NewOrderService",[164,1478,1479],{"class":204},"(repo)\n",[16,1481,1482],{},"Глобальные переменные → невозможно подменить в тестах. Худшее из обоих миров.",[16,1484,1485,1488],{},[44,1486,1487],{},"Hexagonal\u002FClean:","\nСервис принимает интерфейс. Тестируется без БД. Меняется адаптер без рефакторинга домена. См. следующие темы.",[11,1490,1492],{"id":1491},"чек-лист-ревью","Чек-лист ревью",[37,1494,1497,1506,1512,1518,1524,1530,1536],{"className":1495},[1496],"contains-task-list",[40,1498,1501,1505],{"className":1499},[1500],"task-list-item",[1502,1503],"input",{"disabled":188,"type":1504},"checkbox"," Зависимости направлены строго сверху вниз",[40,1507,1509,1511],{"className":1508},[1500],[1502,1510],{"disabled":188,"type":1504}," Handler не импортирует repository напрямую",[40,1513,1515,1517],{"className":1514},[1500],[1502,1516],{"disabled":188,"type":1504}," Repository не возвращает HTTP-структуры",[40,1519,1521,1523],{"className":1520},[1500],[1502,1522],{"disabled":188,"type":1504}," Внутри сервиса есть реальная логика, а не проброс",[40,1525,1527,1529],{"className":1526},[1500],[1502,1528],{"disabled":188,"type":1504}," Один use case = один публичный метод сервиса (без God Service)",[40,1531,1533,1535],{"className":1532},[1500],[1502,1534],{"disabled":188,"type":1504}," Модели БД не утекают в presentation",[40,1537,1539,1541],{"className":1538},[1500],[1502,1540],{"disabled":188,"type":1504}," Если планируется развитие — уже инвертировал зависимости (interface в service)",[11,1543,1545],{"id":1544},"вопросы-для-интервью","Вопросы для интервью",[16,1547,1548,1551],{},[44,1549,1550],{},"Q: Что такое Layered Architecture?","\nA: Подход, где код разделён на горизонтальные слои (presentation, business, data access), каждый зависит только от нижнего. Самая старая и самая распространённая архитектура.",[16,1553,1554,1557],{},[44,1555,1556],{},"Q: В чём главная проблема Layered?","\nA: Бизнес-логика зависит от инфраструктуры (БД). Поменять postgres на mongo — переписать сервисы. Тесты требуют поднятия БД. Зависимости направлены не в ту сторону.",[16,1559,1560,1563],{},[44,1561,1562],{},"Q: Чем отличается Layer от Tier?","\nA: Layer — логическое разделение кода в одном процессе. Tier — физическое (разные серверы). Можно иметь 3 layers в одном бинарнике или раскидать их по 3 серверам (3 tiers).",[16,1565,1566,1569],{},[44,1567,1568],{},"Q: Closed vs open layer?","\nA: Closed — вызов только в соседний слой ниже. Open — можно через слой. Closed строже, но генерит boilerplate. Open удобнее для cross-cutting (логирование, кэш).",[16,1571,1572,1575],{},[44,1573,1574],{},"Q: Почему в Go Layered считают антипаттерном?","\nA: В Go принят DIP: бизнес-логика определяет интерфейсы, инфраструктура их реализует. Layered нарушает это — service напрямую импортирует repository. Поэтому в Go обычно сразу делают Hexagonal\u002FClean.",[16,1577,1578,1581],{},[44,1579,1580],{},"Q: Как из Layered перейти в Hexagonal?","\nA: Объявить интерфейс в пакете сервиса (порт). Перенести реализацию repository в infrastructure-пакет (адаптер). Собрать зависимости в main: инжектить адаптер через интерфейс. Service больше не импортирует postgres.",[16,1583,1584,1587],{},[44,1585,1586],{},"Q: Когда Layered всё-таки уместен?","\nA: Простой CRUD без сложной логики, прототип, MVP, небольшая команда, короткий горизонт жизни. Если нужен быстрый результат и не планируется менять технологии.",[16,1589,1590,1593],{},[44,1591,1592],{},"Q: Что такое анемичная модель?","\nA: Структура с публичными полями и без методов. Вся логика в сервисах. Антипаттерн DDD — нарушает инкапсуляцию, превращает домен в DTO. Следствие неправильно применённого Layered.",[1595,1596],"hr",{},[11,1598,1600],{"id":1599},"практика","Практика",[1602,1603,1607,1610,1627],"quiz",{"answer":1604,"id":1605,"xp":1606},"2","arch-layered-q1","10",[16,1608,1609],{},"В чём главный риск классической Layered Architecture для долгоживущего Go-сервиса?",[1611,1612,1613],"template",{"v-slot:options":5},[37,1614,1615,1618,1621,1624],{},[40,1616,1617],{},"Handler становится слишком тонким",[40,1619,1620],{},"Бизнес-логика начинает зависеть от инфраструктуры: БД, ORM, SQL-моделей",[40,1622,1623],{},"Repository оказывается слишком легко тестировать",[40,1625,1626],{},"HTTP-слой нельзя отделить от роутера",[1611,1628,1629],{"v-slot:explanation":5},[16,1630,1631],{},"Layered часто направляет зависимости сверху вниз: service импортирует repository, а значит бизнесовый слой знает об инфраструктуре. Это усложняет тесты и замену адаптеров.",[1633,1634,1638,1641,1874],"predict",{"answer":1635,"id":1636,"xp":1637},"true\\nfalse\\nfalse","arch-layered-p1","15",[16,1639,1640],{},"Что выведет программа?",[1611,1642,1643],{"v-slot:code":5},[20,1644,1646],{"className":158,"code":1645,"language":160,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc allowed(from, to string) bool {\n    if from == \"handler\" && to == \"service\" {\n        return true\n    }\n    if from == \"service\" && to == \"handler\" {\n        return false\n    }\n    return from == \"service\" && to == \"repository\"\n}\n\nfunc main() {\n    fmt.Println(allowed(\"handler\", \"service\"))\n    fmt.Println(allowed(\"service\", \"handler\"))\n    fmt.Println(allowed(\"repository\", \"service\"))\n}\n",[27,1647,1648,1655,1659,1670,1674,1700,1726,1733,1737,1757,1764,1768,1787,1791,1795,1803,1829,1849,1870],{"__ignoreMap":5},[164,1649,1650,1652],{"class":166,"line":167},[164,1651,178],{"class":177},[164,1653,1654],{"class":181}," main\n",[164,1656,1657],{"class":166,"line":174},[164,1658,189],{"emptyLinePlaceholder":188},[164,1660,1661,1663,1665,1668],{"class":166,"line":185},[164,1662,362],{"class":177},[164,1664,365],{"class":312},[164,1666,1667],{"class":181},"fmt",[164,1669,861],{"class":312},[164,1671,1672],{"class":166,"line":192},[164,1673,189],{"emptyLinePlaceholder":188},[164,1675,1676,1678,1681,1683,1686,1688,1691,1693,1695,1698],{"class":166,"line":208},[164,1677,240],{"class":177},[164,1679,1680],{"class":181}," allowed",[164,1682,261],{"class":204},[164,1684,1685],{"class":246},"from",[164,1687,275],{"class":204},[164,1689,1690],{"class":246},"to",[164,1692,451],{"class":177},[164,1694,255],{"class":204},[164,1696,1697],{"class":177},"bool",[164,1699,205],{"class":204},[164,1701,1702,1704,1707,1710,1713,1716,1719,1721,1724],{"class":166,"line":226},[164,1703,486],{"class":177},[164,1705,1706],{"class":204}," from ",[164,1708,1709],{"class":177},"==",[164,1711,1712],{"class":312}," \"handler\"",[164,1714,1715],{"class":177}," &&",[164,1717,1718],{"class":204}," to ",[164,1720,1709],{"class":177},[164,1722,1723],{"class":312}," \"service\"",[164,1725,205],{"class":204},[164,1727,1728,1730],{"class":166,"line":232},[164,1729,503],{"class":177},[164,1731,1732],{"class":495}," true\n",[164,1734,1735],{"class":166,"line":237},[164,1736,523],{"class":204},[164,1738,1739,1741,1743,1745,1747,1749,1751,1753,1755],{"class":166,"line":291},[164,1740,486],{"class":177},[164,1742,1706],{"class":204},[164,1744,1709],{"class":177},[164,1746,1723],{"class":312},[164,1748,1715],{"class":177},[164,1750,1718],{"class":204},[164,1752,1709],{"class":177},[164,1754,1712],{"class":312},[164,1756,205],{"class":204},[164,1758,1759,1761],{"class":166,"line":309},[164,1760,503],{"class":177},[164,1762,1763],{"class":495}," false\n",[164,1765,1766],{"class":166,"line":319},[164,1767,523],{"class":204},[164,1769,1770,1772,1774,1776,1778,1780,1782,1784],{"class":166,"line":325},[164,1771,328],{"class":177},[164,1773,1706],{"class":204},[164,1775,1709],{"class":177},[164,1777,1723],{"class":312},[164,1779,1715],{"class":177},[164,1781,1718],{"class":204},[164,1783,1709],{"class":177},[164,1785,1786],{"class":312}," \"repository\"\n",[164,1788,1789],{"class":166,"line":334},[164,1790,229],{"class":204},[164,1792,1793],{"class":166,"line":520},[164,1794,189],{"emptyLinePlaceholder":188},[164,1796,1797,1799,1801],{"class":166,"line":526},[164,1798,240],{"class":177},[164,1800,1176],{"class":181},[164,1802,1179],{"class":204},[164,1804,1805,1808,1811,1813,1816,1818,1821,1823,1826],{"class":166,"line":545},[164,1806,1807],{"class":204},"    fmt.",[164,1809,1810],{"class":181},"Println",[164,1812,261],{"class":204},[164,1814,1815],{"class":181},"allowed",[164,1817,261],{"class":204},[164,1819,1820],{"class":312},"\"handler\"",[164,1822,275],{"class":204},[164,1824,1825],{"class":312},"\"service\"",[164,1827,1828],{"class":204},"))\n",[164,1830,1831,1833,1835,1837,1839,1841,1843,1845,1847],{"class":166,"line":769},[164,1832,1807],{"class":204},[164,1834,1810],{"class":181},[164,1836,261],{"class":204},[164,1838,1815],{"class":181},[164,1840,261],{"class":204},[164,1842,1825],{"class":312},[164,1844,275],{"class":204},[164,1846,1820],{"class":312},[164,1848,1828],{"class":204},[164,1850,1851,1853,1855,1857,1859,1861,1864,1866,1868],{"class":166,"line":774},[164,1852,1807],{"class":204},[164,1854,1810],{"class":181},[164,1856,261],{"class":204},[164,1858,1815],{"class":181},[164,1860,261],{"class":204},[164,1862,1863],{"class":312},"\"repository\"",[164,1865,275],{"class":204},[164,1867,1825],{"class":312},[164,1869,1828],{"class":204},[164,1871,1872],{"class":166,"line":791},[164,1873,229],{"class":204},[1611,1875,1876],{"v-slot:hint":5},[16,1877,1878],{},"В классической layered-схеме верхний слой может вызывать нижний, но нижний не должен знать про верхний.",[1880,1881,1885,1896,2038],"code-task",{"expected":1882,"id":1883,"xp":1884},"ok\\nbad\\nbad","arch-layered-ct1","20",[16,1886,1887,1888,1891,1892,1895],{},"Реализуй ",[27,1889,1890],{},"ReviewDependency",": верни ",[27,1893,1894],{},"\"ok\""," только для допустимых зависимостей handler → service и service → repository.",[1611,1897,1898],{"v-slot:template":5},[20,1899,1901],{"className":158,"code":1900,"language":160,"meta":5,"style":5},"package main\n\nimport \"fmt\"\n\nfunc ReviewDependency(from, to string) string {\n    return \"bad\"\n}\n\nfunc main() {\n    fmt.Println(ReviewDependency(\"handler\", \"service\"))\n    fmt.Println(ReviewDependency(\"repository\", \"service\"))\n    fmt.Println(ReviewDependency(\"handler\", \"repository\"))\n}\n",[27,1902,1903,1909,1913,1923,1927,1951,1958,1962,1966,1974,1994,2014,2034],{"__ignoreMap":5},[164,1904,1905,1907],{"class":166,"line":167},[164,1906,178],{"class":177},[164,1908,1654],{"class":181},[164,1910,1911],{"class":166,"line":174},[164,1912,189],{"emptyLinePlaceholder":188},[164,1914,1915,1917,1919,1921],{"class":166,"line":185},[164,1916,362],{"class":177},[164,1918,365],{"class":312},[164,1920,1667],{"class":181},[164,1922,861],{"class":312},[164,1924,1925],{"class":166,"line":192},[164,1926,189],{"emptyLinePlaceholder":188},[164,1928,1929,1931,1934,1936,1938,1940,1942,1944,1946,1949],{"class":166,"line":208},[164,1930,240],{"class":177},[164,1932,1933],{"class":181}," ReviewDependency",[164,1935,261],{"class":204},[164,1937,1685],{"class":246},[164,1939,275],{"class":204},[164,1941,1690],{"class":246},[164,1943,451],{"class":177},[164,1945,255],{"class":204},[164,1947,1948],{"class":177},"string",[164,1950,205],{"class":204},[164,1952,1953,1955],{"class":166,"line":226},[164,1954,328],{"class":177},[164,1956,1957],{"class":312}," \"bad\"\n",[164,1959,1960],{"class":166,"line":232},[164,1961,229],{"class":204},[164,1963,1964],{"class":166,"line":237},[164,1965,189],{"emptyLinePlaceholder":188},[164,1967,1968,1970,1972],{"class":166,"line":291},[164,1969,240],{"class":177},[164,1971,1176],{"class":181},[164,1973,1179],{"class":204},[164,1975,1976,1978,1980,1982,1984,1986,1988,1990,1992],{"class":166,"line":309},[164,1977,1807],{"class":204},[164,1979,1810],{"class":181},[164,1981,261],{"class":204},[164,1983,1890],{"class":181},[164,1985,261],{"class":204},[164,1987,1820],{"class":312},[164,1989,275],{"class":204},[164,1991,1825],{"class":312},[164,1993,1828],{"class":204},[164,1995,1996,1998,2000,2002,2004,2006,2008,2010,2012],{"class":166,"line":319},[164,1997,1807],{"class":204},[164,1999,1810],{"class":181},[164,2001,261],{"class":204},[164,2003,1890],{"class":181},[164,2005,261],{"class":204},[164,2007,1863],{"class":312},[164,2009,275],{"class":204},[164,2011,1825],{"class":312},[164,2013,1828],{"class":204},[164,2015,2016,2018,2020,2022,2024,2026,2028,2030,2032],{"class":166,"line":325},[164,2017,1807],{"class":204},[164,2019,1810],{"class":181},[164,2021,261],{"class":204},[164,2023,1890],{"class":181},[164,2025,261],{"class":204},[164,2027,1820],{"class":312},[164,2029,275],{"class":204},[164,2031,1863],{"class":312},[164,2033,1828],{"class":204},[164,2035,2036],{"class":166,"line":334},[164,2037,229],{"class":204},[1611,2039,2040],{"v-slot:hints":5},[37,2041,2042,2049,2055],{},[40,2043,2044,2045,2048],{},"Прямой вызов ",[27,2046,2047],{},"handler -> repository"," обходил бы бизнесовый слой",[40,2050,2051,2054],{},[27,2052,2053],{},"repository -> service"," разворачивает зависимость в неправильную сторону",[40,2056,2057,2058],{},"Остальные пары можно считать ",[27,2059,2060],{},"\"bad\"",[2062,2063,2064],"style",{},"html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}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 .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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}",{"title":5,"searchDepth":174,"depth":174,"links":2066},[2067,2068,2071,2072,2073,2074,2077,2078,2079,2080,2081,2082,2083,2084],{"id":13,"depth":174,"text":14},{"id":34,"depth":174,"text":35,"children":2069},[2070],{"id":60,"depth":185,"text":61},{"id":81,"depth":174,"text":82},{"id":124,"depth":174,"text":125},{"id":145,"depth":174,"text":146},{"id":808,"depth":174,"text":809,"children":2075},[2076],{"id":833,"depth":185,"text":834},{"id":922,"depth":174,"text":923},{"id":956,"depth":174,"text":957},{"id":1268,"depth":174,"text":1269},{"id":1312,"depth":174,"text":1313},{"id":1380,"depth":174,"text":1381},{"id":1491,"depth":174,"text":1492},{"id":1544,"depth":174,"text":1545},{"id":1599,"depth":174,"text":1600},1781022065717]