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