2领域驱动设计

沟通与语言

通用语言的力量

通用语言限界上下文

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第2章 沟通与语言 - 通用语言的力量 (Communication and Language - The Power of Ubiquitous Language)

一、导读

2.1 本章学习目标

通过本章的学习,读者将能够:

理解通用语言(Ubiquitous Language)在 DDD 中的核心地位

识别软件开发中沟通障碍的来源与影响

掌握建立和维护通用语言的具体方法

能够在团队中推动通用语言的实践

理解代码、文档与通用语言之间的关系

识别并消除领域模型与实现之间的鸿沟

掌握处理术语冲突和歧义的技巧

能够在实际项目中评估通用语言的成熟度

2.2 为什么本章如此重要

在软件开发的众多失败案例中,沟通问题往往是最根本的原因。开发人员理解的"需求"与业务专家想要表达的"需求"之间存在巨大差距,这种差距导致最终交付的系统无法满足业务期望。DDD 提出通用语言的概念,正是为了解决这一核心问题。

通用语言不仅仅是一个命名规范,它是开发团队与业务团队之间共享的知识体系。当团队成员使用相同的术语讨论业务概念时,误解的可能性大大降低。更重要的是,当这些术语直接体现在代码中时,代码本身就成为了业务知识的载体。

在实际项目中,我们常常看到这样的场景:业务人员说"客户下单",开发人员理解为"创建订单记录",测试人员理解为"生成订单号"。每个人都在用自己的语言理解同一个概念,结果导致需求传递过程中的信息丢失和扭曲。通用语言通过强制团队使用统一的术语,消除了这种理解偏差。

本章的重要性还体现在:通用语言是 DDD 其他所有模式的基础。如果没有通用语言,实体、值对象、聚合等概念就失去了命名的依据;如果没有通用语言,限界上下文的边界就无法清晰界定。可以说,通用语言是 DDD 成功的先决条件。

2.3 核心问题引导

在学习本章之前,请思考以下问题:

什么是"通用语言"?它与普通的项目术语表有什么区别?

为什么软件开发中的沟通如此困难?主要的障碍有哪些?

如何确保代码中的命名与业务术语保持一致?

当业务术语存在歧义或冲突时,应该如何处理?

通用语言需要文档化吗?如果需要,应该如何维护?

新加入团队的成员如何快速掌握通用语言?

通用语言会随着项目演进而变化吗?如何管理这种变化?

在分布式团队或多语言团队中,如何建立通用语言?


二、核心概念详解

2.1 沟通困境的本质

软件开发本质上是一个知识转化的过程:将业务专家头脑中的隐性知识转化为显性的软件系统。在这个转化过程中,沟通是唯一的桥梁。然而,这座桥梁往往并不稳固。

沟通困境的根源在于不同角色拥有不同的"心智模型"。业务专家关注的是业务流程、市场机会、客户需求;开发人员关注的是数据结构、算法效率、技术约束;测试人员关注的是边界条件、异常场景、验证规则。每个角色都从自己的视角理解系统,形成了各自的心智模型。

当这些不同的心智模型试图对接时,问题就出现了。业务专家说"这个功能很重要",他们指的是商业价值;开发人员听到的是"这个功能很复杂",他们想到的是实现难度。同样的词语,不同的含义,这就是沟通困境的本质。

更糟糕的是,这种沟通障碍往往是隐性的。团队成员可能以为自己达成了共识,但实际上各自的理解完全不同。直到系统上线后,业务方发现功能不符合预期,问题才暴露出来。此时修改的成本已经非常高昂。

传统的需求文档试图解决这个问题,但效果有限。文档是静态的,而业务是动态的;文档是单向的传递,而理解需要双向的互动;文档容易过时,而知识需要持续更新。仅仅依靠文档无法建立真正的共识。

2.2 通用语言的定义与特征

通用语言(Ubiquitous Language)是 DDD 中用于解决沟通问题的核心工具。它是指在一个特定的限界上下文内,开发团队和业务团队共同使用的、精确的业务术语集合。

通用语言具有以下几个关键特征:

首先,通用语言是共享的。它不是业务专家的专属语言,也不是开发人员的技术黑话,而是双方共同理解和使用的语言。业务人员可以用它描述需求,开发人员可以用它设计模型,测试人员可以用它编写用例。

其次,通用语言是精确的。每个术语都有明确的定义,不存在歧义。例如,"客户"可能指注册用户、付费用户或潜在客户,在通用语言中必须明确其具体含义。精确性是通用语言价值的核心。

