首页 > 代码库 > 角色对象模式

角色对象模式

意图

单个对象透过不同的角色对象来满足不同客户的不同需求。每一个角色对象针对不同的客户内容来扮演其角色。对象能够动态的管理其角色集合。角色作为独立的对象是的不同的内容能够简单的被分离开来,系统间的配置也变得容易。

 

译注:为了行文的流畅性及内容意思的准确性,尽量贴近原文使用英文单词标记特定内容, 如Customer表示客户,Client表示客户端,Component表示组件等。因为有各种图例说明,所以在图例说明时,使用原题中的英文单词对应图中内容。有时也中英文交叉使用。因为网页显示的问题,中文黑体使用绿色标注。

 

动机

面向对象系统是基于一个关键抽象集。每一个关键抽象是为一个基于抽象状态和行为类的模型化。这种情况,适合于较小的程序。但是,一旦我们通过整合不同程序的方式扩展我们的程序,我们不得不在我们的关键抽象中处理好不同的客户端对其特定内容视图(Context View)的需求。

假设我们开发一个对银行投资部门的支持软件。其中一个关键抽象为Customer(客户)这个概念。因此,我们的设计模型应该包含一个Customer类。这个类接口能够管理客户的私人信息,如姓名、地址、存款、支出等。

现在假设银行的放贷部门也需要软件支持。现在看起来我们设计的客户类不足以完成一个借款者的角色。很明显,需要提供进一步的实现状态及操作来管理客户的借贷账户、贷记、证券。

在同一个类中整合不同的环境视图导致关键抽象扩展为臃肿的接口。这样的接口不仅理解起来很困难,而且维护性差。此外,不恰当的变动系统若不能恰当地处理,甚至可能触发大量的重编译。针对特定的客户的类接口部分的变动,很可能影响甚至损害其他子系统或者程序中的客户端。

一个简单的解决方式是扩展Customer类,添加BorrowerInvestor子类,他们分别处理借贷者部分和投资者的部分。如果从对象的身份视角来看,子类化意味着不同子类的两个对象是不一样的客户。一个客户可以使用两个身份来扮演各自借贷者以及投资者角色。身份证明(Identify)仅能通过额外的机制来模拟(译者:因借贷者和投资者其实是一个人,只需要一个身份证)。如果两个对象的身份是一致的,他们所继承的属性始终需要一致性检查。然而,在多态搜索中此类问题就无法避免的,例如,当我们想构建一个系统中客户列表时,同样的Customer对象将会重复出现,除非我们消除这种重复。

角色对象(Role Object)模式建议把一个对象的特定内容视图拆分成角色对象角色对象能过动态的从核心对象(Core Object)中添加和移除。我们把这种由一个核心及角色对象构成的相关的组合对象结构称作主语(Subject)。一个主语通常扮演不同的角色并且同一个角色也很可能是在不同的主语中。比如两个客户可以独立扮演借贷者以及投资者就角色。这两个角色都是在单个的客户对象主语中完成工作。


图1:在银行环境中的客户体系

一个关键抽象(key abstraction),比如Customer,被定义为要一个抽象超类,仅看做一个纯粹的接口,里面没有定义任何实现状态。Customer类具体说明其处理客户地址、账号以及定义了最小的协议来管理角色。CustomerCore子类实现了Customer接口。

CustomerRole为特定内容的角色提供超类,并且提供Customer接口支持。CustomerRole类是一个抽象类并且不能被实例化。CustomerRole的具体子类,例如借贷者以及投资者,定义并实现了针对特定角色的接口。它们仅是是能够在运行时实例化的子类。借贷者的特定内容视图由放贷部门定义,它定了额外的才做来管理客户的贷记以及证券。类似的,投资者类增加具体的操作来表达投资部门对于客户的视图。


图2:角色对象模式中一个对象的示意图

一个客户端,比如借贷程序,通过Customer接口类或者与CustomerCore类的对象一起工作,也可能是与CustomerRole具体子类一起工作。假设一个借贷程序通过Customer接口了解到一个特定的客户实例,借贷程序想知道这个Customer对象是否是借贷者角色,它调用hasRole()方法,来确认此对象是否是满足借贷者的角色定义。通常使用字符串来命名角色,如果Customer对象能够扮演所命名的“Borrower”借贷者角色,借贷程序将要求它返回一个相关联的对象引用,借贷程序现在就可以使用这个对象引用来完成借贷者相关的操作。


可用性

使用角色对象模式,如果

