文章

整洁架构(Clean Architecture)在 GitLab 中的映射

用 GitLab 源码说明 Clean Architecture 的同心圆分层、依赖规则,以及 Entity、Use Case、Interface Adapter 在 Rails 模块化单体中的 pragmatic 实现。

整洁架构(Clean Architecture)在 GitLab 中的映射

参考 Robert Martin 的 Clean Architecture(同心圆 + 依赖规则)。
GitLab 并非严格分层目录,而是 Rails 惯例 + Bounded Context 的务实映射。

1. Clean Architecture 核心

1.1 同心圆(由外到内)

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────┐
│  Frameworks & Drivers                    │  ← DB、Web、Sidekiq、Gitaly
│  ┌───────────────────────────────────┐  │
│  │  Interface Adapters                │  │  ← Controller、API、Presenter
│  │  ┌─────────────────────────────┐  │  │
│  │  │  Use Cases (Application)     │  │  │  ← Service Object
│  │  │  ┌───────────────────────┐  │  │  │
│  │  │  │  Entities (Enterprise) │  │  │  │  ← Domain Model
│  │  │  └───────────────────────┘  │  │  │
│  │  └─────────────────────────────┘  │  │
│  └───────────────────────────────────┘  │
└─────────────────────────────────────────┘

1.2 依赖规则(Dependency Rule)

源码依赖只能指向内层。外层知道内层,内层不知道外层。

  • Entity 不 import Controller
  • Use Case 不 import Grape Entity
  • 框架细节(ActiveRecord、Sidekiq)在最外层或通过抽象隔离

2. GitLab 分层映射

Clean Architecture 层GitLab 实现典型路径
EntitiesActiveRecord Model + POROapp/models/release.rb、app/models/releases/source.rb
Use CasesService Objectapp/services/releases/create_service.rb
Interface Adapters(入)Controller、REST API、GraphQLlib/api/releases.rb、app/graphql/mutations/
Interface Adapters(出)Serializer、Presenter、Hook Builderlib/api/entities/、app/presenters/
Frameworks & DriversRails、PostgreSQL、Sidekiq、Gitalyconfig/、lib/gitlab/git/

GitLab 官方文档(doc/development/software_design.md)区分:

  • Domain layer:app/models、app/services、领域相关 lib/
  • Application adapters:Controller、API(不受 bounded context 强制)
  • Infrastructure:通用 lib/、未来提取为 gems/

3. Entities 层

3.1 持久化 Entity

1
2
3
4
5
6
7
8
9
# app/models/release.rb — Enterprise Business Rules
class Release < ApplicationRecord
  validates :tag, uniqueness: { scope: :project_id }
  validate :sha_unchanged, on: :update

  def upcoming_release?
    released_at.present? && released_at.to_i > Time.zone.now.to_i
  end
end

Entity 封装 不变量 和 领域行为,不依赖 HTTP 或 JSON。

3.2 PORO Entity / Value Object

1
2
3
4
5
6
7
8
9
# app/models/releases/source.rb — 无框架持久化
module Releases
  class Source
    include ActiveModel::Model
    def url
      Gitlab::Routing.url_helpers.project_archive_url(...)
    end
  end
end

3.3 Model 与 Service 的分工

Clean Architecture 中 Entities 与 Use Cases 是相邻的两层,GitLab 用 app/models/ 与 app/services/ 对应。判断标准:

 Model(Entity)Service(Use Case)
职责对象是什么、守什么规则一个完整业务动作怎么执行
依赖尽量只依赖自身字段/关联可编排多个 Model、其他 Service、Finder
副作用无跨系统编排通知、Webhook、Worker、Event、Audit
权限不含用例级权限Ability.allowed? 等前置检查
事务单对象 save 可发生在 Service 内transaction 边界在 Service

