设计匠艺 | 对象的角色

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

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

在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。如下图所示:

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

  • 为了应对创建的变化;
  • 为了隐藏对象创建的复杂逻辑;
  • 为了控制对象创建的时机或数量。

服务提供者角色

关于服务提供者,一个重要认识是:它能提供具有“业务价值”的行为。所谓“业务价值”,即一定是实现业务逻辑中不可缺少的,且相对独立完整的功能。这就意味着,担任服务提供者角色的对象,常常是一个职责完备地,实现了某个业务关注点的可重用对象。此外,业务价值是有层次之分的。在最外层,可能意味着一个完整的业务流程,此时服务对象暴露给客户端的,是一个封装了服务实现细节的对象(可能是接口);而为了实现该外层服务,又可能在整个实现中,需要更为细粒度的内层服务对象提供各个实现步骤的支撑。例如系统需要生成税务报表,假设它的业务流程是读取报表数据后,对数据流进行处理,并以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风马牛不相及,但它仍可以看做是控制者角色。

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

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2014-12-02

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏一“技”之长

iOS开发之AdSupport框架使用

    AdSupport从字面意思上理解是用来进行广告支持,这个框架十分简单,里面只有一个类,类中只有一个方法和两个属性。

1132
来自专栏编舟记

架构整洁之道导读(二)

我是《架构整洁之道》(Clean Architecture) 中文版的技术审校者,在审校的过程当中略有感悟,所以希望通过撰写导读的方式分享给大家。

912
来自专栏CDA数据分析师

一个 Pythoner的 Awesome List

? 从大三接触 Python 到现在几乎已经有两年的接触经验了,除去中间有一年左右接私活写写 Android 和 Lamp 之外,有 Python 实际项目开...

2196
来自专栏养码场

一位资深Java的阿里系公司实战面试经验,套路还是面试官的多

占小狼:一位奋斗在魔都的资深Java开发。去年6月在简书上发第一篇技术文章,已坚持发表76篇技术文章,粉丝数突破4000。

1727
来自专栏Web 开发

如何把捏前端模板颗粒度

今晚看到一篇博文,其原文是讲AngularJS的模板的,但觉得该作者讲的很多思路,不仅仅是AngularJS适用。凡是想在前端进行模板组织的,都可借鉴,故写下读...

650
来自专栏Java帮帮-微信公众号-技术文章全总结

Java面试复习大纲2.0(持续更新)

想要成为合格的Java程序员或工程师到底需要具备哪些专业技能,面试者在面试之前到底需要准备哪些东西呢?本文陈列的这些内容既可以作为个人简历中的内容,也可以作为面...

3627
来自专栏领域驱动设计DDD实战进阶

DDD实战进阶第一波(三):开发一般业务的大健康行业直销系统(搭建支持DDD的轻量级框架二)

了解了DDD的好处与基本的核心组件后,我们先不急着进入支持DDD思想的轻量级框架开发,也不急于直销系统需求分析和具体代码实现,我们还少一块, 那就是经典DDD的...

3796
来自专栏机器学习原理

知识图谱api调用

Wiki和google连不上网,这里中重点试了试CN-Dbpedia,比如,我想找一下苹果公司这个实体的三元组信息;

1572
来自专栏机器学习算法与Python学习

Python:10篇不可错过的~热文~》》真的很热》》

以下是精选了“ Python开发者” 5月份的10篇 Python 热文。其中有基础知识,项目实战等。 《Python 爬虫建站入门手记(1):环境搭建》 本文...

3133
来自专栏开发与安全

在腾讯实习的那段日子:不要在难受的时候选择 '逃避/离开'

时间过得很快,从2014.6.5入职实习到2015.1.5已经是7个月的时间了,在这边还是学到了很多东西,遇到的人大多数比较nice。中间拿到了留任offer,...

2000

扫码关注云+社区