第三,通用语言是嵌入代码的。这是通用语言与普通术语表的最大区别。通用语言中的术语不仅出现在文档和会议中,更直接体现在类名、方法名、变量名中。代码成为通用语言的最终表达形式。

第四,通用语言是演进的。随着对业务理解的深入,通用语言会不断完善。新的术语会被添加,旧的术语可能被重新定义或淘汰。这种演进是团队学习过程的体现。

第五,通用语言是有边界的。它只在特定的限界上下文内有效。在不同的上下文中,同一个词可能有不同的含义。例如,"产品"在销售上下文和库存上下文中的定义可能不同。

2.3 通用语言的形成过程

通用语言不是凭空产生的,它是在团队协作过程中逐步形成的。这个过程通常包括以下几个阶段:

第一阶段:术语收集。团队开始有意识地记录业务讨论中出现的关键词汇。这些词汇可能来自业务文档、用户故事、会议记录等。此时的术语可能是零散的、不完整的。

第二阶段:术语澄清。团队对收集到的术语进行讨论,明确每个术语的定义和使用场景。这个阶段可能需要多次迭代,因为初始的理解往往不够准确。业务专家在这个过程中扮演关键角色,他们需要解释术语的真实含义。

第三阶段:术语整合。将澄清后的术语应用到代码中。类名、方法名、变量名都应该使用通用语言中的术语。同时,文档、测试用例、用户界面也应该使用相同的术语。这个阶段实现了术语的统一。

第四阶段:术语演进。随着项目的推进,团队会发现原有术语的不足,需要添加新术语或修改现有术语。这个过程是持续的,反映了团队对业务理解的深化。

在整个形成过程中,有几个关键原则需要遵循:

一是业务驱动。术语的定义应该由业务专家主导,而不是由开发人员决定。开发人员的作用是确保术语能够被准确地转化为代码。

二是全员参与。通用语言是整个团队的资产,每个成员都应该参与其建设和维护。新成员的加入也是传播通用语言的机会。

三是持续验证。定期回顾通用语言的使用情况,检查代码中的命名是否与通用语言一致,是否存在术语滥用或误用的情况。

2.4 代码作为通用语言的载体

在 DDD 中,代码不仅仅是实现功能的工具,更是表达业务知识的媒介。当通用语言被嵌入代码时,代码就成为了最权威的业务文档。

这种理念带来了几个重要的实践要求:

首先,命名必须反映业务概念。类名应该是业务实体的名称,如 Customer、Order、Product,而不是技术概念如 DataTable、Bean、Entity。方法名应该表达业务行为,如 placeOrder()、cancelReservation(),而不是技术操作如 save()、update()。

其次,代码结构应该反映业务关系。如果业务上 A 包含 B,那么代码中 A 类应该持有 B 类的引用或集合。如果业务上 C 是 D 的一种类型,那么代码中应该体现继承或组合关系。代码的结构就是业务结构的映射。

第三,业务规则应该在代码中显式表达。不要将业务规则隐藏在复杂的条件判断或工具方法中。应该通过领域对象的方法、领域服务的操作来明确表达业务规则。阅读代码的人应该能够直接理解业务逻辑。

第四,避免技术术语污染领域层。在领域层的代码中,不应该出现数据库、框架、中间件等技术概念的痕迹。领域层应该纯粹地表达业务,技术细节应该被隔离在基础设施层。

当代码成为通用语言的载体时,它就具备了自我文档化的能力。新人阅读代码就能理解业务,业务人员审查代码就能验证需求是否被正确实现。这是 DDD 追求的理想状态。

2.5 消除模型与实现的鸿沟

在传统开发中,领域模型(通常是 UML 图或文档)与实际代码之间往往存在巨大的鸿沟。模型是理想化的抽象,而代码是现实的实现,两者逐渐脱节,最终模型变成了一纸空文。

DDD 强调模型与实现的统一:领域模型就是代码,代码就是领域模型。这种统一通过以下方式实现:

一是模型驱动设计。设计活动直接产生代码,而不是产生描述代码的文档。设计师在白板上的讨论很快转化为代码原型,设计决策直接体现在代码结构中。

二是持续重构。随着对业务理解的深入,原有的模型可能不再适用。通过重构,不断调整代码结构,使其更好地反映最新的业务理解。重构是保持模型活力的关键手段。

三是测试验证。单元测试和集成测试不仅验证代码的正确性,也验证模型的合理性。如果测试很难写,可能说明模型设计有问题。好的模型应该是易于测试的。

