怪怪设计论: 抽象无处不在

现代的软件科学中, 很多内容和概念, 实际上是从数学/语言学等相当古老的领域里借来的, 为什么呢? 因为软件科学中的很多方面, 与其它学科中所碰到的问题并无不同. 一套数学理论,某个数学公式,无论从哪个层次去看,和它们有关的人分为两种:发明者,使用者. 这和软件也是相当一致的,  软件首先要有人编制, 然后别人来使用(好不好另说). 数学的一个特性就是他的抽象性, 本文讨论软件设计中, 由抽象所展开的一些问题.

对抽象的理解的误区可能使得很多人忽视了广泛存在的抽象. 因为接口和类如何抽象现实事物的种种说法, 在很多人的观念里混淆了在设计时具有的抽象, 从而对抽象的本质进行的歪曲, 忽略了除OO建模以外抽象, 或者在OO建模这个过程中选择了错误的抽象方式. 显而易见的, 比如数学实际上是一种对现实事物相当高级的抽象, 与此同时, 数学也就成了一个相当良好的解决问题的工具. 而我们编程人员所担负的责任导致我们的工作, 本质上是一个抽象和构造的过程. 所以如何抽象合理合法, 是我们首先要关注的一个问题. 那么我们首先要知道的是,什么是抽象方法?按照数学抽象方法的解释变化一下,我们可以得到如下一个描述:

抽象方法的软件设计版本
抽象方法是从考虑的问题出发,通过对各种经验事实的观察、分析、综合和比较,在人们的思维中撇开事物现象的、外部的、偶然的东西,抽出事物本质的、内在的、必然 的东西,从空间形式和数量关系上揭示客观对象的本质和规律,或者在已有软件设计成果的基础上,抽出其某一种属性作为新的软件设计对象,以此达到表现事物本质和规律 的目的的一种软件设计领域的研究方法。


以上描述,基本是把数学换为“软件设计”就能得出的结果. 比如在几何中, “点”的概念是从现实世界中的水点、雨点、起点、终点等具体事物中抽象出来的,它舍弃了事物的各种物理、化学等 性质,不考虑其大小、仅仅保留其表示位置的性质。从这里我们可以看到, 为了研究和解决问题而进行的抽象, 在数学领域中和现实世界中多么的不同. 但是进行抽象前, 一个潜在的前提被忽略了, 即几何这个数学工具要解决的问题, 决定了抽象的结果. 很显然换一个领域解决其它问题, 比如研究雨滴的物理特性, 抽象的结果就完全不同.

由于软件解决的问题往往是千差万别的, 这就导致我们抽象的结果, 从一个常规的角度看, 往往不能够逼近现实世界, 且对相同的东西, 表示的方法很不统一. 甚至突然看去, 和现实世界是那么的不同. 这也是很多OO狂热者所误解的一个想法: 与现实世界偏离太远的一组模型不是好的模型, 从而忽视了一个人类解决问题而发明的比如数学(或其它科学)这些最高效的工具, 从来和现实世界是不同的. 比如 E = MC2, 它真实的表达了事物的本质, 却并非是一个直观上与现实世界相似的表示方法.

我们考虑软件设计中, 存在某一客观事物. 往往一个熟知面向对象的设计方法的人员, 就把这一事物抽象为一个对象, 然后抽象出它的属性(数据)和方法(行为). 在这一过程中, 经常被忽视的一个问题是, 由于软件设计所面对的客观情况的复杂性, 我们常常会面临解决不同的问题, 在解决不同的问题时, 应该使用不同的抽象. 由于在脑海里没有清晰的问题域, 在很多人看来, 这(同一事物不同抽象)就硬生生的把一个事物割裂开来(其实可以不同抽象同一实现来重新统一, 但设计的合理仍然依赖于不同抽象的必要性), 不自然, 于是不合理不合法.

