我的类该怎样安排(续)——请ahao也进来
在这个帖子里和zhao讨论了很多,zhao还有其他几位朋友给了很多很好的建议,准备把总体框架大改一下。
起初我只想加个NODE类。加它不光是因为我每次传一大堆标志位做函数参数传得我烦,还因为微软的node对象在treeview不可见时不能很好地判断自己的上下左右,至少它的parent会是nothing。前几天我勉强自己每次要取其他node对象的时候把相应的树的可见性toggle一下,但是这种toggle貌似会引起treenode的expand事件,搞的我的见光标志位也是一摊糊涂账。所以,折腾来折腾去我就想加个node类了。
之后ahao和我提到MVP等经典模式。我看了一下资料,感觉我原来设计的对象模型太简单,一个对象负责的东西太多,导致逻辑复杂,变更和维护都很累。所以,思量来去决定大动。具体设计如下:
1、总的思想
基本希望的样子是面向对象的,负责数据的和负责显示的分开,集合类和单体类分开,界面数据和核心数据分开。各司其职,通过事件机制来进行消息传递。
2、节点表和节点细节表
在描述具体的设计结论之前,先介绍一下节点表和节点细节表:
一棵树下可能同时存在多种不同性质的节点,比如,在资源管理器的树中,同时存在文件夹和文件两种节点,它们各自对应不同的属性。如何在数据库中管理这些位于同一棵树下但具有不同数据结构的信息呢?
初步思路是,在节点表中设置strDetailTable和lngDetailId字段,存储节点对应的细节信息所在的表的名字和在其中的ID。即,节点基本信息在节点表中,节点细节信息根据不同的类型存在另外的相应的节点细节表中。这样要找一个节点的细节信息,只要到strDetailTable字段规定的表中找相应记录就可以了。相应的,删除节点的时候,要同时删节点表和节点细节表中的记录。
注意:节点表与树是一一对应的,有几种树就有几张节点表;而由于多挂节点的存在,节点细节表和节点表是一对多的关系,就是,同一细节可能在不同节点表里都存在相应的节点,在同一节点表里也可能存在不止一条记录具有同样的细节。
2.1.节点表的字段
1.lngNodeId
2.lngFatherId:记录它的父节点的节点ID
3.strDetailTable:该节点的细节表名
4.lngDetailId:该节点的细节ID
5.sngValue,用来记录本节点的所有子节点的金额之和;
6.sngPercent,用来记录本节点的金额占父节点金额的百分比。
7.strNodeComment
2.2.节点细节表的字段
a)lngId
b)strName
c)strComments
d)bytMultiGua:多挂计数
e)sngValue:仅当为基础节点时,才有该字段
3、类的安排
a)CTree类和CNode类。一个CTree类实例对应一个节点表,一个CNode类实例对应一条节点记录。CTree类包含由CNode类对象构成的集合。这两个类是界面数据类。
b)CTreeCtl类。负责界面工作。一个CTree类可能对应多个CTreeCtl类。
c)CDetail类。这个是核心数据类。负责细节数据的增删改。
d)CMsgMate类。消息使者类。负责触发各种自定义事件
4、数据的装载
和zhao讨论的一个重要分歧在于数据的装载,zhao建议所有数据一次性载入内存,之后对树的增删改都直接在内存上操作,之后根据情况再批量更新到数据库。我承认也理解这种做法在性能上的优越之处,不过对自己的内存不是很自信,毕竟我想用这个树类日积月累维护很多各种各样的数据。
另外,我感觉我的应用对性能其实要求不高。我的树的特点是很深(层次很多),起初的几层节点并不多,只到最底层的叶子节点(这些节点是日积月累的)会突然增多很多。用户交互的时候,一般一次是只看一个节点,或增删改移一个节点或有限的节点。这时候,即使我有频繁的数据库操作,哪怕是改一点点也要访问数次数据库,因为要操作的数据量小,我想在性能上不会有什么感觉的。
所以,我现在在数据装载方面的设计只部分采纳了zhao的建议。现在在这个模型里只装载了全部节点的部分信息,它们存在各个CTree的CNode列表里。而CDetail里是不存数据的,它只是负责直接对节点细节表操作。CNode类里也不是简单的对应于节点表里的一条记录,比如它还有节点名、节点多挂计数等属性,这些属性是从节点细节表里查出来的。总的来说,CNode类里存的是一些界面上常需用到的数据,我把它存在这个模型中,省得做一些动作的时候需要频繁访问数据库。
这样有些数据我存了两个地方:CNode列表里有一份、数据库里有一份。这和zhao的建议有点相似。不过,我在变动数据的时候,不是攒着到最后返回到数据库的。而是变动的时候一竿子捅到底,直接改数据库,同时更改CNode里的数据,保持它最新。我给自己的做法美其名曰,叫模型里只存界面数据,呵呵。
5 类之间的协作
当用户对树节点进行增删改之后,各个类之间协作完成相应的数据和界面变动。
a)以改节点名字为例。这种数据存在节点细节表里,其变动在界面上要影响到所有树。
i.首先CTreeCtl得到消息,知道用户改了数据
ii.然后CTreeCtl通知CDetail改数据
iii.CDetail改完数据后通知所有CTree这一变动
iv.所有CTree改自己相应的Node的属性,并通知和自己关联的CTreeCtl
v.CTreeCtl得到通知后,做界面的变动
b)以改节点father为例。这种数据存在节点表里,其变动在界面上要影响到所有同名树。
i.首先CTreeCtl得到消息,知道用户改了数据
ii.然后CTreeCtl通知相应的CTree这一变动
iii.CTree改自己相应的Node的属性,并通知和自己关联的CTreeCtl
iv.CTreeCtl得到通知后,做界面的变动
c)以改节点Comments为例。这种数据存在节点细节表里,其变动在界面上不需要显示。
i.假设用户是在细节子窗体变动节点Comments。那么直接改就可以了,不需要做任何事。
d)考虑同树独改的情况。上面讨论的其实都是同挂全改的情况,当节点独改和同树独改的时候,就不一样了,以同树独改为例。独改时需要拷贝新细节,并减少老细节的多挂计数,并且要改原节点的细节ID。前2件事由CDetail负责,后1件事由CNode负责。具体过程如下:
i.首先CTreeCtl得到消息,知道用户改了数据。并通过用户交互,确定用户希望同树独改数据
ii.然后CTreeCtl通知CDetail改数据
iii.CDetail得到通知后,做两件事
1.拷贝制作新细节,并通知相应的CTree这一变动
2.减少老细节的多挂计数,并通知所有CTree这一变动
iv.CTree得到通知后,改自己相应的Node的属性,并通知和自己关联的CTreeCtl
v.CTreeCtl得到通知后,做界面的变动
6 类之间消息的传递
消息使者类CMsgMate定义了许多事件,并提供许多public函数用于触发这些事件。当我需要在2个类之间传递消息时,我就在这2个类里都加入CMsgMate类型的成员变量,并在对象实例化的时候,让这2个成员变量都指向同一个CMsgMate实例。这样1个类的CMsgMate触发事件的时候,另外一个类的CMsgMate成员也能收到。
具体地:
i.CTreeCtl通知CDetail:这2个类的CMsgMate成员都指向同一个全局的CMsgMate对象。
ii.CDetail通知CTree:这2个类的CMsgMate成员都指向同一个全局的CMsgMate对象。
iii.CTree通知CTreeCtl:可以用全局的CMsgMate对象,也可以用CTreeCtl类里的with events的CTree对象。
iv.CTreeCtl通知CTree:这2个类的CMsgMate成员都指向同一个全局的CMsgMate对象。
大致的设计就是如上这些,对数据装载机制和消息机制感觉还是有点不是很清爽,恳请点评一哈。谢谢。
[解决办法]
UI数据和核心数据的概念,这个没有严格定义,我自己发明的:)
要界定很简单,把UI去掉,剩下的必须的数据就是核心数据。
区分开的好处就是,换UI不影响核心数据结构。
持久化就是序列化,Serialize
UI数据不一定都要保存的,大部分应该是运行期才需要的。
我看了你那篇blog,但还是觉得多挂计数只是一个运行期的数据,
你关了程序再运行,需要读这个多挂计数吗?这个时候还没挂上
去,怎么会有计数呢。
[解决办法]
其实讨论的这么久,我决定 slowgrace 你应该先去学习一下UML基础。
这是对你最有用的理论。
1)在UML中,设计类之前先要提取角色(Actor),这通常是从要做什么事中对应出来的:
甲——读取A表数据
乙——构建树结构A
丙——在A树中查询节点
丁——显示A树
戊——在A树展开节点
。。。
癸——读取B表数据
。。。
2)然后根据角色,来决定用多少个对象(Object)来担任这些角色,相近的功能会有同一个对象来担任多个角色。
3)根据对象行为归类,比如所用的树结构对象都可以归纳为 CTree,这样就有了类(Class),而且每个类有什么功能通过角色的行为很清楚地决定了。
由于步骤 1) 其实是个思考的过程,程序结构中只反映了 2)、3) 的结果。
你看再多经典的样例不会区分角色,设计出来的结构才会这么混乱。
专业与否的区别就在步骤 1) 里,即使随着经验的增长只需要在大脑中快速过一遍。但不做就是业余的。
[解决办法]
其实没那么复杂,你可以将所有要做的行为列出来,这就是函数化设计。
然后给每个行为分配一个角色,这就是步骤 1)。
虽然决定哪几个行为同属一个角色比较考验能力,但是几个步骤多反复几次应该可以找到到比较合理的划分。
[解决办法]
Option ExplicitType Type1 s As StringEnd TypeType Type2 s As String * 255End TypeSub Main() Dim t1 As Type1, t2 As Type2 Debug.Print Len(t1), Len(t2)End Sub
[解决办法]
对象设计中要仔细划分职责,术语叫“降低耦合度”,俗语叫“各人自扫门前雪莫管他人瓦上霜”。
比如树中节点的添加/删除,Tree 对象只需要添加/删除父子对应关系,再通知到 DetailList 某个 ID 被使用/解除使用,就完了。
至于多挂计数之类的处理,完全是 DetailList 内部事务,管 Tree 鸟事!?
[解决办法]
COM 对象有引用计数,Set ... = Nothing 其实是调用 IUnknown::Release,由它决定是否要真正释放。所以对象创建比释放慢很多。
这在《高级 Visual Basic 编程》(Advanced Visual Basic) 中用轻量对象来提升性能。
不过这里不用这么复杂,CDetail 改用结构,CDetailList 用数组存放明细。
差别就是必须整个结构进行存取而不能单独变更某个属性,不过正好方便实现消息通知(更新只有一个入口,更容易控制)。
[解决办法]