• 全部
  • 问答

关于接口设计策略的问题,清晰与臃肿的矛盾

mis98ZB 日电卓越软件科技(北京)有限公司 开发总监  2004-04-26 12:17:12
这是在上个项目中发现的问题,不过事后就忘了。幸好现在想起来了,贴出来请大家讨论一下。

比如说有一个类,它有一个属性用来记载一个字符串中的插入点:
class LineProperty{
private int insertOffset;
....
public void setOffset(int newOffset){
insertOffset = newOffset;
}
public boolean needInsert(){
return (insertOffset >= 0);
}
....
}
在这里的内部实现是,insertOffset小于0,则表示不需要插入。没有使用专门的boolean型属性来标志。

问题就出现了,setOffset方法实际上承担了两个职责:
1、禁止插入
2、允许插入,并指定插入位置。

所以我想把它改成这样:
class LineProperty{
private int insertOffset;
....
public void setOffset(int newOffset){
assert(newOffset >= 0); //make sure offset is valid
insertOffset = newOffset;
}
public void clearOffset(){
insertOffset = -1;
}
public boolean needInsert(){
return (insertOffset >= 0);
}
....
}

但是这样一来,就多了一个public方法,也就是这个类的方法列表被扩大了。列表太长,就会影响这个类的易用性。而且第一种方法虽然含糊,但是也是比较常见的。
不过总的来说,我是比较倾向于第二种方法的。:P
不知道大家的意见如何?
...全文
69 点赞 收藏 58
写回复
58 条回复
切换为时间正序
当前发帖距今超过3年,不再开放新的回复
发表回复
mis98ZB 2004-04-27
所以啊,可以避免的错误用assert,不可以避免的错误用异常/返回值。
回复
Chuanyan 2004-04-27
对于错误处理,一个比较烦的东西就是Java里throw完了又要Catch,Catch完了又要Throw,感觉代码很罗嗦。使用assert是很好的,但是调试与测试始终还是不同的。
回复
mis98ZB 2004-04-27
但是要注意,不是什么时候都可以这么省心的。
回复
mis98ZB 2004-04-27
使用assert而不使用返回值,大概就是“Don't call us, we will call you”
:)
回复
mis98ZB 2004-04-27
息怒、息怒…… :)

呃,可能是我表达失误。
我不是说IsLock()没有出现的必要。
我的意思是用户总是可以不检查lock状态直接使用setOffset(),于是IsLock()就被绕过了。
如何用点损招让用户无法绕过去呢?我的方法是assert。
:)

OK,IsLock()是有必要的,而且不允许用户绕过。

好,现在话题转到setOffset()因不因该有返回值的问题上。(说实话,对于错误处理我可是憋了一肚子的话,可是一直没有人愿意讨论……T0T)

标志错误一般有返回值和异常两种方式。而处理错误的方式则有恢复式和终止式。
assert是一种特例,它不传递错误,因此也就不用标记错误。
相反,它执行一种强硬的就地正法的策略:立即终止程序运行。
比它还狠的,恐怕只有core dump了:)
(插一句,以前有个项目中使用core dump来保存进程状态,以便状态恢复,感觉很有意思)

错误分为可以避免的错误和无法避免的错误。
DbC里边把前者叫作错误,后者叫作异常。

“没有检查lock状态直接使用setOffset()”,这是一个编码错误,是可以避免的。
使用assert,在集成测试中就可以检测到这种错误。
而一旦更改了这种错误,发布以后,用户的运行过程中就绝对不会出错。

而“文件不存在”之类的,则是异常,是作为程序开发者的你无法避免的。
因为控制权不在你手里。
回复
Chuanyan 2004-04-27
setOffset()本来就应该有一个成功不成功的返回值。因为现在没有才会导致“用户可能设置了offset,但是实际上insertOffset并没有改变。”的问题。
----------------------
Agreed
回复
moonsilver 2004-04-27
mis98ZB(Effective Typer) :

老大,你的那个类不会只有一个setOffset函数吧。最低限度还有一个Lock和Unlock,你把IsLock设为private的话用户怎么用Lock呢?如果有一天你觉得分析器运行速度太慢想把它改为一个并发的分析器,你就需要改变IsLock的算法,你怎么办呢?你改为支持并发后,如果用户希望使用DCL算法你怎么办呢?setOffset()本来就应该有一个成功不成功的返回值。因为现在没有才会导致“用户可能设置了offset,但是实际上insertOffset并没有改变。”的问题。
回复
mis98ZB 2004-04-27
呵呵,谢谢moonsilver(银月杀手)这么热心的讨论。感动ing……

今天客户仍然没有提供“形态素分析”的资料,看来我还要再闲一天 :D
所以——哈哈哈,继续讨论 :)

嗯,无论如何,对于设置offset的类、读取offset的类,总是需要使用同样的offset语义。
然而负责解释这个语义的类应该是哪一个呢?
设置offset的类?LineProperty?读取offset的类?
这个问题要好好的考虑一下再说。

