领域驱动设计(4)面向深层理解的重构

迄今为止我们已经讨论过了领域以及创建表现领域的模型的重要性了。
我们给出了一些可以用来创建有效模型的技术指南。模型现在已经跟它所源自的领域紧密关联了。
我们也已经说过代码设计应该围绕模型展开,模型自身也会基于设计决定而有所增进。
脱离了模型的设计会导致软件不能反映它所服务的领域,甚至可能得不到期望的行为。
建模如果得不到设计的反馈或者缺少了开发人员的参与,会导致必须实现模型的人很难理解它,并且可能对所用的技术不太适合。
在设计和开发过程中,我们需要一次次得停下来,查看代码。这意味着到了重构的时间了。
重构是不改变应用行为而重新设计代码以让它更好的过程。重构通常是非常谨慎的,按照小幅且可控的步骤进行,这样我们就不会破坏功能或者引入某些bug了。
毕竟,重构的目的是让代码更好而不是更坏。自动化测试可以为我们确保未破坏任何事情提供很大的帮助。
代码重构有很多种方式,甚至存在重构模式。这些模式描述了一个重构的自动化方法。
有些基于这些模式的工具可以让开发人员的生活比以前更容易,缺少了那些工具的支持,重构起来会非常困难。
这类重构更多处理的是代码和它的质量。
还有另一种类型的重构,跟领域和它的模型相关。有时会对领域有新的理解,有些事物变得更清晰,或者两个元素间的关系被发现。
所有的这些会通过重构被包括到设计中。让拥有表现力的代码更易读和理解是非常重要的。
通过阅读代码,我们可能不光了解代码是什么,同时了解它为什么要这样。只有这样才能让代码真正捕获模型的主旨。
基于模式的技术性重构,可以被组织并结构化。
面向更深层理解不能按照同样的方式进行。我们不能为它创建模式。
模型的复杂性和模型的多变性不可能提供让我们按照机械化的方式进行建模的方式。
一个好的模型产生于深层的思考、理解、经验和才能。
我们被教教授的关于建模的第一件事是阅读业务规范,从中寻找名词和动词。
名词被转换成类,而动词则成为方法。这是一种简化,将产生浅层次的模型。
所有的模型开始时都缺乏深度,但我们可以面向越来越深的理解来重构模型。
设计必须灵活,僵硬的设计很难做重构。头脑中若没有对代码建立灵活性的概念,那么这样的代码就会很难维护。
当需要发生变化时,你会看到代码与你交锋,会在原本应该很容易重构的事情上花费很多的时间。
使用经过验证的基础构造元素并使用一致的语言将给开发工作带来成效。
这会减少不良模型所带来的挑战,我们可以捕获到领域专家非常关心的内容,并且以此来驱动实际的设计。
一个忽略表面内容且捕捉到本质内涵的模型是一个深层模型。这会让软件更加和领域专家的思路合拍,也更能满足用户的需要。
从传统意义上讲,重构描述的是从技术动机的代码转换。重构同样可以由对领域的深入理解,以及对模型及其代码表达进行相应的精化所推动。
除非使用迭代的重构过程,加上领域专家和开发人员一起密切关注对领域的学习,否则一个极其复杂的领域模式是很难开发出来的。

凸现关键概念

重构是小幅度进行的,其结果也必然是一系列小的改进。
有时,会有很多次小的变更,每次给设计增加非常小的价值,有时,会有很少的变更,但造成很大的差异。这就是突破。
我们会从一个粗糙的、浮浅的模型开始,然后基于对领域的深层理解以及对关注点的理解来细化它和设计。
我们会对它增进新的概念和抽象,然后执行设计的重构。每一次精化会让设计更清晰。这就建立好了突破的前提。
突破常包括思维上的变化,如同我们理解模型一样。它也是项目重要进程中的一个发起者,但它也有一些缺点。
突破可能隐含了大量的重构。这意味着需要时间和资源,而这两者看上去从来都不够。
它也是有风险的,因为大量的重构会在应用中引入行为上的变化。
为达到一次突破,我们需要让隐式的概念显式化。
当我们跟领域专家交谈时,我们交互了很多的思想和知识。某些概念成为了通用语言,但也有些一直未被重视。
它们是隐式概念,用来解释以及在领域中的其他的概念。在精化设计的过程中,某些隐式概念吸引了我们注意力。
我们发现它们当中的某些在设计中担当了重要的角色。
因此我们需要将这些概念显式化。我们应该为它们建立类和关系。
当它们发生时,我们就拥有了突破的机会。
隐式概念可能不会仅于此。如果它们是领域概念,它们可能被引入模型和设计中。我们应该如何识别它们呢?
第一种发现隐含概念的方式是倾听话语。
我们在建模和设计过程中使用的话语包含了许多关于领域的信息。开始时可能不会太多,或者某些信息没有被正确地使用。
某些概念可能不能被完全理解,或者理解完全是错误的。这是在学习一个新的领域所必须经历的。
但因为我们构建了我们的通用语言,关键概念会被加入其中。在那里我们可以开始查找隐含概念。
有时设计的部分可能不会那么清晰,一堆关系会让路径的计算变得难以进行,或者计算的过程会复杂到难以理解。
这在设计中显得极其笨拙,但这也是寻找隐藏的概念的绝佳之所,可能我们错过了什么。
如果某个关键概念在破解谜团时缺失了,其他的事物就不得不替代它完成它的功能。
这会让某些对象变胖,给它们增加了一些本不应该属于它的行为。设计的清晰度受到了破坏。
努力寻找是否有缺失的概念,如果找到一个,就把它显式化。对设计进行重构让它更简单更具柔性。
当我们构建知识时很可能会遭遇到矛盾。某个领域专家所讲的看上去跟另一个领域专家所持的观点发生了矛盾。
一个需求可能看上去跟另一个需求矛盾。某些这样的矛盾其实不是真正的矛盾,只是因为看待同一事物的方式不同,或者只是因为讲解时缺乏了精确度。

