討論交流
我的两分钱 2023-01-13 139 0 0 0 0
程序开发,DDD有两大战略模式:界限上下文(BoundedContext)和统一语言(UbiquitousLanguage),但在实践中,UbiquitousLanguage(UL)往往是最被忽视的。大型软件最…

DDD有两大战略模式:界限上下文(Bounded Context)和统一语言(Ubiquitous Language), 但在实践中,Ubiquitous Language(UL)往往是最被忽视的。大型软件最大的挑战性就是系统复杂性,而UL就是应对这一挑战的利器。

让我们从一个例子开始,假设我们有这样一个Order类:

public class Order
{
...
public void updateOrder(Order newOrder) throws Exception {
/* check new items and add to current order */
/* remove items if not in newOrder */
/* check item quantity and update order if needed */
/* check item price and update order if needed */
/* check newOrder status and change current order if needed */
}
...
}

这段代码有什么问题吗?

一个很大的问题就是updateOrder作为一个pubilc方法,却没有表示出业务含义,函数名只体现了技术实现,从中完全看不出业务操作的目的,我们向订单中添加 / 删除商品、更改商品数量等都调用的都是相同的updateOrder,技术实现屏蔽了业务目的。

更麻烦的是,updateOrder的调用参数是一个Order对象,比如调用方只是想更新一下订单状态,他也不得不创建创建一个新的订单对象。明明是更新一个现存订单的状态却要创建一个新订单,这很反直觉,技术实现屏蔽业务目的蔓延到了调用方。

那么更好的做法是什么?也许可以这样写:

public class Order
{
public void addNewItems(Item[] items, int[] quantities) throws Exception{
Order newOrder = /* construct a new order */
upateOrder(newOrder);
}

public void removeItems(Long[] itemIds) throws Exception {
Order newOrder = /* construct a new order */
upateOrder(newOrder);
}

public void changeItemQuantity(Long itemId, int quantity) throws Exception{
Order newOrder = /* construct a new order */
upateOrder(newOrder);
}

public void udpateItemPrice(Long itemId, double price) throws Exception {
Order newOrder = /* construct a new order */
upateOrder(newOrder);
}

public void orderPaid() throws Exception {
Order newOrder = /* construct a new order */
upateOrder(newOrder);
}
public void orderCancelled throws Exception {
Order newOrder = /* construct a new order */
upateOrder(newOrder);
}
// more public biz methods to update order status

...
private void updateOrder(Order newOrder) throws Exception {
/* check new items and add to current order */
/* remove items if not in newOrder */
/* check item quantity and update order if needed */
/* check item price and update order if needed */
/* check newOrder status and change current order if needed */
}
...
}

这里把业务和技术明确分离,提供了一套用UL描述的有明确业务语义的public方法,把技术实现细节作为private方法对外进行了隐藏。

有同学可能会说,看起来UL只是让代码的可读性好了一些,这很好,但这真的值得提高到战略高度,称之为一个最重要的战略模式吗?

这是很好的一个问题,我们继续看上面更新后的代码。很明显,在一个Order对象内部进行更新操作,还需要创建一个新的Order对象很笨拙,为什么不直接修改当前对象的属性呢?我们可以把代码改为:

public class Order
{
public void addNewItems(Item[] items, int[] quantities) throws Exception{
// udpate items of current order
}

public void removeItems(Long[] itemIds) throws Exception {
// remove items from current order
}

public void changeItemQuantity(Long itemId, int quantity) throws Exception{
// change current order's item quantity
}

public void udpateItemPrice(Long itemId, double price) throws Exception {
// update current order's item's price and order total
}

public void orderPaid() throws Exception {
// set current order to PAID status
}
public void orderCancelled throws Exception {
// set current order to CANCELLED status
}
// more public biz methods to update order status
...
}

我们的代码变得更加简练了,但更重要的是,我们对外公开的业务方法没有改变,但是其技术实现方式却改变了。

"公开的业务方法没有改变,但是其技术实现方式却改变了",看到这,敏锐的同学会说,这不就是多态么!在面向对象中,一个接口的多种不同实现方式即为多态。UL实际定义了一套业务接口,而技术则是这个业务接口的实现方式。