比如贫血模型的好与坏, 比如Martin所划分的"事物脚本", "表模块", "领域模型", 都是在这样情况下产生争论的. 这个在上个关于充血贫血讨论的回帖中已经有论述, 我已经把回帖单独提出来, 有兴趣的朋友可以讨论. 拿Martin划分的三种方式来说, 以本文开头对于抽象的定义, 想必大家可以达成一个共识, 即无论采用那种方式, 都是对事物的一种抽象, 区别只是好坏问题. 看过我的回帖的, 应该有这么一个印象, 即谁好谁坏, 并不是以符合那种方式就可以分辨的(Martin同学也承认).

在我们的某个项目实现和实施的过程中, 除了对象, 方法, 属性这些东西, 还有什么是对事物的抽象? 不知道大家还记不记得数据库教科书里面教我们的那些? 没错, 就是那些几NF之类枯燥无味乱七八糟的东西. 我发现这样一个事实: 讨论面向对象软件设计的各位牛人之中, 很少有关注数据库设计的. 其实数据库设计的书现在也是车载斗量; 从深度和实用性讲, 数据仓库是一门更博大精深的学问. 其实咱们大多数讨论谈到的数据库, 在数据仓库大牛, 如Ralph Kimball眼里, 不过是个源操作环境(名词可能记的不准确). 而百万行级上百个表甚至更多的数据, 如果要产生价值, 不使用数据仓库的种种设计方法, 那么其实跟废数据区别不大.Ralph是什么观点呢? 数据的设计是如此的重要, 如果没有良好的数据表现形式, 沃尔玛的老板永远不会知道啤酒和尿布之间存在着什么样的联系, 结果基于数据做出的决定让啤酒大幅的增加了销量.

某些兄弟提到, 将软件(不包含数据库等)设计的结果作为数据库设计(作为数据的结构设计的一个子集)的前提和约束. 通过这样的手段, 把数据设计稳定下来, 绝对是一种行之有效的手段, 但它很难变成一种放之四海皆准的设计手段. 这个是自顶而下方法论大师们的一个误导(大嘴们经常犯这个错误): 光拿出一个方法, 却忘了交代该方法所针对的问题.

请注意数据仓库领域中"维度建模"这个词, 不用搞清楚数据仓库到底是什么, 也不用搞清楚什么维度不维度的; 存在这个就足以说明, 建模不只是纯粹的软件结构设计的问题. 至少在Ralph所研究的系统里, 恐怕数据仓库的设计要比软件结构设计重要的多. 道理很简单: 数据的结构是合理的, 那么针对数据的操作可以有各种合理的版本, 未来业务变化, 新增不同数据和构筑在不同数据之间的处理也很容易; 同时, 请不要忽视一点: 数据不应只是简单的行的堆积, 数据本身是存在广泛联系的, 而且数据本身也要表现出这种联系; 更要命的是, 数据如果被要求发挥最大的价值, 那么其表现形式的抽象过程比其它任何东西都需要小心对待.

在我看来, 这个(数据驱动还是对象驱动)问题的关键点在于, 用户到底要的是数据, 还是软件? 可能大多数人回答是软件. 其实拿沃尔玛来说, 他要的是数据, 以及和数据相互作用之后数据. 当然如果没有软件, 沃尔玛的全部员工翻所有数据翻10年也翻不完, 更不论计算了. 但是如果只有软件, 那么我只能一摊双手, 告诉你这儿社么都没有. 软件是人收集数据的簸箕, 观看数据的窗口, 是计算数据的工具. 让我们来看看Brooks曾经说过什么吧:

原文引用更普遍的是, 战略上突破常来自数据或表的重新表达--这是程序的核心所在. 如果提供了程序流程图, 而没有表数据, 我仍然会很迷惑. 而给我看表数据, 往往就不再需要流程图, 程序结构是非常清晰的.

当然, 是不是真的不需要流程图, 我看他在这里也学了Martin大嘴了一把(虽然他年纪更老, 也更大牛一点). 当然也许Brooks的意思是他可以一直研究某种很复杂的表数据长达若干年直到完全搞定, 这样看我也赞同他的观点: 有表数据已经无敌了. 重要的是前半句, 如果我们把"程序流程图"没有指代但在软件设计中除了数据设计外其它内容包含进来, 他们全都没有会怎样, 数据成了废数据? 这就是千百万的$所换来的结果? 那么让我们干脆点, 问一个问题, 没有软件会怎么样?

