文章

六边形架构(Hexagonal Architecture)在 GitLab 中的实践

用 GitLab 源码讲解六边形架构的 Port/Adapter、驱动侧与被驱动侧适配器,以及 REST、GraphQL、Gitaly、Webhook 等实例。

六边形架构(Hexagonal Architecture)在 GitLab 中的实践

六边形架构(Ports and Adapters)由 Alistair Cockburn 提出,与 Clean Architecture、洋葱架构核心思想一致:业务逻辑在中心,外部通过适配器接入。
GitLab 是 Rails 模块化单体,未显式定义 Port 接口,但结构上等价。

1. 六边形架构核心

1
2
3
4
5
6
7
8
                    ┌─────────────────────────┐
         REST API ──┤                         ├── Webhook 出站
         GraphQL  ──┤    Application Core     ├── PostgreSQL
         Web UI   ──┤  (Domain + Use Cases)   ├── Gitaly
         Sidekiq  ──┤                         ├── Redis
                    └─────────────────────────┘
                      ▲ Primary (Driving)    ▲ Secondary (Driven)
                      │ 主动调用应用          │ 应用调用外部
术语含义
Application Core领域模型 + 用例,不依赖具体技术
Port应用与外部交互的 抽象边界(GitLab 多为隐式)
Primary Adapter驱动侧:UI、API、CLI 调用应用
Secondary Adapter被驱动侧:DB、消息、外部服务

2. Application Core(应用核心)

GitLab 的核心 = Bounded Context 内的 Model + Service。

1
2
3
4
5
6
7
8
# 核心:不 import Grape、ActionController
module Releases
  class CreateService < BaseService
    def execute
      # 权限、编排、持久化、副作用
    end
  end
end
1
2
3
4
# 核心:领域规则
class Release < ApplicationRecord
  validate :sha_unchanged, on: :update
end

核心原则:app/services/releases/ 和 app/models/ 不应依赖 lib/api/ 或 app/controllers/。


3. Primary Adapters(驱动侧 / 入站)

外部世界 主动调用 GitLab 业务。

3.1 REST API Adapter

1
2
3
4
5
# lib/api/releases.rb — Grape endpoint
post ':id/releases' do
  result = ::Releases::CreateService.new(user_project, current_user, params).execute
  present result[:release], with: Entities::Release
end
职责说明
HTTP 解析declared_params
鉴权authorize_create_release!
调 CoreCreateService#execute
响应序列化Entities::Release

3.2 GraphQL Adapter

1
2
3
4
5
# app/graphql/mutations/releases/create.rb
def resolve(project_path:, **scalars)
  result = ::Releases::CreateService.new(project, current_user, params).execute
  { release: result[:release], errors: [] }
end

同一 Port(创建 Release 用例),两个 Primary Adapter——六边形架构的典型收益。

3.3 Web Controller Adapter

1
2
3
4
5
6
# app/controllers/projects/releases_controller.rb
def index
  render json: ReleaseSerializer.new.represent(
    ReleasesFinder.new(@project, current_user, params).execute
  )
end

额外处理 HTML、Atom RSS、下载 redirect——Web 特有的 Adapter 逻辑。

3.4 Sidekiq Worker 作为 Primary Adapter

Cron / 队列 驱动 业务执行:

1
2
3
4
5
6
7
8
# app/workers/releases/publish_event_worker.rb
def perform
  Release.waiting_for_publish_event.each_batch do |releases|
    Gitlab::EventStore.publish(
      Projects::ReleasePublishedEvent.new(data: { release_id: release.id })
    )
  end
end

Worker 是 框架入口 Adapter,内部调用 Core(EventStore、Service)。

3.5 EventStore Subscriber 作为 Primary Adapter

事件触发下游 Worker:

1
2
# lib/gitlab/event_store/subscriptions/merge_requests_subscriptions.rb
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent

PipelineCreatedEvent 是 Port 的消息形式;Worker 是 MR 上下文的 Adapter。


4. Secondary Adapters(被驱动侧 / 出站)

Application Core 调用 外部系统。

4.1 数据库 Adapter — ActiveRecord

1
2
3
# Use Case 内
release.save!
Release.where(project_id: project.id)

GitLab 无 Repository 接口;ActiveRecord 即 DB Adapter。Entity 继承 ApplicationRecord 是 Rails pragmatic 妥协。

4.2 Gitaly Adapter — Git 仓库

1
2
3
4
5
6
7
8
9
10
11
# lib/gitlab/git/repository.rb
module Gitlab
  module Git
    class Repository
      # 封装 Gitaly gRPC,对外暴露 commit、find_tag、branch_names
      def commit(sha)
        # Gitaly RPC call
      end
    end
  end
end

Domain 通过 project.repository 访问,不直接依赖 Gitaly protobuf——经典 Secondary Adapter 封装。

1
Release#commit  →  project.repository  →  Gitlab::Git::Repository  →  GitalyClient  →  Gitaly

