概要
在任何应用系统中,每一个对象都可能扮演多种角色,你在家里是父亲,在公司则可能是一个程序员,一个为你提供原材料的公司可能同时又是你的客户。。,这样的问题一次又一次的出现,我经常看到应用系统不能很好处理这些问题,在本文中,我将仔细描述这一重要的分析模式,并使用TAOERP 基本业务元素(TBCC)展示如何使用这一分析模式处理组织机构相关的问题。
By 石一楹
本文从《ERP之道》www.erptao.org转载
======================================================================================
一个应用系统经常需要某一个对象在不同的上下文中具有不同行为的情形,最常见的例子是客户和供应商的问题。 例子:
某制鞋企业有很多为它们提供真皮的合作公司,在处理采购订单时,这些合作公司是它的供应商,但这些合作商同时从该制鞋企业采购皮鞋,所以在处理销售订单时,这些公司又变成了它的客户。
许多建模人员在处理这类问题的时候,经常轻率地做出判断,当用户需求不能满足时,他们才发现这样的判断是不正确的。
正如Martin Fowler所言,作为一个分析模式,我在这里主要关心在何种情况下需要使用什么样的模型,而并不是太关心具体的实现如何。本文中出现的Java代码虽然来自Tao BC库,但是出于示例的目的,都进行了简化。读者也应该把主要精力放在模型本身,或者说是接口上而不是实现。
我要提醒的是,本文虽然论述了比较复杂的Role建模方法,但是读者在应用这些模式之前需要仔细考虑它们是否必要应用到你的模型之中。因为,任何一种复杂的模型虽然能够解决复杂的问题,但是不可避免地会带来额外的开销,所以,如果能够用简单的模型处理你手头上的问题,不要用更复杂的。面向对象和框架开发方法最大的好处之一就是能够在你需要的时候进行修改而对系统的影响最小。按照Kent Beck的Extreme Programming 理论:“只做你现在需要的”。当然,一旦问题发生变化,你需要用其它面向对象的方法进行Iterator,这是另外一个主题,我也许会在其它的专栏中介绍。
上下文和动机
假设我们现在要开发一个企业销售管理系统,在这样的系统中,关键的抽象之一就是合作伙伴客户,因此我们的设计模型中将包括客户Customer类。该类的接口提供对客户名字、地址、电话、Email,客户信用度等属性的操作。
现在,假设我们也需要一个采购管理系统,这时候我们需要一个供应商的抽象,尽管供应商在很多方面和Customer一样,但是譬如供应商的提前期等操作显然和客户有所不同。这是我们抽取一个不同的Supplier类的原因。
但是,当我们的销售系统和采购管理系统一起集成到供应链管理的时候,我们会发现独立抽象Customer和Supplier这样的类会碰到很多问题。一个供应商可能同时是你的客户,又或者你的一个供应商后来变成了你的客户。你也许考虑供应商和客户从一个抽象的Person类继承。
但是,这还是有问题。首先,从对象标识的角度来讲,虽然我们的Customer和Supplier继承自同一个Partner,但是他们的对象属于不同子类的实例,所以他们不可能相同。因此,代表同一个合作伙伴的Customer和Supplier的两个对象具有不同的对象标识。它们之间的相同只能通过其它的机制模拟实现。如果两个对象指代同一个实际对象,他们从Partner上继承的属性应该相同。但是,我们会在多态搜索时发生问题,如果我们搜索系统中所有的Partner,那么相同的Partner(包括、供应商、客户)可能会重复出现,我们必须小心处理这些重复问题。
角色模式把对象在不同上下文(销售系统、采购系统)相关的视图建模成Role Object,这些角色对象动态地连结到核心对象(Core Object)。这样形成的对象聚集表达一个逻辑对象,它能够处理包含多个物理对象的问题。
象Partner这样的关键抽象建模为一个接口,也就是没有任何实现。Partner接口指定了处理类似于伙伴地址、帐户这样的通用信息。Partner另一部分重要的接口是维护角色所需要的接口,象上面的adRole()和getRole().PartnerCore类负责实现这些接口。
Partner的具体角色类由PartnerRole提供,PartnerRole也支持 Partner接口。PartnerRole是一个抽象类,它不能也不打算被实例化。PartnerRole的具体子类,如Customer和Supplier,定义并实现特定角色的具体接口。只有这些具体角色类在运行时被实例化。Customer类实现了该伙伴在销售系统上下文中的那一部分视图,同样,Supplier实现了在采购系统中的那一部分视图。
像供应链管理系统这样的一个客户(使用这些类的代码),可能会用Partner接口来存取PartnerCore类的实例,或者会存取一个特定的PartnerRole的具体类,如Customer.假设一个供应链系统通过Partner接口获得一个特定的Partner实例,供应链系统可能会检查该Partner是否同时扮演Suppiler的脚色。它会使用一个特定的Specification作为参数做一个hasRole()调用。Specification是用于指定需要满足的标准的一个接口,我们会在后面考察这个问题。现在处于简单目的,我们假设这个Specification就是一个字符串,该字符串指定了需要查找的类的名字。如果该Partner对象可以扮演一个名为”Supplier”的角色,销售管理系统可以通过getRole(“Supplier”)得到具体的供应商类的对象。然后供应链管理系统就可以通过此对象调用供应商特定的操作。
什么时候使用该模式
如果你想在不同的上下文中处理一个关键抽象,而每一个上下文可能对应自己的一个应用程序,而你又想把得到的上下文相关的接口放入同一个类接口中。
你可能想能够动态地对一个核心抽象增加或删除一个角色,在运行时而不是在编译时刻决定。
你想让角色/客户应用程序相互独立,改变其中的一个角色可以不影响其它角色对应的客户应用程序。
你想能够让这些独立地角色互相转换,并能辨别不同的角色实际上属于同一个逻辑对象时。
但是,该模型并不适用于所有情况,如果在这些角色之间具有很强的约束和交叉关系。这种模型不一定适用于你。尽管我们后面可以看到,某些约束和关系用Role来处理可能更直观和简洁。
结构和原理
下图显示了Role Object的基本结构:
我们可以看到客户应用程序大多数情况下使用Component接口来进行一些通用的操作。如果该应用程序并不关心具体的子类。在上面的供应链管理系统中,如果我们只关心Partner的一些基本信息,譬如地址、电话等等,客户应用程序只需要存取Partner接口(Component)。Component接口还提供了增加、修改、查询和删除对应Specification具体类的管理接口。客户应用程序在需要特定角色的操作时,可以使用getRole(aSpec)得到。
对那些通用的操作而言,真正操作是由ComponentCore来实现的,不论客户使用Component接口还是具体的角色类。在客户应用具体类的时候,由于每一个具体类都继承了ComponentRole, ComponentRole将这些操作传递给ComponentCore,最后由Core来完成操作。我们同样也可以看到,那些角色管理的接口也是有ComponentCore来实现的,它有一个所有具体对象的列表,可以在其中进行查询和其他操作。
使用RoleObject的优缺点
从概念上来讲,Role Object为系统提供了一个清晰的抽象。属于同一个逻辑对象的各个不同上下文中的角色都服从于这个抽象的接口。Role Object也提供了一个一致的接口对这些角色进行管理。这种一致的管理使得对这个关键抽象具有很强的可扩展性,你可以动态增加、删除角色而不影响客户应用程序。也就是使得客户应用程序和对应Role的绑定很少。
如果我们从面向对象的语言所能提供的设施来看,Role角色实际上提供了一种多重继承的特性。在我们上面的例子中,如果一种语言提供多重继承,我们很可能会让该Partner同时从Customer和Suppiler继承下来。但是,现在绝大多数面向对象语言都不支持这样的继承。即使支持的话,也没有办法动态增加和删除它的继承类。
但是,Role Object模式的应用不可避免地带来一些麻烦。首先如果客户代码需要使用一个具体类的接口,它必须首先询问这个Component是否能够扮演那样的角色。语言本身也不能进行强制的类型检查。
问题在Role之间具有相互约束的时候变得更加复杂。我们在这里申明,如果你所构建的具体类之间具有很复杂的相互约束关系,那么Role Object可能不是你的选择。当然,对Role Object进行适当的扩展和调整可以很好地处理某些约束关系。我们会在后面详述。
实现
基本实现
实现要从Role Object的基本意图来着手,使用Role进行建模最主要的两个目的是透明地扩展关键抽象、动态管理角色。
在面向对象社团中,已经有很好的实践和理论来处理这样的问题。Decorator是满足透明扩展的范例,而Product Trader模式可以让客户进行对象创建,这些被创建的对象匹配一个特定的产品角色协议并且满足附加的Specification.,使用这两个广为模式社团所承认的模式使得Role Object模式更加坚固。
我们所要解决的第一个问题就是Component、ComponentCore和ComponentRole之间的关系,从上面的原理图可以看到,Component抽象了所有角色都具有的通用接口。以后,我们可以使用角色管理协议增加、查询、删除角色。这些角色都从ComponentRole继承得到,它负责把具体角色的操作传递给Core来完成,也就是说ComponentRole实现了对ComponentCore的装修,同时,所有的具体类也对Core进行了装修,这一点和Decorator十分相似,但是至少有两点与Decorator是不同的:
1. 意图:Decorator按照GOF的说法是动态地给对象增加一些额外的职责,也就是增加功能。我们可以看下面的一个Decorator的实例:
在这里,核心的元素是TextView,ScrollDecorator和BorderDecorator的目的是为了透明地为TextView增加滚动条和边框,而客户应用程序仍然可以使用Component接口,使用抽象的Decorator使得装修的增加不会产生组合爆炸问题。
Role模式的意图是展现不同上下文中的不同角色,而其中的ComponentRole和ComponentCore之间具有等同的接口,也就是ComponentRole虽然包装了ComponentCore,但是它的目的不是增加功能而是作为一个中间传递者。把具体Role的所有通用接口(同时也是关键抽象Component的接口)转移到Core的具体实现上。
2. 核心实例和包装实例之间的关系 装修模式参与者之间的装修者都相互链在一起,一个装修者指向另一个装修者,最后指向核心,而所有的Role都直接指向核心,这是因为装修生成的最后类的实例贯穿了所有的包装物,而角色之间基本上都是相互独立的,除非它们之间具有约束,这是后话。
我们关心的第二个问题就是角色的管理,角色的管理需要我们考虑下面几个重要的问题:
1. 角色对象创建:角色实现了对核心对象的运行时包装,所以最重要的问题是如何创建角色实例,以及这些实例如何与核心对象相连。注意客户代码不知道如何以及何时创建这些角色对象,创建过程由核心来控制。
2. 角色对象删除:角色对象的删除是一个标准的垃圾收集问题。某一个客户代码应当无法删除一个角色对象,因为它不知道是否还有其他客户在使用这个角色对象。
3. 角色对象的管理:为了让一个核心对象能够管理它的角色,Component接口声明了一个角色管理协议,其中包括对角色对象的增加、删除、测试和查询。为了支持该接口协议,核心对象维护了一个Map,其中由角色的Specification映像到具体地角色实例。
下面是一个简单的例子:
public interface Partner {
public String getAddress();
public void addRole(String roleName);
public boolean hasRole(String roleName);
public void removeRole(String roleName);
public PartnerRole getRole(String roleName);
}
class PartnerCore implements Partner {
String address;
private Map roleMap = new HashMap();
public String getAddress() {
return address;
}
public void addRole(String roleName) {
PartnerRole aRole = CustomerRole.createFor(roleName,this);
if (aRole!= null)
roleMap.add (aRole);
}
public boolean hasRole(String roleName) {
return roleMap.containsKey(roleName);
}
public void removeRole(String roleName) {
if hasRole(roleName) {
roleMap.remove(roleName);
}
}
public PartnerRole getRole(String roleName) {
if hasRole(roleName) {
return (PartnerRole) roleMap.get(roleName);
} else {
return null;
}
}
}
在这里,我们使用roleName作为Specification,角色的Specification和实际的Role实例之间的映象用一个Map来实现。
现在我们定义一个PartnerRole抽象类:
abstract class PartnerRole implements Partner {
private PartnerCore core;
public String getAddress() {
return core.getAddress();
}
public void addRole(String roleName) {
core.addRole(roleName);
}
public static void createFor(String roleName, PartnerCore core) {
Creator creator = lookup(roleName);
if (creator == null) {
return null;
}
PartnerRole aRole = creator.create();
if (aRole != null)
aRole.core = core;
return aRole;
}
}
这里有几点需要注意,PartnerRole是一个抽象的类,它不能被实例化,对所有角色都通用的操作以及角色管理协议操作均被重新定向到core。在这里PartnerRole最有意义的操作是它的静态方法createFor,注意createFor的格式,它的第二个参数是一个ComponentCore而不是更通用的Component接口,因此我们就限制了一个ComponetRole不会担当ComponentCore的责任。
接下去需要实现具体的Role对象,譬如说我们的Customer,Supplier:
public class Customer extends PartnerRole {
public Money getCredit() {
…….
}
}
public class Supplier extends PartnerRole {
public void schedule() {
…..
}
}
下面是用户可能的使用方法:
Partner aPartner = Database.load(“North Bell”);
Customer aCustomer = (Customer)aPartner.getRole(“Customer”);
if (aCustomer != null) {
Money credit = aCustomer.getCredit();
….
}
这里在获得特定的角色之后需要进行强制的类型转化。
本文从介绍Role Object的动机开始,逐渐深入到它的原理、组成、优缺点和基本实现,并给出了一个基本的实现。Role Object的应用和实现在许多方面都需要进行扩展,包括如何使用Specification模式管理Role,应用Product Trader模式创建Role,以及如何处理Role之间的相互约束问题。同时,Role Object实现Role概念的一种方法,我将在后续的文章中继续本文的内容,提供更为深入和广泛的讨论和研究。欢迎建议。
关于作者 石一楹是一个专注于面向对象领域的系统设计师。目前的工作是国内一家ERP公司的CTO.他住在浙江杭州,有一个两个月的女儿。
|