四是反馈循环。代码实现过程中发现的问题应该反馈到模型设计中,促使模型的改进。这是一个双向的过程,而不是单向的传递。

消除鸿沟的关键在于认识到:模型的价值不在于它的完美,而在于它与实现的一致性。一个简单但与代码完全一致的模型,远胜于一个复杂但与代码脱节的模型。

2.6 处理术语冲突与歧义

在实际项目中,术语冲突和歧义是不可避免的。不同的人对同一个词有不同的理解,或者同一个概念有多个名称。如何处理这些问题是建立通用语言的关键挑战。

处理术语冲突的策略包括:

第一,明确上下文。很多术语冲突是因为脱离了具体的业务场景。通过限定讨论的上下文,可以消除大部分歧义。例如,"订单"在创建阶段和发货阶段的含义可能不同,明确阶段后可以准确定义。

第二,追溯源头。当对某个术语的定义有争议时,追溯到业务的真实场景,看业务实际上是如何使用这个词的。业务事实是解决争议的最终依据。

第三,创造新术语。如果现有术语无法准确表达某个概念,不妨创造一个新的术语。新术语应该简洁、独特、易于理解。避免使用缩写或生造词,除非它们已经被广泛接受。

第四,记录决策。对于重要的术语定义,应该记录下来,包括定义的内容、制定的理由、适用的场景等。这份记录可以作为后续参考,避免重复争论。

第五,接受多样性。在某些情况下,不同的子领域确实需要使用不同的术语。这时不必强求统一,而是应该明确各自的适用范围,通过上下文映射来处理边界处的转换。

处理术语问题需要耐心和开放的心态。目标不是消灭所有分歧,而是建立一个足够清晰、能够支持有效协作的术语体系。


三、程序员的视角

3.1 在实际项目中应用通用语言

作为程序员,在日常工作中践行通用语言需要从以下几个方面入手:

首先,在代码审查时关注命名。不要只关注代码逻辑是否正确,还要检查命名是否使用了通用语言中的术语。如果发现技术化的命名或不一致的术语,应该提出修改建议。

其次,在与产品经理沟通时有意识地使用业务术语。即使在讨论技术实现时,也尽量用业务语言来描述。这有助于强化通用语言的使用,也能帮助产品经理理解技术方案。

第三,编写测试用例时使用业务场景。测试方法的命名、测试数据的准备、断言的表达都应该使用通用语言。这样测试用例就成为了业务规则的 executable specification(可执行规范)。

第四,在技术文档中保持术语一致。无论是 API 文档、架构文档还是操作手册,都应该使用通用语言中的术语。避免在同一份文档中对同一个概念使用不同的名称。

第五,帮助新成员学习通用语言。当有新同事加入时,主动向他们介绍通用语言,解释关键术语的含义。可以维护一份术语表,作为新人的入门材料。

第六,定期回顾和更新术语。在项目回顾会议上,可以花一点时间讨论通用语言的使用情况。是否有新的术语需要添加?是否有旧的术语需要重新定义?是否有术语被误用?

3.2 常见陷阱与误区

在使用通用语言的过程中,团队容易陷入以下陷阱:

陷阱一:术语表与代码脱节。团队花费大量精力维护一份精美的术语表,但代码中的命名却与之不符。这种情况下,术语表变成了另一种形式的废弃文档。正确的做法是让代码成为术语的最终来源。

陷阱二:过度追求统一。试图在整个系统中使用完全统一的术语,即使在不同限界上下文中也是如此。这可能导致术语的定义过于宽泛,失去精确性。应该允许不同上下文有自己的术语体系。

陷阱三:忽视历史遗留。在改造旧系统时,强行将所有旧命名改为新术语,工作量巨大且风险高。更好的策略是新代码使用新术语,旧代码在修改时逐步迁移。

陷阱四:术语过于技术化。虽然名为通用语言,但实际定义的术语仍然带有浓厚的技术色彩,业务人员难以理解。通用语言应该以业务为中心,技术人员需要学习业务语言,而不是反过来。

陷阱五:缺乏维护机制。通用语言建立后就不再更新,无法适应业务的变化。通用语言应该是活的,随着业务发展而演进。需要建立定期的回顾和更新机制。

陷阱六:形式主义。过分关注命名的"正确性",而忽视了实质性的业务理解。命名重要,但更重要的是团队是否真正理解了业务。不要本末倒置。

3.3 与其他实践的协同

通用语言的实践可以与其他工程实践很好地协同:

与测试驱动开发(TDD)结合:在编写测试时,使用通用语言来命名测试方法和描述场景。这使得测试成为业务规则的可执行文档。当业务规则变化时,测试失败提示我们需要更新实现。

与行为驱动开发(BDD)结合:BDD 的 Given-When-Then 格式天然适合使用通用语言。业务人员可以直接阅读和编写 BDD 场景,因为这些场景使用的是他们熟悉的术语。

与敏捷用户故事结合:用户故事的标题和描述应该使用通用语言。验收标准也应该使用通用语言中的术语。这样用户故事就成为了通用语言的来源之一。

与持续集成结合:可以建立自动化检查,确保代码中的命名符合通用语言的规范。虽然完全自动化比较困难,但可以检查一些基本的约定,如禁止使用某些技术前缀。

与知识管理结合:将通用语言纳入团队的知识库。新员工入职时,学习通用语言是必修课。定期举办术语工作坊,让团队成员分享对新术语的理解。


四、代码实战

4.1 Java 示例:电商系统中的通用语言

以下代码展示了如何在电商系统中体现通用语言:

java
/**
 * 客户实体
 * 使用业务术语 "Customer" 而非 "User" 或 "Account"
 */
public class Customer {
    private final CustomerId id;
    private String name;
    private EmailAddress email;
    private PhoneNumber phoneNumber;
    private CustomerStatus status;
    private LoyaltyPoints loyaltyPoints;
    
    public Customer(CustomerId id, String name, EmailAddress email) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.status = CustomerStatus.ACTIVE;
        this.loyaltyPoints = LoyaltyPoints.zero();
    }
    
    /**
     * 放置订单 - 使用业务术语 "placeOrder" 而非 "createOrder"
     * 因为 "place" 更准确地表达了客户的购买行为
     */
    public Order placeOrder(ShoppingCart cart, ShippingAddress address) {
        if (this.status != CustomerStatus.ACTIVE) {
            throw new IllegalStateException("Inactive customer cannot place orders");
        }
        
        if (cart.isEmpty()) {
            throw new IllegalArgumentException("Cannot place order with empty cart");
        }
        
        // 创建订单
        Order order = new Order(this.id, cart.getItems(), address);
        
        // 累积积分
        this.loyaltyPoints = this.loyaltyPoints.add(order.calculateEarnedPoints());
        
        return order;
    }
    
    /**
     * 取消订单 - 业务术语
     */
    public void cancelOrder(Order order, CancellationReason reason) {
        if (!order.belongsTo(this.id)) {
            throw new IllegalArgumentException("Order does not belong to this customer");
        }
        
        order.cancel(reason);
        
        // 扣除相应积分
        this.loyaltyPoints = this.loyaltyPoints.subtract(order.calculateEarnedPoints());
    }
    
    // Getters
    public CustomerId getId() { return id; }
    public String getName() { return name; }
    public EmailAddress getEmail() { return email; }
    public CustomerStatus getStatus() { return status; }
    public LoyaltyPoints getLoyaltyPoints() { return loyaltyPoints; }
}

/**
 * 订单项 - 值对象
 * 使用业务术语 "OrderItem" 而非 "OrderDetail" 或 "LineItem"
 */
public class OrderItem {
    private final ProductId productId;
    private final String productName;
    private final Money unitPrice;
    private final int quantity;
    
    public OrderItem(ProductId productId, String productName, Money unitPrice, int quantity) {
        this.productId = productId;
        this.productName = productName;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }
    
    public Money subtotal() {
        return unitPrice.multiply(quantity);
    }
    
    // Getters
    public ProductId getProductId() { return productId; }
    public String getProductName() { return productName; }
    public Money getUnitPrice() { return unitPrice; }
    public int getQuantity() { return quantity; }
}

/**
 * 购物车 - 领域概念
 */
public class ShoppingCart {
    private final CustomerId customerId;
    private final Map<ProductId, CartItem> items;
    
    public ShoppingCart(CustomerId customerId) {
        this.customerId = customerId;
        this.items = new HashMap<>();
    }
    
    /**
     * 添加商品到购物车 - 业务行为
     */
    public void addItem(Product product, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        
        ProductId productId = product.getId();
        if (items.containsKey(productId)) {
            CartItem existingItem = items.get(productId);
            items.put(productId, existingItem.increaseQuantity(quantity));
        } else {
            items.put(productId, new CartItem(productId, product.getName(), product.getPrice(), quantity));
        }
    }
    
    /**
     * 从购物车移除商品
     */
    public void removeItem(ProductId productId) {
        items.remove(productId);
    }
    
