作者:孟岩 来源:CSDN博客   酷勤网收集 2007-12-02

摘要
  首先你要区分“has-a”和“uses”两个关系。然后我们来谈依赖。一个对象要享用另一个对象的服务,就可以说形成了依赖关系。只不过,has-a的依赖比较稳定持久一些,uses的依赖比较偶然一些,所以可以认为,has-a的依赖比uses的依赖“强度”要大一些。

前几天收到一个朋友的来信,讨论了一下面向对象实现中经常遇到的问题:在问题域中不存在的依赖关系,在代码实现中却不得不存在。我给出了自己的看法,又觉得不是很有把握,请大家一起来考虑考虑。
******************************* 朋友来信 *******************************
您好!
  ......
  Robet C.Martin的《UML for Java programmers》(清华大学出版社),书讲到“开放-封闭原则(OCP)”(65页)时举了个例子。
  图6-4的OCP的违例是Employee类依赖于EmployeeDB。然后作者在图6-5中给出遵守OCP的设计,是Employee依赖于EmployeeDB接口,而EmployeeDB实现那个接口。——这个叙述我明白了。
  但是,EmployeeDB要实现(持久化Employee的)writeEmployee方法,需要给这个方法传入一个Employee的实例。这样可以说EmployeeDB是依赖于Employee的(如64页图6-2所示)。
  这样,Employee和EmployeeDB接口之间不就构成了循环依赖了么?
  即:Employee依赖于EmployeeDB接口;同时EmployeeDB接口中的方法需要Employee实例作为参数或返回值(EmployeeDB依赖于Employee)。而书前面已经提到循环依赖是不好的设计。
  我现在不清楚:
  1)我对图6-5的理解是否正确?
  2)是不是真的构成了循环依赖?
************************** 我的回复 ***************************************
你好!
        抱歉,我在外地,手边没有这本书,只能根据你的问题做一些猜测,如果有错误的话,请原谅。
 
        首先你要区分“has-a”和“uses”两个关系。这两种关系很接近,所以他们之间的关系很微妙。表现在程序里,has-a是指一个对象中内置一个指向另一个对象的引用作为数据成员,而uses关系,使指对象中的一个或多个方法需要指向另一个对象的引用作为参数传入,然后调用该对象的一些方法来完成任务。不管是has-a还是uses,无非都是需要享用对方提供的服务。可以认为,has-a关系是比较稳定的uses关系,而uses关系则是比较偶然的has关系。
 
        然后我们来谈依赖。一个对象要享用另一个对象的服务,就可以说形成了依赖关系。只不过,has-a的依赖比较稳定持久一些,uses的依赖比较偶然一些,所以可以认为,has-a的依赖比uses的依赖“强度”要大一些。
 
        原则上,面向对象设计中应当尽力避免发生对具体对象的依赖,所有的依赖关系最好都是依赖于接口的。从这个意义上来说,最理想的情况是,一个对象中只能包含指向interface的引用,而所有的方法中的参数类型都是interface。
 
        这是理想状况。现实世界里这样做是要累死人的,而且也根本不需要在任何场合不由分说地都搞松耦合。相反,应用程序整体上应当划分为几个尺度适当的模块,模块之间固然应当松耦合,而模块内部就没有必要再搞得松松垮垮的。一般只在应用程序的模块边界上用interface抽象。
 
        当两个对象之间发生所谓循环依赖的时候,通过具体分析,你可以得到一个整体的概念,即究竟哪个对象是client,那个对象是server。比如在你举的例子里,Employee显然是domain中有的实体,而EmployeeDB完全是与domain无关的东西,使为了支持计算而不得不存在的东西。很明显,Employee是client,EmployeeDB是server,是给Employee提供支持服务的。所以在应用领域里,没有循环依赖,而是单向依赖关系。
 
        问题在于,当我们编程实现时,本来是单向依赖的关系,在代码中却出现一种相互依赖的关系。Employee一定要依赖EmployeeDB来进行持久化,这个依赖关系是固有的。而EmployeeDB中的某个方法一定需要Employee对象作为参数,这种依赖关系是偶发的。在这种时候,我们用比较强的依赖关系,也就是has-a来描述固有的那个依赖关系,并且让这个关系依赖于interface而不是具体对象,而用比较弱的依赖关系,也就是uses来描述偶发依赖关系,这是比较合乎逻辑的。你可以这样理解,Employee对于EmployeeDB的依赖,是对象级别的,而EmployeeDB对Employee的依赖,只是个别方法级别上的。故此我们可以说在对象级别上,EmployeeDB并不依赖于Employee,两者的相互关系呈现一种不对称性,而这种不对称性在逻辑上恰恰符合应用域的分析结果。
 
        当然,具体到这个例子,我看问题好解决,让writeEmployee方法接受一个byte[]参数,而让粘结代码负责将Employee实例序列化成byte[],然后传入writeEmployee方法就可以了。但是其它很多情况恐怕就没这么容易。比如,如果writeEmployee方法除了要写入字节流之外,还需要给被写入的Employee对象打个记号,怎么办?还是只能传引用进去。上面讲的是大道理,实践中还是存在一个逐渐调试的过程。也许你发现出于实用考虑,做一个IEmployee接口也很不错,那就这么办。一切以好用为准。
 
         这个问题也不仅仅是面向对象中的问题,在很多地方都有这个问题:分析上不存在的依赖在代码里是存在的;概念上不需要知道的东西,到了具体代码里就不得不知道。我还是觉得,这些问题大概只有反复实践能够最终认识清楚,良好的、合理的接口设计恐怕只能够是重构出来的,没有什么绝妙的分析技术能够预见到所有的细节问题。
 
        祝好!
 
        孟岩
      2006-8