*   你想处理在不同内容中的一个关键抽象,但是你不愿意让所有针对特定内容的接口都在同一个接口类中。*   你想动态的处理可用角色,那样你能够在需要的时候添加或者删除。即使是在运行时,而不是把他们固定在编译时。*   你想让扩展的过程变得透明,并且需要保证相关对象体系中的对象身份逻辑。*   你想保持角色、客户端相互独立,这样的话对于角色的变化不会影响到客户端,因为客户端对角色不感兴趣。

不要使用这种模式在

*   如果潜在的角色具有强烈的相对独立性。

这些就是使用角色的设计变体。有一个关于使用这些变体的指导Fowler97


结构

下列图片为角色对象模式的示意图


图3:角色对象模式结构示意图


组成部分

  • Component(Customer)(客户)组件
    • 通过定义其接口模型化了一个特殊的关键抽象
    • 申明了角色对象需要用的添加、移除、测试、查询协议。一个客户端提供了一个对于具体角色子类的具体需求,它使用字符喘传来定义。
  • ComponentCore(CustomerCore)
    • 实现了一个Component接口,包括角色管理协议;
    • 创建了具体角色实例;
    • 管理其角色对象;
  • ComponentRole(CustomerRole)
    • 存储一个被装饰的ComponentCore的引用;
    • 通过递呈请求到它的core上,实现了一个Component接口
  • ConcreteRole(Investor, Borrower)
    • 模型化并且实现了Component接口的特点内容扩展;
    • 使用ComponentCore作为参数可以被实例化;

协作

核心对象core object 以及角色对象role object的协作如下:

  • ComponentRole递呈求到它的ComponentCore对象上;
  • ComponentCore实例化并且管理具体角色;

一个客户端client与角色对象及核心对象交互如下:

  • 客户端使用角色扩展核心对象。这样,它能够使用具体的对象来描述它期望的角色;
  • 客户端任何时候都是在指定的方式与core交互,完成其工作。如果客户端需要某个角色,从核心对象中请求这个角色。如果这个core对象正好扮演了被请求的角色,它就返回自己的给客户端;
  • 如果核心对象不是扮演说请求的角色,一个错误被抛出。核心对象永远不会独立地创建角色对象;

结果

角色对象模式有如下优点及重要性:

  • 关键抽象定义简洁。Component接口很好的关注了被模型化的关键抽象的本质的状态及行为,他不会因为特定的角色接口的扩展变的臃肿;
  • 角色演化很简单并且角色之间时相互独立的。扩展一个Component接口很容易,因为没有必要改变ComponentCore类。一个具体角色类让你可以添加新的角色以及角色实现并且能够保护关键抽象自身;
  • 角色对象可以动态的添加或移除。一个角色对象在运行时简单地添加或者删除与核心对象。因此,在给定环境中需要的对象是可以实时创建的。
  • 程序之间解耦。通过从角色中准确的分离出Component接口,基于角色扩展的程序需要紧耦合的地方减少了。使用Component接口以及特定的具体角色类的程序A(ClientA)不需要知道被用于*程序*B(ClientB)的具体角色类;
  • 类组合爆炸通过使用多继承得到避免。此模式避免了类组合爆炸,因为它通过多继承来组合不同的角色到一个类中。

角色对象模式的缺点或不利条件:

  • 客户端变的更加复杂。相比使用Component接口,通过对象的ConcreteRole类来与对象一起工作,具有较小的代码量。客户端会在具体问题中检查对象扮演的角色,如果通过,客户端需要为这个角色进行查询,如果没通过,client针对其特定需要负责扩展核心对象,来使得核心对象可以扮演需要的角色;
  • 不同角色间的维护约束变的困难。因为由那些变化又相互依赖的对象组成的主语的维护性约束以为为了维护全部主语的的一致性的需求就变得困难。在现实部分我们将讨论几个产生的问题。
  • 在角色中的角色不能被类型系统强制执行。你可能会想通过角色结合的的方式来排除角色到核心对象上,或者确定的角色依赖于其他的的一些角色。但是在角色对象模式中,你不能依赖类型系统为你强制执行约束,你不得不使用运行时来检查。
  • 维护对象身份变得困难。核心对象以及来至于概念单元的角色实例应该都有其自身的概念身份。技术上的对象身份可以通过编程语言来检查其身份(技术上的对象身份检查通过比较对象引用识别),但是Component接口上概念对象身份的检查需增加额外的操作。这里可以通过实现核心对象的引用比较来识别。

 


实现

