-
2007-04-10
续
一年多的忙碌,一年多的沉淀,匆匆而过,改变的是blog的界面,而不变的是其中的内容。
重拾往日的激情,分享的快乐,为自己留下点什么,为关心自己的人送去一些祝福。
我,回来了。
-
2004-12-15
相等性判断的自动化
在面向引用的OO编程语言中,相等性(Equality)操作有两种:
1、判断两个引用本身是否相等;
2、判断两个引用所引用的对象是否相等。事实上,第二种操作是第一种操作的超集。因为,如果两个引用本身相等,则两个对象一定是相同的,当然也是相等的;但反之则不然。
对于第一种操作,执行起来非常简单。但第二种操作则相对复杂,需要程序员的指定或干涉。在Java中,具体表现为程序员需要为一个class编写equals方法。
两个对象本身的相等性应该具备下列性质:
o Reflexive:即对象应该等于自身,也就是第一种相等性操作;
o Symmetric:对称性,即 a equals b 意味着 b euqals a;
o Transitive:传递性,如果 a equals b,b equals c,则a equals c;
o Consistent:一致性,在对象内容没有变化的情况下,两个对象之间进行多次比较时,结果应该是一致的。对象本身的相等性判断,是高度模式化的操作。两个对象之间的相等性,在绝大多数情况下,其实是对象所包含的部分或全部属性之间的相等性。基于这种现实,我们应该把相等性判断的操作交给编译器去做。
首先,并非所有的class都需要进行对象内容的相等性判断,所以程序员可以指定一个class是否是compareable的,像这样:
public compareable class Foo{
...
}编译器只对声明了compareable的class生成内容相等性判断的代码。如果目标代码是Java的话,则只为声明为compareable的class生成equals方法。对于这种class的对象,就可以对其进行内容相等性判断,比如:
Foo foo1 = new Foo();
Foo foo2 = new Foo();if(foo1 == foo2) // 引用相等性判断,返回false
...if(foo1 equals foo2) // equals是关键字,用以进行内容相等性判断
...只有声明为compareable的类及其子类的对象,才能进行equals操作。编译器会检测这一点。
其次,在对两个compareable class的对象进行内容相等性判断时,并非所有属性都需要对比。所以应该允许程序员指定需要对比的属性。像这样:
public compareable class Foo {
private compareable int value;
private compareable string name;
private int dummy;...
}在这个例子中,在对两个Foo对象进行相等性判断时,应该仅仅对value和name的值进行对比,由于dummy不是compareable的,所以无需对其进行相等性判断。
如果一个属性被指定为compareable的,则属性所属的类型必须是compareable的。否则编译器需要给出错误。对于基本类型,比如int, boolean, string, real等,默认就是compareable的,由于object是所有class的root class,所以object不是compareable的。比如:
public compareable class Foo {
private compareable int value; // valid
private Exam exam; // Invalid, because class Exam isn't compareable.
}public class Exam {
...
}如果一个class的super class是被声明为compareable,则其默认就是compareable的;其相等性判断算法等同于super class。比如:
public compreable class Base {
public compareable int value = 10;
}public class Derived extends Base{
}Base base = new Base();
Derived derived = new Derived();if(base equals derived) // return true
...但是,如果它想扩展super class的相等性判断算法,仍然必须明确声明为compareable的。比如:
public compreable class Base {
public compareable int value = 10;
}// Invalid,因为它新指定了属性name为compareable的,
// 所以Derived也必须被明确指定为compareable的。public class Derived extends Base{
public compareable string name;
...
}还有一种情况,如果你想让一个类在内容相等性上区别于它的父类,也可以通过明确指定自身为compareable的来完成。例如:
public compreable class Base {
public compareable int value = 10;
}public compareable class Derived extends Base{
}Base base = new Base();
Derived derived = new Derived();if(base equals derived) // return false,既然Derived也有自己的compareable声明。
...对于集合性质的属性,如果被指定为compareable的,首先集合元素的类型应该是compareable的,其次,集合内元素的数量如果不相等,则两个集合肯定是不相等的。然后需要比较集合内的元素,此时,需要分为不同的情况来处理。
集合通过两种属性(ordered, unique)分为四类:
1、Set (non-ordered, unique)
2、OrderedSet (ordered, unique)
3、Bag (non-ordered, non-unique)
4、Sequence ( ordered, non-unique)首先如果集合的类型如果不相同,则两个集合肯定是不相等的。
o 对于Set,我们通过判断一个Set中的元素是否包含在另外一个Set中来确定其相等性。
o 对于OrderedSet和Sequence,我们根据顺序依次判断两个集合中元素的相等性。
o 对于Bag的处理要相对复杂一些,我们首先对一个Bag进行过滤操作,把其重复的元素去掉,然后计算每一个非重复元素的数量,得到结果后,通过判断另外一个Bag中的非重复元素的数量是否匹配,得出相等性。以上所描述的方法都是模式化的,这些都可以利用编译器进行自动代码生成。但有时候,相等性判断是非常特殊的操作。比如下面的Java代码:
public class Foo {
private int value;
private string name;public boolean equals(Object object) {
...
Foo foo = (Foo)object;
if(value < 0) {
if(foo.value > 0)
return false;
}
else {
if( foo.value != this.value)
return false;
}return foo.name.equals(this.age);
}
...
}对于这些特殊情况,我们应该允许程序员来自定义特殊的相等性判断函数。可以像这样:
public compareable class Foo {
private int value;
private compareable string name;equals {
this.value < 0 implies rhs.value < 0;
this.value > 0 implies this.value == rhs.value;
}...
}在我们的解决方案中,我们仍然可以指定那些无需特殊处理的属性为compareable的,对于特殊处理的条件,我们可以在equals block中,通过OCL boolean表达式来指定判断条件。在这些表达式都满足的情况下,再对指定为compareable的属性进行默认的相等性判断,以最后的结果来决定两个对象之间的相等性。
我们希望能够尽量避免让程序员编写模式化的操作,通过这些解决方案,应该可以很好的解决相等性模式代码生成的问题。
-
2004-10-20
Link操作
在UML的class图中,class与class之间可以存在关联(Association),它意味着,关联两端的类的实例(Instance)之间,或者类与实例之间(如果一个Association End是静态的)可以建立起link。而Link操作正是为了这一目的而存在的。
1. 语法
Link操作的语法为:
1. relate <object instance handle> to <object instance handle> [ across <association specification> ]
2. relate <object instance handle> to <class> [ across <association specification> ]
3. relate <object instance handle> to <object instance handle> [ across <association specification> ] using <object instance handle>当两个类之间存在关联,关联的两端都不是静态的,就使用第一种语法。
当两个类之间存在关联,并且关联的一端是静态的(两端都是静态的关联是非法的),则是用第二种语法。
当两个类之间存在一个关联类(关联类的关联两端不能使静态的),则使用第三种语法。其中,association specification的形式为:
R<number>
r<number>
R<number>.<role>
r<number>.<role>o 普通关联
如果两个类之间存在着普通关联,当设计师需要将两个类实例通过这个普通关联link起来时,使用第一种语法。比如:
|--------| R1 |-------|
| A |---------| B |
|--------| 1 *|-------|create object instance a of A;
create object instance b of B;
relate a to b across R1;o 静态关联
如果两个类之间存在着静态关联,则使用第2种语法.例如:
|--------| R1 *|-------|
| A |------------>| B |
|--------| all bars|-------|
create object instace b of B;
relate b to A across R1.'all bars';o 关联类
如果两个类之间存在一个关联类,你必须使用之前所述的第三种语法,即必须通过using指明关联类的实例. 比如:
|--------| R1 |-------|
| A |---------| B |
|--------| 1 | *|-------|
|
|
|---------|
| C |
|---------|create object instance a of A;
create object instance b of B;
create object instance c of C;
relate a to b across R1 using c;
2. association specification上述3种语法中的across部分,并非在任何情况下都是需要的,在不引起二义性的情况下,我们可以尽量的简化。下面我们就各种不同的情况来逐一讨论。
o 单一关联
如果两个类之间只存在一个关联,在进行link操作时,你无需指定association specification. 比如:
|--------| R1 |-------|
| A |---------| B |
|--------| 1 *|-------|图中,class A和B之间仅仅只存在关联R1,当你执行A和B的实例之间的link操作时,不需要指定association specification,编译器为自动识别这一点。比如:
create object instance a of A;
create object instance b of B;
// relate a to b across R1;
relate a to b;
o 多个关联如果两个类之间存在一个以上的关联,在进行link操作时,你必须指定association specification. 比如:
|--------| R1 |-------|
| |-----------| |
| | 1 * | |
| A | R2 | B |
| |-----------| |
| | * 0..1| |
|--------| |-------|这种情况,如果不指定association specification,编译器无从知道需要在两者之间建立那种link,这就会引起二义性。此时,设计师需要明确的指定association specification。比如:
create object instance a of A;
create object instance b of B;
relate a to b across R2;两个类之间存在的关联也应该把它们的super class的关联计算在内,比如:
|--------| |-------|
| B0 |<|------| B |
|--------| |-------|
| 1 |*
R1| R2|
| * |0..1
|--------| |-------|
| A0 |<|------| A |
|--------| |-------|以及这种情况:
|--------| |-------|
| B0 |<|------| B |
|--------| |-------|
| 1 |*
R1| R2|
| |0..1
| |-------|
|-------------| A |
* |-------|这两个例子中,Class A和B之间的关联数量都为2,在link A和B的实例时,都必须指定Association specification.
或许你会建议,在这些情况下, 如果不指定association specification,可以认为设计师的意图是在两个实例之间建立所有的link,这样做看起来合情合理,但事实上存在很大的问题. 这是因为:
1.在现实的模型中,如果两个类之间存在多个关联,往往从语义上,这些关联基本上不会同时出现在同样的两个实例之间.
2.给设计师提供了更多由于疏忽而造成错误的机会.如果设计师确实想在两个实例之间建立起所有可能存在的link,它必须亲自逐个的执行link操作.
o 自关联
如果关联存在于在两个不同的类之间,当你指定association specification的时候,你只需要制定关联的名字.但,如果一个关联是一个自关联,
你首先必须使用association specification,其次你还必须在association specification中指名角色.
例如:
|------------|
| |
| A | parent
| |------|
| |0..1 |
|------------| |
*| children | R1
| |
|------------|依据这张图,如果你想将class A的两个实例a和b通过R1连接起来,你必须制定它们二者所需要扮演的角色,否则会引起二义性. 比如:
create object instance a of A;
create object instance b of A;
// a is a child of b
relate a to b across R1.children;最后一条语句指明了,在a和b建立起的新的连接中,a扮演的角色是children,b扮演的则是parent.
o 普通关联,关联类和静态关联
需要特别指明的是,普通关联,关联类和静态关联是三种不同的关联,在考虑是否会引起二义性的时候,需要分开考虑.例如:
|--------| R1 1..*|---------|
| |------------| |
| | | |
| | R2 * | |
| A |----------->| B |
| | bars| |
| | R3 | |
| |------------| |
|--------| | |---------|
|
|----------|
| C |
|----------|在这个例子中,class A和B之间存在三个关联,其中:
R1是普通关联;
R2是静态关联;
R3是关联类。
尽管A,B之间存在三个关联,但由于他们的类型不同,所以我们对他们进行关联的操作的时候,无需指定association specification。如下:create object instance a1 of A;
create object instance b1 of B;
// relate a1 to b1 across R1;
relate a1 to b1;create object instance b2 of B;
// relate b2 to B accross R2;
relate b2 to B;create object instance a3 of A;
create object instance b3 of B;
create object instance c of C;
// relate a3 to b3 across R3 using c;
relate a3 to b3 using c; -
2004-10-18
在GC系统上的Destroy语义
关于xUML中是否需要GC的问题,着实让我困惑了很久,其原因是:
o xUML是一种面向设计的语言,在设计级别,不应该让设计者考虑内存管理的因素.更何况很多实现级别的编程语言都已经让程序员免于内存管理之苦.
o 但xUML的目标是由设计生成实现,而实现所对应的目标语言千差万别,有些语言是支持GC的,而另外一些语言则不然.
o 即使我们可以为那些不支持GC的语言实现一套GC库,但有效的GC策略都有一个共同的弊病:实时性不好.这对于实时性要求比较高的系统存在比较大的影响.针对以上原因,一个可行解决方案是:
提供两种版本,一种是面向实时系统的,对于此类系统,设计者需要自己去考虑内存管理问题;另外一种则面向非实时系统,若使用它,就让xUML来为你管理内存吧.OK,大方向问题已经解决. 我们来看点细节.
众所周知,人为的内存管理可能会造成如下问题:
o 悬挂指针; 即一个指针引用着一个已经不存在的对象.
o 内存泄漏; 一些对象已经不再被系统中的任何指针引用,但它所占用的内存没有得到释放.GC会帮程序员解决上述问题.
o 它要求(这是一个要求)程序员只需要创建对象,而不要自己去destroy它们.
o 然后,它会去找到系统中所有已经不再被引用的对象,并释放它们所占用的内存.
前者着重于解决悬挂指针的问题;后者则着重于解决内存泄漏.两者之间的协作共同解决上述的内存管理问题.GC确实可以帮助程序员免于内存噩梦,但我们也必须了解,GC并不能帮你做好所有关于资源回收的事情.还是那句话: 没有银弹.
由于GC要求程序员不要自己去destroy对象(在Java/C#等语言中,根本就没有提供destroy对象的方法),这可能会造成,一个对象,按照设计意图,在某个时刻应该不再存在了,(也就是说在设计者的眼中,它应该成为一个垃圾了),但由于程序员的编程错误,它没有让所有针对它的引用失效,这个对象就会依然存在于系统中.
比如:
|-----------------| |------------|
| Company | | Person |
|-----------------| R1 * |------------|
|-----------------|--------------->|------------|
|+getSumOfSalary()| employees|+getSalary()|
|-----------------| |------------|
*| residenters
|
| R2
|
1| residented
|-------------|
| Community |
|-------------|假如,一个Person Object is dead,从语义上,这个对象应该从系统中cleanup,但程序员仅仅断开了这个Person Object在R2上的link,但却忘记断开R1上的link,那么GC则不会自动清理这个对象,当调用一个Company Object的getSumOfSalary方法的时候,这个本应不存在的Person Object却仍然被算在内,从而造成错误的结果.更糟糕的结果是,这样的bug可能非常难以被发现.因为,如果没有GC,程序员会去destroy这个Person Object,这样R1上的引用就会变成悬挂指针,最终这个悬挂指针会造成系统崩溃,从而让程序员意识到系统存在问题,然后,他会去尝试跟踪和发现问题的所在.
所以,即使在支持GC的系统上,为设计师或程序员提供Destory语义,会为解决这类问题带来便利.
1. 设计师可以通过Destroy告诉系统,我要在这里销毁这个对象.但系统是否真正销毁这个对象并回收其所占用的内存,依赖于其是否已经满足了GC的条件. 也就是说,在Destory操作被perform之后,如果系统仍然存在对其的引用,则其所占用的内存仍然不会回收.这样就不会引起任何内存问题.
2. 一旦设计师明确的Destroy一个对象,系统会尝试断开其和其它对象之间的所有link.这样,系统可以试图让其满足内存回收的条件.
3. 一旦设计师明确的Destroy一个对象,此后对此对象进行的一切操作(set/get attributes, link/unlink association, access operations)都会引起系统异常. 这可以帮助设计师尽快的发现问题.OK,all in a word, If a designer intent to express "This object is NOT required in this system any more", Don't hesitate, just use it!!!
-
2004-10-11
Composite的约束
多重性
组合(Composite)有很强的整体与部分关系语义,一个被组合的对象最多能够被一个组合对象拥有。这意味着,组合端的多重性(Multiplicity)只能为0..1或1..1,其它的多重性都是不符合语义的。
如下图所示(由于用文本作图无法直观的表现出组合关系,所以我们用聚合来表示组合):
|------| 1 *|-------|
| A |<>------| B |
|------| |-------|
|------|0..1 *|-------|
| A |<>------| B |
|------| |-------|一个Class上的多个组合
另外,一个class可能被超过一个其它class组合,比如:
|------| 1 *|-------|* 0..1|-------|
| A |<>------| B |-------<>| C |
|------| |-------| |-------|这种情况下,并不表示一个B的实例能够同时被一个A的实例和C的实例拥有;而是说,一个B的实例要么被一个A的实例拥有,要么被一个C的实例拥有,或者干脆谁也不拥有(因为C端的多重性为0..1)。换句话说,如果一个class被多个其它class组合,那么这些组合关联之间存在着Exclusive的约束。更进一步,如果所有的聚合端的多重性都为1,那么这些组合关联之间存在着XOR约束(Exclusive OR)。
自组合
关于组合的一个有趣的问题是,一个Class可不可以自组合,比如:
|-------| 1
| |<>--|
| A | |
| |----|
|-------| 1..*如果一个对象可以组合同一class的其它实例,并且被组合端的多重性的最小数量大于0(比如1..*, 1..5, 2),那么就会造成无穷的递归问题。因为对象一级的组合链绝对不能形成环,否则就违背了组合的语义,其他类型的关联不直接包含这样的语义。
但是,如果被组合端多重性的最小数量为0(比如*,0..1, 0..5),那么Class自身的组合关系是允许的。因为对象级别的拥有关系存在终止条件。
自组合的一个变形是:
|--------|
|--| B |
| |--------|
|----------------| |
| |<|--|
| | |
| | | |--------|
| A | |----| |
| |1..* | C |
| |-------<>| |
| | |--------|
|----------------|这种关系下,C is a A,但C组合了A,由于B is also a A,所以,在这种关系下,由于存在着对象组合链存在着终止条件,所以即使被组合端的多重性的最小值大于1,这种关系仍然是合法的。
-
2004-10-04
再谈Invariant Constraint
我们已经知道,不变式约束(Invariant Constraint)用来检查一个类的实例(Instance)是否处于合法状态。但一个Instance的合法状态,除了需要满足设计者明确给出的Invariant Constraints之外,还要满足更多的内容。
举一个例子,Class A和B之间存在一个关联(Association),这个关联的两个关联端(Association End)的多重性(Multiplicity)分别为1和3..*,如下图所示:
|----------| |----------|
| A | | B |
|----------| 1 3..*|----------|
| |-----------| |
| | | |
|----------| |----------|这个Class Diagram指明,一个A的实例至少和3个B的实例有关系,一个B的实例和一个B的实例有关系(不能为0个)。否则它们就是非法的。
但一个Class A的实例刚刚被创建之时(此时系统已经调用了Class A的相关构造函数(Constructor)),很有可能还未来得及和至少3个B的实例进行关系的建立,如果此时对这个实例进行合法性检查,系统将由于此实例所处的非法状态而无法正常的运转下去。
在明确我们究竟什么时候检查这种类型的检查之前,我们先需要先明确一下我们创建一个对象是为了什么。
一个对象存在的意义无非有3个:
1、向外界提供其包含的或存储的信息;
2、作为导航路径的连接点;
3、接受外界发送的消息/外界调用其Public Operations。OK,如果能够保证一个对象在向外界提供上述3种服务时能满足Invariant Constraint和那些Multiplicity Constraints就不会存在任何问题。其它时候对实例的状态进行的改变,由于不会影响其对外提供的上述3种服务,所以不需要进行这些检查。
单纯的改变实例状态行为包括:
1、改变一个属性的值或集合;(set方法)
2、改变Link上连接的对象或对象的集合。(link/unlink操作)So,在这两种行为所对应的生成代码中,我们无需做任何实例状态的检查。
由此类推,一个实例被构造完成之后,我们也无需对其状态进行检查。因为,在没有要求当前实例提供任何上述3种服务之前,它处于非法状态并不存在任何问题。
警告是:设计师必须保证,在你要求一个实例提供服务之前,它的状态要满足所有Instance Scope的约束。
-
2004-10-02
Invariant Constraint
o Basic Concept
一个Class可以有它自己的Invariant Constraints.它们被用来检查一个Class是否符合契约(Contract).什么是契约,先来看一看牛津英语辞典对其所做的定义:
An agreement between two or more parties, especially one that is written and enforceable by law.对于class得invariant constraints而言,契约是Class instance和其使用者之间的Agreement. Class instance是service provider,而使用者是clients.如果clients想使用某个class的service,它们就必须遵守这些constraints.否则,将造成这个class的某个或所有的instance处于invalid状态.从而让整个系统失效.
Invariant Constraints的不变性体现在Class/Class Instance由于外部(Clients)对其发送消息的前后,我们可以用表达式描述为:
{INVs} public operation {INVs}
即,当外部调用其public operation(发送消息)之前,应该检测Class/Class Instance是否处于合法的状态.由于一个operation可能会改变Class/Class instance的状态,所以在public operation被调用之后,也应该检测其是否处于合法状态. 但在一个operation被调用期间,这些Invariant Constraints不必要被遵守.
我们来看一下一个class instance的Lifecycle.
1) 被创建 (Constructor被调用)
2) 接收外界发送的消息. (其它函数被调用)
3) 被销毁 (Destructor被调用)在一个class instance被创建之后,就应该满足invariant constraints. 随后,每次当其接受外部消息之前,以及消息处理之后都要检测这些Constraints.在被销毁之前,没有必要再检测此class instance是否合法,很有可能destroy它的原因就是因为其不满足自身的约束.
以下图为例,当一个class instance被创建之后,则处于状态I,随后等待clients给它发消息.收到一个消息后进入状态1,然后再收到一个消息后进入状态2,随后又由于另外一个消息而进入状态3,随后被销毁.图中的箭头表示外部消息的处理过程,所以其invariant constraints需要在进入状态I,1,2和3处之前进行检测,并在状态I,1和2之后进行检测.
Create Destroy
-------->(I)---->(1)----->(2)---->(3)--------->(X)从这个图中,你可能会产生一个疑问,你或许认为并没有必要在一个外部消息被处理前后都检测invariant constraints,因为在一个连续的调用过程中,上一次的外部消息处理结束时的状态就等于下一个外部消息处理之前的状态,所以只需要在外部消息处理之后进行检测即可.
但实际的情况是,一个class instance可能会调用自身的public operation,比如:
class A{
public void op1()
{
...
}
public void op2()
{
...
op1();
...
}
};当外部调用op2时, op2经过一些其它处理之后,会调用op1, 由于在调用op1之前,系统状态可能已经发生了变化,所以控制流进入op1的时候,并不等于上一次外部消息处理结束时的状态.只有在调用op1之前也同样进行检测,才能保证系统的安全.
看到这个例子,你可能仍然不服气,因为,理论上,op2调用op1的过程是一次内部处理过程,这期间系统可以不用进行检测.甚至你会建议在这样的情况下,op1调用之后都无需检测.
OK,我们看另外一种情况:
class A{
private B b = new B(this);
public void op1()
{
...
}
public void op2()
{
...
b.op();
...
}
};class B{
private A a;
public B(A pa) { a = pa; }
public void op()
{
...
a->op1();
...
}
};在这个例子中,class A与class B之间有一个1对1的关联,在class A的op2中,调用了class B的op,在class B的op中,调用class A的另外一个外部操作op1,对于class A的instance来说,class B的instances绝对是外部实体.所以对于任何public的operation,都应该在调用之前和之后均进行检测.
o Query Operation
类型为Query的public operation除外,因为Query operation不会对系统状态造成任何改变,所以在query operation执行之后无需重新进行检测,只需要在之前检测即可.
o Class Scope Operation
一个class可以有class scope的operation,由于这些operation不会发给任何一个instance,所以在调用这些operation的时候,也无法进行invariant constraints的检查。
一个solution是在定义invariant constraits的时候,把它们分类为Instance scope和class scope的;另外一个solution是我们可以把这些class scope的invariant constraints放到class scope operation的post condition里.前者相对于后者的好处是: 我们无需把相同的约束在每个class scope operation的post condition里重新定义一遍;缺点是: OCL没有区分invariant constaits的scope,我们必须自行扩展OCL。
o Visibility
由于Invariant Constraints是一个class instance和其clients之间的一种契约,所以无需在private和protected的operation里进行检测.
更重要的原因是: 所谓Invariant Constraints是指当外部消息被处理之前和处理之后class instance所应该处于的状态,在消息被处理期间,这些Constraints并无必要遵守。所以,对于private,protected的operation,如果进行Invariant Constraints的检测,可能会造成系统失效。
o Attributes
对于Attributes,按照严格的OO理念,应该完全设置为Private的,然后提供set/get operation,这些set/get operation的visibility被设置为相应的值.其中public set operation可能会改变系统的状态,也应该在其中进行Invariant Constraints检测.







