逸言

对象的角色

| Comments

若要获得良好的对象设计,就必须对职责进行合理的分配。每个对象承担的职责不能太多,也不能太少,恰如其分即可。职责分配如乐谱中对音符的组织,高明的音乐家总是能让不同的音符放在合理的位置,奏成悦耳的心曲,表达音乐家的内心感情。然而,即使设计师明确职责分配的重要性,在面临纷乱复杂的需求时,常常被乱花迷了眼,或者无法识别正确的职责,又或者顾此失彼,将职责放错了位置,变成了对职责混乱的涂鸦。

要识别职责,进而合理分配职责,有许多秘诀,或云“技巧”。不过,将对象的角色作为职责分配的开始,不失为一个好的起点。角色是对象的身份,若以拟人化的方式思考对象世界,就可以设想:究竟是怎样的身份,需得承担怎样的职责,才会与其身份相当,不至于乱了规矩。红楼梦中,刘姥姥进了大观园,出尽了洋相,就是因为身份失当;又可以想想倘若林黛玉像尤三姐那般爱恨分明,也不至于见花落泪,惹人爱怜了。故而在分配职责时,我们能首先明确对象的角色,即可将思想带入到这一角色中,设身处地,推断这一角色可以或者必须承担哪些职责。

Object Design:Roles, Responsibility, and Collaborations一书中,将对象的角色分为了五种,分别为信息持有者、构造者、服务提供者、协调者和控制者。这种分类差不多涵盖了对象在软件系统中扮演的角色。以此为基础,在进行软件设计时,可以思考你要设计的对象,究竟属于哪一种角色。

信息持有者角色

首先来看信息持有者。顾名思义,这种角色的对象必然持有相关的信息。不过,俯瞰对象世界,除了某些特殊的行为对象而言,大多数对象都必然持有相关的信息。所以,这里的角色划定,其主要意图在于让设计者明确,与信息相关的行为,如处理信息的方式,信息变化造成的影响等,都应首先考虑是否由该信息的持有者来承担。这近似于Larman在Applying UML and Patterns一书中提到的“信息专家模式”。

例如,我们需要设计一个Web服务器,它提供了一个对象HttpProcessor,能够接收由HttpConnector发送来的Socket请求,对Request进行处理,并在处理后将相关信息放入Response中。请求和响应被封装在对应的HttpRequest和HttpResponse对象中。在处理请求和响应信息时,需要对Socket消息进行处理,并为Request和Response对象设置相关属性。我们当然可以在HttpProcessor中处理对这些消息的解析工作,但涉及到Request和Response自身的信息,遵循信息持有者角色的要求,最好还是将这些处理逻辑封装到各自对象中。如下图所示:

遵循信息持有者的特征,HttpProcessor、HttpRequest与HttpResponse之间的权责变得更加清晰。此外,这一设计方式还有利于改善性能。某些Http请求解析可能牵涉到系统开销较大的字符串操作,而解析的内容并不是在一开始就需要使用。将解析职责转移到HttpRequest中,就使得HttpProcessor的process()操作可以快速完成,并将相关请求数据流塞到HttpRequest对象中。只有真正需要相关请求信息时,才向HttpRequest对象发出解析的请求消息。这种方式颇像是对象的Lazy Load。

构造者角色

构造者角色主要承担对象的创建,以及对复合对象的组装。如果熟悉设计模式,可以发现构造者角色基本上囊括了构造型模式的意图。例如创建对象,组合对象,以及选择对象构造的方式。此外,还有一种特殊的构造者角色对象,即它可能具有双重角色,一方面作为构造者角色,另一方面也作为构造者所创建出来的产品。这种双重角色的构造者角色,常常会形成一条构造链。例如,在JMS中,若要获得Queue对象,就可能由ConnectionFactory对象创建出Connection对象,则通过该对象创建Session对象,最后由Session对象创建的Queue。如下图所示:

为何我们需要构造者角色?毕竟对象自身可以拥有构造函数,以提供给调用者完成对象的创建。通常情况下,之所以引入构造者角色,主要是为了:

(1)为了应对创建的变化;

(2)为了隐藏对象创建的复杂逻辑;

(3)为了控制对象创建的时机或数量。

服务提供者角色

关于服务提供者,一个重要认识是:它能提供具有“业务价值”的行为。所谓“业务价值”,即一定是实现业务逻辑中不可缺少的,且相对独立完整的功能。这就意味着,担任服务提供者角色的对象,常常是一个职责完备地,实现了某个业务关注点的可重用对象。此外,业务价值是有层次之分的。在最外层,可能意味着一个完整的业务流程,此时服务对象暴露给客户端的,是一个封装了服务实现细节的对象(可能是接口);而为了实现该外层服务,又可能在整个实现中,需要更为细粒度的内层服务对象提供各个实现步骤的支撑。例如系统需要生成税务报表,假设它的业务流程是读取报表数据后,对数据流进行处理,并以HTML格式呈现,然后生成PDF文件。对外而言,税务报表的生成就应该是一个完整的服务,且对于客户端的调用者而言,其实根本不需要了解其实现细节。此时,我们可以定义一个TaxReportGenerator对象,它能够接收报表数据,并生成报表的PDF文件。显然,它具有非常重要的业务价值。