最底层 Gitlab::GitalyClient::*Service 是 Client(协议/重试);Gitlab::Git::Repository 是面向用例的封装;app/services/ 编排何时调用。详见 Release §5.6 Client/Service 分工。

4.3 Webhook 出站 Adapter

1
2
3
4
5
6
7
8
9
10
11
# lib/gitlab/hook_data/release_builder.rb
class ReleaseBuilder < BaseBuilder
  def build(action)
    { object_kind: 'release', action: action, ... }
  end
end

# app/models/release.rb
def execute_hooks(action)
  project.execute_hooks(to_hook_data(action), :release_hooks)
end

Core 产生领域对象 → Builder Adapter 转为 HTTP Webhook JSON → 发送到外部系统。

4.4 通知 Adapter

1
2
# create_service.rb
NotificationService.new.async.send_new_release_notifications(release)

NotificationService 封装 Email、Slack 等通道——出站 Adapter。

4.5 外部 Import API Adapter(入站+翻译)

GitHub API 响应 → ACL → Core:

1
2
3
4
# lib/gitlab/github_import/representation/issue.rb
def self.from_api_response(issue, additional_data = {})
  { iid: issue[:number], title: issue[:title], ... }
end

Representation 层是 Inbound Adapter + Anti-Corruption Layer 的组合。

4.6 Redis / Cache Adapter

1
2
3
# app/models/import/export_status.rb
Gitlab::Cache::Import::Caching.read(cache_key)
Gitlab::Cache::Import::Caching.write(cache_key, status.to_json)

Import 状态读写 Redis——Infrastructure Adapter。


5. Port 在 GitLab 中的体现

GitLab 很少写 interface CreateReleasePort,Port 多为 隐式:

Port 类型GitLab 体现
Use Case PortCreateService#execute 方法签名
Query PortReleasesFinder#execute
Event PortGitlab::EventStore::Event + JSON Schema
Repository Port(隐式)ActiveRecord 模型 API
Git Port(显式封装)Gitlab::Git::Repository 公共方法

显式 Port 示例——Event Schema 即契约:

1
2
3
4
5
class Projects::ProjectCreatedEvent < Gitlab::EventStore::Event
  def schema
    { 'properties' => { 'project_id' => { 'type' => 'integer' } }, ... }
  end
end

订阅方只依赖 Schema,不依赖 Projects::CreateService 内部实现。


6. 依赖方向:Core 与 Adapter 的边界

六边形架构的 依赖规则 = 所有 Adapter 指向 Core,Core 不指向具体 Adapter 实现。

6.1 Primary Adapter → Core(驱动侧向内)

1
2
3
4
5
6
7
REST / GraphQL / Controller / Worker
         │
         ▼  只调用,不被回调
   CreateService#execute
         │
         ▼
      Release (Entity)
1
2
3
4
5
# Primary:lib/api/releases.rb
::Releases::CreateService.new(...).execute   # ✓ 外层 → 内层

# Core 内不应出现:
# require 'grape'  /  include API::Helpers   # ✗ 内层 → 外层

Worker 也是 Primary Adapter:Sidekiq 框架从外驱动业务,内部仍调 Service:

1
2
# app/workers/releases/create_evidence_worker.rb
::Releases::CreateEvidenceService.new(release, pipeline: pipeline).execute

6.2 Core → Secondary Adapter(被驱动侧向外)

Core 可以 依赖 Secondary Adapter 的 抽象接口(GitLab 多为具体类封装):

1
2
3
4
5
CreateService
    ├── release.save!                    → ActiveRecord Adapter → PostgreSQL
    ├── Tags::CreateService              → Git Adapter → Gitaly
    ├── HookData::ReleaseBuilder         → Webhook Adapter
    └── EventStore.publish(Event)        → Message Adapter → Sidekiq

关键:Domain 不 import Gitaly protobuf,只调 Gitlab::Git::Repository#commit。

6.3 Port 即依赖倒置点

Port谁依赖 Port谁实现 Adapter
CreateService#executeREST、GraphQL、ControllerService 本身
Event JSON Schema订阅 Worker发布方 Event 类
Gitlab::Git::Repository 公共 APIEntity、Servicelib/gitlab/git/

Event Port 实现 发布方与订阅方解耦:

1
2
3
Ci::CreatePipelineService ──publish──▶ PipelineCreatedEvent (Port 契约)
                                              ▲
MergeRequests::UpdateHeadPipelineWorker ──────┘ subscribe

CreatePipelineService 不 import UpdateHeadPipelineWorker——六边形 + 依赖倒置。

6.4 读路径的 Adapter 链

1
2
3
4
Controller (Primary)
  → ReleasesFinder (Core Query)
  → Release scope (Entity)
  → ReleaseSerializer / Entities::Release (Secondary 出站)

每一环 只依赖下一层向内,最后一环才序列化 outward。

6.5 如何验证依赖方向

在 app/services/releases/ 中检查:

  • ✗ 无 API::Entities、Grape、ActionController 引用
  • ✓ 可引用 app/models/、app/finders/、其他 BC 的 Service
  • ✓ 可引用 lib/gitlab/git/、Gitlab::EventStore(Infrastructure 封装)