来自:http://blog.csdn.net/myan/archive/2006/08/13/1057579.aspx

评论

#   myan 发表于2006-08-13 14:19:00  IP: 58.49.252.*
呵呵,韩磊兄刚刚完成CSDN重大项目,又逢《Beginning C# Object》印行,理论与实践具备,可以给我们好好讲讲C#中的委托。

#   partech 发表于2006-08-13 14:58:00  IP: 222.248.19.*
相互依赖的例子太多,不能完全看作不好的设计。例如java中的Object有一个toString方法,String类是Object的子类。他们之间就是一种相互依赖。在同一个包中的相互依赖是可以接受的,这就是为什么需要将接口放入client端的原因。不过,最好还是考虑优先通过单向关联来解决问题,因为双向关联毕竟复杂些。

具体到Employee和EmployeeDB接口的问题,其实,Employee不一定非要通过EmployeeDB来持久化自己。老马的PEAA中给出了UnitOfWork,可以优雅的解决DomainObject持久化的问题。

说到依赖问题,更好的设计是持久化依赖于DomainObject而不是相反。考虑一个find方法,EmployeeDB就不得不返回Employee对象了。实际上是DomainObject依赖于持久化接口,持久化类实现持久化接口。DomainObject同持久化接口在同一个包中。持久化类在另一个包中。

Employeee------>EmployeeDB<-------EmployeeDBImp

另外,从分层的观点来看,最低层应该是业务层,其他各层都依赖于它,可以把持久化看作是对于业务层的一个扩展方面。

#   韩磊 发表于2006-08-13 13:27:00  IP: 61.49.224.*
这样的讨论真好!面向对象技术中的很多手段,就是为了解决依赖关系而存在的。如孟岩所说,对象之间,只要有提供服务与享受服务的需要,就自然会有依赖,has-a/uses是不同强度的依赖关系。接口实际上并不能把依赖关系彻底干掉,无论是本地接口还是webservice接口,而只是虚化了对象间的服务引用,看透了,还是对象与对象的依赖:1、你总得使用一些类(和类的实体——对象)去实现接口 2、追根究底,接口也是一种object(不是说语言层面,而是自然层面)。收信者可以看看C#中的委托,那又是一种有趣的依赖——也许可以叫“三角债”吧。

#   redsea 发表于2006-08-13 15:14:00  IP: 219.136.215.*
软件系统无非是通过一些业务模块/技术模块搭建而成的.

如果相关的业务模块,技术模块, 并没有什么要动态组合, 或者可能某个模块以后会被整体替换掉, 那么这些模块之间有依赖并不是什么很大的事情.

但如果某个模块以后会在其他地方被重用, 那么这个模块和系统其他地方的依赖就需要越小越好.

这种情况下, 我目前的做法很简单:
1. 一个模块通过 class 和 free function 提供service

2. 用 free functor, interface 等手段定义模块需要外界提供的 service

3. upper composite 模块负责将其下层模块结合起来.

为了让模块不必知道松耦合合作模块的细节, 结合的过程就有点罗索, 例如互相依赖的模块的初始化步骤,final 步骤等. 所以我专门有一个模块初始化的代码.
模块初始化代码分析模块之间的依赖关系, 安排出正确的初始化顺序, 然后调用各个模块的初始化代码.

非紧密耦合模块的绑定问题, 是在composite 模块的 InitConnect_extThread 这一个 stage 中解决的.

举例, 我的一个网络监视程序中, 抓包的模块在某个操作系统平台上,提供不了 cancel 代码, 需要给自己发一个包才能实现 cancel; 但是发包的lib 有许多, 我不想让抓包模块固死依赖某个; 因此抓包的模块只是在 api 中定义了一个functor variable, 上级模块要负责给这个变量赋值.
在这里, 上级模块就是main_mod, main_mod 依赖于 monitor_mod 和 packet_mod, 那么,main_mod 的 InitConnect_extThread 执行的时候, 两个下层模块的相应初始化代码就已经执行完毕, 可以从 packet_mod 获得发包对象绑定到 monitor_mod 的 functor 中了.


