文章

GitLab 中的 DDD 概念与 Project 示例

基于 GitLab 代码库目录结构与官方设计文档,理解 DDD 战术模式在 Rails 模块化单体中的映射,以 Project 为例。

GitLab 中的 DDD 概念与 Project 示例

本文档基于 GitLab 代码库的目录结构、文件名及官方设计文档整理,用于理解 GitLab 如何将 DDD(领域驱动设计)战术模式映射到 Rails 模块化单体架构中。
分析日期:2026-05-29

1. 核心结论

GitLab 没有采用 textbook DDD 的分层目录(如 domain/entities/、domain/value_objects/),而是:

  1. 用 Ruby 命名空间(Bounded Context) 组织领域代码;
  2. 用 ActiveRecord Model 承载大部分持久化 Entity;
  3. 用 Service / Finder / Event 等 Rails 惯用法补位;
  4. 用 EventStore 做跨 Bounded Context 解耦。

Project 在 GitLab 中是一个典型的 容器型 Entity(omniscient class):它关联大量子资源,但 GitLab 官方设计指南明确要求——与 Project 生命周期强相关的行为 才放在 Projects:: 命名空间下;仓库、CI、Issue 等特性应归属各自 Bounded Context,而不是全部堆在 Project 类上。

config/bounded_contexts.yml 中对 Projects:: 的定义:

Managing projects as workspaces and their lifecycle. Feature specific behavior must not go here.


2. DDD 概念对照总表

DDD 概念GitLab 实现规模(约)Project 示例
EntityActiveRecord + namespaced model232 顶层 + 83 子目录Project、ProjectSetting、ProjectFeature、Projects::BranchRule
Value ObjectPORO、FixedItemsModel、app/enums/无统一 ValueObject 基类Projects::Forks::Details、visibility 枚举、ProjectImportData 中的结构化数据
Aggregate Root隐式;无显式 Aggregate 类—Project 作为 workspace 根;子 aggregate 分散(如 Repositories::、Ci::)
Domain Serviceapp/services/<context>/128 个 service 目录Projects::CreateService、Projects::TransferService、Projects::ArchiveService
Repository基本没有;直接用 AR + Finder194 个 finderProjectsFinder、GroupProjectsFinder、Projects::BranchRulesFinder
Domain Eventapp/events/ + Gitlab::EventStore51 个 event 类Projects::ProjectCreatedEvent、Projects::ProjectDeletedEvent
Factory / Builderlib/gitlab/data_builder/、import/representation/、hook_data/按场景分散Gitlab::ImportExport::Project::ObjectBuilder、Gitlab::HookData::ProjectBuilder

3. Bounded Context:战略设计基础

3.1 注册与强制

机制路径说明
Bounded Context 注册表config/bounded_contexts.yml定义允许的顶层命名空间
RuboCop 强制Gitlab/BoundedContexts新类必须落在某个 bounded context 下
设计指南doc/development/software_design.mdUbiquitous Language、God Object 治理等

3.2 Project 所在的 Context

Project 属于 Projects:: Bounded Context,但该 context 只负责 workspace 生命周期,不负责特性逻辑:

1
2
3
4
5
6
7
8
9
10
Projects::                    ← Project 生命周期(创建、转移、归档、删除)
├── CreateService
├── TransferService
├── ArchiveService
└── ProjectCreatedEvent

Repositories::                ← 仓库相关(不应放在 Projects:: 下)
Ci::                          ← CI/CD(Project 只是 tenant 引用)
Issuables:: / WorkItems::     ← Issue、MR、Work Item
MergeRequests::               ← MR 特有逻辑

设计原则:Project 是 tenant 容器;Repository、Runner、Pipeline 等是独立 Bounded Context 中的概念,通过 project_id 关联,而非嵌套在 Projects:: 命名空间下。


4. 各 DDD 概念详解(以 Project 为例)

4.1 Entity — 持久化领域对象

GitLab 实现:app/models/ 下的 ActiveRecord 类,有唯一 ID,可持久化,代表业务中的”事物”。

Project 相关 Entity 目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
app/models/
├── project.rb                          # 核心 Entity(omniscient class)
├── project_setting.rb                  # 项目设置
├── project_feature.rb                  # 功能开关(issues、wiki、snippets 等)
├── project_ci_cd_setting.rb            # CI/CD 设置
├── project_repository.rb               # 仓库元数据(非 Git 对象本身)
├── project_group_link.rb               # 与 Group 的共享关系
├── project_authorization.rb            # 授权记录
├── project_import_state.rb             # 导入状态
├── project_export_job.rb               # 导出任务
├── project_deploy_token.rb             # Deploy token 关联
├── project_pages_metadatum.rb          # Pages 元数据
├── project_auto_devops.rb              # Auto DevOps 配置
├── project_daily_statistic.rb          # 日统计
├── project_label.rb                    # 项目 Label
├── project_snippet.rb                  # Snippet
├── project_wiki.rb                     # Wiki
├── project_team.rb                     # 团队成员(非 AR,见下文)
└── projects/                           # namespaced 子 Entity
    ├── branch_rule.rb
    ├── branch_rules.rb
    ├── project_topic.rb
    ├── sync_event.rb
    ├── repository_storage_move.rb
    ├── data_transfer.rb
    ├── build_artifacts_size_refresh.rb
    └── import_export/
        ├── relation_export.rb
        └── relation_import_tracker.rb

Schema 文档(Entity 的数据契约):

1
2
3
4
5
6
7
db/docs/
├── projects.yml
├── project_settings.yml
├── project_features.yml
├── project_ci_cd_settings.yml
├── project_repositories.yml
└── ...

Concern 复用领域行为:

1
2
3
4
5
6
7
8
9
app/models/concerns/
├── projects/
│   ├── custom_branch_rule.rb
│   ├── squash_option.rb
│   └── target_projects.rb
├── update_project_statistics.rb
├── cascading_project_setting_attribute.rb
├── project_features_compatibility.rb
└── select_for_project_authorization.rb