    /**
     * 判断购物车是否为空
     */
    public boolean isEmpty() {
        return items.isEmpty();
    }
    
    /**
     * 获取购物车中的所有商品项
     */
    public Collection<CartItem> getItems() {
        return Collections.unmodifiableCollection(items.values());
    }
    
    /**
     * 计算购物车总价
     */
    public Money calculateTotal() {
        return items.values().stream()
                .map(CartItem::subtotal)
                .reduce(Money.zero(), Money::add);
    }
}

/**
 * 订单状态 - 使用业务术语
 */
public enum OrderStatus {
    CREATED,      // 已创建
    CONFIRMED,    // 已确认
    PAID,         // 已支付
    SHIPPED,      // 已发货
    DELIVERED,    // 已送达
    CANCELLED,    // 已取消
    REFUNDED      // 已退款
}

/**
 * 取消原因 - 业务概念
 */
public enum CancellationReason {
    CUSTOMER_REQUEST,     // 客户主动取消
    OUT_OF_STOCK,         // 缺货
    PAYMENT_FAILED,       // 支付失败
    SUSPECTED_FRAUD,      // 疑似欺诈
    SYSTEM_ERROR          // 系统错误
}

这个示例展示了:

类名和方法名都使用业务术语,如 Customer.placeOrder() 而非 UserService.createOrder()。

值对象如 OrderItem 封装了业务计算逻辑(小计计算)。

枚举类型使用业务状态,如 OrderStatus 和 CancellationReason。

业务规则在领域对象内部得到体现,如只有活跃客户才能下单。

4.2 Python 示例:图书馆系统中的通用语言

python
from dataclasses import dataclass
from datetime import date, timedelta
from enum import Enum
from typing import List, Optional


class MembershipStatus(Enum):
    """会员状态 - 业务术语"""
    ACTIVE = "active"           # 正常
    SUSPENDED = "suspended"     # 暂停
    EXPIRED = "expired"         # 过期
    BLACKLISTED = "blacklisted" # 黑名单


class BookStatus(Enum):
    """图书状态 - 业务术语"""
    AVAILABLE = "available"         # 可借阅
    BORROWED = "borrowed"           # 已借出
    RESERVED = "reserved"           # 已预约
    LOST = "lost"                   # 遗失
    DAMAGED = "damaged"             # 损坏


@dataclass(frozen=True)
class ISBN:
    """值对象:ISBN号"""
    value: str
    
    def __post_init__(self):
        if len(self.value) not in [10, 13]:
            raise ValueError("ISBN must be 10 or 13 digits")


@dataclass(frozen=True)
class MemberId:
    """值对象:会员ID"""
    value: str


@dataclass(frozen=True)
class LoanPeriod:
    """值对象:借阅期限"""
    days: int
    
    def __post_init__(self):
        if self.days <= 0:
            raise ValueError("Loan period must be positive")
    
    def end_date_from(self, start_date: date) -> date:
        """计算归还日期"""
        return start_date + timedelta(days=self.days)


class Book:
    """实体:图书"""
    
    def __init__(self, isbn: ISBN, title: str, author: str, loan_period: LoanPeriod):
        self._isbn = isbn
        self._title = title
        self._author = author
        self._loan_period = loan_period
        self._status = BookStatus.AVAILABLE
        self._borrower_id: Optional[MemberId] = None
        self._due_date: Optional[date] = None
    
    @property
    def isbn(self) -> ISBN:
        return self._isbn
    
    @property
    def title(self) -> str:
        return self._title
    
    @property
    def status(self) -> BookStatus:
        return self._status
    
    def borrow(self, member_id: MemberId, borrow_date: date):
        """借阅图书 - 业务行为"""
        if self._status != BookStatus.AVAILABLE:
            raise RuntimeError(f"Book is not available: {self._status.value}")
        
        self._status = BookStatus.BORROWED
        self._borrower_id = member_id
        self._due_date = self._loan_period.end_date_from(borrow_date)
    
    def return_book(self, return_date: date) -> 'ReturnResult':
        """归还图书 - 业务行为"""
        if self._status != BookStatus.BORROWED:
            raise RuntimeError("Book is not currently borrowed")
        
        overdue_days = max(0, (return_date - self._due_date).days)
        
        self._status = BookStatus.AVAILABLE
        previous_borrower = self._borrower_id
        self._borrower_id = None
        self._due_date = None
        
        if overdue_days > 0:
            return ReturnResult.overdue(previous_borrower, overdue_days)
        else:
            return ReturnResult.on_time(previous_borrower)
    
    def reserve(self, member_id: MemberId):
        """预约图书 - 业务行为"""
        if self._status != BookStatus.AVAILABLE:
            raise RuntimeError("Can only reserve available books")
        
        self._status = BookStatus.RESERVED


