怪怪 | Nothing, Everything

"有过一个发疯的时刻,有感觉的钢琴以为它是世界上仅有的一架钢琴,宇宙的全部和谐都发生在它身上." - 狄德罗
随笔 - 109, 文章 - 3, 评论 - 2072, 引用 - 60
数据加载中……

漫谈数据存取与对象设计

先重复一下问题:

以学生和老师为例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 怪怪 阅读(3184) 评论(27)  编辑 收藏

评论

#1楼    回复  引用  查看    

不好意思,没看明白
2008-05-13 20:43 | 皇帝的新装      

#2楼 [楼主]   回复  引用  查看    

@皇帝的新装
能否说说为啥没看明白? 我看看如何改进表达~
2008-05-13 21:08 | 怪怪      

#3楼    回复  引用  查看    

@怪怪
呵呵,可能我太愚钝吧。感觉穿插的东西太多,要表达和说明的问题不集中。
2008-05-13 21:17 | 皇帝的新装      

#4楼    回复  引用    

感觉为了OO而OO
有点把问题复杂了
2008-05-13 21:18 | nonediff [未注册用户]

#5楼    回复  引用  查看    

没看明白你要讨论的点是什么
2008-05-13 21:25 | 炭炭      

#6楼    回复  引用  查看    

无论是充分表达还是优化都有一个上限, 这个上限是由用户如何使用软件所决定的。
我们找出的这个方法, 看起来必须是可以自我检查的, 自我说明的,在一定意义上就是说它必须是静态的, 而且学习成本必须很低。 我们不能让使用者使用不能进行不合法的操作, 但也不知道怎么回事, 除非从花费好几天时间熟悉一下我们的文档和框架。
同意这2句
2008-05-13 21:27 | lovecherry      

#7楼 [楼主]   回复  引用  查看    

@皇帝的新装
@炭炭
啊, 确实, 我没有交代清楚问题.., 就呼噜呼噜说下去了..

@nonediff
我没有说要OO,XX方法不OO, 怎么改就OO了呀..

@lovecherry
别光说同意的, 也说说不同意的 :)
2008-05-13 21:35 | 怪怪      

#8楼    回复  引用  查看    

1、学生和老师
如果用数据库的方式来作的话,就很简单了,建一个老师表,建一个学生表,
然后再建立一个关联表,老师教授了那些学生,就是记录他们的ID的表。

只是没想到,OO了,会出现互相调用的情况。看来我OO确实很弱。

后面的先不看了,今天头痛。
2008-05-13 21:54 | 金色海洋(jyk)      

#9楼    回复  引用    

LZ看了你的文章很有感触,思路很清晰的,我开发学校教务系统的时候,都涉及都你些东西,开始的时候没有抽象出很多方法来,但是后来就慢慢变了

突然发现整个系统内都是关联的,复用的情况非常多,相互的数据依赖也相当的多,所以就可以把很多方法都抽象出来了.

但是话又说到数据库这一块,很多方法都必须要依靠数据之间的依赖来完成,最终我又回到了起点,总之要解决一个又一个数据表的关系,做到相对的抽象出方法,还是比较困惑的,我OO的水平还不够,.NET也是自学的,看了你的这篇文章,又有了很多思考,可惜系统已经开发的差不多了,我已经在也没精力去重写这些方法,唉`~`感慨啊
2008-05-13 22:22 | Leejor [未注册用户]

#10楼    回复  引用  查看    

个人主张在1:n的关系中保持n对1端的引用,即student.Teacher存在,而teacher.Students不存在
因为保持n端的话
1.对需求的变更不好应付,就像文中所说,又要加teacher.Courses
2.如果每个students又有其他的n端,这样形成的一张图将是非常庞大的,如果没有lazy load的话可能整个数据库就这么出来了-__而有lazy load的话应对wcf之类的无连接的环境又不方便
2008-05-13 22:32 | Gray Zhang      

#11楼    回复  引用  查看    

怪怪,感觉这文有点散哈,虽然吐露了你的思想,但结构比较散,有些细节介绍得不是很全面,总体感觉,我的设计原则和你有些类似,所以在看到中间的时候,总会产生想插两嘴的感觉.
2008-05-13 22:43 | 坏人      

#12楼    回复  引用    

要想架构优美,只需要扔掉性能限制就完成了一大半.
另外,我觉得像类sql+2d表格的形式很有前途,如果能方便的解决动态分派的问题的话。
2008-05-13 23:46 | deer.chao [未注册用户]

#13楼 [楼主]   回复  引用  查看    

@Leejor
不用轻言自己“OO水平不够”, 有时只是个如何看待(包括自己的作品)的视角问题, 比如换个视角, 我提出的一些和将要提出的一些方法, 就不那么OO了。

@Gray Zhang
你说的情况确实是问题, 受你启发, 我把文章重新更新了。

@坏人
呃, 确实, 所以起了个漫谈的标题。 我本来只想随便说说, 然后不知不觉就把一些打算成熟以后再写的东西带出来了。

另外, 那种每个段落后面都可以让回复者标注的文章系统很好.., 要不要考虑开发一个? :)

@deer.chao
有时候另外一半更不好解决 :P

能把“动态分派”解释的更详细一些吗? 我看法跟你差不多, 但是我需要更多的思路启发~
2008-05-14 02:30 | 怪怪      

#14楼    回复  引用  查看    


@皇帝的新装