接下来考虑该对象的内部实现。由于报表生成需要执行多个业务步骤,如果将这些职责均交给TaxReportGenerator来处理,无疑会导致该对象承担过重的职责。此外,呈现HTML格式与PDF文件生成对于报表生成而言,不过是整个业务流程中的一环;但从单个职责而言,无疑它们也是独立的。可以设想,倘若系统还有其他业务功能需要生成PDF文件,又或者需要按照规定形式呈现为HTML页面,将这些职责封装到单独的职责中,就可能很好地支持重用。从“业务价值”的角度看,它们无疑具备了服务提供者的能力。整个TaxReportGenerator对象的内部协作如下图所示:

协调者角色

协调者有些像设计模式的Mediator模式所要承担的职责,即用于协调对象职责的协作,又或者负责转发或委派请求。协调者是孜孜不倦助人为乐的居委会大妈,既善于也乐于协调邻里之间的纠纷。除了可以以中间人的身份协调对象,从而简化对象之间的协作,以降低复杂的依赖关系外,协调者还能很好地隐藏这些交互细节。这就使得调用者变得简单,还能让这种关系协调的实现集中在一处,即使将来协调关系发生了变化,也可以做到仅修改一处,即可应对变化。从这一点来看,似乎协调者又体现了Facade模式。

在一个大型复杂系统中,提供了许多Web Service。不同的Web Service可能需要支持不同的消费者,而这些服务的部署位置也可能并不相同。消费者需要准确定位到相关服务,然后通过一些相对复杂的实现逻辑,完成对服务的调用。这类逻辑就牵涉到消费者、服务以及服务调用与服务位置之间的协作。如果没有合适的对象去封装,既可能导致细节暴露,增加复杂度,也无法做到有效重用。一旦协作的逻辑发生变化,可能还会导致这种变化蔓延到系统的各个地方。这时,就是体现协调者角色价值所在了。在这个场景下,我们可以引入ServiceLocator对象来负责整个协调逻辑,它能够根据消费者请求的服务类型,定位服务,然后找到服务端口,发送服务请求。下图展示了这种协调逻辑的具体做法,注意不同的服务消费者都经由相同的ServiceLocator角色完成了不同的服务调用:

控制者角色

看到控制者,或许我们会想到MVC模式的Controller。确乎它们具有相似的特性,即用于控制多个对象之间的交互,甚至是驱动对象。或者,我们可以将这里所谓的控制者角色,看做是Controller的外延,即它具有更加宽泛的职责意义。凡是需要控制角色交互,并具有一定控制逻辑的对象,均可看做为控制者角色。注意,控制者角色与协调者角色的区别,最为明显的区别在于前者多少具有一定的管理特征,被控制的对象似乎在级别上低于控制者角色;而后者则体现一种平等的层级关系。前者是政府官员,后者是居委会大妈。

当然,在设计时,有时似乎也很难泾渭分明地界定这二者。这就好似用例中的使用与扩展关系,许多设计者还在孜孜以求,绞尽脑汁地要分辨出二者的不同,以保证正确地运用用例关系,求得完美的设计,孰知早有用例专家给出忠言,不必一定区分使用与扩展,它对用例的编写不会产生直接的重大影响。参考此例,我也希望设计师不必去钻牛角尖,只需明白此两种角色,其本质还在于隐藏对象的协作或交互细节,降低复杂度,保证重用以及对变化的应对。

在软件设计中,我们经常遇到控制者角色。一个常见的例子是由控制者角色承担判断逻辑,根据不同的请求,经由不同的分支,调用不同的对象来应对此请求。例如在一个系统中,我们需要对页面的内容合法性进行验证。不同的内容对验证的要求不尽相同。一个简单的判断是看内容是否只需要对页面头进行验证。我们在设计时,引入了ValidationProcessor来控制这种验证逻辑。站在调用者的角度,验证的事情交给ValidationProcessor去处理就好。管它是否仅是一个控制枢纽,真正的验证却是它要委派的对象呢?

当然,在这里的ContentController同样属于控制者角色,它事实上就是MVC模式中的Controller,用于控制Content与ContentView之间的交互。ValidatorProcessor与MVC风马牛不相及,但它仍可以看做是控制者角色。

如果我们能识辨出系统模型中各种对象的角色,就可以根据角色的特征来分配角色。又或者,我们可以根据角色来判别现有的职责分配是否合理,是否均衡,甚至能够帮助我们找到缺失的对象。除了信息持有者角色,其余四种角色通常不会出现在领域模型中,它们事实上都属于设计对象。但它们在软件设计中的地位却举足轻重,没有它们,设计就可能走向混乱,无法保证重用性与扩展性,并导致系统对象之间的协作变得复杂。每当我们在分配职责时,若有顾此失彼的感觉存在,就可能说明缺乏了承担不同角色作用的这一类设计对象。找到它,并给它以承担职责的权利,设计一定会大为改观。

Comments