EE 扩展(同一 Entity 的分层扩展):

1
2
3
4
5
6
ee/app/models/
├── ee/project.rb                       # prepend 到 CE Project
└── projects/
    ├── all_protected_branches_rule.rb
    ├── branch_rules/
    └── compliance_standards/

要点:

  • Project 是历史遗留的顶层 omniscient class(>1000 LOC),官方指南建议新行为放入 dedicated class,而非继续往 Project 上堆方法。
  • 子 Entity 如 ProjectSetting、ProjectFeature 通过外键关联,分担 Project 的数据与职责。
  • Projects::BranchRule 等 namespaced model 代表较新的代码风格。

4.2 Value Object — 无独立身份的值对象

GitLab 实现:没有统一的 ValueObject 基类,而是通过以下形式表达:

形式路径模式Project 示例
PORO(Plain Old Ruby Object)app/models/<context>/ 或 lib/gitlab/<context>/Projects::Forks::Details
FixedItemsModelgems/activerecord-gitlab/静态配置型对象(Project 本身不用,Work Item 类型常用)
Enumapp/enums/、app/models/concerns/enums/visibility level、access level 等
结构化嵌入字段Entity 内的 JSON/结构化属性ProjectImportData

典型示例:Projects::Forks::Details

1
app/models/projects/forks/details.rb
  • 无独立数据库表;
  • 封装 fork 与 source project 的分叉计算逻辑(ahead/behind counts);
  • 通过 initialize(project, ref) 构造,生命周期绑定于一次计算;
  • 符合 Value Object 特征:无 ID、不可变语义、描述一个计算结果。

Enum 示例:

Project 的 visibility、各 feature 的 access level 等,通常以 Rails enum 或 concern 中的常量定义,而非独立 Value Object 类。GitLab 的 app/enums/ 目录目前文件较少(主要在 EE 的 Vulnerabilities::、Security:: 下),大量 enum 仍分散在 model concern 中。

官方文档中的 Value Object 范例(非 Project,但说明模式):

  • Ci::Minutes::Usage — 计算用量
  • DesignManagement::DesignAtVersion — 组合 design + version

Project 领域可类比:Projects::Forks::Details 是对 fork 关系的值对象式封装。

Project 相关的 enum 与常量取值是 Value Object 最集中的区域,详见 第 11 章。


4.3 Aggregate Root — 聚合根(隐式)

GitLab 实现:没有显式的 Aggregate 类或 Aggregate Root 基类。聚合边界通过以下方式隐式表达:

机制Project 场景
核心 Entity 作为根Project 是 workspace 聚合根,关联多个子 Entity
子 Entity 外键project_settings.project_id、project_features.project_id 等
Domain Service 协调Projects::CreateService 在一个事务中创建 Project + 关联对象
Domain Event 通知外部Projects::ProjectCreatedEvent 发布后,其他 context 各自响应
禁止跨 aggregate 直接修改通过 Service + Event 而非直接调用其他 context 的内部逻辑