同感
2008-05-14 09:20 | 赵瑞峰      

#15楼    回复  引用  查看    

能不能把例子写的具体一点,结合powerdesigher或者类图,最好是有程序。
2008-05-14 09:23 | 鹰击长空      

#16楼    回复  引用  查看    

说点我个人的看法,我认为对象之间多对多的关系永远都是间接的,而不是直接的,任何一个多对多的关系从逻辑上来讲都可以分成多个一对多的关系,如果以老师和学生为例,事实可以分解为一个班有很多学生,一个学生选了很多课程,一个课程对应多个老师,一个老师对应多个班,学生和班级、课程关联,老师也和班级、课程关联,学生和老师并无直接关联。
而且我感觉一对多的双向联系问题不大,我们不可能消灭所有的强耦合,当然代价是有的,只是觉得还可以接受,因为优点更多。
-----------
还有一点没想好,如果一个老师比较强,他能教两门课了,那么逻辑调节似乎比较大,有没有更好的方法可以解决也是个问题。
2008-05-14 10:06 | 球球      

#17楼    回复  引用  查看    

@怪怪
有点类似虚函数,就是根据数据的不同,动态采用不同的算法.
当然这个分派不一定只与类型挂钩,也可能与数据的值挂钩,这点上和动态化后的C++模板的分派有点像.
2008-05-14 11:11 | deerchao      

#18楼    回复  引用  查看    

怪怪你的MSN老不上线,没法即时讨论问题。呵呵
2008-05-14 11:47 | 装配脑袋      

#19楼 [楼主]   回复  引用  查看    

@赵瑞峰
@鹰击长空
确实缺乏例子, 不过本质上可能是因为我没做好规划。 加上例子的话, 一次的写作成本太高了, 怕写不完。

所以应该先做规划, 宁可说的有条理 、 仔细一些, 也不应该弄得呼噜呼噜的, 是这样吗?
2008-05-14 12:02 | 怪怪      

#20楼 [楼主]   回复  引用  查看    

@球球
你说的也有道理, 也就是说我的方法不是面对所有情况的, 只是解决某一些问题。

@装配脑袋
来了。

@deerchao
在C#下我估计只能框架实现, 实现还是有望的, 但我希望有静态的检查, 咱们在琢磨琢磨。
2008-05-14 12:05 | 怪怪      

#21楼    回复  引用  查看    

呵呵,写的好棒,支持了。
2008-05-14 13:37 | 李涛      

#22楼    回复  引用  查看    

多谢怪怪兄台和兄弟们的解惑,很抱歉小弟因为一些事情耽搁来晚了,非常对不起。现在说说我的一些想法,如有误请怪怪兄台和兄弟们指正。
1。关于问题
如果使用CASE工具建模,建立类图后通过代码生成的功能就会生成上面学生和老师的例子这种形式,这也是我举出这个例子的原因,假设采用OOD方式设计程序,建模是一项必须的工作,无疑将使用到CASE工具。也许这种形式不灵活,最好的办法可能是使用NHIBERNAGE之类的ORM来维护关联。

2.说一点其他的
我个人感觉使用面向对象的方法来设计程序正是太难了,我最近在做OA内的一个会议预定的模块,要求表现好,复杂程度属于中等吧,本想使用OOD的方法
来做一个实战,无奈交付时间短,为求稳妥,只能放弃。类似的经历在我参与的项目之中都有出现,我现在很迷茫,一直在
使用面向对象设计与向现实妥协之间摇摆.我不是为了OO而学习OO,OO对我而言有一种莫名的吸引力。

2008-05-16 00:08 | songcan      

#23楼    回复  引用  查看    

@songcan
为什么不能一点一点地应用OO呢?

要么全OO,要么全都不OO吗?不会吧。

我就是一点一点改的,如果感觉OO的某一块对我有利,我就会应用进来,而其他的部分还是过程式的。
2008-05-16 17:19 | 金色海洋(jyk)      

#24楼 [楼主]   回复  引用  查看    

@songcan
如果想听真话, 我说一句“OO对我而言有一种莫名的吸引力”, 就已经是“为了OO而学习OO”了。

为什么这么说呢, 因为我非常熟悉这种感觉... T _ T

几点:
1. 不要相信工具, 充其量是一个代码生成的工作, 其次这些工具的过程无法定制。

2. 不要因为没用上OO就迷茫了, OO很可能存在本质缺陷, 所以种种表达障碍非常正常, 这个后面再说。

3. 不要因为OO存在这样那样的问题, 就放弃OO的尝试。 我的想法是, 作为一种当代最流行的方法, 没有一定程度的了解是不行的, 就像牛顿定律一样。

解释OO缺陷问题的帖子:
http://www.cnblogs.com/guaiguai/archive/2008/05/17/1201138.html


2008-05-17 07:08 | 怪怪      

#25楼    回复  引用  查看    

@怪怪兄台
您好,很感谢您每一次不厌其烦的解答,给您添麻烦了,再次感谢!
2008-05-19 22:28 | songcan      

#26楼    回复  引用  查看    

比较赞成球球的说话

另外楼主这篇文章跳转多了点,自己看没问题,别人看会有一种小新带小白散步的感觉
2008-05-30 14:31 | 九流      

#27楼 [楼主]   回复  引用  查看    

...., 这个比喻用的 :P
2008-05-30 17:28 | 怪怪      

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-05-14 02:35 编辑过


相关链接: