设计匠艺 | 对象的角色

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

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

在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 条评论
登录 后参与评论

相关文章

来自专栏圣杰的专栏

DDD理论学习系列(12)-- 仓储

1. 引言 DDD中Repository这个单词,主要有两种翻译:资源库和仓储,本文取仓储之译。 说到仓储,我们肯定就想到了仓库,仓库一般用来存放货物,而仓库一...

2177
来自专栏Python攻城狮

人生几何,何不Python当歌

学习Python也有一段时间了,学到了很多,从什么也不懂到入门,现在谈谈python怎么入门。

684
来自专栏Java进阶架构师

dubbo源码解析-详解directory

由于明天还要加班(心疼自己一秒),之前答应过小伙伴每周更新一篇dubbo的源码解析的,鉴于上次讲到了集群容错的总体架构,这次主要讲讲第一个关键词director...

755
来自专栏Kirito的技术分享

设计RPC接口时,你有考虑过这些吗?

RPC 框架的讨论一直是各个技术交流群中的热点话题,阿里的 dubbo,新浪微博的 motan,谷歌的 grpc,以及不久前蚂蚁金服开源的 sofa,都是比较出...

1021
来自专栏CSDN技术头条

Fourinone如何实现并行计算和数据库引擎

彭渊,在Java技术领域从业十多年,曾撰写多款开源软件,历任淘宝高级专家和华为中间件首席架构师。开源代表作有Fourinone(四不像)分布式核心技术框架、Co...

2145
来自专栏Golang语言社区

在 Go 语言中,如何正确的使用并发

从多个花絮中提取,但是如果我斗胆提出主要观点的总结,其内容就是:抢占式多任务和一般共享状态结合导致软件开发过程不可管理的复杂性, 开发人员可能更喜欢保持自己的一...

1002
来自专栏PPV课数据科学社区

大规模爬虫流程总结

爬虫是一个比较容易上手的技术,也许花5分钟看一篇文档就能爬取单个网页上的数据。但对于大规模爬虫,完全就是另一回事,并不是1*n这么简单,还会衍生出许多别的问题。...

27711
来自专栏Java架构师历程

SolrLucene优劣势分析

摘要: 最早lucene2.4以及以前,追溯到2008年前后,lucene刚刚引起大家的关注,到后来Nutch、solr的出现,lucene变得更加热。Nutc...

1154
来自专栏Java架构解析

阿里P7面试经历JAVA总结,技术面,HR面

总体上来看,还是比较注重基础的 尤其是java的多线程和并发安全性及数据库相关,另外对有关开源框架的具体底层实现需要多阅读源码并进行总结。

990
来自专栏数据和云

无微不至:调整_lm_cache_res_cleanup解决Shared Pool 的4031问题

李真旭(Roger) 云和恩墨西北区技术总监 Oracle ACE, ACOUG 核心会员 前不久某客户的一套核心数据库(10.2.0.4.12),据说每间隔...

2957

扫码关注云+社区