领域驱动设计-软件核心复杂应对之道:第九章

9. 将隐式概念转变为显式概念

若开发人员识别出设计中隐含的某个概念或是在讨论中受到启发而发现一个概念时,就会对领域模型和相应的代码进行许多转换,在模型中加入一个或多个对象或关系,从而将此概念显式地表达出来。

9.1 概念挖掘

9.1.1 倾听语言

用户总是不停地谈论报告中的某一项。该项可能来自于各种对象的参数汇编,甚至还可能来自一次直接的数据库查询。同时,应用程序的另一部分也需要这个数据集来进行显式、报告或派生操作。但是,你却一直认为没有必要为此创建一个对象。也许你一直没有真正理解用户所说的某一特点术语时的含义,也没有意识到它的重要性。

倾听领域专家使用的语言。有没有一些术语能够简洁地表达出复杂的概念?他们有没有纠正过你的用词(也就是很委婉地提醒)?当你使用某个特点短语时,他们脸上还流露出迷惑的表情吗?这些都暗示了某个概念也许可以改进模型

9.1.2 检查不足之处

你所需要的概念并不总是浮在表面上,也绝不仅仅是通过对话和文档就能让它显现出来。有些概念可能需要你自己去挖掘和创造。要挖掘的地方就是设计中最不足的地方,也就是操作复杂且难于解释的地方。每当有新的需求时,似乎都会让这个地方变得更加复杂。

9.1.3 思考矛盾之处

不同的领域专家对同样地事情会有不同的看法,这取决于他们的经验和需求。即使是同一个人提供的信息,仔细分析后也会发现存在逻辑上不一致的地方。在挖掘程序需求的时候,我们会不断遇到这种另人烦恼的矛盾,但他们也为深层模型的实现提供了重要线索。有些矛盾只是术语说法上的不一致,有些则是由于误解而产生的。但还有一种情况是专家们会给出相互矛盾的两种说法。

9.1.4 查阅书籍

在很多领域中,你都可以找到解释基本概念和传统思想的书籍。你依然需要与领域专家合作,提炼与你的问题相关的那部分知识,然后将其转化为适用于面向对象软件的概念。但是,查阅书籍也许能够使你一开始就形成一致且深层的认识。

开发人员还有另一个选择,就是阅读由在此领域中有过开发经验的软件专业人员编写的资料。

<分析模式>

9.1.5 尝试,再尝试

9.2 如何为那些不太明显的概念建模

9.2.1 显式地约束

有时,约束很自然地存在于对象或方法中。Bucket(桶)对象必须满足一个固定规则-装载量不能超出它的容积。

class Bucket{
    private float capacity;
    private float contents;
    
    public void pourIn(float addedVolume){
        if(contents + addedVolume > capacity){
            contents = capacity;
        }else{
            contents = contents + addedVolume;
        }
    }
}

class Bucket{
    private float capacity;
    private float contents;
    
    public void pourIn(float addedVolume){
        float volumePresent = contents + addedVolume;
        contents = constrainedToCapacity(volumePresent);
    }
    
    private float constrainedToCapacity(float volumePlaceedIn){
        if(volumePlaceedIn > capacity){
            return capaciity;
        }
        return volumePlaceedIn;
    }
}

下面是一些警告信号,表明约束的存在正在扰乱它的宿主对象(host object)的设计。

1)计算约束所需的数据从定义上看并不属于这个对象

2)相关规则在多个对象中出现,造成了代码重复或导致不属于同一族的对象之间产生了继承关系

3)很多设计和需求讨论是围绕这些约束进行的,而在代码实现中,它们却隐藏在过程代码中

如果约束的存在掩盖了对象的基本职责,或者如果约束在领域中非常突出但在模型中却不明显,那么久可以将其提取到一个显式地对象中,甚至可以把它建模为一个对象和关系的集合。

案例:运输业务,超订策略

预定超出运输能力10%的货物,这种程度的超额可以弥补因客户临时取消订单而给货运公司所带来的损失,这样就可以保证货轮基本能够满载起航了。

image

9.2.2 作为领域对象的过程

首先要说明的是,我们都不希望过程变成模型的主要部分。对象是用来封装过程的,而我们只需考虑对象的设计目的或意图就可以了。

我们必须在模型中把这些过程表示出来。否则当这些过程显露出来时,往往会使对象设计变得笨拙。

如果过程的执行方式有多种方式,那么我们也可以用另一种方法来处理它,那就是将算法本身或其中的关键部分放到一个单独的对象中。这样,选择不同的过程就变成了选择不同的对象,每个对象都表示一种不同的strategy。

过程是应该被显式表达出来,还是应该被隐藏起来呢?区分的方法很简单:它是经常被领域专家提起呢,还是仅仅被当做计算机程序机制的一部分?