实现角色对象模式需要解决两个关键问题:为角色透明的扩展关键抽象以及动态管理这些角色。对于透明扩展,我们可以使用装饰模式Gamma+95。对于创建及管理角色,我们运用产品交易者模式Bäumer+97。因此,角色对象模式结合两种著名的模式来完成新的语义。

  1. 提供接口一致。因为我们想角色对象的使用是透明的,即核心对象能够被使用,但是他们需要通过一个通用接口。注意,从模型化的观点看,一个角色类被当做它核心的特殊化(如投资者是一个客户)。装饰模式告诉我们如何完成。首先,我们考虑一个针对所有对象的通用接口。此接口能够动态的添加角色到所有对象上去,并且结构图中的Component类提供这个类,并且在装饰模式中与此Component类协作。对于所有特定内容角色可以扩展这个Component的功能,我们介绍的抽象超类ComponentRole就是在装饰模式中的协作类。ComponentRole通过递呈操作请求到核心对象上来实现Component接口。因此,RoleCore透明的包装了。ConcreteRole类必须继承于ComponentRole。他们相互协作如装饰模式中的具体装饰者。
  2. 隐藏角色对象的创建过程。在运行时角色实例被用于装饰核心对象。一个关键的问题是ConcreteRole实例是如何创建及添加到核心对象上去的。注意,ConcreteRole不是由客户端创建,角色的创建过程是由ComponentCore初始化的,因此避免了角色对象可能自我存在的可能(如角色对象独立于核心对象而存在,是不允许的)。这也保证了客户端不知道角色对象如何被实例化。
  3. 角色类从核心类中解耦。角色的创建及管理是普通行为。换句话说,使用新的并且无法预知的角色来扩展ComponentCore而又不改变ComponentCore这种做法是不现实的。因此,创建以及管理的过程必须独立于具体的角色类,ComponentCore代码不必静态引用这些角色对象。使用规范的对象specification objects可以完成这个目标。请求一个对象,客户端传递一个具体的对象到核心上,最简单的解决方案是使用类型名作为规范。核心对象返回满足规范的角色对象。相同的规范对象被用于创建。为了完成这个目标,使用产品交易者模式来获得支持。角色对象商人为那些相关的创建者对象维持一个规范对象的容器,例如,类对象,原型或者模范者。当客户的想要创建一个新角色时,它传递一个规范对象到核心,核心把这个创建委托给角色对象商人。
  4. 选择合适的规范对象。在许多例子中,使用类型名作为规范对象十分有效的,但是一些时候,使用较复杂的规范机制可能会更好:假如你已经模型化了人Person这个该概念作为核心对象类。一些人可能是雇员Employee,因此有一个Employee类。以为存在不同的雇员类型,你必须是使用雇员接口类并且模型化具体的角色作为雇员子类,例如销售,开发者,经理等。当客户端需要关于Person人的工资的信息时,他将会从核心对象中请求角色“Employee”。这就不可能使用类型名类完成需要的工作,因为具体角色对象是销售人员等类型名。在一些情况中,你可以使用Type Objects 作为规范方式。核心对象在随后取回请求的角色对象通过评估子类或者超类之间的类型关系。
  5. 管理角色对象。现在让核心对象管理角色,Component接口声明角色管理协议,此协议包含了添加、删除、测试、查询角色对象的操作。为了支持角色管理协议,核心对象维护一个映射了对象规范与具体角色实例的字典。无论何时一个角色对象被添加到核心对象上,新的角色对象就与其角色规范被注册在角色字典中。请注意,一个核心对象管理它的角色对象是通过ComponentRole类型引用,因此排除了扮演角色的ComponentCore实例!因为核心拥有角色,它必须照顾他们。比如,当需要的删除的时候,它能够删除他们。
  6. 维护一致的核心以及角色对象的状态。对于核心对象或者角色对象的改变可能需要更新更深入的一些对象。看一个例子,考虑改变“人Person”这个核心对象的名称,此时人也是借贷者。人收到一个新名字的任何时候,在借贷者中的必须有一个标记来指明借贷者的名字的改变。这个标记为系统指明了名字的改变必须报备于全局机构中,这个全局机构包含并传递了诸如借贷者的信誉度之类的信息。在银行中,通知是必须步骤。有几个可能的解决方案来确保这个约束,但是所有的都是有代价的。通常依赖使用硬编码的方式。在下面一个模块中我们将会讨论一个精心组织的解决方案。
  7. 使用产权及观察者来维护角色属性约束。如果一个状态的整合以为相互依赖变得复杂,(部分)核心对象的实现状态可以使用一个所有权列表property list来代表,或者叫做可变状态。所有权列表使用key/value键值对来代表属性名称与属性值。它通常使用字典实现,字典就是映射了属性名与属性值。角色对象可以注册感兴趣的属性(此属性为核心对象的部分状态),并且在它感兴趣的这些状态改变操作发生时被通知。为了完成这个过程,改变了属性的每一个对象必须通知核心对象它的这个改变,如此,它能够通知以来的角色对象它的这种改变。所有权列表通常是一个不好的方法,因为违背了封装原则,暴露了内部状态。属性名的改变可能需要所有的角色类适当地的变动。另外,糟糕的代码也可能导致负面影响。然后,当小心地处理后,这些问题可以避免。可能最好的例子是抽象语法树中被装饰节点的分布使用以及在许多编译器或者软件开发环境中关键抽象。
  8. 维护概念上的身份。角色对象模式让你管理核心类及其角色类作为一个独一的整合了状态的概念上的对象。因此,对于客户端来说,它能够清晰的分辨出技术上的两个对象其实是一个概念对象。客户端必须使用由Component接口提供的具体身份比较操作。通常,此操作使用直接比较两个核心对象的引用。
  9. 维护角色中的约束。在不同角色间,可能存在一些约束。一个通用的事例是角色B需要角色A所扮演的对象。例如,客户和借贷者都是人的角色,存在一个客户角色对象是是前提条件,因为允许人中借贷者角色完整角色扮演。这个就是角色级约束。一个角色对象的扮演角色B的能力必须是在能够扮演角色A的前提下,或者扮演角色A的对象A已经存在。如果没有角色A,角色B不能完成扮演动作。这个约束来至于特定的系统域。一般来说,角色B不仅仅依赖于角色A而且可以把角色A作为一个特殊状态。在复杂的事例中,你将不可避免的使用约束解决系统。幸运的时,在实践中,不会进一步增加其复杂度,并且许多务实的解决方案是行之有效的。典型的案例是使用两阶段提交协议,在移除一个角色对象前,需要首先征询所有角色是否同意完成这个请求。
  10. 维护因为递归角色对象模式而产生的约束。角色级role-level的约束中,许多问题都能通过递归角色对象来解决。如果角色A是一些类角色B、C、D等的前提条件,角色A必须作为这些角色的一个关键抽象被定义。借贷者以及投资可以使用客户视角来观察,客户以及担保人可以使用人的视角来观察。因此作为一个担保人,不需要一个客户,也不需要被模型化为一个客户,下面的图表现了一个递归角色对象模式的运用。


