随便说两句: 表设计兼一些设计分析的讨论
一个兄弟的问题:
1. 比方说有一个学生选课系统,设计老师,学生和课程等实体,是不是为了扩展或者说为了灵活性亦或为了更OO应当在实际的应用中加上person这个基类?2.再有比方说老师有教授和讲师,他们有各自不同的属性,如果不考虑数据的持久化,那么这个好像很好设计,但涉及到保存到数据库的时候,好像就很难处理了,在现实中我们应该不会有教授表老师表和讲师表,也许只有一个老师表,而会出现标志位的字段判断是否是讲师,是否是教授,但如果这样一个类对应一个数据库表应该是不错的选择, 这样的话,实体类就应当有一个老师类就可以了,教授类和讲师类就没了存在的必要,那么OO继承的优势何在?对于这样的问题解决方法是什么呢?
关于建表和对象继承:
首先, 我觉得如果你对面向对象设计的信息系统感兴趣, 可以看看Martin Fowler的《企业应用架构模式》。简单的说两句, 对于一个继承体系, 至少有三种建立关系模型的方式:1. 一个老师表, 但是这个老师表, 同时有字段对应教授和讲师的属性, 一个教授的行, 其讲师的字段是空的, 一个讲师的行, 其教师特有的字段是空的。 对于很多现代数据库来说, 其冗余的Cell是不会占用存储空间的。
2. 假设没有老师这一具体职位(也就是说老师是抽象的基类),一个讲师表, 一个教授表, 这两个表具有一些相同的字段, 既教授和讲师共同具有的属于老师的特性的字段。
3. 有老师表, 讲师表, 和教授表三个表。 讲师表仅存储讲师的特性, 教授表仅存储教授的特性。 继承体系可能是这两种(讲师<- 老师 -> 教授),或(老师->讲师->教授), 但无论是哪种, 子类通过父类的ID, 和父类表向链接。 比如, 无论多了一个教授, 还是一个讲师, 老师表里都会增加一条老师记录, 如果是讲师, 同时在讲师表里插入讲师特有的信息, 如果是教授, 则同时在教授表里插入教授特有的信息。
所以, 对于一个继承体系来讲, 不见得在现实里只有一个表。 另外一点, 我个人认为, 这些不同的表的划分方式, 其实即使没有面向对象设计, 也是合理的, 同时自有其判断依据: 比如方式3, 有些场景, 我们仅仅需要老师的那些特性, 所以仅存取老师表即可; 有时候我们可能仅处理教授的特性, 而那些作为老师的共同特性反而不关心。 一旦我们关心全部特性, 我们可以将表连接起来。
以上三种方式, 都有一个前提, 既讲师和教授, 都有比起其父类, 更多的特性。 如果讲师和教授, 仅仅是身份不同, 像你说的, 最多有一个标志为标识其身份,我们不谈面向对象, 就谈对这件事的认识。
在现实生活中, 讲师和教授也许是有很多不同点, 以至于我们必须明确的分割这两种身份。 但是我们就我们所处理的任务的角度, 他们或许没什么不同: 系统内部只关心他们作为老师的一些特性; 而一些差异, 则仅仅是是一个子特性, 没有必要甚至不能够,上升到让这两种身份在*任务的视角上*产生真正的差异。
比如: 我们给不同身份的人发不同的工资。 这些仅仅是我们根据特性展开的, 其具体内容属于另一范畴的东西, 对于老师这一个概念来说, 身份只是一个很普通的标志, 就好比他是男还是女一样。 因为大多数关心的主要任务和这个属性关系不大, 所以就不会在系统内部设立两个不同的概念。 比如男的上男厕所, 女的上女厕所, 这是一个额外的规矩, 是在与人这个概念关系不大的其它部分进行的; 但是如果我们任务关心的问题, 就是女人和男人的差别, 情况则不同了。 比如女人和男人的体貌、 行为、 性格, 这些特点如果是我们要处理的, 很显然, 我们应该明确划分这两个概念。 从面向对象、特别是继承的观点来看, 我们需要至少两个对象; 当我们有时候同时关心男女共同为人的特点时, 我们还需要一个叫做“人”的基类对象。
在建表时, 也就会产生我上面所说的三种方式的不同差异。 至于三种方式的优劣, 暂时不能逐一展开, 不过Fowler那本书里都有提到; 不过读哪本书时, 可千万别当成什么金科玉律, 肺炎的种类很多, 不是吃茴香都能治的。
闲谈:
不过我们至少可以看出, 我们如何建表, 如何找到合适的模型, 并非是照搬现实生活里存在的概念的; 而是从我们的任务的视角上看过去, 存在哪些概念。 后者的判断, 在于我们关心些什么, 关心的这些内容, 造成了哪些程度上的不同, 有很大不同的时候, 我们需要不同的表达, 没有不同时, 我们就采用统一的表达。 而类设计和表设计, 只是我们表达的体现而已。 那么, 你所面对的问题, 从各方面来看, 需要几种表达方式才能完善呢?同时, 我们在这时候, 不要过多的去考虑全局到具体, 而是把问题割裂开。 这个事情和谈恋爱正好相反,粘在一起的东西, 扯开比较费劲; 而把东西粘起来, 则相对容易。 这是因为我们人脑同一时间可以理清的东西有限。 所以我们要明确的认识到, 哪些东西可以规划到“男人上男厕所, 女人上女厕所”这样的外部规则, 哪些东西满足“男人和女人不同的特征”。 如果一时不容易分清, 至少我们在信息的量上可以做把握(不过这样把握的方式其实不是实质性的, 随着理解的深入, 就要放弃了), 后者往往涉及到一系列信息, 而前者中“男”“女”仅仅是一个用于填入规则的标志。
当然, 我们设计了一个表, 其中有一个字段存储“男”、“女”, 进行面向对象的设计的时候, 我们照样可以弄出一大堆类, 继承人这个类。 当我们调用, 人.上厕所()时, 就各去各的厕所了。 面向对象一般鼓励以这样的方式, 去给出统一的接口(比如这个例子里“人”)。 其优势在于, 当我们多出其它性别时, 比如“半男半女”、“不男不女”, 那些调用“人.上厕所()”的代码, 全都无需改变。
这里有一个假设, 在于“人”这个接口, 比如具有一些属性, 具有一个“上厕所”的方法, 这个接口是稳定的, 不会改变的。 当我们在进行设计时, 要衡量这个假设是不是成立的。 比如上厕所这个行为, 有可能在未来变化为“上厕所(带手纸)”, “上厕所(没带纸)”, 这样一个接口的变动。 很多面向对象设计只所以问题连连, 就在于做出了时效性太强的假设。
为什么这么说呢? 因为接口是有很大概率会变化的, 其区别仅仅是一个月之内就变, 还是这个系统用了3年才变。接口一旦发生变动, 我们往往就要为当初获得的好处埋单了。 所以如何设计, 有时候不是一个绝对化的问题, 而是一个想办法付出最小代价的问题。
对于上厕所, 我们可以统一的把上厕所的具体问题外包出去, 比如针对各种情况可能采取的各种方式, 这样我们就产生一个“上厕所的办法”这样一个外部概念。 我们可以想像, 当一个人脑子里有这个概念的时候(“人”这个类别的对象持有某一“上厕所策略”), 就可以根据判断“我是女的, 我上女厕”、“我没带纸、找张报纸”进行行为, 你可以说这些是这女的想的, 但是你也可以认为这些仅仅是外部规则, 这女的只是按照规则机械的行动而已; 这后一种看法, 实际上使得我们容易在计算机上设计。
这时候, 我们无需使用继承的体系, 而是采用“人.上厕所(策略)”这样的接口, 那么如果我们所关心的“男”、“女”, 和带纸没带纸一样, 仅仅是上厕所策略用以判断的素材而已; 这样, 我们就不需要男人类和女人类了。
从以上例子来看, 类的设计和表的设计, 不是说一定要有必然的联系。 因为既然我们写了非存取的代码, 就说明我们除了可以记录下来的特征, 还关心其它的非可固化性质的行为, 那么我们如何设计类、 对象, 则必须从这些行为以及系统中各个对象交互的视角上出发, 而不是考虑我们的表如何设计的。 注意, 这不是说我赞成以实体类去设计表, 关系模型关心和表现的是事情的另一个侧面, 所以设计表恰恰不能从对象设计倒回去。 它们之间应该是正交的, 不要存在一个依赖另外一个这样的耦合。 如何让他们兼容, 需要且应该做额外的工作。
当上面的例子简化到一定地步, 比如 if(男) ... else ...时, 我们也不需要什么策略的对象或者类, 有一些用于体现策略脚本处理即可。 到了这个地步, 我们也许一个实体类都不需要有。 我们就简单的把系统看作根据具体情况作出判断和处理的策略系统就可以了, 这时候我们需要的仅仅是信息的载体, 这就是为什么我经常会说, 当产生了贫血对象后, 我们要考虑的不是把它变复杂, 而是看看能不能把它从系统中抹去的原因。 很多时候, 贫血类比起一个通用的信息载体, 除了字段和属性强类型的好处, 仅仅发挥一个自身标识的作用; 但是当我们调用“人能做的事情静态帮助类.上厕所(信息载体)”的时候, 实际上我们已经隐式的传递了这个标识; 我们也可以设置一些具体的手法, 保证安全。
总结:
最后, 我们再来看看这些区别, 强化一下印象:1. “人能做的事情.上厕所(信息载体)”、“人.上厕所()”和“人.上厕所(策略)”之间如何取舍, 在于复杂度, 复杂度上升到一定程度, 我们要采取后者; 这是为了避免前者“上厕所”这一Rotine其内部复杂度上升的太高, 或者被频繁修改。 另外一个更明确的判断点在于, 人是否带有可变化的状态; 上厕所这件事是否有很多的方式。
2. 假设我们采取了含有“人”这一样一个概念的具体表达的设计, 人这个接口如何保持稳定是我们要注意的。 比如“上厕所”这一行为隐含的变化, 或者又增加“吃饭”等行为, 都属于接口的不稳定, 如果这种不稳定在短期内就会出现, 我们应该把行为和规则外包给其它概念, 而不是让它们依附在人之上。 如果我们一出生就是狼孩, 本来我们也不会具有这些概念所指的行为; 并不是说“主语.谓语”, 这样的设计方式就是合理的。
3. 我们是否需要“男人”、“女人” 或者 “讲师”、 “教授” 这样的继承体系, 可以这样判断: 必须共性大、同时差异也大。 组合比继承好, 这是从95年《设计模式》以后被大家广泛接受的原则。 很显然, 共性和差异同时非常大的情况不是特别多见。 也许我们平时见惯了我们的aspx页面继承于Page, Page又继承于TemplateControl和Control。 且不说合理与否, 这并不能说明需要继承的情况很多。 因为这样一个继承, 使用了10000遍, 它其实还是一个继承。
4. 表设计和对象设计是两码事, 要按照各自的特点来做; 同时追求两边的合理性, 要付出的代价就是多做中间工作, 这些上面说的比较详细, 就不多说了。唯一可以说的是, 在成本上考虑, 我们要做好折中的选择。 更多的则是, 无论哪个设计, 无论什么设计方法, 我们别想太多, 更多的要找出我们关心的全部内容, 而不要把在现实生活中的知识带入过多; 因为对我们要解决的问题所在视角, 这些知识可能就成了不必要的偏见, 或者至少像用大学物理解释初中物理题。
这些问题其实在学习面向对象以后, 做类似的信息管理时, 非常普遍。 我想你先问问自己, 你自己想采用什么样的设计呢? 其次, 看看自己能不能找到这种设计的扩展或灵活性的好处。很多时候, 我感觉咱们想面向对象的冲动, 是因为不面向对象, 好像就不是一个设计, 简陋了些。 首先要克服这个心理。 如果找不到一个设计的好处, 而非要面向对象的话,即使存在一些好处, 也不是我们可以掌握的; 如果反而添了不少麻烦, 这些麻烦也不容易被找到。
总而言之, 我觉得, 不求巧, 但求拙, 不给自己的思路下套子, 不引入过多的杂念, 是设计的一些好的准则。