class Member:
    """实体:会员"""
    
    MAX_BORROW_LIMIT = 5  # 最大借阅数量
    
    def __init__(self, member_id: MemberId, name: str, membership_type: str):
        self._id = member_id
        self._name = name
        self._membership_type = membership_type
        self._status = MembershipStatus.ACTIVE
        self._borrowed_books: List[ISBN] = []
        self._overdue_count = 0
    
    @property
    def id(self) -> MemberId:
        return self._id
    
    @property
    def status(self) -> MembershipStatus:
        return self._status
    
    def borrow_book(self, book: Book, borrow_date: date):
        """借阅图书 - 业务行为"""
        if self._status != MembershipStatus.ACTIVE:
            raise RuntimeError(f"Cannot borrow: membership is {self._status.value}")
        
        if len(self._borrowed_books) >= self.MAX_BORROW_LIMIT:
            raise RuntimeError("Borrow limit reached")
        
        if self._overdue_count >= 3:
            raise RuntimeError("Too many overdue returns")
        
        book.borrow(self._id, borrow_date)
        self._borrowed_books.append(book.isbn)
    
    def return_book(self, book: Book, return_date: date) -> 'ReturnResult':
        """归还图书 - 业务行为"""
        result = book.return_book(return_date)
        
        if book.isbn in self._borrowed_books:
            self._borrowed_books.remove(book.isbn)
        
        if result.is_overdue:
            self._overdue_count += 1
        
        return result
    
    def can_borrow(self) -> bool:
        """判断是否可以借阅"""
        return (self._status == MembershipStatus.ACTIVE and 
                len(self._borrowed_books) < self.MAX_BORROW_LIMIT and
                self._overdue_count < 3)


@dataclass
class ReturnResult:
    """值对象:归还结果"""
    is_overdue: bool
    member_id: Optional[MemberId] = None
    overdue_days: int = 0
    
    @classmethod
    def on_time(cls, member_id: MemberId):
        return cls(is_overdue=False, member_id=member_id)
    
    @classmethod
    def overdue(cls, member_id: MemberId, days: int):
        return cls(is_overdue=True, member_id=member_id, overdue_days=days)


# 使用示例
def library_scenario():
    """图书馆业务场景演示"""
    # 创建图书
    isbn = ISBN("9780321125217")
    loan_period = LoanPeriod(14)  # 借阅期限14天
    book = Book(isbn, "Domain-Driven Design", "Eric Evans", loan_period)
    
    # 创建会员
    member = Member(MemberId("M001"), "张三", "premium")
    
    # 借阅图书
    borrow_date = date.today()
    member.borrow_book(book, borrow_date)
    print(f"Book status: {book.status.value}")  # borrowed
    
    # 归还图书(按时)
    return_date = borrow_date + timedelta(days=10)
    result = member.return_book(book, return_date)
    print(f"Return result: {'overdue' if result.is_overdue else 'on time'}")

这个 Python 示例展示了:

使用业务术语命名类和属性,如 Member、Book、borrow()、return_book()。

值对象如 ISBN、LoanPeriod 封装了业务规则和验证。

业务规则在领域对象中实现,如借阅限制、逾期计数等。

返回结果使用专门的值对象 ReturnResult,清晰表达业务结果。

4.3 领域服务示例:使用通用语言的转账服务

java
/**
 * 转账领域服务
 * 使用业务术语表达转账流程
 */
public class FundsTransferService {
    
    private final AccountRepository accountRepository;
    private final TransferLimitPolicy transferLimitPolicy;
    private final FraudDetectionService fraudDetectionService;
    
    public FundsTransferService(AccountRepository accountRepository,
                               TransferLimitPolicy transferLimitPolicy,
                               FraudDetectionService fraudDetectionService) {
        this.accountRepository = accountRepository;
        this.transferLimitPolicy = transferLimitPolicy;
        this.fraudDetectionService = fraudDetectionService;
    }
    