我的代码中, 一个普通的模块, 从CModule 中派生, 要选择实现下面的函数

// -----------------------
// 参数 sysConf 是一个 smart_ptr

// 模块的初始化, 按依赖数顺序, 调用所有模块的第一个函数之后, 再调用所有模块的第二个函数, 然后是第三个.
// 如果是线程模块, 接下来还会调用 线程模块的 Init_threadedModule.

// 初始化模块内部, 由外部线程调用, 模块应该做好内部的工作, 模块api 中, 申请对象,句柄,接收信息,接收msg 等api 都要能够被调用, 而不失败.
virtual bool InitInternal_extThread(PSmart_sysConf & sysConf){return true;}

// 初始化模块之间的关系.
// 如果 A 的 api 中, 用 functor 来 bind 合作模块, C模块使用到A 和 B 两个子模块, C 会将 B 的功能assign 给 A 的functor.
// 如果有这种工作, 在这步做

#   redflow 发表于2006-08-13 16:45:00  IP: 58.61.171.*
软件的裁剪技术就是来解决代码中的依赖关系,但俺对这个不是很了解,不知道孟老师是否了解,可详细介绍一下

#   刘典 发表于2006-08-13 20:16:00  IP: 218.27.64.*
在很复杂的系统里循环依赖的确经常出现。
但是我觉得这个例子里存在循环依赖的根本原因是持久类依赖了具体业务类的实现,比如持久类要在编译时刻知道职员类是如何实现的, 我觉得这个就是那个人所说的“在问题域中不存在的依赖关系,在代码实现中却不得不存在。” 因为“持久类”依赖的是“业务类”而非雇员类或者部门类等, 也就是说应该是 DB类依赖BusinessObject类 然后Employee类继承自BusinessObject。这样就不存在循环依赖了。至于说DB类如何知道业务类里有哪些属性,对于支持反射的语言可以用反射。不支持的可以在BusinessObject实现两个抽象方法,比如 getPropertys()和getPropertyValue() 然后由具体的业务对象实现。

这个例子很常见的一个问题是你对业务对象进行修改必须同时维护相应的DB类, 这是紧耦合的一个例子。

#   BugOnline 发表于2006-08-14 10:51:00  IP: 210.22.82.*
有意思
--- 在线的BUG管理系统,无需安装,注册即可使用! http://BugOnline.net

#   hbxjzx 发表于2006-08-14 13:22:00  IP: 222.66.70.*
大树无枝向北风,千年一泣思英雄。浮生到头皆是梦,男儿何必尽成功。
这首诗是谁作的?什么意思?


#   baseyueliang 发表于2006-08-14 13:02:00  IP: 58.246.32.*
需要为每一个业务类设计相应的持久类吗,需要吗,真的需要吗,不要把客户需要简单的拿过来就变成系统设计了

#   charon 发表于2006-08-14 13:45:00  IP: 210.52.78.*
很好,这个问题曾经困惑我好长时间,深有同感。
如有必要的话,Interface是比较优雅的解决办法。

#   sinall 发表于2006-08-14 15:00:00  IP: 218.97.242.*
作者对uses的关系表述恐怕不够确切。1)不一定是引用,还有可能是指针,或是传值;2)并非一定是参数吧?局部对象或静态对象不也是uses吗?

#   redsea 发表于2006-08-14 15:07:00  IP: 124.248.65.*
to myan:
free functor 是我生造的. 和 free function 对应.

在我的模块的 api 定义中, free function 和 class function 用来提供服务.

functor 是 function pointer or callable object.

functor variable 和 class member variable functor 有两个作用, 一个是用来登记模块/instance 提供的 callback; 另外一个是, 用来保存该模块/instance 需要的外界服务.

我不喜欢什么东西都用 interface, interface 带来更多的编程概念实体, 还需要配合的类都有下层公共定义, 大大增加耦合度.

functor 简单很多, 合适的函数随便可以赋值过去, 两个松耦合的模块不需要公共下层定义.

我同意你下面的说法. 换个角度看, 依赖于handler 和 callback function pointer 提供 api 的很多 c 库, 重用性都是很好的; 如果是几个严重依赖interface 定义 api 的 java/c++ 库, 要协作起来工作, 需要的粘合代码就比较多了,并且还是粘合类, 编程的概念实体增加不少.


--------
我认为:松耦合的方式是区别面向对象和面向构件的一个关键点。面向对象把程序逻辑分成一个个对象,彼此之间依据对象的接口协作,相互依赖,耦合度还是很高的。即使加入interface作为润滑剂,也如韩磊所说,不过是把耦合性放松了一点点而已。而构件则不同,每个构件都相对独立,对于外界的依赖很小,一堆构件放到一起,彼此之间都没有什么很强的依赖,当然也形成不了完整的app。于是就需要一个粘合层来把构件粘到一起。最后的形式,构件之间不彼此依赖,而是粘合层依赖于构件。