我们应该努力去解决矛盾,有时这确实会帮助我们发现重要的概念。即使并没有发现它们,能够保持所有事物清晰也是很重要的。
其他明细的挖掘模型概念的方式是使用领域文献。现在有众多为几乎任何可能的主题而编写的书,它们包含了许多关于特定领域的知识。
这些书通常不包含要展现的领域的模型,它们包含的信息需要进一步处理、精炼并细化。
但是,在书中发现的信息是有价值的,会给我们提供对领域的深层视图。
在让概念显式化时,还有其他一些非常有用的概念:约束、过程和规约。
约束是一个很简单的表示不变量的方式。无论对象的数据发生了变化,都要考虑不变量。
这可以简单地通过把不变量的逻辑放置在一个约束中去实现它。
下面是一个简单的案例,其目的是为了解释这个概念,而不是为了表现对相似案例的建议方法。

image

我们可以向一个书架添加书籍,但是我们永远不能增加超过它容量
的部分。这儿可以被视为书架行为的一部分,见下面的java 代码。
public class Bookshelf
{
    private int capacity = 20;
    private Collection content;
    public void add(Book book)
    {
        if(content.size() + 1 <= capacity)
        {
            content.add(book);
        }
        else
        {
            throw new IllegalOperationException(“The bookshelf has reached its limit.”);
        }
    }
}
我们可以重构这个代码,将约束提取为一个单独的方法。
public class Bookshelf
{
    private int capacity = 20;
    private Collection content;
    public void add(Book book)
    {
        if(isSpaceAvailable())
        {
            content.add(book);
        }
        else
        {
            throw new IllegalOperationException(“The bookshelf has reached its limit.”);
        }
    }
    private boolean isSpaceAvailable()
    {
        return content.size() < capacity;
    }
}

将约束置于一个单独的方法让它显示化有很多优点。它很容易阅读,并且每个人都会注意到add()方法从属于这个约束。

如果约束变得更复杂,这可以为向该方法增加更多逻辑提供增长空间。

过程通常在代码中被表现为procedure。从我们开始使用面向对象语言后我们就不再用一个过程化的方法,所以我们需要为过程选择一个对象,然后给它增加行为。

最好的实现过程的方式是使用服务。其他的处理过程的不同的方式如,将算法封装进一个策略对象。

并不是所有的过程都必须显式化。如果通用语言中提到了某个过程,那就是将它显式实现的时机了。
我们在此要介绍的最后一个将概念显式化的方法是规约。

简单得说,规约是用来测试一个对象是否满足特定条件的。
领域层包含了应用到实体和值对象上的业务规则。那些规则通常与它们要应用到的对象合成一体。

在这些规则中,某些只是用来回答“是”和“否”的一组问题的,某些规则可以被表现成一系列操作布尔值的逻辑上的操作,最终结果也是一个布尔值。

一个这样的案例是在一个客户对象上执行测试,看它是否符合特定的信用条件。
这个规则可以被表现成一个方法,起名叫isEligible(),并且可以附加到客户对象上。

但这个规则不是严格基于客户的数据进行操作的一个简单的方法。评估规则涉及到验证客户的信用,检查他过去是否支付过他的债务,检查他是否具有足够的余额等。

这样的业务规则可能非常的大,非常复杂,让对象的功能肿胀,不再满足其原始的目的。

在这种情况下我们可能会试图将整个规则移动到应用层,因为它看上去已经超越了领域层了。实际上,到了重构的时候了。
规则应该被封装到一个负责它的对象中,这将成为客户的规约,并且被保留在领域层中。新的对象将包含一系列布尔方法,这些方法用来测试一个客户对象是否符合某种信用。

每一个方法担负了一个小的测试的功能,所有的方法可以通过组合对某个原始问题给出答案。

如果业务规则不能被包含到一个规约对象中,对应的代码会遍布到无数的对象中,让它不再一致。
规约用来测试对象是否满足某种需要,或者他们是否已经为某种目的准备完毕。它也可以被用来从一个集合中筛选一个特定的对象,或者作为某个对象的创建条件。
通常情况下,一个单个的规约负责检查是否满足一个简单的规则,若干个这样的规约组合在一起表现一个复杂的规约,例如:

Customer customer = customerRepository.findCustomer(customerIdentiy);

Specification customerEligibleForRefund = new Specification(new CustomerPaidHisDebtsInThePast(),new CustomerHasNoOutstandingBalances());
if(customerEligibleForRefund.isSatisfiedBy(customer)
{
    refundService.issueRefundTo(customer);
}

测试简单的规则变得非常简单,只需阅读这段代码,这段代码很明显地告诉我们一个客户是否符合偿还条件的真正含义。

posted on 2011-12-20 14:17  zhouyonghua0520  阅读(474)  评论(0编辑  收藏  举报

导航