"规格 specification"提供了用于表达特定类型的规则的精确方式,它把这些规则从条件逻辑中提取出来,并在模型中把它们显式地表示出来。

9.2.3 模式:specification

image
Invoice发票,检测发票是否过期,拖欠票据发出催款通知,另一些发给收账公司,还要考虑客户的付款历史记录、公司在不同产品线上的政策等等。Invoice作为付款请求是明白无误的,但它很快就会消失在大量杂乱的规则计算代码中。Invoice还会发展出对领域类和子系统的各种依赖关系,而这些领域类和子系统却与Invoice的基本含义无关。

主要目标都是完全用对象来实现逻辑

业务规则通常不适合作为entity或value object的职责,而且规则的变化和组合也会掩盖领域对象的基本含义。但是将规则移出领域层的结果会更糟糕,因为这样一来,领域代码就不再表达模型了。

逻辑编程提供了一种概念,即"谓词"这种可分离、可组合的规则对象,但是要把这种概念用对象完全实现是很麻烦的。同时,这种概念也非常笼统,在表达设计意图方面,它的针对性不如设计那么好。

它们都是一些小的事实测试,可以提取到一个单独的value object。而这个新对象则可以用来计算另一个对象,看看谓词对那个对象的计算是否为“真”
image
换言之,这个新对象就是一个规格。specification中声明的是限制另一个对象状态的约束,被约束对象可以存在,也可以不存在。specification有多种用途,气其中的一种用途体现了最基本的概念,这种用户就是:specification可以测试任何对象以检验他们是否满足指定的标准。

为特殊目的创建谓词形式的显式的value object。specification就是一个谓词,可用来确定对象是否满足某些标准。
image
specification的基本概念非常简单,这能帮助我们思考领域建模问题。但是模型驱动设计要求我们开发出一个能够把概念表达出来的有效实现。要实现这个目标,必须要更深入地挖掘应用这个模式的方法。

只要切当地应用模式,就可以得出一整套如何解决领域建模问题的思路,同时也可以从这种长时间搜寻有效实现的经验中受益。

9.2.4 specification的应用和实现

specification最有价值的地方在于它可以将看起来完全不同得应用统一起来。

1)验证对象,检查它是否能满足某些需求或者是否已经为实现某个目标做好了准备

2)从集合中选择一个对象(如上述例子中的查询过期发票)

3)指定在创建新对象时必须满足某种需求
image

class DelinquentInvoiceSpecification extends InvoiceSpecification{
    private Date currentDate;
    
    public DelinquentInvoiceSpecification(Date currentDate){
        this.currentDate = currentDate;
    }
    
    public boolean isStatisfiedBy(Invoice candidate){
        //宽限期
        int gracePeriod = candidate.customer().getPaymentGracePerior();
        Date firmDeadline = DateUtility.addDaysToDate(candidate.dueDate(),gracePeriod);
        return currentDate.after(firmDeadline);
    }
    
    //假设销售人员看到一个欠账客户的信息时,系统需要显示一个红旗标识。只需要在客户类中编写一个方法即可
    public boolean accountIsDelinquent(Customer customer){
        Date today = new Date();
        Specification delinquentSpec = new DelinquentInvoiceSpecification(today);
        Iterator it = custmer.getInvoices().iterator();
        while(it.hasNext()){
            Invoice candidate = (Invoice)it.next();
            if(delinquentSpec.isStatisfiedBy(candidate)){
                return true;
            }
        }
        return false;
    }
}

选择(或查询

验证是对一个独立对象进行测试,检查它是否满足某些标准,然后客户可能根据验证的结论来采取行动。另一种常见需求是根据某些标准对对象集合中选择一个子集。

public Set selectStatisfying(InvoiceSpecification spec){
    Set results = new HashSet();
    Iterator it = invoices.iterator();
    while(it.hasNext()){
        Invoice candidate = (Invoice)it.next();
        if(spec.isStatisfiedBy(candidate)){
            results.add(candidate);
        }
    }
    return results;
}

Set delinquentInvoices = invoiceRepository.selectSatisfying(new DelinquentInvoiceSpecification(currentDate));

根据要求来创建(生成)

五角大楼需要一架新的喷气式战斗机,政府官员会先编写规格。在规格中可能会要求这家喷气机的速度达到2马赫,航程1800英里,并且成本不高于5000万美元等等。无论规格有多详细,它都不是飞机的设计,更不是飞机本身。航空航天工程公司将接收这份规格并且据此创建出一个或多个设计。各个竞争公司可能会提出不同的设计,所有这些方案都需要满足原始规格。

posted @ 2023-06-05 09:11  LHX2018  阅读(23)  评论(0编辑  收藏  举报