#   myan 发表于2006-08-14 11:33:00  IP: 58.49.250.*
to redsea:
什么叫Free functor?

我认为:松耦合的方式是区别面向对象和面向构件的一个关键点。面向对象把程序逻辑分成一个个对象,彼此之间依据对象的接口协作,相互依赖,耦合度还是很高的。即使加入interface作为润滑剂,也如韩磊所说,不过是把耦合性放松了一点点而已。而构件则不同,每个构件都相对独立,对于外界的依赖很小,一堆构件放到一起,彼此之间都没有什么很强的依赖,当然也形成不了完整的app。于是就需要一个粘合层来把构件粘到一起。最后的形式,构件之间不彼此依赖,而是粘合层依赖于构件。

#   张胜华 发表于2006-08-14 11:34:00  IP: 58.34.45.*
这个问题要写这么多字,汗。

#   redsea 发表于2006-08-14 15:14:00  IP: 124.248.65.*
to anrxhzh

serialization 这基本机制, 如果语言上提供足够的 RTTI, Non Intrusive 应该不会很难的, 接着 rmi 之类也会很容易, 可惜, 我目前用到的 C++ 是支持得不够, 你提起的问题无法解决.

这也是我前一段时间关心 FOG 的原因. 可惜 FOG 没有继续开发下去.

#   张胜华 发表于2006-08-14 11:46:00  IP: 58.34.45.*
myan:

本来不想具体评价了,发完上面几个字又发现你在扯蛋,不得不发。

“而构件则不同,每个构件都相对独立,对于外界的依赖很小,一堆构件放到一起,彼此之间都没有什么很强的依赖,当然也形成不了完整的app。于是就需要一个粘合层来把构件粘到一起。最后的形式,构件之间不彼此依赖,而是粘合层依赖于构件。”

——你这是在捣糨糊嘛!你到底懂不懂什么叫构件(Component)?“粘合层来把构件粘到一起”——这“粘合层”是啥东西来着?

#   anrxhzh 发表于2006-08-14 11:49:00  IP: 221.13.130.*
我倒是由这篇文章想到了另一个问题,那就是对象序列化和单一职责原则(SRP)有时候会发生矛盾。要实现 Non Intrusive Serialization(不需改变类的定义即可对该类对象序列化),被序列化的类必须暴露足够的信息以重建类的状态,这一前提并不是总能够得到满足。比方说有人实现了一个简单的拆炸弹游戏。这里有一个炸弹类,它只提供一个方法:拆炸弹,返回的结果要么成功拆除,要么不幸爆炸。这种炸弹的爆炸机制是随机的,在系统初始化时须指定一个伪随机序列。这个系统虽然简单,但运转得很好。然后某一天另外一个人说,“我要对炸弹类进行序列化。嗯,我必须知道这个炸弹走到哪一步了,什么?炸弹类竟然没有暴露这个信息!我必须和设计炸弹类的家伙谈谈,他应该增加一个方法。”商谈的结果很糟糕,那个家伙不但会设计炸弹类,还知道单一职责原则。“一个类应当只有一个改变的原因,这是原则问题。我的职责就是爆炸,或者不爆炸,并保持神秘感,如果有人想设计一个真正的随机序列,我还可以考虑一下,但是你所说的不在此列,所以我不会为你添加这样的方法。”

#   myan 发表于2006-08-14 20:13:00  IP: 221.232.171.*
to redsea:
Free function是不是就是普通的非成员函数?

to anrxhzh:
你举的这个炸弹的例子,真是令我感触良多。我们学习面向对象的人,类似这样的事情恐怕都遇到过。大家都遇到这样的问题,为什么就不能怀疑一下,是不是在这里,我们笃信不疑的面向对象原则根本就是错误的?为什么这个Bomb类一定要封装起来?一个Bomb一定知道自己会被怎么用吗?我能不能把它的引信拆掉,当锤子来砸钉子?一把椅子一定是用来坐的?我能不能用它来砸人?我早就感觉,面向对象的封装被过分强调了,给我们带来很多麻烦,需要的信息得不到,面对变化脆弱得不堪一击。

前不久我在研究IFC标准,这是一个土木工程领域的数据交换标准,其中有一个概念给我很大的启发,就是所谓资源的概念。在IFC中规定了很多的资源,这些资源是纯粹的客体,其唯一的存在价值就是给别人用的,但怎样使用,它自己不知道。所以,它很简单地把自己的所有数据成员都public了,简单地安排几个基本的方法。在实践中,这种资源使用起来非常容易。可是由于它的数据成员是公有的,所以违反了最基本的面向对象原则。

