我们引入了UML类图的概念,比较了在java编程语言和UML类图中表示类、属性、操作和关联关系的不同之处。下面我们来看看如何在UML中表示两个重要的Java概念——继续,接口。
继续
在Java中,我们可以声明一个类扩展(extends)另一个类,还可以声明一个类实现(implements)一个或者多个接口。下面我们来看看如何在UML中表达这些概念。
下面是三个Java类的基本骨架。第一个类是代表某种支付方式的Payment抽象类,另外两个类分别扩展Payment类,描述两种不同的支付方式:
/** 描述支付方式的抽象类 */
abstract public class Payment {
public Payment() { }
public Payment(BigDecimal amount) {
this.amount = amount;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
PRivate BigDecimal amount;
}
/** 一个扩展了Payment类的子类,描述信用卡支付方式 */
public class CreditCardPayment extends Payment {
public CreditCardPayment() {
}
public CreditCardPayment(BigDecimal amount) {
super(amount);
}
public String getCardNumber() {
return cardNumber;
}
public void setCardNumber(String cardNumber) {
this.cardNumber = cardNumber;
}
public boolean authorize() {
return false; //暂不实现
}
private String cardNumber;
}
/** 一个扩展了Payment类的子类,描述现金支付方式 */
public class CashPayment extends Payment {
public CashPayment() {
super();
}
public CashPayment(BigDecimal amount) {
super(amount);
}
public BigDecimal getAmountTendered() {
return amountTendered;
}
public void setAmountTendered(BigDecimal amountTendered) {
this.amountTendered = amountTendered;
}
private BigDecimal amountTendered;
public BigDecimal calcChange() {
return amountTendered.suBTract(super.getAmount());
}
}
图一用UML显示了同样的三个类。在操作和属性声明中,类型和参数之类的细节都没有显示出来,这是为了更清楚地显示出类的整体结构以及各个类之间的关系。
图一:UML一般化关系
Java中的extends要害词声明了继续关系,相当于UML中的“一般化”(Generalization,也译为“泛化”)关系,在UML图形中用子类向超类的实线空心封闭箭头表示。图一额外增加了一个Sale类,这是为了更清楚地说明UML一般化关系与UML定向关联关系所用箭头的不同。关联关系与一般化关系的另一个不同之处在于,一般化关系的两端不需要说明多重性或角色名称。
显然,UML类图比三个Java源代码文件更清楚直观地显示出了三个类之间的继续关系。假如你要与别人探讨设计思路,绘制UML草图也要比直接使用代码简单快捷得多。
也许有人会说,系统的类结构图就在他们的头脑中,他们只需要直接使用Java代码。实际上,对于规模较大的系统,这种说法显然是不成立的;即使对于规模较小的系统,假如一定的时间之后要由其他程序员修改,没有UML图也会寸步难行——很难保证每一个人都了解你头脑中的类结构图。
在UML中,抽象类的标志是类的名字以斜体显示。在白板或纸张上手工画UML草图时,很难区分字体是否是斜体。为此,一些人建议这些场合可以在类名称的右下角加上{abstract}标记以示区别。
另一些人认为,在白板上写{abstrac t}显得太罗嗦,他们倾向于打破UML常规,在类名称的右下角加上一个0表示零个实例,假如在该位置写上1,则表示该类是一个singleton类(永远只有一个实例的类);假如在该位置写上N,则表示它是一个枚举类(拥有固定实例数量的类,如一星期中的天数,彩虹的颜色,等等)。不过,这一切都不是标准的UML,只能用于手工绘制UML图的场合,看来也不可能得到UML建模工具的支持。
历史知识:UML首先由Rational公司的一个工作组发明,Ration公司是UML建模工具Rose的生产者。UML于1995年的OOPSLA会议上被公诸于世,随后,OMG(对象治理组织)于1997年采用了UML规范。不难理解,继续负责发展UML规范的OMG任务组包含了来自几乎所有主流UML工具厂商的代表。因此,除了严格遵从规范的UML软件工具,在一些书籍或网页上发现不规范的UML符号也不足为怪。
继续使得一个类能够使用另一个类的属性和方法,就象使用自己的属性和方法一样。当这类继续机制第一次出现时,人们普遍把它视为重用现有代码的理想方法。令人遗憾的是,规模过于庞大的继续树变得很脆弱,修改继续树的一部分,就会在整棵继续树中引起一系列的连带反映。在面向对象的编程中,假如要实现有效的封装,就应该让改动局部化,即一个地方的改动不至于引起其他地方的变化。而修改继续树一个地方引起其他地方的变化恰恰违反了上述设计思想。UML图使得我们能够方便地把握继续关系图,从而为应用继续关系带来了方便。那么,什么时候适合运用继续关系呢?按照《Java Design》一书,对于超类A和子类B,执行如下检查:
命题“B是一个由A扮演的角色”不成立。
B永远不需要变形成为其他某些类别中的对象。
B扩展而不是覆盖或废弃A的行为。
A不仅仅是一个工具类(一些可以重用的实用功能)。
对于一个问题域(特定的业务对象环境):A和B定义了同一类型的对象,或者是用户事务、角色、实体(团体、位置或其他东西),或其他物体的相似类别。
假如上述任意一个判定不成立,那么把A和B定义成继续关系可能是不合适的,改用关联关系可能更加稳固、正确。例如,图二违反上面的第一个判定,因为“雇员是一个由人扮演的角色”成立。另外,它还违反了第二个判定,因为雇员确实可能改变其类别(身份),例如某个时候它可能是顾客。这样,一个既是顾客又是雇员的人就要有两个独立的对象来描述,从而使保存在Person类里面的信息重复出现,带来了两个数据副本之间数据不一致的风险。
图二:不恰当的extends用法
图三显示了改用关联关系后的UML图。现在,一个人可以在同一时刻(或不同时刻)既是顾客又是雇员(或任意一种)。
图三:改用关联关系
接口
Java编程语言中接口(Interface)的概念也能够与UML概念匹配。UML中的接口是一种实现继续的形式,但这种继续形式与Java中通过要害词extends实现的继续有所不同。
在Java中,extends要害词描述了一种继续形式,它既继续接口也继续行为。这种类型的继续有时被称为Sub-classing。与其他的面象对象编程语言不同,Java类只能从一个类继续。许多时候,设计UML图的人熟悉多种编程语言,经常会引入多重继续的思想,例如C++的多重继续思想。从已有的Java代码生成UML图(这个过程称为反向工程)不会带来多重继续的问题,但假如要求一个Java程序员去实现一个带有多重继续的UML类图,就会出现问题。假如多重继续中的超类是纯抽象类,这部分类可以用Java的接口来描述,但是,假如只做这种转换不足以把UML类图中的多重继续全部转换成单重继续,这时就必须修改UML类图重新建模了。
虽然Java不支持C++之类语言那样的多重继续,但它支持实现多重接口。这种由Java要害词implements声明的继续只继续接口,这种继续有时被称作Sub-typing。在UML中,实现接口的类与接口定义之间的关系叫做Realization关系,用一个虚线封闭箭头表示,从实现接口的类指向接口。接口本身的UML图与普通类一样,但它的名字上面要加上“”。图四由图一修改而成,Payment类被一个接口取代。(关于Realization名称的说明:Realization最常见的中文译名是“实现”。但是,Java的implements也叫做“实现”。为避免混淆,本文中凡是出现Realization的地方一律直接使用英文)。
图四:接口与实现接口的类之间的Realization关系
接口可以从一个或者多个其他接口扩展。UML一般化关系(实线封闭箭头)可用来描述这种关系,如图五所示。
图五:UML一般化关系,用来表示一个接口扩展了一个或者多个其他接口
UML还支持另一种接口符号,即用圆圈表示接口(加上连线之后就成了棒棒糖的样子),但这种表示法多用于UML组件图,在UML类图中比较少见。
假如UML图规模较大,有大量的类实现一个常用接口,整个UML图可能乱成一团糟。《Java Design》一书提出了一种简化方法,后来又被《Streamlined Object Modeling》一书的作者采用,这就是在实现接口的类中,用接口的名字替代从接口继续的方法,不过这不属于标准方法。遗憾的是,目前似乎还没有工具支持这种转换。图六是用Together ControlCenter工具加工出来的简化类图。