漫谈数据存取与对象设计

先重复一下问题:

以学生和老师为例public class Student
{
string name;
Teacher teacher;
}

public class Teacher
{
string name;
List<Student> students=new List<Student>;
}

双向的关联意味着强耦合,在我看来这就像BLL层与DAL层的双向调用一样,很不爽,是不是这种双向关联在程序设计中是必要的呢?另外假设我要找到老师教 授的全部学生是不是需要分两步加载才能得到结果,还是一步就可以了?假设老师的信息和他教授的学生的信息都有了改变,是不是要分别更新还是一条SQL语句 就搞定了?如果需要分别更新,那么这样做还有什么意义呢?我把他们放到一个事务里面不是更好吗?起码不会出现数据不一致的情况。

就这个问题的回答

这个问题: 老师教授的全部学生, 我们看这个问题的时候, 先不要关注老师教授和学生的具体属性的不同。 而是从这个具体问题的结构上来看。 如果画出组织架构图, 很显然这是一个树状的关系, 只是仅有两层。在这个认识下, 我们来说加载的问题。 我不知道问题中的加载具体含义, 如果是打开数据库读取数据的次数的话, 考虑性能可以使用打开一次数据库的做法。  我们可以让数据库一次返回两个结果。

我不知道工具是否可以自动的做这个工作(因为我是土人,没怎么使用过工具),在这里说一下手工的做法。

先说一个老师的情况。我们要求数据库返回的结果分别是: 第一个,老师的全部属性; 第二个,他的学生的列表, 列表中的每一条是学生的全部属性。 在DAL, 读取完老师的属性后, 使用NextResult(), 可以获得学生的列表, 然后一条一条将数据Populate出来,将学生加入List<Student>即可。

如果是多个老师和多个学生, 那么第一个结果是老师的列表, 第二个结果是所有这些老师的学生的列表。 在DAL, 先填充老师的列表。 我们Populate学生数据的时候, 根据TeacherID或者是什么, 顺便将他们加入各个老师的Students中去。 这种方式也可以适应有学生属于多个老师。

这是一个比较普遍的结构, 我们可以使用delegate将老师数据的提取和学生数据的提取和学生插入到老师的属性这一动作单抽出来, 而将上面的算法(其实我不喜欢管这种东西叫算法但又不知道叫什么)单独列出, 就可以较大程度的复用这个过程(也许应该直接这么叫), 适用所有类似的情况。

然后说说数据更改后更新的情况。 这个问题很好, 是多次更新还是一次更新呢? 我个人也是倾向于一次更新。 这里面, 我们可以参考微软的做法, 当修改以后, 使用SubmitChanges()之类的一个方法, 同时将所有被改变的数据一次提交到数据库。

但如果这样设计, 就会产生一个问题, 哪些数据被改变过? 在这里, 我们必须设计一个记录数据改变的东西。 可以在实体中使用类似于_isDirty的标志, 也可以使用一个列表, 只要数据一经改变, 我们就将其加入列表。 然后在DAL中取出被改变的数据, 根据他们的情况提交。

有关这些方面的知识, 还是上次说的那本书, Fowler的POAEE中都有一些介绍。 虽然我对此书不太感冒, 但不得不说确实是一个很不错的参考手册。 不过在这里, 一个需要问自己的问题是, 我这样有必要吗? 如果没有明确的需求, 我们不必弄得如此麻烦, 随便呼噜一个自己知道的写法就行了。

最后说说关于Teacher.Students和Student.Teacher的这种导航。 先回答双向引用的问题。 如果采用问题中这样方案, 这是必然要产生的。 因为这种方式决定了我们要将现实中的关系反映到设计中。 而本来老师和学生的关系就是双向的。

上述说明忽略了具体细节, 其实不是特别难, 如果我表述的不清楚请在回复中提出, 我会给出具体的例子。

自由发挥

对象导航存在的问题, 一种使用数据上下文配合Mixin的解决方法

其实我个人是很不爽这种导航的, 因为这个一点也不抽象, 太具体了, 具体到很难做出改变。 明天一个学生就有多个老师了, 那么Student.Teacher又变成Student.Teachers, 后天我们需要扩展出对课程的管理, 那么就又加上Teacher.Courses、 Student.Courses ?

解决这个问题, 我觉得需要一定的勇气和一定的技巧。 勇气是扔掉书本和大道理的勇气, 技巧则是为了我们不断的找出经常产生的问题的解决之道。

对于Teacher.Students这种方式, 我们要有勇气认识到那些“car.Wheels, duck.Quack()”的例子很可能是一种错误的方式, 因为它实际上无视于可能的变化, 给出一个对于当前场景来说,可能既固定又庞大接口。 尤其是当我们考虑到, 对于静态类型系统, 你不能把Students这个属性抹去而最终会造成的问题: 随着属性和行为越来越多, 我们的脑子就必须记住哪些*现在*是合法的, 哪些在当前场景下是不合法的; 假装没看见有点不靠谱: 过去我们缺乏文字性的说明或者头文件, 很难知道我们可以调用什么; 现在问题反过来了, 面对一大堆方法、属性, 我们不知道我们不能怎么做。