写在 Model ✓

  • 校验、不变量(validates、validate)
  • 基于自身属性的方法(upcoming_release?、name fallback)
  • 关联、scope、enum
  • PORO 值对象式计算(Releases::Source#url)

写在 Service ✓

  • #execute 编排完整用例
  • 权限与前置条件
  • 跨 Bounded Context 调用(Tags::CreateService)
  • 副作用链(通知 + webhook + Worker + audit)
  • ApplicationRecord.transaction
  • API 参数白名单(Links::Params)

写在 Finder ✓ — 复杂只读查询(ReleasesFinder)。

写在 Adapter ✓ — HTTP/JSON(lib/api/entities/、Presenter)。

Release 完整对照表与决策流程见 Release 文档 §5.5。


3.4 Client 与 Service 的分工

Clean Architecture 最外圈 Frameworks & Drivers 里,GitLab 用 Client(lib/gitlab/**/client.rb)对接 Gitaly、GitHub、K8s 等;Use Case 仍对应 app/services/。

 Client(出站基础设施)Service(Use Case)
职责怎么连、怎么发请求、怎么重试何时调、权限、编排、业务错误
位置lib/gitlab/gitaly_client/、lib/gitlab/github_import/client.rbapp/services/releases/
依赖gRPC/HTTP SDK、protobufModel、Finder、其他 Service、repository 封装
不含权限、Webhook、Audit、用例编排gRPC 请求体、channel 配置、API 限流实现

Service 通常 不直接 new GitalyClient,而调 Gitlab::Git::Repository(Interface Adapter 封装 Client)。

完整示例、Release 调用链与决策流程见 Release 文档 §5.6。


4. Use Cases 层

Use Case = 应用特定的 业务规则 + 编排。

1
2
3
4
5
6
7
8
9
10
11
# app/services/releases/create_service.rb
module Releases
  class CreateService < BaseService
    def execute
      return error(...) unless allowed?           # 应用级权限规则
      evidence_pipeline = EvidencePipelineFinder.new(...).execute
      tag = ensure_tag
      create_release(tag, evidence_pipeline)      # 编排 Entity + 外部 Service
    end
  end
end

依赖方向(符合依赖规则):

1
2
3
4
CreateService  →  Release (Entity)
              →  Tags::CreateService (其他 Use Case)
              →  EvidencePipelineFinder (Query)
              ✗  不依赖 lib/api/entities/release.rb

Controller/API 依赖 Service;Service 不依赖 API DTO。

另一完整示例:Issue / Project / Release 创建分别见 Issues::CreateService、Projects::CreateService、Releases::CreateService。详见 Issue 创建分层 · Project 创建分层 · Release 创建分层。


5. Interface Adapters 层

5.1 入站适配器(Driving Adapters)

将外部输入转为 Use Case 调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
# lib/api/releases.rb
post ':id/releases' do
  authorize_create_release!
  result = ::Releases::CreateService
    .new(user_project, current_user, declared_params(...))
    .execute

  if result[:status] == :success
    present result[:release], with: Entities::Release
  else
    render_api_error!(result[:message], result[:http_status])
  end
end
1
2
# app/graphql/mutations/releases/create.rb
result = ::Releases::CreateService.new(project, current_user, params).execute

同一 Use Case,多个 Adapter——Clean Architecture 的典型特征。

5.2 出站适配器(Driven Adapters — 展示)

Entity → 外部世界所需的格式:

1
2
3
4
5
6
7
8
9
10
11
# lib/api/entities/release.rb — 不是 Domain Entity!
module API
  module Entities
    class Release < BasicReleaseDetails
      expose :author, using: Entities::UserBasic
      expose :assets do
        expose :sorted_links, as: :links, using: Entities::Releases::Link
      end
    end
  end
end
1
2
3
4
# lib/gitlab/hook_data/release_builder.rb — Webhook DTO
def build(action)
  { object_kind: 'release', action: action, assets: { ... } }
end

5.3 Presenter(视图适配)

1
2
3
4
5
# app/presenters/releases/link_presenter.rb
def direct_asset_url
  return @subject.url unless @subject.filepath
  release.download_url(@subject.filepath)
end

6. Frameworks & Drivers 层

6.1 数据库(ActiveRecord Driver)

1
Release.save!   # Entity 使用 AR,但 Use Case 编排 save 时机

GitLab 没有 Repository 接口层;ActiveRecord 即 persistence framework。

6.2 Gitaly(外部 Git 服务 Driver)

1
2
3
4
5
6
7
8
9
# lib/gitlab/git/repository.rb — 封装 Gitaly RPC
module Gitlab
  module Git
    class Repository
      include Gitlab::Git::WrapsGitalyErrors
      # commit、find_tag、branch_names ...
    end
  end
end

Domain 通过 project.repository 访问 Git,不直接调 Gitaly protobuf—— Infrastructure 封装在 lib/gitlab/git/。

6.3 Sidekiq(异步 Driver)

1
2
3
4
# app/workers/releases/create_evidence_worker.rb
def perform(release_id, pipeline_id = nil)
  ::Releases::CreateEvidenceService.new(release, pipeline: pipeline).execute
end

Worker 是框架入口,内部仍调用 Use Case(依赖向内)。


7. 内外层依赖关系详解

依赖规则不是目录名,而是 谁 import 谁、谁调用谁。在 GitLab 里主要靠调用方向体现。

7.1 总览:四层依赖链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────┐
│ 外层 Frameworks & Drivers + Interface Adapters           │
│  lib/api/  app/controllers/  app/workers/               │
│  lib/gitlab/git/  lib/gitlab/hook_data/                 │
└───────────────────────────┬─────────────────────────────┘
                            │ 只向内调用,不被内层引用
                            ▼
┌─────────────────────────────────────────────────────────┐
│ 内层 Use Cases                                           │
│  app/services/releases/create_service.rb                │
│  app/finders/releases_finder.rb                          │
└───────────────────────────┬─────────────────────────────┘
                            │
                            ▼
┌─────────────────────────────────────────────────────────┐
│ 更内层 Entities                                          │
│  app/models/release.rb  app/models/releases/source.rb   │
└───────────────────────────┬─────────────────────────────┘
                            │ 经 Adapter 访问外部
                            ▼
┌─────────────────────────────────────────────────────────┐
│ 出站 Secondary Adapters / Infrastructure               │
│  ActiveRecord→PG  Git::Repository→Gitaly  EventStore    │
└─────────────────────────────────────────────────────────┘

7.2 写操作:依赖由外向内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ① 外层 REST Adapter
# lib/api/releases.rb
result = ::Releases::CreateService.new(user_project, current_user, params).execute

# ② 内层 Use Case — 不 import lib/api/entities/
# app/services/releases/create_service.rb
release.save!
release.execute_hooks('create')

# ③ 更内层 Entity
# app/models/release.rb
def execute_hooks(action)
  Gitlab::HookData::ReleaseBuilder.new(self).build(action)
end

识别方法:在 app/services/ 中搜索 API::Entities 或 Grape——理想情况下 不应出现。

7.3 读操作:同样向内

1
2
3
4
5
GET /releases
  → lib/api/releases.rb              (外层)
  → ReleasesFinder#execute           (内层 Query)
  → Release.for_projects(...).tagged (Entity scope)
  → Entities::Release                (出站 Adapter,最外层)

Finder 不依赖 Controller;Controller 依赖 Finder——符合依赖规则。

7.4 多入口、单核心

1
2
3
4
5
# GraphQL Adapter
Releases::CreateService.new(project, current_user, params).execute

# REST Adapter
Releases::CreateService.new(user_project, current_user, params).execute

两个外层 Adapter 依赖 同一个 内层 Use Case;Use Case 不知道 HTTP 还是 GraphQL。

7.5 出站:内层经 Adapter 访问基础设施

内层不直接绑 Gitaly protobuf,而经封装层:

1
2
3
4
Release#commit
  → project.repository
  → Gitlab::Git::Repository    # lib/gitlab/git/ — Secondary Adapter
  → Gitaly RPC                 # Infrastructure

Webhook 同理:Release → HookData::ReleaseBuilder → HTTP payload。

7.6 跨上下文:Event 作为向内契约(依赖倒置)

1
2
3
4
5
6
7
# Ci 用例发布事件 — 不依赖 MR Worker
Gitlab::EventStore.publish(
  Ci::PipelineCreatedEvent.new(data: { pipeline_id: pipeline.id })
)

# MR 上下文订阅 — 依赖 Event Schema,不依赖 CreatePipelineService
store.subscribe ::MergeRequests::UpdateHeadPipelineWorker, to: ::Ci::PipelineCreatedEvent

发布方 不知道 订阅方列表——比 perform_async(UpdateHeadPipelineWorker) 更符合 依赖倒置 + 开闭原则。

7.7 如何在代码里判断内外层

判断方式外层内层
路径lib/api/、app/controllers/、app/graphql/app/services/、app/models/
import 方向引用 Service、Finder不引用 API Entity、Controller
BC 约束应用层 不受 bounded context 强制必须在 Releases:: 等 BC 内
职责协议、序列化、鉴权入口规则、编排、不变量

7.8 GitLab 的 pragmatic 妥协

理想GitLab 现实
Entity 纯 Ruby,Repository 持久化Release < ApplicationRecord 直接 save!
Domain 零框架依赖Entity 使用 Rails validation、enum、callback
严格 Repository 接口ActiveRecord + Finder 替代

Release#execute_hooks 在 Entity 内调 Hook Builder,是 内层略向出站 Adapter 依赖 的妥协,但 API → Service → Model 的主链仍然清晰。

7.9 常见违反(仍在治理中)

违反示例GitLab 对策
Entity 堆业务Project 4000+ 行Taming Omniscient Classes 指南
Controller 写业务legacy code迁移到 Service
God WorkerPostReceiveEventStore 解耦
Use Case 直接 perform 其他域 Worker旧 CI 代码改为 publish Event

8. 设计模式在 Clean Architecture 中的体现

GoF / 企业模式在 GitLab 中 按层分布,共同支撑「依赖向内」。

8.1 按架构层归类

层常见设计模式GitLab 实例
EntityState、Strategy(enum)Release#upcoming_release?、Link#link_type enum
Use CaseCommand、Template MethodCreateService#execute、BaseService 共享逻辑
QueryQuery ObjectReleasesFinder#execute
入站 AdapterAdapterlib/api/releases.rb 适配 HTTP → Service
出站 AdapterAdapter、Builder、FacadeHookData::ReleaseBuilder、Gitlab::Git::Repository
跨上下文Observer、MediatorGitlab::EventStore pub-sub
ImportAnti-Corruption Layer、FactoryGithubImport::Representation::*、ExportStatus.for_context
展示Decorator/PresenterReleases::LinkPresenter#direct_asset_url

8.2 Command 模式 → Service Object

每个 Use Case 是一个 Command,统一入口 #execute:

1
2
3
4
5
6
7
module Releases
  class CreateService < BaseService
    def execute
      # 前置条件 + 编排 + 返回 ServiceResponse
    end
  end
end

Controller/API 只发 Command,不包含业务分支。

8.3 Template Method → BaseService

1
2
3
4
5
6
7
8
9
10
# app/services/releases/base_service.rb
class BaseService
  def milestones
    MilestonesFinder.new(...).execute  # 子类复用的算法骨架
  end

  def execute_hooks(release, action = 'create')
    release.execute_hooks(action)
  end
end

CreateService / UpdateService 共享 tag、milestone、hook 逻辑——Template Method 在 Use Case 层复用。

8.4 Adapter 模式 → Interface Adapters

入站:HTTP 参数 → Ruby Hash → Service

1
2
3
# lib/api/releases.rb
result = ::Releases::CreateService.new(user_project, current_user, declared_params(...)).execute
present result[:release], with: Entities::Release  # 出站 Adapter

出站(Git):Gitaly protobuf → Gitlab::Git::Repository 领域 API

1
2
3
4
5
6
7
8
9
# lib/gitlab/git/repository.rb
module Gitlab
  module Git
    class Repository
      include Gitlab::Git::WrapsGitalyErrors
      def commit(sha); end  # 隐藏 gRPC 细节
    end
  end
end

8.5 Builder 模式 → 出站 DTO

1
2
3
4
5
6
7
8
# lib/gitlab/hook_data/release_builder.rb
def build(action)
  {
    object_kind: 'release',
    action: action,
    assets: { links: release.links.map(&:hook_attrs), ... }
  }
end

Builder 把 Entity 转为外部系统格式,Entity 不拼 Hash 字段。

8.6 Strategy + Factory Method → Import

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/models/import/export_status.rb — Factory
def self.for_context(pipeline_tracker, relation)
  if pipeline_tracker.entity.bulk_import.offline_export?
    Import::Offline::ExportStatus.new(...)
  else
    ::BulkImports::ExportStatus.new(...)
  end
end

# 抽象基类 — Template Method + Strategy
def in_progress?
  raise Gitlab::AbstractMethodError  # 子类实现不同策略
end

同一概念(导出状态),不同导入场景 策略切换,调用方只依赖抽象接口。

8.7 Observer / Mediator → EventStore

1
2
3
4
5
# 发布(Subject)
Gitlab::EventStore.publish(Ci::PipelineCreatedEvent.new(...))

# 订阅(Observer)
store.subscribe MergeRequests::UpdateHeadPipelineWorker, to: Ci::PipelineCreatedEvent

EventStore 充当 Mediator:发布方与订阅方互不直接引用。

8.8 Facade 模式 → 聚合根 API

1
2
3
4
# app/models/project.rb(节选)
with_options to: :project_setting do
  delegate :squash_option, :squash_option=
end

Project 作为 Facade 隐藏 ProjectSetting 内部结构——官方文档也将其作为 Taming Omniscient Classes 的过渡手段。

Gitlab::Git::Repository 同样是 Facade:对上层隐藏 Gitaly 复杂性。

8.9 Presenter / Decorator → 视图适配

1
2
3
4
5
# app/presenters/releases/link_presenter.rb
def direct_asset_url
  return @subject.url unless @subject.filepath
  release.download_url(@subject.filepath)
end

在不改 Entity 的前提下,为 UI/API 增加展示逻辑。

8.10 Anti-Corruption Layer → Import Representation

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], state: issue[:state] == 'open' ? :opened : :closed }
end