图5:角色对象模式的递归运用

在运行时,这导致角色链以及核心对象。下列图片描述了这个种过程:


图6:角色上继续添加角色的动态对象示例图

角色级别约束由角色对象借贷者或者投资者执行。而不是进入更高的层级-客户层级。因此,模型化的客户作为一个关键抽象是相对于人这个更一般的关键抽象来完成其特定的角色扮演约束的。


简单代码

下列C++代码描述了一个在动机里面讨论的如何实现客户的例子。我们假设存在一称作Customer的Component。

01class CustomerRole;
02 class Customer {
03 public:
04 // 客户规范操作
05 virtual list<Account *> getAccounts() = 0;
06 // 角色管理
07 virtual CustomerRole * getRole(String aSpec) = 0;
08 virtual CustomerRole * addRole(String aSpec) = 0;
09 virtual CustomerRole * removeRole(String aSpec) = 0;
10 virtual CustomerRole * hasRole(String aSpec) = 0;
11};

CustomerCore的实现像这样子:

01class CustomerCore : public Customer {
02 public:
03 CustomerRole * getRole(String aSpec)
04 {
05 return roles[aSpec];
06 };
07 CustomerRole * addRole(String aSpec)
08 {
09 CustomerRole * role = NULL;
10 if ((role = getRole(aSpec)) == NULL)
11 {
12 if(role = CustomerRole :: createFor(aSpec, this)) roles[Spec] = role;
13 }
14 return role;
15 };
16 list<Account *> getAccounts() { ... };
17 private:
18 map<String, CustomerRole *> roles;
19};

角色规范的中使用字符串来带代表具体的角色类。使用字典映射角色规范以及角色对象。