这么多年以后,我终于想通了,管他什么面向对象原则!好用才是硬道理。

你的Bomb类,恐怕应该是一个资源类,其所有数据成员都应该是公开的。

#   redsea 发表于2006-08-14 22:56:00  IP: 219.136.166.*
to myan
free function 就是普通的非成员函数.

和你提到的面向原则有关系, 我和新同事们交流的时候, 经常会强调一个事情:

所谓的分析方法,设计方法 编程方法, 其根本的目的是将一个复杂度过高, 不能一口气解决/理解的问题,分解成多个相对独立的小问题, 以便可以解决/理解或者进一步分解.

在符合这个大方向的前提下, 采用任何技术手段, 只要是可靠的, 不会使得以后的编程容易出现错误, 那么这个技术手段就是正确的.

具体的 结构化设计, OO 设计, 都是手段, 不是目的. 由于很多人用某种手段做多了事情之后, 会将手段误认为是目的, 所以我才强调过这个问题不止一次.

我是实用主义者, 举个例子, 我用 C++, 我并不象现代C++ 强调的那种, 避免用宏. 举个例子, 我用宏就可以轻松保证, 枚举的次序和相应函数指针数组的下标正确定义, 为什么不用? 而非要用代码/对象初始化去填写这个数组 ?

同样, 如果暴露一些数据成员可以让很多工作得到大量简化, 为什么我不暴露? 而非要弄getter, setter, 然后搞个interface, 多了这么多工作 ? 当然, 暴露了数据成员可能不好控制别的地方对它乱修改, 这个确实很遗憾, 如果可以控制数据成员在哪些namespace 中可读写, 哪些中只是可读, 其他不可见, 就好多了.


#   redsea 发表于2006-08-14 23:08:00  IP: 219.136.166.*
再写几句:
分析,设计,编程方法的目的是将复杂的问题 "分而治之", 能妥善地达成这个目标的方法, 就是好方法.

抽象, 分割, 封装是基本和重要的手段, 在目前的技术手段上, 也可以认为就是目的.

OO 只是完成这些手段工作一种而已, 不是什么唯一正确得东西. GP 在抽象方面表达能力比OO 还更强, 分割和完成得不差.

对于某些应用而言, 可能先分析好业务, 完善地组织好业务的数据模型, 然后再开工, 可能更合适 --- 类似于结构化编程, 先组织数据库的模型.

本来这些数据就是很多地方都需要使用的, 而且是本质性的, 不会轻易变化, 比OO 的方法还更稳定, 这样的数据, 暴露出来大家都轻松, 偏偏没有先整理好这些信息, 非要封装隐藏起来, 然后发现很多地方都要用, 再给类增加很多方法, 不是自己找麻烦吗 ?

#   bokeland 发表于2006-08-15 00:19:00  IP: 221.221.147.*
您的博客备份了? 博客蓝提供博客备份和同步功能, 希望您能试用, 已经支持CSDN了

#   hoping 发表于2006-08-15 09:00:00  IP: 10.61.98.*
to myan:
你所说的资源类 可能 会造成的最大问题就是:当程序规模变大时,无法很容易的知道有那些功能以依赖于这些资源数据,所以当这些资源发生变化时,可能就是噩梦。

什么该隐藏,什么该暴露,是和你要解决的问题领域密切相关的,可能在某个领域中椅子是用来座的,而在另外一个领域中椅子确实用来砸人的。分析的第一步就是确定特定领域中的和领域相关的关键概念抽象,这些概念很可能和现实世界相差十万八千里。当然,这些抽象的稳定程度,正确与否可能和经验和运气有关,所以重构和迭代在所难免。

原则本身只是一个权衡的依据,让你知道该如何获取最大的利益。其实,在很多情况下,好的设计就是把很多可能会极其糟糕的东西,通过某些手段进行分摊,使得每一部分都不那么糟糕。

就拿Bomb类而言,与其把内部细节全部公开于众,到不如只让那些负责Bomb持久化的类知道为好。stairway to heaven这个模式可以参考一下。

#   abc 发表于2006-08-15 10:04:00  IP:
serialization is part of the object. to seperate the two, of course u will get dependency. there's no way u can get rid of all the dependency. there's thousand and one ways to serialize, when will the information exposed by the object be enough? it's better to provide a interface which the object will actively supply the information. the interface will do the transformation to and from the target.

#   hoping 发表于2006-08-15 09:33:00  IP: 10.61.98.*
to myan:
你所说的资源类 可能 会造成的最大问题就是:当程序规模变大时,无法很容易的知道有那些功能以依赖于这些资源数据,所以当这些资源发生变化时,可能就是噩梦。

