设计匠艺 | 对象的角色

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

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

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

相关文章

来自专栏腾讯Bugly的专栏

如何定位Obj-C野指针随机Crash(二):让非必现Crash变成必现

继上一篇如何定位Obj-C野指针随机Crash介绍了思路后,这次我们继续看,如何让非必现Crash变为必现。 ? 陈其锋,腾讯SNG即通产品部音视频技术中心软件...

3703
来自专栏编程一生

架构师之路--从原理角度来分析性能

  埃及艳后Cleopatra,很小的时候看过妈妈买的一本书里把她的名字翻译成克娄巴特拉,里面有很多描写她美貌的场景描写。然而这个以美貌著称的奇女子,我看到书里...

642
来自专栏Golang语言社区

Go 的垃圾回收机制在实践中有哪些需要注意的地方?

之前回答问题的时候Go还处在1.1版本,到了1.2和1.3,Go的GC跟踪命令和GC内部实现已经有一些变化,并且根据评论中的反馈,这边一并做补充说明。 Go ...

4036
来自专栏牛客网

C++后台研发工程师2018年BAT华为网易等面经总结

先介绍下个人情况,国内top5本硕科班,英特尔和腾讯两段实习经历,几个项目和还没中的论文QVQ。目前提前批和内推已经基本结束,有意向的offer也有了几个,现整...

453
来自专栏码神联盟

【原创】 元芳,这个BUG,你怎么看?

无论是桌面应用程序、Web应用程序,还是分布式系统和嵌入式系统应用程序等,Java编程语言已经被广泛用于开发各类应用及代码中的复杂功能。 不过在编写代码时,bu...

3309
来自专栏刘笑江的专栏

notes-on-7-concurrency-model-in-7-weeks

1503
来自专栏Python中文社区

基于Redis的Bloomfilter去重

专栏作者简介 九茶 Python工程师,目前居于广州。Github知名开源爬虫QQSpider和SinaSpider作者,经常会在CSDN上分享一些爬虫、数据等...

4098
来自专栏牛客网

【后台开发】百度,头条,腾讯面经

半年了,从七月的迷之自信,到十月的0offer,迷茫、反思、不甘,各位战友的鼓励激励着我前进... 终于拿到了offer,感谢牛客网长期以来的陪伴,在此献上面经...

3935
来自专栏1013310的专栏

基于内容关键性的高效 FEC 抗网络丢包算法

导语 VoIP是基于Internet实时音视频传输的通信业务。丢包是普遍现象,也是影响主观体验最主要的因素。常规方法是构造更多的冗余以便能在丢包后用冗余信息进行...

3959
来自专栏菩提树下的杨过

中小型商城系统中的分类/产品属性/扩展属性的数据库设计

声明:之所以定位在"中小型"商城系统,而非“大型”(指淘宝、拍拍这类巨无霸),理由很简单----我一直都呆在(创业型的)小公司,没见过这些大家伙是怎么设计的:)...

2238

扫描关注云+社区