另外,我说“这样IsLock()方法才有设置成public的意义”是因为:
如果按照你原来的setOffset()实现,用户总是可以不检查lock状态直接使用setOffset()。所以我才说IsLock()没有public的必要。
但是这样直接使用setOffset()有个问题:用户可能设置了offset,但是实际上insertOffset并没有改变。
错误被隐藏了起来。这样的错误很难发现,发现了也很难定位。
所以我比较倾向于使用攻击性较强的assert方式。
回复
mis98ZB 2004-04-27
嗯,BirdGu(鲲鹏)说的很好,严重同意!
终于要开始干活了,结帖。
谢谢大家的参与。
回复
BirdGu 2004-04-27
我的理解,assert应该用于这样的场合:调用程序也是自己写的,或是同一项目中的人写的。assert用于强调、保证某种设计上的接口约定。大家都把assert enable的话,可以比较容易发现违反这种接口约定的方式。
如果是在实现某种会被广泛使用的接口,比如一个类库,或是框架,那么使用assert是不够的。因为不能保证调用者始终会把assert enable。这时还是应该使用Exception。
至于解决JAVA中Exception比较多,频繁地try/catch的情况,可以使用RuntimeException,或其子类。不过这应该仅适用于这样的情况:即发生的错误,调用者一般是无法处理的。比如一些系统错误、数据库连接错误等。这些错误直接调用者除了再此throw外,一般是无法处理的。
使用RuntimeException,使得无法处理错误的程序可以忽略这些Exception,而留给能处理的程序去处理。
当然使用RuntimeException,就得不到编译时的检查的帮助,可能会遗留应该处理的Exception。这一点需要权衡。
回复
moonsilver 2004-04-27
其实还是算一个示意,不过可用于某些单线程环境中的“并行”场景。如果是多线程程序,有不同的实现,那个token参数可以没有。

不过话说来,这个问题没有什么太多好讨论的了,离题万里了。
回复
moonsilver 2004-04-27
更正:
public bool IsLock(int token){
if ((token != ownerToken) ||(ownerToken == 0))
return false;
return true;
}
回复
moonsilver 2004-04-27
其实我加那两个lock函数只是一个示意,没想到都基于这个来讨论了,如果要基于此,我想有必要写完整一些。

class LineProperty{
private int insertOffset;
private int ownerToken;
....
public void setOffset(int token, int newOffset){
if(IsLock(token)) return; / or assert(IsLock(token));
insertOffset = newOffset;
}
public int Lock(){
if (ownerToken != 0) return 0;
return GenerateToken();
}
public bool Unlock(int token){
if (token != ownerToken)
return false;
ownerToken = 0;
}
public bool IsLock(int token){
if (token != ownerToken)
return false;
return true;
}
....
}

我个人不喜欢用assert。

在本题中,如果用assert, 那么用户不检查setOffset返回值就必须检查Lock的返回值,总要检查一个,这样对于一些特殊的算法是比较麻烦的,而返回值不存在这样的问题。在某些有特定要求的地方,将assert作为规格对于使用者来说也是很不方便的。
回复
Chuanyan 2004-04-27
俺现在是可以避免的错误用Exception,不可以避免的错误就不管了(Framework去管)。
回复
ozzzzzz 2004-04-26
不太明白你为什么要记录这个插入的点呢?一般情况都是把插入的点和插入的东西在一起传递出去啊。肯定有你特别的动机。
如果是只要一个方法,我想就有个是抛出异常还是设定返回值的问题,也许异常好一点。
回复
mis98ZB 2004-04-26
请大家讨论一下。
回复
moonsilver 2004-04-26
mis98ZB(Effective Typer) :

前面主要是抛开具体应用,从纯技术角度的观点。如果从具体你的应用来讲,有更好的设计。
回复
moonsilver 2004-04-26
mis98ZB(Effective Typer) :
IsLock是不是public和setOffset没有关系,setOffset一定要用IsLock判断,原因见redguardtoo()的帖子。
=================================================================
[redguardtoo() ( )]

另外关于接口的设计可以参考Bjarne Stroustrup
http://www.artima.com/intv/elegance.html

他提出的方法是只写最少的接口,然后在增加一个工具类的接口在最少的接口上进行逻辑组合.
这样就避免了维护的问题(大意如此).

我自己开发经验有限,没有用过这个方法.


回复
moonsilver 2004-04-26
[mis98ZB(Effective Typer) ( ) ]
可是万一有一天你打算“把所有负值的offset视为从行首开始插入”呢?
这样就会感到算法耦合的痛苦了吧 :P

--------------------------------------------------------
前面已经说过了,如果你想实现一个完全隔离的设计,你应该做一个iterator. 否则,你同样有可能有一天把行首定义为2, 而不是1或者0。我想说的是你将0,1,2,3置为
什么位置,那么-1,-2,-3也应该置为什么位置。

其次,“把所有负值的offset视为从行首开始插入”这是具体算法的事情,也就是你所说的“使用者”的工作,你在你这个property类的现有接口中式无法实现的,除非你实现新的借口,比如一个iterrator和allocator。
回复
mis98ZB 2004-04-26
嗯,回家之前最后说一点我的看法。
不要说我吹毛求疵、死缠烂打哦,我只是想多讨论点问题。
m(__)m

moonsilver(银月杀手)的:
public void setOffset(int newOffset){
if(bReadOnly) return;
insertOffset = newOffset;
}
以及ozzzzzz(希望敏捷)的:
在这里可以采用我告诉了你然后就不管的策略,也就是使用者只负责告诉你是否需要在某个点进行某个操作,然后具体这个操作是否完成它就不负责任。

在DbC里,数据的有效性是由数据的提供者来保证的。
而数据的使用者只需要验证它。
所以我觉得moonsilver(银月杀手)的代码应该改成:
public void setOffset(int newOffset){
assert(false == bReadOnly);
insertOffset = newOffset;
}
这种方法禁止用户在lock之后使用setOffset方法。
这样IsLock()方法才有设置成public的意义,同时也强制用户养成先检查lock状态的习惯。
回复
相关推荐
发帖
研发管理
创建于2007-08-27

1206

社区成员

软件工程/管理 管理版
申请成为版主
帖子事件
创建了帖子
2004-04-26 12:17
社区公告
暂无公告