这种方式在本质上可能是忽视的是这一点: 即使是在一个系统内部的一个概念身上, 我们在系统这部分和系统那部分, 关注的要点和细节都是不同的; 所以我们一次只应关注系统的一个部分。

那么在面向对象中, 这类问题如何解决呢? 一个对象不只一个接口, 我们可以通过给与更多的不同的接口, 只保留那些在当前场景下合法的属性和行为; 至于是通过实体持有数据, 由不同的接口暴露, 还是第三方持有数据, 由不同的接口给出, 需要根据具体情况作出判断, 有时候是工程上的考量。

我 个人虽然经常性的采用实体持有数据的做法, 但从原则上更倾向于另外一种方法: 将数据载体的工作放到数据上下文对象或者其它对象里去,这样更加灵活; 我们可能要付出执行更多的构造动作的代价, 但是我们完全可以选择是否交换, 谁说拿数据上下文中的某一个数据字典, 直接往界面上绑定就是不行的呢?也许这样在一些人眼里很脏, 但它随时可以在需要时变得干净起来, 同时也不会影响那些干净对象的纯洁性, 个人认为保证这一点就足够了, 因为预测变化是不现实的。

面向无数小粒度接口的一个代价是我们要付出更多的工作量去管理它们。 面向对象给了我们一种手段去管理复杂性, 但并不是说我们就不必付出代价。 有时候管理的代价会显的得不偿失, 所以我们要经常问这样一个问题, 我需要去管理吗?  

答案也许经常是否定的。不过, 一 旦得到肯定的回答, 我们就可以大胆的采用一切可能的手段去进行设计。

上述问题的一个可行的解决之道是使用各种方式的Mixin。 要么我们需要一个Mixin框架, 让老师只有基本属性, 而不包含和其它实体的关系, 当我们需要加入这种关系时, 通过Mixin<Teacher, IList<Students>()这样的形式, 返回一个满足IHasManyStudents的接口(话说COM...)。 另外一种方式是使用Extension Method, 像Students这样的属性放到GetStudents(this Teacher obj)中去。

如此一来, 由于数据并没有由Teacher类携带,多设计一个当前请求或场景下的数据上下文, 就是必要的了: 这样的Mixin, 要求Students的数据也必须是读出来的, 否则两次或更多的数据库连接不可避免。 在一个场景产生时(实际上对于用户来说是进入这个场景), 我们要根据需要准备好数据上下文, 我们的Mixin对数据上下文进行操作, 并将这种操作以我们喜欢的风格封装之, 避免暴露出来。

当然, 我们需要设计一个(最好是tongyon)数据上下文的数据结构: 选择总意味着交换。

新问题的提出

可数据上下文, 也还是会碰到天花板的。 而且这些天花板使用传统由实体保存各自的数据, 由方法和属性进行操作和导航, 也照样存在。

第一个问题是, 我们如何保证数据的合法性? 比如, 数据上下文中, 由于我们的失误或者某方法原来不是自己设计的, 并没有保存某一数据, 但是我们却试图在对象上获取数据, 难道就凭我们的小心翼翼吗?

第二个问题是, 对于分页这样的问题, 是来自于软件和硬件的限制, 而不是来自于对业务的抽象。 那么Teacher.Students或者Teacher.GetStudents()到底意味着什么? 是当前场景下读出的数据, 还是真正的全部学生?

第二个问题的讨论

第二个问题有现成的解决之道, 而且和不采用数据上下文而是由对象承载数据时的做法差不多。 比如, 我们可以不使用简单的List<T>, 而是使用这样一个List, 它知道一共有多少条数据, 也知道当前读取了多少数据。 当index大于当前读取的值时, 再次连接数据库。 所谓的Lazy Load嘛。 这样一个List, 付出一定代价是可以实现的。

解决之道似乎都是以多次连接数据库为代价的, 最初我们试图解决多次连接数据库的问题, 何着根本没有解决? 我们可以如此考虑这个问题: 我们要解决的是可以一次读取的, 不要分成两次; 而不是任何时候都不需要读两次。这样似乎就解决了Teacher.Students的形象问题: 我们现在可以认为Students(几乎)就代表了全部学生了,无论它是属性还是Mixin。

反过来看这个Lazy Load的方案,他还会有什么其它好处吗?考虑一个Web请求: 当用户请求第n页数据的时候, 需要的数据项就确定了, 我们在任何时候都有必要使用这样一个结构吗?再考虑对象序列化后的传输,这样一个结构, 比如可以自动化加载的List<Student>,真能适合所有情况吗?在我看来, 无论是充分表达还是优化都有一个上限, 这个上限是由用户如何使用软件所决定的。

在这样一个前提下, 我觉得唯一保证纯洁性的方法, 就是承认我们的对象是不纯洁的。 实际上无论VO/PO还是BO, 都只能根据使用需求确定其设计; 而我们做的种种努力, 除了实现目的, 也只是为了让我们的工作避免麻烦和无意义的付出。 所以我们要注意的问题其实是这种努力和回报的比例如何, 而没有确定的答案。