多态虽然是一个技术思想,但UL就是这一技术思想在业务层面的应用。

UL就是业务层面的多态

那么这种业务层面的多态真的重要到可以称之为战略模式吗?

大型软件最大的挑战性就是系统复杂性,而这种复杂性的根源就在于业务复杂性和技术复杂性的耦合。

来源:Patterns, Principles, and Practices of Domain-Driven Design 第一章

One of the main reasons software becomes complex and difficult to manage is due to the mixing of domain complexities with technical complexities

《Patterns, Principles, and Practices of Domain-Driven Design》

业务复杂性和技术复杂性是因我们要解决的问题而必然存在的,假设业务复杂性为N,技术复杂性为M,当两者耦合在一起的时候,其复杂度为O(M*N),当两者解耦时,理论上业务和技术可以独立演进,其复杂度为O(M + N)。假设N为10,M为100,业务和技术耦合的时候其复杂度是不耦合的10倍。

还是拿文章开头updateOrder的例子,由于技术实现屏蔽了业务目的,每当updateOrder的实现方式发生改变的时候,其所有的调用方都会被影响,即使这个改变实际和调用方没有关系的时候,调用方也会被影响。

反过来,当业务规则发生改变时,比如向订单中增加Item有数量限制,在不耦合的情况下,我们增加一个新的业务操作就可以了:

...
public void addNewItemsWithLimit(Item[] items, int[] quantities, int limit) throws Exception{
// udpate items of current order, but no more than limit
}
...

而在耦合的情况下,我们则需要修改updateOrder函数,因为所有的业务操作都调用updateOrder,所以所有的业务操作即使不相关也全部都会受到影响。

UL作为一种多态,并不仅仅反映在代码层面,在沟通中也一样有所反映。一个普遍现象是业务团队和技术团队互相听不懂,进而导致产品和市场脱节。为什么大家互相听不懂?一个主要原因就是在沟通中使用了太多自己领域内部的行话,当大家使用UL来沟通的时候,就把大家统一到了一致的业务层面,对内部的细节进行了隔离。

比如在CRM中,一条销售线索,技术团队可能称之为Lead,销售团队称之为潜客,在没有UL前,技术团队说Lead,销售团队可能就会很懵,不是在讨论潜客嘛,怎么突然谈起了领导?在UL中统一为『线索』一词后,无论技术团队内部怎么实现销售线索,大家的沟通都很容易互相理解。

UL作为业务层面的多态,不仅将业务和技术解耦,避免业务和技术间的沟通鸿沟,而且业务本身也会因为UL变得更加健壮,从而驱动更加健壮的技术实现。

UL作为一种多态,本质上是对业务进行抽象,隔离实现细节,一旦我们习惯了隔离细节而聚焦核心,我们很自然地就会对业务的抽象进行分层比如最顶层的业务抽象可以是 "设计->生产->销售->售后" 这样一个流程,而销售又可以抽象为 "线索->拜访->签约->客情" 这样一个流程,而线索又可以抽象为 "集->分类->派单->跟踪" 这样一个流程。线索流程的变更不影响大的销售流程,销售流程的变更不影响大的公司业务流程。

这种高层业务不被细节干扰的业务抽象,通过UL传递到技术团队,其必然的结果就是技术实现也会出现相应的抽象层次,而正确的抽象恰恰就是解决软件系统复杂性的利器。

DDD中人们往往注重战术模式,忽视战略模式,而在战略模式中,人们又更多关注界限上下文(Bounded Context),忽视UL。如果Bounded Context对应OO中的封装,那么UL就对应了OO中的多态,理解了UL就是业务层面的多态,我们就可以将业务复杂性和技术复杂性解耦,抹平业务和技术的沟通鸿沟,并对业务进行更好的分层抽象,从而更好地应对大型软件的系统复杂性。


Tag: 程序开发
歡迎評論
未登錄,
請先 [ 註冊 ] or [ 登錄 ]
(一分鍾即可完成註冊!)
返回首頁     ·   返回[討論交流]   ·   返回頂部