实际上, 数据库已经存在千百万年了, 只是在计算机上实现, 是这几十年的事. 我们考虑60年代的人口普查表, 和现在的不会有什么不同. 在这个表上就存在着种种抽象: 一个人不是姓名年龄性别出生日期就能代表的, 实际上表上行, 就是对真实存在的人的抽象. 因为人口普查这个问题域, 需要这种抽象. 往往这种抽象, 还是经历几十年甚至上百年反复调整和重构, 才得到的结果. 所以抽象并建模对于人类来说实际上早就开始了. 那么进行数据驱动式的设计理由是充分的: 我们首先需要的是数据本身的表现形式的正确性. 看看Ralph和Brooks的潜台词: 数据的表达应该包括足够的信息. 在数据这一层面, 对于Brooks来说, 良好的抽象能造成软件整体的突破(对计算机世界); 对于Ralph来说, 良好的抽象能更多的挖掘出数据所隐含的信息(对现实世界).

当然, 这不说明(在一些领域内)对象驱动是不合理的, 但这足够说明, 数据库仅仅用来存储对象的状态, 而由面向对象的设计来抽象整个世界, 这个情况并不常常发生, 其原因是从某种程度上来说, 至少对于沃尔玛的老板, 追逐软件而不是数据本身是舍本逐末的. 经常的, 对于居委会老大妈来说, 数据表现形式的设计就是对世界最好也最急需的抽象. 所以对于是否对象驱动通吃, 就引出了下一个问题(答案往往是层层拨开的) : 面向对象的设计最终产生的存储对象状态的数据结构, 是不是在任何领域内都能和直接抽象出来的数据结构一样好? 抱歉我暂时没法回答这个问题, 因为这篇文章简直是走到哪儿, 写到哪儿, 我还没真正思考过这个问题. 当然, 如果我是大嘴Martin, 我会直接告诉你不能...

不过没有答案, 不妨碍不能从其它周边的角度来进行一下讨论:

公元前二百二十一年, 秦始皇统一中国. 如果我们不过分的贬低古人的智慧, 可以考虑这样一个问题, 如果OOA/OOD真的能够得到良好的数据结构, 而且比直接数据设计合理, 那么是不是OOA/OOD中, 最后能决定数据的表现形式的这一部分方法, 早在秦始皇记录到底谁有几本书好烧掉它们的时候, 就开始被人研究和发掘了呢? 毕竟薛定谔的代数方程和哥本哈根学派的矩阵力学最后的表达的都是一码事, 只要有条路通到罗马, 这条路就应该有人走才对.可历史上对数据的建模并不存在这样一个设计步骤, 当然你可以说这个那个OO里也有, 但这是OO借用人家的; 这是这个考虑的一个方面.

另一个方面, 我们需要看看抽象出来的工具中最璀璨的明珠, 数学. 数学的抽象, 不但实际上和面向对象背道而驰, 而且他们的数据表现形式, 往往和居委会老大妈掰脚趾头全无区别, 比如那些和自然数列一一对应的问题. 它们对现实世界面向过程的抽象方式, 跟真实世界简直是驴唇不对马嘴, 却更贴切的表达了很多事物的本质.

与面向对象设计相反的, 提炼关系(函数)/元素/集合的面向过程抽象方法, 关系型数据库本身和基于它的设计和抽象, 却是从数学, 尤其是集合论等抽象的工具中, 直接衍生出来的. 从这里, 我给出一个个人对软件设计的认识, 它对一些人来说是颠覆性的, 信不信由你:

面向过程和数据表现形式的设计, 是比面向对象更高层次的抽象, 而且优秀的面向过程和数据设计, 是对解决问题更有效的抽象, 因为它们抛弃了一切可以抛弃的无用负担.

当然, 能合理驾驭面向过程和关系模型的人, 恐怕脑容量得远远高于常人, 以至于Martin这个大嘴说:

原文引用事实上, 我一直坚信面向对象的最大优点在于它能够使复杂逻辑易于处理.