这个就像我过去说的, 抽象, 应该是以计算机如何完成任务为目标的, 而不是还原现实世界, 我们是上帝吗?从这个角度来看, 这个问题是不存在的; 倒是意图制造某种假象的设计, 如果其隐藏的实现稍微跟不上, 就会让使用者迷惑。

第一个问题的讨论

呃, 一下子又写了一堆。 最后再讨论一下采用数据上下文或者额外的数据持有物, 从该混沌中随时产生当前过程中所需的对象这一做法中,如何保障数据上下文含有正确数据的问题。 实际上即使由实体持有数据(无论是带有混沌物, 或者最传统的DTO、 ActiveRecord等), 也可能会碰到这个问题。

由上面所述的第二个问题我们知道, 我们似乎可以使用Lazy Load通过请求就加载的方式, 来解决这个问题; 但是同时经过上面的讨论我们也知道, Lazy Load的方式有其局限性。 而且我们很难预测一个加载过程在哪些情况下被重用: 是非多次加载不可, 还是一个对多次加载的性能损失非常敏感的场合。所以偏向于一个只顾当前需求的过程实现是合理的。

这样,我们大多数时候编程, 很可能都是选择小心一点就是了, 我们可以继续这样下去。那么首先要问的是, 解决这个问题有何好处呢?

反思这个问题, 我个人感觉, 其实这个问题解决与否对个人、 此时, 影响不大。 因为我们在构造一个流程的时候, 对需要应用的数据的来龙去脉, 一清二楚; 就算出点小问题, 也可以瞬间解决。 问题在于别人不可能像我们这样清楚, 半年以后我们自己也可能不清楚了。 谁知道XxxxManager.GetXxx()给出的一堆相互关联的对象间, 或者一堆数据里, 有没有我们想要的呢? 还不如直接重写一个保险。

让我们讨论一下, 有没有更好的做法。

首先, 在返回对象是Teacher.Students这样的非Mixin形式中, 我个人认为找不到解决之道; 我们可以抛出异常, 问题是本来它自己就会抛出异常, 仅仅是更详细的说明是没啥用的。 何况如果对象关系够复杂, 整个对象网络中, 那么多可能抛出异常的地方, 不能让使用者一个个去试。

第二, 我们找出的这个方法, 看起来必须是可以自我检查的, 自我说明的,在一定意义上就是说它必须是静态的, 而且学习成本必须很低。 我们不能让使用者不能进行不合法的操作, 但也不知道怎么回事, 除非花费好几天时间熟悉一下我们的文档和框架。

呃, 这篇文章我是在回复框里写的, 已经这么多啦, 后面这部分估计也少不了, 暂停一下,先设想一个情况: 返回的这个Teacher,如果包含了Students数据, 我们就有Teacher.Students, 如果不包含, 在Teacher后面打一个英文半角句号, 抱歉, 没有Students或GetStudents(); 如果硬写, 编译器就会罢工。 同时, 我们要有一个清晰的结构, 三分钟之内方法的使用者就知道这是怎么回事: 这样无论是修改代码, 还是写一个新方法, 他可以立刻做出判断。 同时, 最好在可能的情形内提供这样一种支持: 使用者只要申明“给我Students!”, Students就有了,而且保证包含着合法的数据。

我管这个叫面向场景的设计方法,它是“管中窥豹”式的: 我们通过管子, 只给出有限的视野。抱歉不能马上将我的构思全部表达出来,改天再写....。 感谢提出问题的兄弟给我一个整理思路和与大家进行讨论的机会。(未完待续

回顾

我们回顾一下本文, 介绍了几个问题的某些特定的解决方法, 除了这些方法, 还有很多其它的方法。 要不要使用它们, 关键在于我们对碰见的情况是如何判断的。 只是一概而论却忽视了衡量必要性、 适用性的工作是不行的;  进行判断的时候, 忘掉所有的面向对象原则和建模指南吧。 另外一个情况就是, 也许“程序本身需要组织和管理”的需求是突然发现的, 重构这一话题的流行, 也足以证明一开始就固定一个漂亮设计的风险。

这也是一般程序员的饭碗所在, 如果有一个一劳永逸的方式, 那么它很容易被固定下来, 减轻工作量, 可也意味着这些地方不再需要程序员了。

多余的话

上面说的一些问题, 不要拿动态语言、UnitTest、 自动构建和持续集成来忽悠我, 它们确实能解决其中部分问题, 问题是哥们我有比它们更安全更敏捷的做法(就这个问题); 更何况我只有鱼吃的情况下, 你不能总跟我说, 吃猪吧, 猪没刺; 我们只能去找挑刺的办法不是?

另外, 祝福地震中的受难者吧,唉...

posted on 2008-05-13 20:05  怪怪  阅读(3850)  评论(27编辑  收藏  举报

导航