外部 GitHub 模型 不直接进入 GitLab Entity,先经 ACL 翻译。

8.11 设计模式与依赖方向

模式依赖方向
Command / Service外层 Adapter → Use Case
Adapter(入站)HTTP → Core
Adapter(出站)Core → Infrastructure
EventStore发布方 ⊥ 订阅方(经 Event Schema 耦合)
ACL外部模型 → 内部 DTO → Core

9. 与 DDD / 六边形的关系

架构与 Clean Architecture 关系
DDDEntities + Use Cases 概念重叠;DDD 增加 Bounded Context
HexagonalInterface Adapters = Ports & Adapters 层
Onion几乎同构,强调 Domain 在中心

GitLab 同时运用:Clean 的分层思想 + DDD 的上下文拆分 + Hexagonal 的多 Adapter。


10. 读/写路径对照

读(Query)

1
2
3
4
5
GET /releases
  → Adapter: lib/api/releases.rb
  → Finder: ReleasesFinder          ← 可视为 Use Case 的只读变体
  → Entity: Release scope
  → Adapter: Entities::Release → JSON

写(Command)

1
2
3
4
5
6
POST /releases
  → Adapter: lib/api/releases.rb
  → Use Case: CreateService
  → Entity: Release.save!
  → Adapter: HookData::ReleaseBuilder → Webhook
  → Driver: Sidekiq Worker

11. 关键路径速查

层路径
Entityapp/models/
Use Caseapp/services/<context>/
Queryapp/finders/
REST Adapterlib/api/
GraphQL Adapterapp/graphql/
Web Adapterapp/controllers/
出站 DTOlib/api/entities/、lib/gitlab/hook_data/
Infrastructurelib/gitlab/git/、app/workers/
设计指南doc/development/software_design.md

12. 阅读建议

  1. 选一个 API endpoint,从 lib/api/ 追到 app/services/,确认 Adapter 不含业务逻辑(§7)。
  2. 区分 app/models/issue.rb(Entity)与 lib/api/entities/issue.rb(DTO Adapter)。
  3. 对照 §8 设计模式表,在源码里定位 Adapter、Builder、EventStore。
  4. 读 Release §5.5 理解 Model 与 Service 如何分工。
  5. 读 doc/development/software_design.md#design-software-around-use-cases-not-entities。
  6. 结合 Release 文档 与 六边形架构 交叉阅读。
本文由作者按照 CC BY 4.0 进行授权