下一步,我们定义客户的子类叫做CustomerRole类,我们将对他子类化来获得具体的角色。CustomerRole装饰CustomerCore类通过引用core实例变量。对已每一个Customer接口每一个操作,CustomerRole递呈请求给core。注意,core的实例变量被CustomerCore分型。因此,为了保证客户角色不被用于core对象,角色规范以及相对应个的可以创建角色实例化的创建者对象之间使用查找表。详细的如何实现一个管理创建者请看Bäumer+97。

01class CustomerRole : public Customer {
02 public:
03list<Account *> getAccounts() { return core->getAccounts() }; CustomerRole * addRole(String aSpec) { return core->addRole(aS pec); };
04 static CustomerRole * createFor(String aSpec, CustomerCore * aCore)
05 {
06 CustomerRole * newRole = NULL;
07 if (newRole = lookup(aSpec)->create()) newRole->core = aCore;
08 return newRole;
09};
10 private:
11 CustomerCore * core;
12};

CustomerRole子类规范了各种角色。例如,类Borrower添加了证券以及贷记的操作。子类不应复写继承的角色管理操作。

1class Borrower : public CustomerRole {
2 public:
3list<Security *> getSecurities() { return securities; };
4 private:
5 list<Security *> securities;
6};

注意,client在他们在角色实例中调用规范的角色操作之前,必须向下转换由core组件返回角色引用。

1Customer * aCoustomer = Database :: load(“Tom Jones”); Borrower * aBorrower = NULL;
2if (aBorrower = dynamic_cast<Borrower *> aCustomer->getRole( “Borrower”)) {
3      // access securities
4         list<Security *> securities = aBorrower->getSecurities();
5   };

已存在的系统

GEBOS系列面向对象的银行项目就使用此模式Bäumer+97扩展。它为一系列的银行商业应用提供支持,包括出纳、借贷、投资部门以及自我服务及账户管理。GEBOS系统基于通用的商业领域的分层模型化银行的核心概念。具体的工作场合运用程序使用角色对象模式扩展这些核心的概念。

Riehle+95a及Riehle+95b的Tool-And-Material框架通过复制、粘贴、多继承、装饰者以及包装者探索了角色模型设计空间来取得角色对象模式相同的效果。这些变体也在Fowler97中有介绍。

当前得Geo系统在Ubilab发展,Ubilab是瑞士联合银行信息技术研究实验室,它使用角色对象模式作为一个角色变体的实现做一个程序第一级实体。

Kristensen与Østerbye报告了在编程语言中为角色使用装饰者模式Kristensen+96。然后,他们没有说明创建及管理角色的细节。

我们使用特定领域例子,人及其角色,来达到达到上述目的。为了表现它拥有的模式,此例子是能够提供共性的东西。因为人的抽象需要许多的内容,也有许多不同的角色需要人来扮演。Schoenfeld96讨论了几个例子,例如人及其角色在基于文件为中心的商业处理过程。我们选择人及其角色在银行商务系统的中的扮演客户的内容。当然另外一个例子是人及其角色官僚体系中收入管理问题。

一个不相关的角色对象模式使用是在抽象语法树(AST)中的装饰节点。在大部分的开发环境中AST是基本的抽象。它们在许多的不同工具中被使用及被考虑,例如语法制导编辑器、符号浏览、交叉引用,编译支持、依赖分析变化影响。每一个工具需要注释AST节点使用规定的信息,它之针对整棵树的某一个细小的部分。角色对象模式通过特定的工具针对特定的的节点是很有效的。Mitsui+93讨论了使用这种模式在C++编程语言中内容中的的情况,它针对了特定的内容用,当然也讨论了更多一般性问题。


涉及到的模式

扩展对象模式Gamma97处理了同样的问题:一个组件通过扩展的对象完场扩展,使用以下方式:统计特定内容的需求。但是这些模式没有讨论组件以及组价角色对象之间如何完成处理的,这个反而是角色对象模式中的关键内容。另外,扩展对象模式仅仅触及到了扩展对象(角色对象)的创建及管理问题。我们整合了装饰模式以及商品交易者模式作为角色对象模式的一部分。

扩展对象模式在Zhao+97及Schoenfeld96被使用。Zhao和Foster讨论了角色对象作为扩展对象,但是他们显然没有包装核心对象。他么的关键例子是(角色)标记作为的一个透明软件系统中的关键点。Schoenfeld选择如我们同样的例子,人及其角色,也使用扩展对象模式而不是使用装饰类透明的包装core

Post模式Fowler96描述了此模式一个有趣的变体,类似于扩展对象模式,它描述了在特定程序中核心对象的内容的职责。然而,Post对象的存在是独立与core的,并且不需要core来分配。

角色对象模式