7. 设计模式与 Ports/Adapters

六边形架构中,设计模式主要落在 Adapter 实现 与 Core 编排 上。

7.1 模式总表

模式在六边形中的角色GitLab 实例
AdapterPrimary / Secondary 适配器本体lib/api/releases.rb、Gitlab::Git::Repository
Port交互契约(隐式或 Schema)Service#execute、Event#schema
Facade简化 Core 对外接口Gitlab::Git::Repository、Project delegate
BuilderSecondary 出站 DTO 构建HookData::ReleaseBuilder
CommandCore 中的用例对象Releases::CreateService
Query ObjectCore 中的只读 PortReleasesFinder
Observer + Mediator跨 Adapter 通信Gitlab::EventStore
Strategy + Factory多 Secondary 实现切换Import::ExportStatus.for_context
Anti-Corruption Layer入站外部模型翻译GithubImport::Representation::*
PresenterUI 出站 DecoratorReleases::LinkPresenter
Template MethodCore 基类复用Releases::BaseService

7.2 Adapter 模式(核心)

Primary Adapter — 协议适配:

1
2
3
4
5
# HTTP world → Ruby Service world
post ':id/releases' do
  result = ::Releases::CreateService.new(user_project, current_user, declared_params(...)).execute
  present result[:release], with: Entities::Release
end

Secondary Adapter — 技术适配:

1
2
3
4
5
# Domain world → Gitaly world
# lib/gitlab/git/repository.rb
def find_tag(name)
  # gRPC call hidden inside
end

7.3 Port 作为 Observer 契约

1
2
3
# lib/gitlab/event_store/subscriptions/merge_requests_subscriptions.rb
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent
store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent

每个 *Event 是一个 Port 消息类型;Worker 是 Observer Adapter。

7.4 ACL + Adapter 组合(Import)

GitHub API 是外部 Hexagon;GitLab Import 用双层适配:

1
GitHub JSON → Representation::Issue (ACL) → Issues::CreateService (Core) → Issue AR (Secondary DB)

7.5 测试:替换 Secondary Adapter

1
2
3
# spec/services/releases/create_service_spec.rb
# 直接测 Core,mock Gitaly / Notification 等 Secondary Adapter
subject { described_class.new(project, user, params).execute }

这正是六边形架构 可测试性 的来源:Primary 换成 spec 调用,Secondary 换成 double。


8. 完整实例:创建 Release

1
2
3
4
5
6
7
8
9
10
11
[Primary]  POST lib/api/releases.rb
              │
              ▼
[Core]     Releases::CreateService#execute
              │
              ├── [Secondary] Tags::CreateService → Gitaly (tag)
              ├── [Secondary] release.save! → PostgreSQL
              ├── [Secondary] NotificationService → Email
              ├── [Secondary] HookData::ReleaseBuilder → Webhook HTTP
              ├── [Secondary] CreateEvidenceWorker → Sidekiq → PostgreSQL
              └── [Secondary] audit → Audit log
1
2
3
4
5
6
[Primary]  Sidekiq PublishEventWorker (Cron)
              │
              ▼
[Core]     Gitlab::EventStore.publish(ReleasePublishedEvent)
              │
              └── [Primary for subscriber] MergeRequests::SomeWorker ...

9. 与 Clean Architecture / 洋葱架构对比

概念六边形Clean ArchitectureGitLab
中心Application CoreEntities + Use Casesmodels + services
外层AdaptersInterface Adapters + Frameworksapi/controllers/workers/lib
依赖方向向内向内向内(理想状态)
Port显式接口用例边界Service#execute、Event schema

三者 可互换理解;GitLab 文档主要用 Bounded Context + Use Case + EventStore 表述。


10. 测试中的 Hexagonal 收益

Spec 常 绕过 Adapter 直接测 Core:

1
2
# spec/services/releases/create_service_spec.rb
subject { described_class.new(project, user, params).execute }

Mock Secondary Adapter(Gitaly、Notification)而测 Use Case——六边形架构可测试性的体现。


11. 关键路径速查

Adapter 类型路径
REST Primarylib/api/
GraphQL Primaryapp/graphql/
Web Primaryapp/controllers/
Worker Primaryapp/workers/
Event Primarylib/gitlab/event_store/subscriptions/
DB SecondaryActiveRecord / app/models/
Git Secondarylib/gitlab/git/repository.rb
Webhook Secondarylib/gitlab/hook_data/
Import ACLlib/gitlab/github_import/representation/
Application Coreapp/services/、app/models/

12. 阅读建议

  1. 画 Release 创建的 Primary / Secondary Adapter 图(§8)。
  2. 对照 §6 检查 app/services/ 的 import 方向。
  3. 对照 §7 模式表,定位 Adapter、Event Port、ACL。
  4. 对比 lib/api/entities/release.rb(出站)与 app/models/release.rb(核心)。
  5. 延伸阅读:整洁架构 §7–§8、Release 分层。
本文由作者按照 CC BY 4.0 进行授权