角色对象模式

作者:Dirk Bäumer, Dirk Riehle, Wolf Siberski, and Martina Wulf  原文链接  译者:f0tlo <1357654289@qq.com>

意图

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

译注:为了行文的流畅性及内容意思的准确性,尽量贴近原文使用英文单词标记特定内容, 如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。因此,角色对象模式结合两种著名的模式来完成新的语义。

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

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

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

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


简单代码

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

class CustomerRole;
 class Customer {
 public:
 // 客户规范操作
 virtual list<Account *> getAccounts() = 0;
 // 角色管理
 virtual CustomerRole * getRole(String aSpec) = 0;
 virtual CustomerRole * addRole(String aSpec) = 0;
 virtual CustomerRole * removeRole(String aSpec) = 0;
 virtual CustomerRole * hasRole(String aSpec) = 0;
};

CustomerCore的实现像这样子:

class CustomerCore : public Customer {
 public:
 CustomerRole * getRole(String aSpec)
 {
 return roles[aSpec];
 };
 CustomerRole * addRole(String aSpec)
 {
 CustomerRole * role = NULL;
 if ((role = getRole(aSpec)) == NULL)
 {
 if(role = CustomerRole :: createFor(aSpec, this)) roles[Spec] = role;
 }
 return role;
 };
 list<Account *> getAccounts() { ... };
 private:
 map<String, CustomerRole *> roles;
};

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

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

class CustomerRole : public Customer {
 public:
list<Account *> getAccounts() { return core->getAccounts() }; CustomerRole * addRole(String aSpec) { return core->addRole(aS pec); };
 static CustomerRole * createFor(String aSpec, CustomerCore * aCore)
 {
 CustomerRole * newRole = NULL;
 if (newRole = lookup(aSpec)->create()) newRole->core = aCore;
 return newRole;
};
 private:
 CustomerCore * core;
};

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

class Borrower : public CustomerRole {
 public:
list<Security *> getSecurities() { return securities; };
 private:
 list<Security *> securities;
};

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

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

已存在的系统

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

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

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

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

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

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


涉及到的模式

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

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

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

鸣谢

我们感谢Shepherd Ari Schoenfeld对于本文的帮助与提高。

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/120292.html

(0)
上一篇 2021年8月28日 12:11
下一篇 2021年8月28日 12:11

相关推荐

发表回复

登录后才能评论