Project 聚合结构(简化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Project (Aggregate Root — workspace 层面)
├── ProjectSetting
├── ProjectFeature
├── ProjectCiCdSetting
├── ProjectRepository (元数据)
├── ProjectGroupLink
├── ProjectAuthorization
└── ... (lifecycle 相关的直接子对象)

NOT in Project aggregate (独立 Bounded Context):
├── Repository (Git 对象)     → Repositories::
├── Ci::Pipeline              → Ci::
├── Issue / MergeRequest      → Issuables:: / MergeRequests::
├── Pages::Domain             → Pages::
└── Packages                  → Packages::

对比 Work Item(GitLab 中较清晰的 aggregate 示例):

1
2
3
4
WorkItem (隐式 Aggregate Root)
├── WorkItems::Type
├── WorkItems::Widgets::*     (Description, Assignees, Labels, ...)
└── WorkItems::Transition

Project 的 aggregate 边界更模糊,因为它是 tenant 容器 而非单一业务概念。GitLab 通过 Bounded Context 拆分来避免 Project aggregate 无限膨胀。

Project + ProjectSetting 是 workspace 聚合中最清晰的父子关系示例,详见 第 12 章。

跨 Aggregate 协作:EventStore

1
2
3
4
5
Projects::CreateService
    └── publish Projects::ProjectCreatedEvent
            ├── → Onboarding workers 订阅
            ├── → Analytics workers 订阅
            └── → 其他 context 的 Sidekiq worker 订阅

4.4 Domain Service — 领域服务

GitLab 实现:app/services/<bounded_context>/ 下的 Service 类,封装单个用例(use case)的业务逻辑。

Project 相关 Domain Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
app/services/projects/
├── create_service.rb                   # 创建项目
├── update_service.rb                   # 更新项目
├── destroy_service.rb                  # 删除项目
├── archive_service.rb                  # 归档
├── unarchive_service.rb                # 取消归档
├── transfer_service.rb                 # 转移到其他 namespace
├── fork_service.rb                     # Fork
├── import_service.rb                   # 导入
├── restore_service.rb                  # 恢复
├── mark_for_deletion_service.rb        # 标记待删除
├── cleanup_service.rb                  # 清理
├── update_statistics_service.rb        # 更新统计
├── protect_default_branch_service.rb   # 保护默认分支
├── git_deduplication_service.rb        # Git 去重
├── update_repository_storage_service.rb  # 更新仓库存储
├── schedule_bulk_repository_shard_moves_service.rb
├── after_rename_service.rb             # 重命名后处理
├── base_move_relations_service.rb      # 移动关联数据基类
├── move_project_members_service.rb     # 移动成员
├── move_forks_service.rb               # 移动 forks
├── move_access_service.rb              # 移动访问权限
├── ... (约 60+ 个 service 文件)
└── 子目录/
    ├── alert_management/
    ├── auto_devops/
    ├── branch_rules/
    ├── container_repository/
    ├── deploy_tokens/
    ├── forks/
    ├── group_links/
    ├── hashed_storage/
    ├── import_export/
    ├── lfs_pointers/
    ├── operations/
    └── prometheus/

命名规范(Ubiquitous Language):

1
2
3
4
5
6
7
# Good — 使用产品语言
Projects::CreateService
Projects::TransferService
Projects::ArchiveService

# Bad — CRUD 术语泄漏(官方文档反例)
EpicIssues::CreateService   # 应为 Epic::AddExistingIssueService

Service 职责:

  1. 接收参数,执行业务规则;
  2. 协调多个 Entity 的创建/更新(在一个 transaction 内);
  3. 发布 Domain Event;
  4. 返回 ServiceResponse 结果。

示例流程(CreateService → Event):

1
2
3
4
5
6
Projects::CreateService#execute
    ├── 创建 Project AR 记录
    ├── 创建关联 ProjectSetting、ProjectFeature 等
    ├── 初始化 Repository
    └── publish Projects::ProjectCreatedEvent
            └── Gitlab::EventStore.publish(event)

4.5 Repository — 仓储(Query Object 替代)

GitLab 实现:基本没有 classic Repository 模式。数据访问通过:

替代方案路径Project 示例
ActiveRecord 直接查询Model 类方法 / scopeProject.find(id)、Project.active
Finder(Query Object)app/finders/ProjectsFinder、GroupProjectsFinder
专用 Providerlib/gitlab/Gitlab::CycleAnalytics::GroupProjectsProvider

Project 相关 Finder(约 30+ 个):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
app/finders/
├── projects_finder.rb                          # 主 Finder:按多种条件过滤 Project
├── contributed_projects_finder.rb
├── fork_projects_finder.rb
├── group_projects_finder.rb
├── personal_projects_finder.rb
├── starred_projects_finder.rb
├── users_star_projects_finder.rb
├── merge_request_target_project_finder.rb
├── autocomplete/project_finder.rb
└── projects/
    ├── branch_rules_finder.rb
    ├── daily_statistics_finder.rb
    ├── export_job_finder.rb
    ├── group_group_links_finder.rb
    ├── groups_finder.rb
    ├── project_group_links_finder.rb
    ├── topics_finder.rb
    ├── members/
    │   ├── effective_access_level_finder.rb
    │   └── effective_access_level_per_user_finder.rb
    └── ml/
        ├── candidate_finder.rb
        ├── experiment_finder.rb
        ├── model_finder.rb
        └── model_version_finder.rb

Finder vs Repository 的区别:

  • Repository(经典 DDD):封装 aggregate 的持久化,提供 find、save、delete 等语义化接口;
  • Finder(GitLab):封装复杂查询逻辑,返回 ActiveRecord::Relation,只读为主。

GitLab 选择 Finder + ActiveRecord 而非 Repository,是因为 Rails 生态中 ActiveRecord 已经提供了足够的 persistence abstraction,再加一层 Repository 会被视为 over-engineering。


4.6 Domain Event — 领域事件

GitLab 实现:app/events/<namespace>/ + Gitlab::EventStore 发布-订阅系统。

Project 相关 Domain Event:

1
2
3
4
5
6
7
8
9
10
11
12
13
app/events/projects/
├── project_created_event.rb
├── project_deleted_event.rb
├── project_archived_event.rb
├── project_transfered_event.rb
├── project_path_changed_event.rb
├── project_visibility_changed_event.rb
├── project_features_changed_event.rb
└── release_published_event.rb

ee/app/events/projects/                         # EE 扩展
├── compliance_framework_changed_event.rb
└── security_attribute_changed_event.rb

Event 基础设施:

1
2
3
4
5
6
lib/gitlab/event_store/
├── event.rb            # 基类 Gitlab::EventStore::Event
├── store.rb            # 发布/订阅
├── subscriber.rb
├── subscription.rb
└── cloud_event.rb

命名规范:

1
2
3
4
5
6
<DomainObject><PastTenseAction>Event

Projects::ProjectCreatedEvent       ✓
Projects::ProjectDeletedEvent       ✓
Projects::AddProjectEvent           ✗ (应为 CreatedEvent)
Project::MergeRequestCreatedEvent   ✗ (scope 不对,MR 不属于 Projects context)

Event Schema 示例(Projects::ProjectCreatedEvent):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module Projects
  class ProjectCreatedEvent < ::Gitlab::EventStore::Event
    def schema
      {
        'type' => 'object',
        'properties' => {
          'project_id' => { 'type' => 'integer' },
          'namespace_id' => { 'type' => 'integer' },
          'root_namespace_id' => { 'type' => 'integer' }
        },
        'required' => %w[project_id namespace_id root_namespace_id]
      }
    end
  end
end

发布方(Producer):

1
2
app/services/projects/create_service.rb
    └── Gitlab::EventStore.publish(Projects::ProjectCreatedEvent.new(...))

订阅方(Consumer):

各 Bounded Context 通过 Sidekiq worker 订阅 event,在 config/event_store_subscriptions.yml 或 initializer 中注册。订阅方与发布方解耦,符合 Open-Closed Principle。

设计原则(来自 doc/development/event_store.md):

原则好坏
SemanticProjectCreatedEventNotifyAdminEvent
SpecificProjectVisibilityChangedEventProjectChangedEvent
ScopedProjects::ProjectCreatedEventNamespace::ProjectCreatedEvent

4.7 Factory / Builder — 工厂与构建器

GitLab 实现:按场景分散,没有统一的 Factory 基类。

Project 相关的 Builder / Factory:

类型路径用途
Import ObjectBuilderlib/gitlab/import_export/project/object_builder.rb导入时 find-or-create 关联对象
Hook Data Builderlib/gitlab/hook_data/project_builder.rb构建 webhook payload
Hook Member Builderlib/gitlab/hook_data/project_member_builder.rb构建成员变更 webhook payload
Data Builderlib/gitlab/data_builder/repository.rb构建系统事件 payload
Import Representationlib/gitlab/github_import/representation/外部数据 → 内部对象的 DTO
Project Transferlib/gitlab/project_transfer.rb项目转移逻辑协调
Legacy Creatorlib/gitlab/legacy_github_import/project_creator.rbGitHub 导入创建 Project

Import ObjectBuilder 示例:

1
2
3
4
5
6
lib/gitlab/import_export/
├── base/object_builder.rb              # 基类
├── project/object_builder.rb           # Project 导入时的 find-or-create
└── group/object_builder.rb

ee/lib/ee/gitlab/import_export/project/object_builder.rb   # EE 扩展

职责:给定 class + attributes,在 group/project 层级 find 或 create 对象(如 Label、Milestone)。

Hook Data Builder 示例:

1
2
3
4
lib/gitlab/hook_data/
├── base_builder.rb
├── project_builder.rb                  # 构建 project_rename 等 webhook payload
└── project_member_builder.rb

职责:将 Project Entity 转换为 webhook 消费的 Hash 结构(读模型 / DTO)。

Data Builder 示例:

1
2
3
4
5
lib/gitlab/data_builder/
├── repository.rb                       # 构建 push 等系统事件的 payload
├── push.rb
├── pipeline.rb
└── ...

与 DDD Factory 的区别:

  • Factory(经典 DDD):创建 complex aggregate,保证 invariant;
  • GitLab Builder:更多是 DTO 转换(Entity → Hash/JSON)或 导入场景的 find-or-create,而非 aggregate 工厂。

Project 的创建 invariant 保障主要在 Projects::CreateService 中,而非 Factory 类。


5. 支撑层(非 Domain Object,但密切相关)

以下层不参与领域建模,但围绕 Domain Object 运作:

层路径Project 示例角色
Policyapp/policies/project_policy.rb、projects/授权决策
Presenterapp/presenters/project_presenter.rb视图层展示逻辑
Serializerapp/serializers/project 相关 serializerJSON 响应
API Entitylib/api/entities/project.rb、project_detail.rbREST API DTO(不是 DDD Entity)
GraphQL Typeapp/graphql/types/project_type.rbGraphQL 响应
Workerapp/workers/project 相关 async job异步副作用
Controllerapp/controllers/projects/按 scope 组织(非 bounded context)HTTP 入口

6. 架构关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─────────────────────────────────────────────────────────────────┐
│                    Application Layer (无 BC 强制)                  │
│  app/controllers/projects/   lib/api/   app/graphql/            │
└───────────────────────────────┬─────────────────────────────────┘
                                │ 调用
┌───────────────────────────────▼─────────────────────────────────┐
│                      Domain Layer (Bounded Context)                │
│                                                                     │
│  ┌────────────── Projects:: ──────────────────────────────────┐    │
│  │  Entity: Project, ProjectSetting, ProjectFeature, ...      │    │
│  │  Service: CreateService, TransferService, ArchiveService   │    │
│  │  Event:   ProjectCreatedEvent, ProjectDeletedEvent, ...    │    │
│  │  Finder:  ProjectsFinder, GroupProjectsFinder              │    │
│  │  PORO:    Projects::Forks::Details                          │    │
│  └────────────────────────────────────────────────────────────┘    │
│                                                                     │
│  ┌────────────── Repositories:: ──────────────────────────────┐    │
│  │  lib/gitlab/git/commit.rb, blob.rb, branch.rb              │    │
│  │  (Git 领域对象,通过 project.repository 访问)                │    │
│  └────────────────────────────────────────────────────────────┘    │
│                                                                     │
│  ┌────────────── Ci:: / MergeRequests:: / WorkItems:: / ... ──┐    │
│  │  (各自独立,通过 project_id 关联 Project)                    │    │
│  └────────────────────────────────────────────────────────────┘    │
│                                                                     │
│  EventStore: Gitlab::EventStore.publish / subscribe               │
└───────────────────────────────┬─────────────────────────────────┘
                                │ 持久化
┌───────────────────────────────▼─────────────────────────────────┐
│  PostgreSQL (db/docs/projects.yml, project_settings.yml, ...)   │
│  Gitaly (Git 对象存储)                                             │
└─────────────────────────────────────────────────────────────────┘

7. Project 生命周期中的 DDD 协作示例

以 创建 Project 为例,各 DDD 概念如何协作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. [Application Layer]
   POST /projects → ProjectsController#create

2. [Domain Service]
   Projects::CreateService#execute
   ├── 校验参数、权限
   ├── 创建 Project (Entity)
   ├── 创建 ProjectSetting, ProjectFeature 等 (子 Entity)
   ├── 初始化 Repository (Repositories:: context)
   └── publish Projects::ProjectCreatedEvent (Domain Event)

3. [Domain Event → 异步订阅]
   Gitlab::EventStore.publish(event)
   ├── Namespaces::Onboarding::Worker 订阅 → 更新 onboarding 状态
   ├── Analytics::Worker 订阅 → 记录 metrics
   └── ... 其他 context 各自响应

4. [Factory/Builder → 后续场景]
   ├── Webhook 触发时: Gitlab::HookData::ProjectBuilder 构建 payload
   └── 导入场景: Gitlab::ImportExport::Project::ObjectBuilder find-or-create 关联对象

5. [Finder → 查询场景]
   ProjectsFinder.new(current_user, params).execute → 返回 Project 列表

8. 关键参考路径速查

类别路径
Bounded Context 注册config/bounded_contexts.yml
设计指南doc/development/software_design.md
EventStore 指南doc/development/event_store.md
FixedItemsModel 指南doc/development/fixed_items_model.md
抽象复用指南doc/development/reusing_abstractions.md
Project Entityapp/models/project.rb
Project Servicesapp/services/projects/
Project Eventsapp/events/projects/
Project Findersapp/finders/projects_finder.rb、app/finders/projects/
Project Builderslib/gitlab/hook_data/project_builder.rb
Import Factorylib/gitlab/import_export/project/object_builder.rb
EventStore 基础设施lib/gitlab/event_store/
Schema 文档db/docs/projects.yml

9. 与 Textbook DDD 的主要差异

Textbook DDDGitLab 实际
独立的 domain/ 层分散在 app/models/、app/services/、lib/gitlab/
显式 Aggregate Root 类隐式,通过 Entity 关联 + Service 协调
Repository 接口ActiveRecord + Finder
ValueObject 基类PORO / Enum / FixedItemsModel,无统一基类
Domain Event 框架自研 Gitlab::EventStore(Sidekiq 之上)
Factory 创建 AggregateService 创建 + Builder 做 DTO 转换
Ubiquitous Language强制(RuboCop + 代码审查)
Bounded Context 隔离命名空间 + EventStore,仍在同一 monolith

10. 阅读建议

  1. 从 Bounded Context 入手:先理解 config/bounded_contexts.yml 中 Projects:: 的边界,再看 Project 相关代码。
  2. 跟踪一个用例:从 Projects::CreateService 开始,跟踪 Entity 创建 → Event 发布 → Worker 订阅的完整链路。
  3. 区分 Entity 与 DTO:lib/api/entities/project.rb 是 API 响应 DTO,不是 domain entity;app/models/project.rb 才是。
  4. 注意 God Object 治理:阅读 doc/development/software_design.md#taming-omniscient-classes,理解为何新代码不应继续膨胀 Project 类。
  5. 对比 Work Items:app/models/work_items/ 是较新的、更清晰的 aggregate 设计,可作为与 Project 的对比阅读。
  6. Project 的 enum 与 VO:阅读 第 11 章,理解 Gitlab::VisibilityLevel 与 Featurable 常量模式。
  7. Project 聚合边界:阅读 第 12 章,理解 ProjectSetting 如何作为聚合内实体挂载在 Project 上。

11. Project 相关 Enum 与 Value Object 专章

GitLab 没有统一的 ValueObject 基类。Project 领域的「enum」也不集中在 app/enums/(CE 中该目录几乎为空),而是分散在 模块常量、Rails enum、GraphQL Enum、State machine 四种形式中。

11.1 四种实现形式概览

实现方式典型路径Value Object 特征
模块常量lib/gitlab/visibility_level.rb、app/models/concerns/featurable.rb闭集、按值比较、封装领域行为
Rails enumproject.rb 及关联 model 的 enum 列闭集、持久化为整数、生成谓词方法
GraphQL Enumapp/graphql/types/*_enum.rb对外暴露同一套闭集取值
State machine 状态project_import_state.rb 等有限状态 + 转换规则

11.2 项目可见性:Visibility Level

不是 Project 上的 Rails enum,而是 整数列 + 模块常量。

1
2
3
lib/gitlab/visibility_level.rb              # Gitlab::VisibilityLevel
app/models/project.rb                       # include Gitlab::VisibilityLevel
app/graphql/types/visibility_levels_enum.rb
整数值常量字符串
0PRIVATEprivate
10INTERNALinternal
20PUBLICpublic

如何体现 Value Object:

  • 无独立 ID:projects.visibility_level 只是整数,语义由 Gitlab::VisibilityLevel 定义。
  • 闭集:仅三种合法值;valid_level?、allowed_for? 封装校验。
  • 领域行为:private? / internal? / public?、level_name、closest_allowed_level。
  • Ubiquitous Language:与产品中的 Private / Internal / Public 一致。

这是 GitLab 最典型的 Value Object 模式:值 + 规则 + 行为挂在 module 上,Entity 只存整数。

11.3 功能开关级别:Feature Access Level

同样不是 Rails enum,而是 ProjectFeature 上多个 *_access_level 整数列 + Featurable concern 常量。

1
2
3
app/models/concerns/featurable.rb
app/models/project_feature.rb
app/graphql/types/project_feature_access_level_enum.rb
整数值常量含义
0DISABLED对所有人关闭
10PRIVATE仅团队成员可用
20ENABLED能访问项目的人可用
30PUBLIC对所有人开放(主要用于 Pages)

约 20 个 feature 字段复用同一套取值:issues_access_level、merge_requests_access_level、repository_access_level、pages_access_level、container_registry_access_level 等(完整列表见 ProjectFeature::FEATURES)。

如何体现 Value Object:

  • 同一 FeatureAccessLevel 值对象在多个 Entity 属性上复用。
  • PAGES_ACCESS_LEVELS_BY_PROJECT_VISIBILITY 将 Visibility Level 与 Feature Access Level 两个值对象组合成业务规则。
  • GraphQL 的 Types::ProjectFeatureAccessLevelEnum 直接映射 ProjectFeature::DISABLED 等常量。

11.4 成员角色:Gitlab::Access(Project 权限基础)

1
lib/gitlab/access.rb
整数值常量角色
10GUESTGuest
20REPORTERReporter
30DEVELOPERDeveloper
40MAINTAINERMaintainer
50OWNEROwner

Project 成员(ProjectMember)、授权(ProjectAuthorization)、Feature 可见性(ProjectFeature.required_minimum_access_level)都引用这套值对象,而非各自定义一套。

11.5 Project 关联 Model 上的 Rails enum

Project 本体

1
2
# app/models/project.rb
enum :auto_cancel_pending_pipelines, { disabled: 0, enabled: 1 }

ProjectSetting

1
2
3
4
# app/models/project_setting.rb
enum :reviewer_assignment_strategy, { disabled: 0, code_owners: 1 }
# via Projects::SquashOption concern:
enum :squash_option, { never: 0, always: 1, default_on: 2, default_off: 3 }
字段取值产品语义(squash)
reviewer_assignment_strategydisabled, code_owners—
squash_optionnever, always, default_on, default_off不允许 / 强制 / 鼓励 / 允许

ProjectAutoDevops

1
enum :deploy_strategy, { continuous: 0, manual: 1, timed_incremental: 2 }

ProjectRepository

1
enum :object_format, { sha1: 0, sha256: 1 }

Git 对象哈希算法,属于仓库领域的值选择。

ProjectCiCdSetting

1
2
3
4
5
6
enum :pipeline_variables_minimum_override_role, {
  no_one_allowed: 1, developer: 2, maintainer: 3, owner: 4
}
enum :resource_group_default_process_mode, {
  unordered: 0, oldest_first: 1, newest_first: 2, newest_ready_first: 3
}  # 引用 Ci::ResourceGroup::RESOURCE_GROUP_PROCESS_MODES

Projects 命名空间下的 enum

1
2
3
4
5
# app/models/projects/ci_feature_usage.rb
enum :feature, { code_coverage: 1, security_report: 2 }

# app/models/projects/import_export/relation_import_tracker.rb
enum :relation, { issues: 0, merge_requests: 1, ci_pipelines: 2, milestones: 3 }

EE 扩展

1
2
# ee/app/models/ee/project_ci_cd_setting.rb
enum :restrict_pipeline_cancellation_role, { developer: 0, maintainer: 1, no_one: 2 }

11.6 相关但不在 Project Model 上的 enum

位置说明
Users::ProjectCallout#feature_nameUI 横幅 dismiss 状态,偏 presentation
LfsObjectsProject#repository_typeproject / wiki / design
Types::ProjectSortEnumGraphQL 项目列表排序
Types::NamespaceProjectSortEnumGraphQL namespace 下项目排序
Types::ProjectMemberRelationEnumGraphQL 成员关系筛选
Types::Organizations::GroupsProjectsDisplayEnum组织视图展示模式

11.7 State Machine 状态(类 enum,强调生命周期)

Model路径状态示例
ProjectImportStateapp/models/project_import_state.rbnone → scheduled → started → finished / failed / canceled
Projects::ImportExport::RelationImportTrackerapp/models/projects/import_export/relation_import_tracker.rbcreated / started / finished / failed
ProjectExportJobapp/models/project_export_job.rbqueued → …

状态值同样是闭集,但额外封装了 状态转换规则(比单纯 enum 更接近状态 Value Object + 领域规则)。

11.8 DDD Value Object 特征对照

DDD 特征GitLab 中的体现Project 示例
按值相等,无独立身份存为整数,不靠 id 区分语义visibility_level = 20 即 Public
闭集常量 Hash 或 Rails enumdeploy_strategy 三选一
不可变(理想)模块常量为 frozen;AR 字段可 update改 visibility 是替换整列值
封装校验与行为module / concern 类方法VisibilityLevel.allowed_for?(user, level)
Ubiquitous Language命名与产品一致default_on → UI「Encourage squash」
与 Entity 分离Entity 存整数,语义在 moduleProject#visibility_level + Gitlab::VisibilityLevel

两种实现层次:

1
2
3
4
5
6
7
┌─────────────────────────────────────────────────────────┐
│  Entity: Project / ProjectFeature / ProjectSetting       │
│  (有 id,有生命周期)                                    │
│    ├── visibility_level: Integer  ──→ Gitlab::VisibilityLevel
│    ├── issues_access_level: Integer ──→ Featurable
│    └── auto_cancel_pending_pipelines: enum(VO 内嵌在 AR)
└─────────────────────────────────────────────────────────┘
  • Visibility / Feature Access / Access Level:VO 语义在 module/concern,Entity 只存整数 → 最接近 textbook VO。
  • Rails enum 字段:VO 语义 内嵌在 ActiveRecord enum → 简单闭集,行为较少。
  • GraphQL Enum:对外 VO 视图,映射领域常量。

11.9 与 Textbook Value Object 的差异

  1. 大多不可单独实例化:没有 VisibilityLevel.new(:public),而是整数 + 模块方法。
  2. 可变性:Entity 上的 enum 字段会随 update 改变;严格 immutable VO 在这里是 pragmatic 妥协。
  3. 行为分散:校验在 model validation,转换在 VisibilityLevel.level_value,查询在 scope。
  4. app/enums/ 不是主路径:Project 领域几乎不用 Sorbet 风格 *Enum 类;更常见的是 concern 常量 + Rails enum。

11.10 Project 相关 Enum 速查表

领域概念实现类型
项目可见性Gitlab::VisibilityLevel模块常量 VO
功能开关级别Featurable::{DISABLED,PRIVATE,ENABLED,PUBLIC}模块常量 VO
成员角色Gitlab::Access::{GUEST..OWNER}模块常量 VO
自动取消 PipelineProject#auto_cancel_pending_pipelinesRails enum
Squash 策略ProjectSetting#squash_optionRails enum
Reviewer 分配策略ProjectSetting#reviewer_assignment_strategyRails enum
Auto DevOps 部署策略ProjectAutoDevops#deploy_strategyRails enum
Git 对象格式ProjectRepository#object_formatRails enum
Pipeline 变量覆盖角色ProjectCiCdSetting#pipeline_variables_minimum_override_roleRails enum
Resource Group 模式ProjectCiCdSetting#resource_group_default_process_modeRails enum
导入关系类型RelationImportTracker#relationRails enum
CI 功能使用类型Projects::CiFeatureUsage#featureRails enum
Pipeline 取消限制角色 (EE)ProjectCiCdSetting#restrict_pipeline_cancellation_roleRails enum
API 可见性Types::VisibilityLevelsEnumGraphQL VO
API 功能级别Types::ProjectFeatureAccessLevelEnumGraphQL VO

11.11 关键路径

类别路径
可见性 VOlib/gitlab/visibility_level.rb
功能级别 VOapp/models/concerns/featurable.rb
成员角色 VOlib/gitlab/access.rb
Feature Entityapp/models/project_feature.rb
Squash enumapp/models/concerns/projects/squash_option.rb
GraphQL 可见性app/graphql/types/visibility_levels_enum.rb
GraphQL 功能级别app/graphql/types/project_feature_access_level_enum.rb

12. Project 聚合示例:Project 与 ProjectSetting

GitLab 没有显式声明 AggregateRoot 类,但 Project + ProjectSetting 的组合在代码与 schema 上呈现出清晰的 聚合(Aggregate) 特征:Project 是聚合根,ProjectSetting 是聚合内实体,生命周期与身份都依附于 Project。

注意:GitLab 官方文档称 Project 为 omniscient class(上帝对象),整个 monolith 里它关联的内容远多于一个 strict DDD aggregate 应包含的范围。本章只讨论 workspace 设置子域 中 ProjectSetting 与 Project 的关系,而非把 Issue、Pipeline 等也纳入同一聚合。

12.1 聚合边界(务实视角)

在 Projects:: Bounded Context 内,可以把 Project workspace 设置 理解为一个松散聚合:

1
2
3
4
5
6
7
8
9
10
11
12
13
Project (Aggregate Root — workspace 身份与生命周期)
├── ProjectSetting          ← 项目级配置(本章重点)
├── ProjectFeature          ← 功能开关(issues/wiki/builds 等)
├── ProjectCiCdSetting      ← CI/CD 配置
├── ProjectRepository       ← 仓库元数据(非 Git 对象)
├── ProjectAutoDevops       ← Auto DevOps 配置
└── …(其他 1:1 设置型 Entity)

不在此聚合内(独立 Bounded Context / 独立生命周期):
├── Issue / MergeRequest / WorkItem
├── Ci::Pipeline
├── Repository (Git 对象,经 Gitaly)
└── Packages / Deployments / …

ProjectSetting 与 ProjectFeature、ProjectCiCdSetting 等同属 「Project 的 1:1 配置实体」 簇;它们共享相似的挂载模式(has_one + autosave + nested attributes + delegate)。

12.2 数据模型层:身份依附于聚合根

project_settings 表的设计直接体现了 组合(Composition) 而非独立聚合:

设计点实现DDD 含义
主键project_id 即 PRIMARY KEY子实体 没有独立 surrogate id,身份 = 所属 Project
外键project_id REFERENCES projects(id) ON DELETE CASCADE删除 Project 时级联删除 Setting
分片键db/docs/project_settings.yml → project_id: projects与 Project 同生命周期、同 sharding 边界
描述Stores settings per project纯附属配置,不能脱离 Project 存在
1
2
3
4
db/docs/project_settings.yml
db/structure.sql → CREATE TABLE project_settings ( project_id bigint NOT NULL, ... )
                   → PRIMARY KEY (project_id)
                   → FK ... ON DELETE CASCADE

这是 textbook DDD 中 聚合内 Entity 的典型数据库信号:子对象以根的身份作为主键,而非全局唯一 ID。

12.3 ActiveRecord 关联层

1
2
3
4
5
6
# app/models/project.rb
has_one :project_setting, inverse_of: :project, autosave: true
accepts_nested_attributes_for :project_setting, update_only: true

# app/models/project_setting.rb
belongs_to :project, inverse_of: :project_setting
机制作用聚合含义
has_one / belongs_to1:1 组合关系Setting 从属于 Project
autosave: true保存 Project 时自动持久化关联 Setting单一持久化单元(pragmatic)
accepts_nested_attributes_for ..., update_only: true通过 project_setting_attributes 批量更新只允许经聚合根修改(创建后)
inverse_of双向关联一致性对象图完整性

Lazy 初始化(聚合内对象按需构建):

1
2
3
4
# app/models/project.rb
def project_setting
  super.presence || build_project_setting
end

访问 project.project_setting 时,若 DB 中尚无行,则在内存中 build_project_setting,首次保存发生在 CreateService 或后续 project.save/update 时。这保证聚合根始终是访问 Setting 的入口。

对比:ProjectFeature 在 after_create 中强制 create_or_load_association(:project_feature) 并 validates :project_feature, presence: true;ProjectSetting 则更 lazy,允许创建后延迟落库。

12.4 聚合根 Facade:Delegate 到 ProjectSetting

Project 将大量设置 委托 给 project_setting,对外呈现为聚合根 API:

1
2
3
4
5
6
7
8
# app/models/project.rb(节选)
with_options to: :project_setting do
  delegate :squash_option, :squash_option=
  delegate :mr_default_target_self, :mr_default_target_self=
  delegate :merge_commit_template, :merge_commit_template=
  delegate :runner_registration_enabled, :runner_registration_enabled=
  # ... 数十个 delegate
end

DDD 解读:

  • 外部调用方(Controller、Service、View)通常调用 project.squash_option,而非 project.project_setting.squash_option。
  • 聚合根充当 Facade,隐藏内部结构,符合「通过 Aggregate Root 访问聚合内对象」的原则。
  • 部分 CI 相关设置则委托给 ci_cd_settings(另一个 1:1 子实体),说明 workspace 聚合实际上是一个 cluster,而非单一 ProjectSetting 表。

12.5 生命周期:创建

1
2
3
4
5
6
7
8
9
Projects::CreateService#execute
    ├── 创建 Project 记录(Entity 落库)
    ├── after_create_actions
    │     ├── create_project_settings
    │     │     ├── Gitlab::Pages.add_unique_domain_to(project)  # 可选
    │     │     └── @project.project_setting.save if changed?    # 首次持久化 Setting
    │     ├── publish Projects::ProjectCreatedEvent
    │     └── ...
    └── ...
1
2
3
4
5
# app/services/projects/create_service.rb
def create_project_settings
  Gitlab::Pages.add_unique_domain_to(project) if Gitlab::CurrentSettings.pages_unique_domain_default_enabled
  @project.project_setting.save if @project.project_setting.changed?
end

聚合一致性:

  • Project 必须先存在(有 id),Setting 才能以 project_id 为主键落库。
  • 创建用例由 Projects::CreateService 编排,而非单独 ProjectSetting.create。
  • 创建完成后发布 Projects::ProjectCreatedEvent,其他 context 通过 event 响应,而非在 CreateService 里直接修改它们的数据。

12.6 生命周期:更新

标准路径:经聚合根 + Domain Service

1
2
3
4
# app/services/projects/update_service.rb
def update_project!
  project.update!(params.except(*non_assignable_project_params))
end

调用方传入 nested attributes:

1
2
3
Projects::UpdateService.new(project, current_user,
  project_setting_attributes: { squash_option: squash_option }
).execute

Web 表单同样经聚合根:

1
2
project[project_setting_attributes][squash_option]
project[project_setting_attributes][show_default_award_emojis]

子 Service 也路由到 UpdateService(不直接改 Setting 表):

1
2
3
4
# app/services/projects/branch_rules/squash_options/update_service.rb
Projects::UpdateService.new(project, current_user,
  project_setting_attributes: { squash_option: squash_option }
).execute

UpdateService 内的聚合级校验(根协调子对象规则):

1
2
3
4
5
# 例:修改 pages 相关 setting 前,由 Service 调用 Gitlab::Pages.add_unique_domain_to(project)
def add_pages_unique_domain
  return unless params.dig(:project_setting_attributes, :pages_unique_domain_enabled)
  Gitlab::Pages.add_unique_domain_to(project)
end

ProjectSetting 自身校验(实体不变量,引用聚合根常量):

1
2
3
4
# app/models/project_setting.rb
validates :merge_commit_template, length: { maximum: Project::MAX_COMMIT_TEMPLATE_LENGTH }
validates :target_platforms, inclusion: { in: ALLOWED_TARGET_PLATFORMS }
validate :validates_mr_default_target_self

子实体校验依赖 Project 类常量,表明两者在同一 一致性边界 内。

级联设置(Cascading):ProjectSetting include CascadingProjectSettingAttribute,部分属性可从 Group / Instance 继承;读取时合并祖先值,写入时校验是否可覆盖。这是 aggregate 内实体与 外部 policy(namespace/instance)之间的规则,仍由 ProjectSetting 模型封装。

12.7 生命周期:删除

1
2
3
Projects::DestroyService
    └── project.destroy!
            └── DB CASCADE → project_settings 行随 projects 删除
  • project_settings 无 dependent: 声明在 has_one 上,因为 数据库 FK CASCADE 已保证删除一致性。
  • 子实体不能独立于 Project 存在,符合组合生命周期。

12.8 与聚合内兄弟实体对比

子实体关联创建时机聚合特征
ProjectSettinghas_one, autosaveCreateService lazy savePK = project_id
ProjectFeaturehas_oneafter_create 强制创建validates presence
ProjectCiCdSettinghas_one, autosave, dependent: :destroyafter_create + UpdateService backfillnested attributes
ProjectAutoDevopshas_one按需nested attributes

三者共同构成 Project workspace 配置聚合簇;GitLab 用相同模式(nested attributes + delegate + Service)管理它们,但未在代码中显式命名 aggregate。

12.9 偏离 Strict Aggregate 之处

GitLab 的实现是 pragmatic DDD,并非严格聚合 enforcement:

现象位置说明
直接查询 ProjectSettingProjects::RecordTargetPlatformsServiceProjectSetting.find_or_initialize_by(project: project)
Worker 绕过聚合根Pages::ResetPagesDefaultDomainRedirectWorkerProjectSetting.find_by_project_id(...)
无 Repository 封装全局任何代码均可 ProjectSetting.where(...),无编译期边界
Project 关联过多project.rbIssue、Pipeline 等与 Setting 同属一个 AR 类,但 不是 同一 aggregate
Lazy vs Eager 创建Setting lazy / Feature eager历史上曾出现缺失 Setting 的行,需 background migration 修复

因此:概念上 可把 ProjectSetting 视为 Project 聚合的一部分;工程上 依赖约定(Service + nested attributes + delegate)而非硬隔离。

12.10 协作关系图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
                    ┌─────────────────────────────────────┐
                    │     Projects::CreateService         │
                    │     Projects::UpdateService         │
                    │     Projects::DestroyService        │
                    └──────────────┬──────────────────────┘
                                   │ 编排
                    ┌──────────────▼──────────────────────┐
                    │  Project (Aggregate Root)           │
                    │  app/models/project.rb              │
                    │  • has_one :project_setting          │
                    │  • accepts_nested_attributes        │
                    │  • delegate squash_option, ...        │
                    └──────────────┬──────────────────────┘
                                   │ 1:1 组合
                    ┌──────────────▼──────────────────────┐
                    │  ProjectSetting (聚合内 Entity)      │
                    │  app/models/project_setting.rb      │
                    │  PK = project_id                    │
                    │  • squash_option (enum)             │
                    │  • merge_commit_template            │
                    │  • pages_unique_domain              │
                    │  • CascadingProjectSettingAttribute │
                    └─────────────────────────────────────┘
                                   │
                    ┌──────────────▼──────────────────────┐
                    │  project_settings (PostgreSQL)      │
                    │  ON DELETE CASCADE                  │
                    └─────────────────────────────────────┘

  对外 API / UI ──→ project.update(project_setting_attributes: {...})
                 ──→ project.squash_option  (delegate,不暴露内部结构)

12.11 关键路径速查

类别路径
聚合根app/models/project.rb
聚合内 Entityapp/models/project_setting.rb
Squash 设置 concernapp/models/concerns/projects/squash_option.rb
级联属性 concernapp/models/concerns/cascading_project_setting_attribute.rb
创建编排app/services/projects/create_service.rb → #create_project_settings
更新编排app/services/projects/update_service.rb → #update_project!
Squash 更新(经根)app/services/projects/branch_rules/squash_options/update_service.rb
Schemadb/docs/project_settings.yml、db/structure.sql
表说明Stores settings per project
本文由作者按照 CC BY 4.0 进行授权