什么该隐藏,什么该暴露,是和你要解决的问题领域密切相关的,可能在某个领域中椅子是用来座的,而在另外一个领域中椅子确实用来砸人的。分析的第一步就是确定特定领域中的和领域相关的关键概念抽象,这些概念很可能和现实世界相差十万八千里。当然,这些抽象的稳定程度,正确与否可能和经验和运气有关,所以重构和迭代在所难免。

原则本身只是一个权衡的依据,让你知道该如何获取最大的利益。其实,在很多情况下,好的设计就是把很多可能会极其糟糕的东西,通过某些手段进行分摊,使得每一部分都不那么糟糕。

就拿Bomb类而言,与其把内部细节全部公开于众,到不如只让那些负责Bomb持久化的类知道为好。stairway to heaven这个模式可以参考一下。

#   hoping 发表于2006-08-15 09:38:00  IP: 10.61.98.*
to myan:
你所说的资源类 可能 会造成的最大问题就是:当程序规模变大时,无法很容易的知道有那些功能以依赖于这些资源数据,所以当这些资源发生变化时,可能就是噩梦。

什么该隐藏,什么该暴露,是和你要解决的问题领域密切相关的,可能在某个领域中椅子是用来座的,而在另外一个领域中椅子确实用来砸人的。分析的第一步就是确定特定领域中的和领域相关的关键概念抽象,这些概念很可能和现实世界相差十万八千里。当然,这些抽象的稳定程度,正确与否可能和经验和运气有关,所以重构和迭代在所难免。

原则本身只是一个权衡的依据,让你知道该如何获取最大的利益。其实,在很多情况下,好的设计就是把很多可能会极其糟糕的东西,通过某些手段进行分摊,使得每一部分都不那么糟糕。

就拿Bomb类而言,与其把内部细节全部公开于众,到不如只让那些负责Bomb持久化的类知道为好。stairway to heaven这个模式可以参考一下。

#   abc 发表于2006-08-15 10:06:00  IP:
serialization is part of the object. to seperate the two, of course u will get dependency. there's no way u can get rid of all the dependency. there's thousand and one ways to serialize, when will the information exposed by the object be enough? it's better to provide a interface which the object will actively supply the information. the interface will do the transformation to and from the target.

#   myan 发表于2006-08-15 10:42:00  IP: 221.232.168.*
to hoping:
你说的这些问题我当然知道,多年以来面向对象的标准课本里就是这么说的。

然而,我现在认为,资源固然有可能发生变化,但比较起人的想法来说,其变化的剧烈和频繁程度差得太远了。对资源层进行不由分说的封装,实际上是拣了芝麻丢了西瓜。而且资源层往往比较基础,会逐渐固定下来,甚至形成标准,对资源层的依赖风险不大。

一个合格的程序员,应当有能力对相当规模的程序(5000-10000行)的复杂性有效地管理,而这个尺度上已经可以做出很强的component了。我的观点是,不要总想着怎么解决这个规模之内的模块化问题,我写个7000行代码的模块,只要对外接口良好,里面的对象就算把数据成员都共有了,也没什么大不了,可以有效管理。比如Python中,所有数据成员缺省都是公有的,我觉得用起来很好,没有像那些专家诅咒地那样把世界搞成一团糟。

#   hoping 发表于2006-08-15 11:17:00  IP: 10.61.98.*
to myan:

原则本来就是仅供参考的,不过对于学习来说,严格遵守的阶段还是需要的,呵呵。

不过你的话,让我想起了Ivar Jacobson著名的三板斧:
boundary controller entity。

其中的entity就是mindless data。这样做的原因就是为了这些entity能够独立于domain重用,其中只能是些triivial behavior了。

另外对于你说的资源成为标准,风险不大的说法,我不同意。标准修订导致数据模式变化的情况我也见过很多。为了不再上演“千年虫”的悲剧,还是封装一下为好。

软件就是软件,无论你用什么方法基本上可以完成功能,都可以应对更改,区别就在于每个人的“任劳任怨”程度不同而已 :)

#   ibiswang 发表于2006-08-15 11:29:00  IP: 219.131.199.*
关于has-a与uses的关系,我个人的理解是has-a代表的是两个相关部分是同一个实体,而uses代表则是两个相关部分是不同的实体。

例如:Order是一个实体,它至少两部分组成,OrderHeader与OrderDetail(sometimes it is the products ordered),所以我们说Every OrderHeader has lots of OrderDetails, 这样OrderHeader Class has-a OrderDetail Class, 所以OrderHeader 与 OrderDetail是has-a 关系。

Has-a关系,我通常用于表达整体与部分的关系,即聚合关系。也就是子体(Right Part) 不能独立于母体(Left Part)“存在”,注意,这里的“存在”是指“有独立意义的存在”。例如:每个具体的订单明细(订单产品条目),如果没有对应的订单主体,是没有任何意义的数据。