这很显然就是在说: Martin的脑子不够处理某些逻辑, 所以不得不借助面向对象这个拐棍. 当然估摸着脑筋够用的人很少存在, 于是大多数复杂性够高, 同时面向过程的项目失败了. 不过请考虑一下我们在集合论中学过的那些, 函数的定义, 关系的定义, 等等等等, 由集合论的方法来看, 我们解决问题时,要找到的实际上常常是一个集合S, 一个a和一个b, 以及一个连接a和b的关系R.

在这里需要申明的是我绝非一个DBA跳到程序员堆里玩深沉, 我从来没设计过真正需要数据仓库的系统, 而且我现在的工作与数据库完全无关. 同时我是一个坚定的面向对象的信徒, 我也绝对不会说, 实用主义的话应该怎么怎么样, 因为我不相信什么实用主义. 象遗留系统等问题, 是客观存在的情况, 一个新的系统是可以对由于遗留系统产生的问题进行一次抽象, 让新系统至少是新的; 上帝的归上帝, 凯撒的归凯撒, 本该如此就是最大的实用主义. 但是我要知道我使用的(面向对象这一)工具到底是干吗地的, 帮我解决了什么问题, 而不是神话之(过犹不及嘛), 这样才能最大化这一设计工具的最大价值: 面向对象的抽象方法就是把我玩不转的形式, 转化为玩的转形式的这么一个工具. 在这里, 给出第二个我个人的认识:

面向对象仅仅是帮我们前进的一个手段, 他通过增加一些从根本上讲毫无意义的细节和表达形式等冗余, 使得我们能更好的组织我们的过程与数据.

接下来说说另外一个抽象过程中经常发生的问题: 忽略了对计算机世界进行抽象和建模的重要性. 而这个问题和如何对现实世界进行建模有着相同的重要性, 很显然, Int32是一个抽象, String是一个抽象, 问题是当我们编制软件的时候, 光是这些对象是不够的(人家Martin Fowler说了, 这叫基本型迷恋), 很多人对这一点认识不深, 导致重视不足. 而对远离基本型迷恋的原因, 大嘴们给出的解释又过于浮浅, 将很多与基本型迷恋相似的问题给淹没了.

我提请大家注意一个非常重要的事实: 在.NET Framework里, 或者Java的各种框架里, 实际上存在着非常大量的与现实世界无关的模型(即使一些模型与现实世界存在着相似性). 当我们构建一个软件或系统的时候, 每个人数的都是自己的代码行数, 其实要是将操作系统也作为提供的解决方案的一部分(甚至不包括操作系统仅包含用到的基础框架), 即便上亿美金的项目, 恐怕仍然是对非现实世界无关模型的使用占很大部分.

让我们基于这一事实进行一个想像, 假设.NET Framework是一个十分蹩脚的设计(其实ASP.NET的一些方面就相当蹩脚), 那么我们基于.NET Framework的项目, 综合考虑时间/人力等等成本在解决来自Framework的问题, 还有几个能成功呢? 而现在情况是, 基础环境或多或少的和我们特定的问题不合拍, 而大多数项目中的设计, 恰恰是根本缺乏对计算机世界的合理建模而直接构筑在通用框架或工具箱提供的模型上; 又或者有一个比如各种ORM这样的通用工具, 拿一个持久化的概念, 就把我们可怜的计算机, 操作系统, 物理存储的文档或者数据库给打发了. 这样的设计必然会造成, 要么数据表现形式看上去不合时宜, 要么面向对象的设计方法看上去很别扭. 在这种情况下, 特别是那种数据驱动的项目, 不能本能的考虑去修改数据库, 难道你能轻易抱怨.NET对String的实现吗? 所以在设计伊始, 就要考虑到你这个设计如何对来自计算机世界的现有事物进行抽象, 或者对现有事物与设计中其它部分的关系进行抽象, 才能最大程度的保证设计的完备与统一.