    /**
     * 执行资金划转
     * 使用业务术语 "transferFunds" 而非 "moveMoney" 或 "updateBalance"
     */
    public TransferReceipt transferFunds(TransferRequest request) {
        // 验证转账请求
        validateTransferRequest(request);
        
        // 获取付款账户和收款账户
        Account payerAccount = accountRepository.findByAccountId(request.getPayerAccountId());
        Account payeeAccount = accountRepository.findByAccountId(request.getPayeeAccountId());
        
        // 反欺诈检查
        fraudDetectionService.checkForFraud(request);
        
        // 限额检查
        transferLimitPolicy.validateLimit(payerAccount, request.getAmount());
        
        // 执行转账
        Transaction transaction = Transaction.initiate(
            payerAccount.getId(),
            payeeAccount.getId(),
            request.getAmount(),
            request.getDescription()
        );
        
        payerAccount.debit(transaction);
        payeeAccount.credit(transaction);
        
        // 保存账户和交易记录
        accountRepository.save(payerAccount);
        accountRepository.save(payeeAccount);
        
        // 生成转账回执
        return TransferReceipt.issue(transaction);
    }
    
    private void validateTransferRequest(TransferRequest request) {
        if (request.getPayerAccountId().equals(request.getPayeeAccountId())) {
            throw new IllegalArgumentException("Payer and payee cannot be the same account");
        }
        if (request.getAmount().isLessThanOrEqual(Money.zero())) {
            throw new IllegalArgumentException("Transfer amount must be positive");
        }
    }
}

/**
 * 转账请求 - 使用业务术语
 */
public class TransferRequest {
    private final AccountId payerAccountId;   // 付款人账户
    private final AccountId payeeAccountId;   // 收款人账户
    private final Money amount;               // 金额
    private final String description;         // 描述
    
    public TransferRequest(AccountId payerAccountId, AccountId payeeAccountId,
                          Money amount, String description) {
        this.payerAccountId = payerAccountId;
        this.payeeAccountId = payeeAccountId;
        this.amount = amount;
        this.description = description;
    }
    
    // Getters
    public AccountId getPayerAccountId() { return payerAccountId; }
    public AccountId getPayeeAccountId() { return payeeAccountId; }
    public Money getAmount() { return amount; }
    public String getDescription() { return description; }
}

/**
 * 转账回执 - 业务凭证
 */
public class TransferReceipt {
    private final String receiptNumber;
    private final LocalDateTime timestamp;
    private final AccountId payerAccountId;
    private final AccountId payeeAccountId;
    private final Money amount;
    private final String description;
    
    private TransferReceipt(String receiptNumber, LocalDateTime timestamp,
                           AccountId payerAccountId, AccountId payeeAccountId,
                           Money amount, String description) {
        this.receiptNumber = receiptNumber;
        this.timestamp = timestamp;
        this.payerAccountId = payerAccountId;
        this.payeeAccountId = payeeAccountId;
        this.amount = amount;
        this.description = description;
    }
    
    public static TransferReceipt issue(Transaction transaction) {
        return new TransferReceipt(
            generateReceiptNumber(),
            LocalDateTime.now(),
            transaction.getPayerAccountId(),
            transaction.getPayeeAccountId(),
            transaction.getAmount(),
            transaction.getDescription()
        );
    }
    
    private static String generateReceiptNumber() {
        return "TR" + System.currentTimeMillis();
    }
    
    // Getters
    public String getReceiptNumber() { return receiptNumber; }
    public LocalDateTime getTimestamp() { return timestamp; }
    public AccountId getPayerAccountId() { return payerAccountId; }
    public AccountId getPayeeAccountId() { return payeeAccountId; }
    public Money getAmount() { return amount; }
    public String getDescription() { return description; }
}

五、深入思考

5.1 历史背景

通用语言的概念源于 Eric Evans 对多个成功软件项目的观察。他发现,在这些项目中,开发团队和业务团队之间都形成了一种共享的语言体系。团队成员能够用相同的术语讨论业务问题,而这些术语也直接体现在代码中。

这一观察与语言学中的"萨丕尔-沃尔夫假说"有一定的呼应:语言影响思维。当团队使用精确的业务语言时,他们对业务的理解也会更加精确。反之,如果语言模糊不清,思维也容易混乱。

在软件工程史上,有许多因沟通问题导致失败的案例。最著名的莫过于美国航空航天局(NASA)的火星气候轨道器任务,由于一个团队使用英制单位,另一个团队使用公制单位,导致探测器在进入火星轨道时坠毁。这个案例生动地说明了统一语言的重要性。

5.2 前沿方向

通用语言的实践正在向以下几个方向发展:

首先是自动化辅助。一些工具开始尝试分析代码中的命名,并与术语表进行比对,自动发现不一致的地方。这类工具可以帮助团队维持通用语言的一致性。

其次是多语言支持。在全球化团队中,通用语言可能需要支持多种自然语言。如何在不同语言之间保持术语的一致性是一个挑战。一些团队采用英文作为基准语言,其他语言作为翻译。

第三是与 AI 辅助编程的结合。随着 GitHub Copilot 等 AI 编程助手的普及,如何让 AI 理解并使用团队的通用语言成为一个新课题。可能需要训练专门的模型或提供术语上下文。

5.3 延伸阅读

Eric Evans, 《领域驱动设计》第2章 - 原始论述

Vaughn Vernon, 《实现领域驱动设计》第3章 - 实用指南

Alberto Brandolini, 《Introducing EventStorming》- 事件风暴中的语言发现

Martin Fowler, 《Ubiquitous Language》- 在线文章


六、实验笔记

实验一:术语审计

实验目的:通过审计现有代码中的命名,发现与通用语言不一致的地方。

实验步骤:

选择一个中等规模的模块或包。

列出其中所有的类名、接口名、方法名。

对照项目的术语表(如果没有,就先创建一个),检查每个命名是否符合通用语言。

标记出不一致的命名,分析原因:是历史遗留?是理解偏差?还是术语表本身有问题?

制定改进计划:哪些可以立即修改?哪些需要谨慎处理?

预期收获:培养对命名的敏感度,发现项目中存在的术语问题。

实验二:建立通用语言工作坊

实验目的:模拟一次通用语言建立的工作坊,体验术语收集和澄清的过程。

实验步骤:

选择一个简单的业务场景,如咖啡店点餐系统。

邀请 2-3 位同学或同事,分别扮演业务专家和开发人员。

进行 30 分钟的业务讨论,记录所有出现的关键术语。

讨论结束后,整理术语列表,尝试给每个术语下定义。

发现定义模糊或有争议的术语,进一步讨论澄清。

将澄清后的术语应用到简单的代码设计中。

预期收获:体验通用语言建立的过程,理解其中的挑战和技巧。


七、本章小结

本章深入探讨了通用语言在 DDD 中的重要作用。我们首先分析了软件开发中的沟通困境,理解了为什么需要一种共享的语言体系。然后,我们详细定义了通用语言的特征:共享性、精确性、代码嵌入性、演进性和边界性。

通用语言的形成是一个渐进的过程,需要团队持续投入和维护。它不是静态的术语表,而是活的知识体系,随着团队对业务理解的深入而不断演进。

从程序员的视角,我们讨论了如何在日常工作中践行通用语言,包括代码审查、沟通方式、测试编写等方面。我们也识别了常见的陷阱,如术语表与代码脱节、过度追求统一等。

通过三个代码示例,我们展示了如何在 Java 和 Python 中体现通用语言。关键在于使用业务术语命名,将业务规则封装在领域对象中,避免技术术语污染领域层。

最后,我们回顾了通用语言的历史背景,展望了前沿方向,并提供了两个实验任务帮助读者实践所学内容。

通用语言是 DDD 的基石。没有通用语言,其他 DDD 模式就失去了根基。希望读者能够在实际项目中重视通用语言的建立和维护,让它成为团队协作的有力工具。


八、习题

概念理解题:用自己的话解释什么是通用语言,它与普通的项目术语表有什么本质区别?

案例分析题:以下代码中的命名存在哪些问题?如何改进以符合通用语言的要求?

java
public class UserData {
    private String userName;
    private String userEmail;
    public void saveData() { ... }
    public void updateInfo() { ... }
}

实践题:在你当前参与的项目中,找出 5 个业务概念,检查它们在代码中的命名是否一致。如果不一致,分析原因并提出改进方案。

思考题:为什么说"代码是通用语言的最终表达形式"?如果代码与文档中的术语不一致,应该以哪个为准?为什么?

设计题:为一个医院挂号系统设计通用语言。至少包括 10 个核心术语,给出每个术语的定义,并说明它们是实体还是值对象。

辨析题:在不同限界上下文中,同一个术语可能有不同的含义。这种情况是否违背了通用语言的原则?应该如何处理?

情景题:新加入团队的成员抱怨通用语言太难学,很多术语记不住。作为团队资深成员,你有什么建议帮助他快速掌握?

论述题:通用语言的维护成本是否过高?在快速迭代的敏捷项目中,如何平衡通用语言的精确性与开发效率?

开放题:在远程协作的团队中,建立通用语言面临哪些额外挑战?你有什么解决方案?

综合题:选择一个开源项目,分析其代码中的命名风格。判断它是否体现了通用语言的思想,并给出你的评价和改进建议。

2/12沟通与语
0%