而,uses关系,我通常将它理解为refrence关系,也就是说相关互之间(可能是单双或双向)只是参照或者利用关系。例如:Order与Customer之间,一般就是参照关系,Customer可以独立于Order存在,它是具有独立业务意义的实体,反之,亦然。 Employee与EmpoyeeDB之间应当是利用关系,即Uses关系,也就是说Employee在业务意义是实际上可以也EmployeeDB没有任何关系,只是利用EmployeeDB进行数据相关的操作。

#   ibiswang 发表于2006-08-15 11:34:00  IP: 219.131.199.*
关于has-a与uses的关系,我个人的理解是has-a代表的是两个相关部分是同一个实体,而uses代表则是两个相关部分是不同的实体。

例如:Order是一个实体,它至少两部分组成,OrderHeader与OrderDetail(sometimes it is the products ordered),所以我们说Every OrderHeader has lots of OrderDetails, 这样OrderHeader Class has-a OrderDetail Class, 所以OrderHeader 与 OrderDetail是has-a 关系。

Has-a关系,我通常用于表达整体与部分的关系,即聚合关系。也就是子体(Right Part) 不能独立于母体(Left Part)“存在”,注意,这里的“存在”是指“有独立意义的存在”。例如:每个具体的订单明细(订单产品条目),如果没有对应的订单主体,是没有任何意义的数据。


而,uses关系,我通常将它理解为refrence关系,也就是说相关互之间(可能是单双或双向)只是参照或者利用关系。例如:Order与Customer之间,一般就是参照关系,Customer可以独立于Order存在,它是具有独立业务意义的实体,反之,亦然。 Employee与EmpoyeeDB之间应当是利用关系,即Uses关系,也就是说Employee在业务意义是实际上可以也EmployeeDB没有任何关系,只是利用EmployeeDB进行数据相关的操作。

#   redsea 发表于2006-08-15 22:11:00  IP: 219.136.166.*
嗯, 炸弹好, 我们说说怎么对付炸弹:

1. 一种做法是, 允许我们的工兵对炸弹的原理结构有了解和基本的假设: 炸弹用可以爆炸产生大量气体的物质作为核心, 外部用金属或者非金属包裹,它们是坚硬物质(容易破裂成碎片,或者可以保持整体), 或者可以融化成炽热金属流.

好了, 我们允许工兵根据这些知识做假设, 自行操作炸弹. 例如要引爆炸弹, 可以将炸弹放到非常厚实结实的容器, 然后用小型炸弹引爆.


2. 另外一种做法实, 炸弹完全是一个黑盒子, 工兵们码不允许对炸弹有了解和假设, 要做任何事情, 都需要问炸弹自己--炸弹上面必须贴一个操作说明, 除此之外的事情, 都是不能做的.

第一种做法的好处是, 由于工兵对炸弹有了解, 以及基于了解的基础上的进一步推论和处理, 不必等到炸弹上的操作说明, 就可以做很多事情, 例如引爆废炸弹. 但是, 万一送来一个新品种, 例如中子弹, 我们的工兵决定扔到十米厚度混凝土里面引爆---轰的一声, 混凝土没有坏, 但是周围的人都死掉了, 真是糟糕.

另外一种做法, 嗯, 我们不会犯这样引爆中子弹的错误, 不过, 前线送来很多炸弹需要排除, 嗯, 上面没有拆弹说明书, 我们先堆在一边吧, 等我们研究所的人员过来一个一个贴上说明书再说, 但是由于什么事情都需要研究所的人去贴说明书, 他们很忙很忙, 只好堆了很多很多炸弹, 最后, 连路都给堵上了, 军队都无法调度. 终于某一天, 不知道什么原因, 这堆炸弹轰的一声炸了, 又死掉很多人.

#   abc 发表于2006-08-16 11:53:00  IP:

第三種,炸弹有一個通用接口(類似文件系統接口),通過這個接口,可以拿到炸弹的工程圖,當然有些部分是省略了的,比如螺絲是鐵的還是鋁合金的。怎樣用就是外邊的事了。
具體到應用,比方說可以輸出“名字:數值”。

#   wreched master 发表于2006-08-17 17:06:00  IP: 207.46.89.*
我觉得面向对象解决不了这样的问题吧。我觉得可能需要面向模块的概念,像老vb那样。面向对象只能在模块内部实现,因为一个对象本省就根本不能独立存在,而一个模块才是一个完整的实体。模块之间的组合可以使用组件中的包容和聚合的方式来实现。
或者说模块式一个软件中最小的生命体,是软件的原子

#   韩磊 发表于2006-08-23 17:01:00  IP: 218.247.0.*
----------------------------------------------------------------
——你这是在捣糨糊嘛!你到底懂不懂什么叫构件(Component)?“粘合层来把构件粘到一起”——这“粘合层”是啥东西来着?
----------------------------------------------------------------