至于面向过程比较容易适应, 恰恰是因为这种抽象的表达方式更本质, 除非问题域变了, 否则 f = ma 做成一个静态方法, 摆在哪里永远不会废弃. 有新情况? 来个新公式吧. 当然, 面向过程也能做到重用, 多态, 问题是逻辑复杂度高于Martin的7.42, 人脑就到了极限, 正巧这7.42到底是啥还没个准谱. 所以说, 面向对象总是有益的, 但是如果对什么是抽象, 怎么个抽法,  都抽谁, 还存在着疑问, 那么实际上降低复杂度的同时, 也提高了各种事故出现的概率. 实际上一些基本问题, 现代编程语言已经帮我们处理了(这个以后探讨), 但是来自软件概念性层面上的问题和复杂性, 只有我们自己能解决.

关于面向对象如何帮助我们, 我会在后续话题里进行讨论. 毕竟这篇文章是介绍抽象的, 同时很大篇幅的讨论了数据驱动, 对象驱动和面向过程, 面向对象等抽象方式不同的意义. 让我们回顾一下:

1. 软件设计领域内, 抽象无处不在, 在面向对象中, 在面向过程中, 在数据表现形式中, 在界面设计中(这个没有提到). 加一句在本文没有体现的(也许未来我会加上相关论述, 这个认识相当重要), 软件设计, 归根结底不应该是对现实世界抽象, 而是对"就某个问题, 使用计算机去更好的解决"这一过程的抽象, 虽然这一过程往往包含对现实世界的抽象, 但多了个大方向, 进行对现实世界的抽象时, 方式方法就不一样了. 如果不认清这个问题, 无论是面向对象(多这个大方向并不会导致"不够OO"这种问题, 只会OO的更正确)还是其它什么方法, 无论你是Anders还是Gosling, 都不可能获得正确的结果.

2. 好的抽象的唯一标准是是否在正确的问题下良好的运转, 而并非是在直观上和现实世界多么的相似. 而且我们在生活中所能看到的很多最好的抽象, 往往在直观上与现实世界的事物毫不相似, 却在问题所在的领域内更接近现实世界事物的本质.

3. 面向过程和数据表现形式的设计, 是比面向对象更高层次的抽象(难道搞出一个汽车类, 不是具体化么), 而且优秀的面向过程和数据设计, 是对解决问题更有效的抽象, 因为它们抛弃了一切可以抛弃的多余内容. 面向对象的抽象方式仅仅是帮我们前进的一个手段, 他通过为抽象增加一些从根本上讲毫无意义的细节和表达方式等冗余, 使得我们能更好的组织我们的过程与数据.

4. 在一个大的问题域内, 如果数据表现形式的设计更根本,  就绝对不存在对象驱动还是数据驱动的问题,  只能数据驱动. 同时我们也要承认, 存在着很多领域,  数据只是操作模型的附属, 在这样的情况下, 数据到底怎么设计, 甚至可以发展到毫无重要性的程度. 与Martin对复杂度的分辨不同, 到底用户要的是什么, 这不是一个7.42, 而总会偏向于某一方.

5. 在设计中, 尤其是数据驱动的设计中, 既要考虑对现实世界的抽象,  也要考虑对计算机世界的抽象. 软件部分面向对象的产物与其它已知结构的冲突(比如与数据库设计的冲突), 其原因是没有把计算机世界某一部分(如关系型数据库及构筑于其上的数据表现形式)就软件所处理的问题域进行再次的合理抽象, 或者太轻易的处理了它们(比如, 忽视了数据本质也是一个接口这个隐含的约束).

最后送给大家Brooks另一句话, 这一句话曾经指导过1000W行以上的单一项目的设计, 同时也指导了软件设计长达40余年, 并且我想它还会作为一句少有的经典, 继续存在下去:

原文引用数据的表现形式是编程的根本.

这句话本身所真正拥有的真知灼见, 比他当时语境下所要表达的含义还要多的多. 这句话绝非从什么主义出发,无论你是一个面向对象的狂热信徒还是一个随便造个二进制文件当数据持久的高人, 我想仍然需要好好咀嚼一下.

posted on 2007-09-17 01:30  怪怪  阅读(7230)  评论(57编辑  收藏  举报

导航