component这个词,有翻译做组件的,有翻译成构件的,它在不同体系架构里面,有不同的概念、功用、实现。Delphi中的component,是组件。而“构件”,目前并无明确的定义。国内说“构件”最多的,当属普元公司的中间件体系。myan所说的构件,应该是普遍意义上的“构成部分”,类似建筑中的预制板。

#   xyz 发表于2006-08-29 22:17:00  IP: 221.6.3.*
依赖总是存在的,只不过是以另一种形式存在而已。
就上面所说的,做一个接口就不存在依赖了吗?不,还是存在。只不过感觉松了一点,但是如果就只有一个Employee实体类,做一个IEmployee未免有画蛇添足之嫌。
办法总是有的,就这个例子来讲,你可以将EmployeeDB的相关函数的参数拆分成一系列的string,int等基元类型。但是从根本上讲你还得依赖于这些基元类型,好处是它们是稳定的依赖。坏处显而易见,这样还不如用C来写面向过程的程序呢。

#   aha100 发表于2006-09-01 10:53:00  IP: 10.11.153.*
首先定义一个概念:什么是代码不影响--1.只加入一个类,其他包括client和server都不用修改代码,而程序运行的结果就会产生相应于那个加入的类的期望输出.
那么工厂模式是否达到了这个目标呢?就以简单工厂模式来说
加入了新类之后:
1.server端从接口或抽象类继承产生一个新的product类,不影响server的其他代码,是可插入性的.
2.如果不使用工厂方法,client端使用新产品的时候必须构造这个新产品类,起码的,必须使用
abstractproduct p1=new newproduct(); 其他的可以保持不变.
3.如果采用工厂方法,代码为,abstractproduct p1=creatot.factory("newproduct");这里的代码不是也要改变吗???????????????
4.两者的区别仅仅是后者用一个统一的界面封装了new操作.
我的问题是:
这两者的区别到底有多大,使用了工厂方法之后到底有什么好处呢?
个人想法:
1.这里的好处首先如上所说是用一个统一的界面封装了new操作,这个界面更好.
2.其次是整个系统的代码可以分离开发,只要遵守一定的接口规范.
3.是否真的不需要修改client的代码了呢?这是个问题.
4.难道仅仅是这么些好处吗?

#   aha100 发表于2006-09-01 10:54:00  IP: 10.11.153.*
想问问孟老师,如上帖.

#   Mongoose 发表于2006-09-29 16:18:00  IP: 218.24.136.*
其实这个问题,每个人说的都有道理,只不过是同一个问题的不同Context下的解决方案。
1.就我个人来说,解决方案的确定更多不是看封装、接口是否体现了,而是要看这个问题处于什么样的项目中。如果是一个人或者是若干个高人的合作,那么接口、封装的必要性不是很重要,因为他们都有很好的编程准则和很好的认知水平,知道那些可以访问,哪些不可随便访问。但对于一个更普遍意义上的项目来说,就我个人观点,我不相信合作者的承诺。这里不是对合作者人品的怀疑,而是再高尚的人也是有缺点,有疲惫,有应付的时候,这时候如何保证约定的执行,口头约定是没有用的。只能依赖于封装、接口这些可以通过编译器检查来保证约定的执行。
2.就Bomb问题来说,我觉得redsea分析的很好,封装还是不封装各有优缺点。这要看问题域哪个方面更重要。标准面向对象设计文章中讲的封装,固然有data hiding的意思,但是原意是将变化的和不变的分开封装。这里变化的不变的含指任何意义上的元素,不光是数据。
3.即使应用依赖接口,隐藏实现原则设计。变更时一样可能会造成大的灾难,那是因为接口也是一个事物,接口能应对变化吗。这依赖于对问题域的分析和经验。
4.依赖不可怕,没有依赖(关系)的程序无法执行。关键是这种依赖是依赖于易变的还是稳定的。
5.to redsea关于工兵、研究人员与Bomb
这个我觉得如果工兵了解bomb的结构,且这个(些)工兵热爱、忠心于自己的工兵使命,那么是好的,他们能有效的解决大部分问题,即使偶尔一次的中子弹爆炸带来了损失,但是总体看,成绩是主要的。但是如果这些工兵处于一个高度、快速发展的军工系统中,即使他们热爱本职工作,那么不光他们的命有危险,恐怕其他人也要一命呜呼。宁不如研究人员每次研发出或改进一个新型的Bomb都附上说明书,如果研究人员太少,来不及,那么也不应该让工兵来了解结构,而是要增加研究人员。
注:这里没有对工兵人员的任何轻视。如果工兵人员能较好的理解、掌握、改进Bomb,那么人还是原来的人,但是角色已经应该是研究人员了。但,现实情况中毕竟工兵角色的实体大部分时候还是工兵角色。

以上是个人不成熟的看法,请指正。

分类: 设计模式 系统架构



关于酷勤 | 联系方式 | 免责声明 | 友情链接