OO真经——关于面向对象的哲学体系及科学体系的探讨

序言 Perface

      “佛曰:苦海无涯,回头是岸。——佛教用语”

      面向对象(Object-Oriented),这是一条令无数开发人员魂牵梦绕的短语。几乎每个软件分析师、设计师和程序员都时刻将它铭记于心,对它顶礼 膜拜。然而,对大多数人来说,它又像是天边的霞光,可望而不可及,无数次伸出双手,总是抓不住这虚无缥缈的圣物。于是,我们依然每天将面向对象高高供其, 却始终无法悟得其道,更不要谈娴熟运用其道法了。
      面向对象像一滩苦海,无数人游弋其中,却久久不得其要领;类、对象、继承、多态、接口、UML、设计模式……无数概念看得我们眼花缭乱,却也悟不透其真 谛。佛教有云:苦海无涯,回头是岸。如果置身苦海中无法脱离,那么,我们是否应该提高一个层面去看这片苦海:从哲学及科学的角度,去审视面向对象。
      曾有人说:艺术的极致是科学,科学的极致是哲学。此话不无道理,牛顿、爱因斯坦等科学界泰斗,在其后期都不约而同地转向哲学研究。当然,这里本人无意更不 敢将自己与上面两位大师相提并论,而且本人也不奢求此文能成为一篇颇有思想的佳作。只不过,本人在平时的实践和思考中,略有小得,于是,在这里拿出,和大 家一起分享讨论。虽然肤浅,但希望本文能成为一丝波纹,为各位脱离苦海提供一点点的推动作用。

真经第一章——世界 Weltanschauung

      “世界观(德文:Weltanschauung)意为‘着眼世界之上’,是人们对世界的总的根本的看法。任何哲学问题的探讨,归其出发点和本源,都是世界观的问题。什么样的世界观决定了什么样的哲学观点。——马克思”

      我们知道,哲学领域中,最根本的对立是唯物主义和唯心主义的对立,而附属其下,又有许多对立,如形而上学和辩证法的对立、可知论和不可知论的对立等等。这 些对立形成了哲学的基本体系、派别和出发点。实际上,这些对立,都是世界观的对立。世界观,简而言之即如何看待这个世界。世界观是一切哲学问题的本源和出 发点。
      同样,在程序世界里,也有着不同的世界观。而这其中最根本的对立便是过程论和对象论的对立,这个对立,衍生出了面向过程和面向对象两种方法论。于是,要真正理解面向过程和面相对象,我们就不得不先深究一下程序世界中这两种世界观。
      首先要提到的是,不论是过程论还是对象论,都承认一点,那就是程序世界本质上只有两种东西——数据和逻辑。数据天性喜静,构成了程序世界的本体和状态;逻辑天性好动,作用于数据,推动程序世界的演进和发展。尽管上述观点是统一的,但是在数据和逻辑的存在形式和演进形式上,过程论和对象论的观点截然不同。

      过程论认为:数 据和逻辑是分离的、独立的,各自形成程序世界的一个方面(Aspect)。所谓世界的演变,是在逻辑作用下,数据做改变的一个过程。这种过程有明确的开 始、结束、输入、输出,每个步骤有着严格的因果关系。过程是相对稳定的、明确的和预定义的,小过程组合成大过程,大过程还可以组合成更大的过程。所以,程 序世界本质是过程,数据作为过程处理对象,逻辑作为过程的形式定义,世界就是各个过程不断进行的总体。

      对象论认为:数 据和逻辑不是分离的,而是相互依存的。相关的数据和逻辑形成个体,这些个体叫做对象(Object),世界就是由一个个对象组成的。对象具有相对独立性, 对外提供一定的服务。所谓世界的演进,是在某个“初始作用力”作用下,对象间通过相互调用而完成的交互;在没有初始作用力下,对象保持静止。这些交互并不 是完全预定义的,不一定有严格的因果关系,对象间交互是“偶然的”,对象间联系是“暂时的”。世界就是由各色对象组成,然后在初始作用力下,对象间的交互 完成了世界的演进。

      上面的描述也许有些不够直观,那么,下面我们通过一个实际的例子,直观感受一下在两种世界观下,对同一件事物是怎么看的。
      大家都听过这么个智力题吧:

      说 有甲、乙、丙三人住店,一间房30。于是每人10元,共计给店老板30元住进一间房。后来店老板发现弄错了,房价应该是25元,于是给小二5元让小二退给 房客。小二黑心,贪污了2元,退给甲乙丙每人1元。这样房客每人付了10-1=9元,三九27,加上小二贪污的2元,共29元,问那1元哪里去了?

      不知各位聪明的看官是否已经参透其中玄机。不过参不透也没有关系,这不是重点,重点是,我们现在来分别用过程论和对象论分析一下这件事。
      首先,我们来看看过程论是怎么看这件事情的。


图1.1、过程论看世界

      如图1.1所示,这就是过程论下看这件事的样子。左边是过程的各个步骤,而右边红字表示在每个过程步骤的数据情况,这种数据情况反映了世界当前的状态。为简单起见,我们只考虑在这个过程中参与分配的数据。
      初始时甲乙丙各10元,老板和小二没有钱,这可以认为是这个过程的初始状态,这些数据是输入。随着各个步骤的进行,数据不断更新,而在每个步骤,数据如何 更新、更新多少,都是由步骤严格确定的。经历五个步骤后,数据变为甲乙丙各1元,老板25元,小二2元,这就是终止状态,也是这个过程的输出。

      下面,再来看看对象论下如何看这件事。


图1.2、对象论看世界

      对象论眼中,世界是由各种对象组成的,每个对象有自己的数据和逻辑,如图1.2所示。在这件事里,有五个基本对象:甲、乙、丙、小二和老板(注意,这里我们还没有提到类和抽象等概念,所以不要让固有思维跳出来,在这里要只认识对象,不认识类等概念。现在我们只讨论世界观的基本问题:程序世界的本质,至于更具体的问题,留待后面讨论)。每个对象有自己的一系列数据和逻辑,这里只列出了我们关心的部分。
      然后呢?没有然后了。没错,在对象论眼里,这就是这件事的本质模样,这件事所涉及的东西就是这么几个对象,本来它们各自独立,老死不相往来。只不过在“住 店”这个外部驱动力下,几个对象“偶然”、“暂时”互相联系,利用其他对象提供的公开服务,完成了一些交互。在交互中,各自的数据可能会发生一些变化,但 对象的本质没有变。这里也要注意,这种交互虽然在一定程度上由既定逻辑预定义,但不像过程论认为“万事万物都已注定”,在对象论下,对象间的交互是“偶然 的”、“暂时的”,这次五个人因为住店这个外部驱动力交互了一次。但下次如果魏国和蜀国交战变为驱动力,他们间的交互就不是拿钱给钱了,而是刀兵相见。所 以,对象论不认为“一切都已注定”。

      通过上面一个例子,不知各位是否已经明白程序世界中两种世界观看事物的不同。下面,有一些问题还要明确一下。
      I. 过程论和对象论是两种看世界的观点,没有孰对孰错、孰好孰坏之分。
      II. 过程论和对象论不是一种你死我活的绝对对立,而是一种辩证统一的对立,两者相互渗透、在一定情况下可以相互转化,是一种“你中有我、我中有你”的对立。如 果将对象论中的所有交互提取出来而撇开对象,就变成了过程论,而如果对过程论中的数据和逻辑分类封装并建立交互关系,就变成了对象论。
      III. 过程论相对确定,有利于明晰演进的方向,但当事物过于庞大繁杂,将很难理清思路。因为过程繁多、过程中又有子过程,容易将整个世界看成一个纷繁交错的过程网,让人无法看清。
      IV. 对象论相对不确定,但是因为以对象为基本元素,即使很庞大的事物,也可以很好地分离关注,在研究一个对象的交互时,只需要关系与其相关的少数几个对象,不 用总是关注整个流程和世界。但是,对象论也有困难。例如,如何划分对象才合理?对于同一个驱动力,为什么不同情况下参与对象和交互流程不一样?如何确定? 其实,这些困难也正是面向对象技术中的困难。

      综上,我们知道在程序世界中,存在着过程论和对象论两种对立的世界观,并且其各有千秋,无法定夺孰好孰坏。但是,对象论似乎更有助于分析规模较大的事物。 本文是探讨面向对象的,所以,在下文中,都会选择对象论作为世界观。这种以对象为本的世界观,也是本文后续一切的基础和出发点。

真经第二章——抽象 Abstraction

      “金、木、水、火、土元素,构成宇宙万物,并作为各种自然现象变化之基础——五行说”

      上文探讨了世界观问题。我们知道,要想真正理解面向对象,首先要用对象论去审视世界。而在对象论中,万事万物的本源是对象,对象是组成世界的基本元素。但是,要真正看透一个世界,只有基本元素是不行的。
      中国古代的朴素唯物主义哲学中,比较有代表性的是五行说。五行说认为,世界的基本元素是“金、木、水、火、土”,但若说世界只有“金、木、水、火、土”, 也是不成的,所以后续有云:五行相生相克,相互交织结合,组成了大千世界。虽然从现代科学角度看,五行说并不完全准确,但其有一点事非常正确的,那就是世 界首先有基本元素,然后基本元素还要衍生出各种其它东西。
      在第一章中,我们说了在对象论中,对象是组成世界的基本元素,但这还不能构成真正的世界。下面,我们来看看对象是如何构成和衍生出其它事物的。

      和真实世界中构成和衍生方式不同,程序世界中,最重要的衍生方式是抽象。例如,众所周知的类(Class),就是从对象上首先抽象出来的概念。下面我们看一看类是怎么来的。
      从哲学角度说,先有对象,然后才有类,类和对象是“一般和特殊”这一哲学原理在程序世界中的具体体现。这可能和很多人的直觉不同,因为在具体写程序时,是 先定义类,然后才能实例化对象。在这里,我们是从哲学层面进行探讨,所以,对象是本源,类的概念是衍生。为什么?因为从认识论来说,首先有具体认知能力, 才能有抽象认知能力,抽象认知能力是一种高层的,人类特有的认知能力,它使我们可以从大量具体认知中,舍弃个别的、非本质的属性,提取出共同的、本质的属 性,是形成概念的必要手段。
      还是以住店的故事为例吧。在我们的世界观中,那个故事涉及了五个对象,刚开始我们没有抽象的概念,而只是从具体认知角度对这五个对象进行认知:首先是甲, 他有头、有身子、有胳膊有腿,头上有眼睛鼻子耳朵,他还有个名字叫刘备,有个身份是顾客……除了这些数据,这个对象还可以做一些事情,可以吃饭、呼吸、喝 水,还能给钱和拿钱……好的,一通认知后,我们对甲这个对象有具体认知了;然后,我们对乙进行认知:他有头、有身子、有胳膊有腿,头上有眼睛鼻子耳朵,他 还有个名字叫关羽,有个身份是顾客……除了这些数据,这个对象还可以做一些事情,可以吃饭、呼吸、喝水,还能给钱和拿钱……认知完了,接着是丙、小二和老 板……当具体认知足够多后,我们发现一件事情:这几个对象很相似啊,有相似的数据(但具体值可能不同),有相同的逻辑,于是,我们的抽象认知能力告诉我 们,这五个对象很相似,可以看做一类东西,于是,我们给出一个类,叫“人”,并且认为这五个对象都是“人”这个类的具体例子,我们叫其为实例。以后遇到类 似的对象,我们都可以知道,这个对象属于“人”类。


 图2.1、“人”类的由来

      所以,类其实是抽象认知能力作用于程序世界的基本元素——对象后所衍生出来的抽象概念,是抽象思维在程序世界中物化后的产物。当然,现实世界中每个对象都 有无数的数据和逻辑,但在具体到程序世界时,我们往往只关心具体场景中相关的数据和逻辑。例如,在住店场景中我们关心现金这则数据,至于这个人力气大不大 无所谓;而如果上战场打仗,我们就关心攻击力和力量,现金就不重要了。

      知道了类是怎么来的,那么类的作用是什么,我们为什么需要类呢?
      类可以帮助我们方便地认识和定义世界中的对象。这个作用是显而易见的。例如当今世界有60几亿人,如果不会抽象思维,我们每遇到一个人,都要认知一遍: 啊!这个对象有眼睛,有耳朵,有鼻子有嘴,有胳膊有腿……要是真这样,世界也太疯狂了。有了类的概念,我们就可以只记类的数据和逻辑,而对于具体对象,只 要知道它属于什么“类”,一切就都知道了,所需要区分的只是不同对象的数据具有不同值而已。
      其实,这不仅仅是类的作用,我们进行抽象思维,就是为了这个目的。

      这一章叙述了类的哲学本质、衍生过程和作用。要记住,抽象是形成和衍生概念的基本方法,不只是类,后面的很多概念,都是通过抽象形成的。所以,我们可以说:上天只给了这个世界各种对象,但我们用抽象去更好地认识世界。

真经第三章——层次 Arrangement

      “道生一,一生二,二生三,三生万物——老子”

      上文提到,在对象论中,抽象是衍生概念的基本方法。但是你有没有一个疑问?所谓抽象,是对许多对象撇开个性,抽出共性,这样,抽象过程就不是确定的、唯一 的。例如,我们在看过很多对象后,发现有一类对象有四个轮子、有发动机、可以驾驶、是可以被意识反映的客观实在。我们抽象出一个叫“汽车”的类。这次抽象 中,我们将有四个轮子看做了共性,但是,如果撇开这条性质,仅看后三条,摩托车、轮船、飞机都符合,于是,我们又可以抽象出“机动交通工具”类。再把有发 动机撇掉,自行车、脚踏三轮车,甚至马都符合,所以,又得出个“代步工具”类,最后,把可以驾驶也撇掉,只剩下“是可以被意识反映的客观实在”,如果这 样,所有物质都符合,这样,就得出一个“物质”类。
      这下子困难就来了,你说我家的奔驰应该归到哪一类呢?我家的奔驰和一只是不是一类东西呢?如果从前三类看,当然不是,但是从最后一个“物质”类看,又确实 是一类东西。那到底哪一个对?事情究竟是怎样的?其实答案很简单:归到哪一类都正确。至于后一个问题,无法回答,因为这个问题单独问根本没有意义。为什 么?
      关键在于:抽象是有层次的。
      上文说到,对象是基本,我们从对象上抽象出类。但是,世界可并不是一层对象一层类那么简单,对象抽象出类,在类的基础上可以再进行抽象,抽象出更高层次的类。所以经过抽象的对象论世界,形成了一个树状结构。


图3.1、抽象层次树示例(参见原文下面链接)

      图3.1展示了一棵抽象层次树的示例。不要怀疑,在对象论中,经过初步抽象思维加工后的世界就是这样样子。本来,世界只有各个具体对象(最底下紫色文字表 示的层次),这是第0层,是一切抽象的本源和起始,然后,抽象思维作用其上,抽象出初步的类,然后在既有类和对象的基础上可以再进行抽象……如此归纳下 去,最终整个世界归结于树的根节点:本体。所谓本体,即万物之源、万物之本,是哲学层面上最高层次的抽象。在这里,我们将其看成是一个特殊的类,作为抽象 层次树的根。
      千万不要小看了这棵抽象层次树,如果能参透其中的奥秘,就能明白很多面向对象中的玄机,而且很多问题就都迎刃而解了。这种抽象层次树理论也是后续诸多内容 的理论基础。例如,OO中重要的概念——继承(Inheritance)和多态(Polymiorphism),如若探究其哲学本源,就是从这里来的。

      下面,对这棵树做一些必要的说明。
      I. 这是一棵单根树,最顶层“本体”为唯一的根,最下层叶子节点为基本对象。一切中间节点都为类。
      II. 越往上的类抽象层次越高,具体度越低,其内涵越小,外延越大;越往下的类抽象层次越低,具体度越高,其内涵越大,外延越小。说明一下,所谓类的内涵,是指 类对属于自己的对象的说明力度,而外延是指类能包含的具体对象的总和。例如,家用电器这个类,其内涵是使用电作为能源并完成特定功能的家用器具,各个电冰 箱、洗衣机、电磁炉、游戏机、DVD机等都在其外延之内;而娱乐家用电器这个类,作为比家用电器更低层次的类,其内涵除了“使用电作为能源并完成特定功能 的家用器具”外,还要是具有娱乐功能,其内涵明显大了,但外延却缩小了,只包括了各个游戏机、DVD机等对象。
      III. 抽象层次树不是从根部向下长的,而是从叶子节点向上归纳生成的。
      IV. 某一个叶子节点所代表的对象可以归入所有其祖先结点所代表的类
      V. 直接问两个叶子节点属不属于一个类没有意义,而要指定抽象层次才有意义。例如在较低层,一辆宝马属于汽车,而一只苍蝇属于昆虫,不是一类。但如果指定在较高层比较,两个都属于具体物质,属于一个类。
      VI. 我们定义,如果一个节点CNode非叶子节点也非根节点,那么在哲学意义上,这个节点继承于其父节点PNode,并且说PNode是CNode的泛化。
      VII. 我们定义,如果一个节点CNode非叶子节点也非根节点,如果强行将它看成其任何一个祖先节点ANode,并当做ANode使用,那么在哲学意义上,叫做多态性。

      先说明这么多了,随着后续内容的深入,还会有更多丰富的内容进来。例如,后面会看到,所谓的“里氏代换原则(LSP)”,在哲学本质上不过是在这棵树上所 加的一条限制规则,而“面向接口编程”、“低耦合、高内聚”、“依赖倒置”等一系列耳熟能详的短语,归结到哲学上也只是这棵树的一些精化。
      另外,看了上面的理论,我想本章开头留下的疑问也已经烟消云散了吧。
      再提示一遍,这棵树非常重要,得其精髓,就能理解诸多OO中概念、原则和方法的本质。后续讨论中,抽象层次树理论将作为重要的理论基础。

真经第四章——继承 Inheritance

      “子类型必须能够替代掉其父类型——Barbara Liskov”

      这一章我们讨论继承(Inheritance)。
      我们先看一看继承在哲学意义上时怎么来的。对象论的世界观认为,世界的基本元素是对象,我们将抽象思维作用于对象,形成了类的概念,而抽象的层次性形成了 抽象层次树的概念。接着,我们就可以定义:在抽象层次树上,除根节点和叶子节点外,任一节点CNode非严格继承其所有祖先节点所组成的集合中的任一元 素,而CNode严格继承其父节点PNode。
      继承概念,看似简单,若深入思考,却隐藏众多玄机。首先,继承描述的实际是抽象层次树上祖先节点与子孙节点的关系,但我个人一直不赞成使用继承(Inheritance)一词来描述这种关系,而推荐使用泛化(Generalization)一词。为什么呢?因为我们已经知道,从哲学和认识论角度来说,是先有对象,然后有类;先有子类,然后有父类,是一种自底向上形成的体系。 而继承一词,明显带有自顶向下的暗示,因为往往是先有爷爷、有父亲继承爷爷、然后才能有儿子继承父亲。这样,就容易让人误解成是先有父类才有子类。所以, 为了更好的体现继承的哲学本质,我更倾向于使用“泛化”代替“继承”。当然,由于继承一词已经被普遍使用和接受,接下来我还是会沿用继承一词,只不过希望 各位时刻牢记,其实是先有了子类,才从子类泛化出父类。
      当然,当父类被抽象出来后,可能还会有新的子类加进来。但是,当初父类一定是从某些子类中泛化出来的,而不会是凭空突然出现的。

      探讨了继承的本质,然后我们来探讨继承存在的意义。一切存在的东西都是有意义的,否则就不可能存在。注意,这里的“意义”是中性词,指事物存在的原由,不要理解成褒义。
      我们需要继承这个概念,本质上是因为对象论中世界的运作往往是在某一抽象层次上进行的,而不是在最低的基本对象层次上。举个例子,某人发烧了,对其他人 说:我生病了,要去医院看医生。这句简短的话中有一个代词“我”和三个名词“病”、“医院”、“医生”。这四个具有名词性的词语中,除了“我”是运作在世 界的最底层——基本对象层外,其他三个都运作在抽象层次,在这个语境中,“病”、“医院”、“医生”都是抽象的,他并没有在医院里拉着某个医生对别人说: 我生了这个,需要去这里看这个。但是,本质上他确实是生了一个具体的病,要去一个具体的医院看一个具体的医生,那么在哲学上要如何映射这种抽象和具体呢? 就是靠继承, 拿医生来说吧,所有继承自“医生”类的类所指的所有具体对象都可以替换掉这里具体的医生,这都不影响这句话语义的正确性。
      所以,继承的哲学作用就是:规定了抽象与具体之间的可映射性。形式化一点说:设G(c1,c2)意为c1非严格泛化自c2,I(c,o)意为对象o属于c的外延,其中c1,c2,c均为类,o为对象。那么,c可在哲学语义上映射成o,当且仅当o∈{o|I(c,o)}∪{o|I(c’,o) 且 G(c,c’)}

      如果你讨厌看形式化的东西,那么上面蓝色文字不看也罢,但是,有一条原则你一定很感兴趣,那就是著名的开放-关闭原则(OCP)。

      开放-关闭原则(OCP):软件实体应该可以扩展,但不可以修改。

      为什么忽然扯到OCP呢?因为,OCP正是上文讨论的哲学原理在程序世界的具体表述。我们来对比看一下,到底OCP是个什么意思。
      还是上面看病那个例子,什么叫可以扩展?就是说,因为在某个抽象层次是进行表述,就不能把话说死了,不能全是这个、那个的把每个对象都指派明白。如,那句 话改成“我的右脚扭到了,要去北京航空航天大学医院去看胡青牛医生”,这句话就没有扩展性可言了,所有话都说死了,你如果去的是北医三院或临沂市人民医 院,那么语义就不对了,而如果找的不是胡青牛而是华佗或扁鹊,语义也不对了。为什么无法扩展?因为所有点都指定了具体的对象。
      而原话“我生病了,要去医院看医生”则扩展性很大,因为只要不违反可映射性定义,映射到任何符合条件的对象都正确。扩展性和灵活性大大提高了。所以,“可 以扩展”四字从哲学上其实是要我们在设计和开发软件时提高抽象层次,不要总在具体对象层面上进行处理。这下,你明白为什么说OCP可以提高软件的可扩展性 和灵活性了吧。
      再来说说“不可以修改”,因为如果随便乱改,那就天下大乱了。还是医院那个例子,“医院”这个类所映射到的对象,一定是治病的地方。如果这东西随便改,例 如明天“医院”和“食堂”的概念对换了,那麻烦了,我们所有人都要改,要把两个概念从脑子中对换过来,全世界的书、报纸、Internet……凡是依赖这 两者进行表述的地方都要改,那不是天下大乱么?软件世界中也会发生这种牵一发而动全身的问题。所以我们提倡设计好的类一定要“对修改关闭”。
      以上,就是OCP的哲学意义。

      不过,要想世界正常运作,只有OCP似乎还有点问题。到目前为止,我们都是在抽象层次树已经存在,并且假定它完全正确的前提下讨论的,可是,我们并没有任 何规则限制抽象层次树的正确性,例如,如果我把食堂挂到医院下,让食堂成为医院的子类,在理论上时没有错的,但如果这样随便乱规定继承关系,那么一切依赖 继承正确性的原则、概念都没有意义了。所以,只有OCP是不够的,需要对继承进行一个限制。
      Barbara Liskov在1987年的OOPSLA大会上发表了一篇文章——《Data Abstraction and Hierarchy》,其中提出了一个非常重要的原则,叫里氏代换原则(LSP)。

      里氏代换原则(LSP):子类型应该能代替掉其父类型,且代替后程序运行情况不会错乱。

      我们还是用例子去理解LSP。
      现代办公几乎都要用到个人计算机,个人计算机本身是一个抽象概念,台式PC是其中一个子类。后来,发明了笔记本电脑,我们想把笔记本电脑归为个人计算机的 子类,是否合理呢?根据LSP,我们将台式PC都替换成笔记本电脑,世界应该是照常运行的(当然,实际情况可能复杂些,有些地方不能用笔记本电脑替换,但 这里我们忽略这种差别)。我们办公时依赖的类是“个人计算机”,而笔记本电脑完全可以替代这个类型而使得世界运行正常,所以,我们说将笔记本电脑归于个人 计算机的子类是符合LSP的。
      后来,又发明了转基因黄瓜,我们也想将它归到个人计算机的子类中去,行不行呢?好的,现在我们再运用LSP,将世界上每个依赖个人计算机的地方都替换成一 根转基因黄瓜。好的,世界人民都疯了!明显这种替换会令世界运行错乱。所以,我们不能让转基因黄瓜继承个人计算机。
      上面的例子是显而易见的,但有些却不那么明显。例如,现在问,兽医是医生的子类吗?这个问题,一下子还真不是很好回答,但我们可以LSP一下,现在,我们把医院里的医生都替换为兽医,你还敢去医院看病吗?嗯,这下子不用我多说了吧。
      最后一定要说明的是,LSP应用于程序世界和现实世界时有很大差别的,现实世界繁杂、不确定性因素多,而程序世界简单、确定。总之,LSP就是让你记住一 条,凡是系统中有继承关系的地方,子类型一定能代替父类型,而且替换后程序运行要正常。换言之,继承是一种严格的“IS-A”关系,也是“一般和特殊”的 哲学原理在程序世界中的体现。

      继承的话题就讨论到这里了。很多朋友在运用继承时有疑惑,或不能很好的确定继承关系,归其根本是没有真正理解继承的意义。只要能理解继承的本质意义,加上OCP和LSP的运用,是可以写出正确的继承体系。

真经第五章——耦合 Couple

      “一只蝴蝶在巴西轻拍翅膀,可以导致一个月后德克萨斯州的一场龙卷风——蝴蝶效应”

      做程序的人,往往感觉“耦合(Couple)”不是什么好东西。经常有人、有书、有文章对我们谆谆教导:要降低耦合,要降低耦合……久而久之,好像耦合在程序界成了贬义词,弄得我们恨不得把耦合从程序里全部拿掉。
      这误解可委屈耦合了。要是哪天没了耦合,这世界还真玩不转。其实耦合还有另一个名字,叫“联系”,试问要是世界上所有对象间的联系都没了,世界还能运作 么?耦合的存在是世界演进的途径,如果没有耦合,世界就变成了“死世界”,无法演进和发展。所以,耦合可是好东西,我们要感谢它!但是任何东西都有两面 性,过度的耦合确实会令世界的运作产生困难,所以我们提倡降低耦合,这些是后话。

      下面,我们探讨各种耦合式怎么出现的。
      上一章讲述了继承,其实,继承的概念出现后,有父子、祖孙关系的类就有了一种联系,这种联系叫做“泛化耦合”。这就是我们认识的第一种耦合。

      泛化耦合(Generalization Couple):由于泛化(继承)关系的存在,在两个有祖孙、父子关系的类间形成的一种逻辑关联。

      然后,我们讨论另一种耦合。
      在文章开始,我们说对象论将对象看做基本元素,而对象中有数据和方法。在现实世界中,数据并不总是简单数据。客观存在一些对象,它们的数据是另一个或另一 些对象。例如,一个具体的羊群,有一项数据是很多具体的羊。其中羊也是对象。当抽象成抽象的“羊群”和“羊”类的时候,这种包含关系也随之被抽象到了类 中,由此在两个类之间就形成了耦合。
      这种耦合出现的哲学基础是,对象本身固有的包含关系,在进行事物抽象时被同时抽象到了类中。所以,我个人将其称为包含耦合。
      包含耦合又分为两种情况,一种是被包含对象单纯聚合在包含对象中,但没有形成哲学意义上“整体与部分”的关系,这是一种相对较弱的联系,叫做聚合。例如,上例中羊群和羊就是聚合关系,如果拿掉一两只羊,羊群还是羊群。

      聚合(Aggregation):一种弱的拥有关系,体现A对象可以包含B对象,但B对象不是A对象的一部分。

      另一种情况是,被包含对象和包含对象形成了哲学意义上“整体与部分”的关系,如汽车和轮子,把轮子拿掉,汽车就不再是完整意义上的汽车了。这种关系叫做组合。

      组合(Composition):一种强的拥有关系,体现了严格的部分和整体的关系,部分和整体具有一样的生命周期。

      通过上面的探讨,我们认识了泛化耦合、聚合和组合三种耦合形式,最后,还有一种耦合叫依赖。什么是依赖呢?我们知道,在对象论中,将世界的演进看成是在初 始作用力下,对象之间相互调用、相互协作完成的。如果两个类在需求范围内,既定逻辑上存在协作的可能,那么这两个类就存在依赖关系(或叫关联关系)。其 实,我们常说的“低耦合,高内聚”、“降低耦合”等建议,主要是针对依赖说的。

      依赖(Dependency):由于逻辑上相互协作可能,而形成的一种关系。

      好的,到目前为止,我们已经认识了四种基本耦合。下面用一副图,直观感受一下世界的各种耦合。


图5.1、耦合示例

      图5.1展示了几种耦合的示例。其中汽车和交通工具属于泛化耦合,轮子和方向盘组合于汽车,汽车聚合成车队,而汽车和司机具有依赖关系。这幅图只是耦合的 一个小片段,实际上,世界上各种对象形成了一张复杂的耦合网,正因为有耦合的存在,世界才能演进。正如马克思主义哲学所说:联系是普遍的、客观的。所以, 耦合的存在,有其深刻的哲学意义。
      不知你是否会有这样的疑问:文章开始,不是说对象论将对象看做相互独立的吗?怎么又耦合起来了。这是矛盾的吗?实则不矛盾。因为我们所处的境界已经不同。 刚开始,我们抛开一切,忘记一切,从本质的角度用对象论去看世界,我们看到的对象是相对孤立的。而后来,我们的抽象思维作用于这个世界,所衍生出来的一系 列概念,是我们的抽象能力给这个世界抹上的色彩。就如我们用唯物主义看世界时,刚开始要抛开一切,认为世界只有“可被意识所反映的客观实在”,而后,这个 物质为本的世界在我们的抽象思维中衍生出各种概念。为了让我们更好的、系统的认识对象论,刚开始,我们抛开一切直取本质,而后来,我们要层层衍生,将抛却 的东西再找回来,在这个“找”的过程中,我们才能领会OO中的各种概念、事物其在哲学意义上是怎么来的。

真经第六章——运作 Moving

      “运动是绝对的——牛顿”

6.1、导言

      在前五章中,我们从世界观的这话题开始,逐步引出了抽象、层次、继承和耦合。这些内容,形成了对象论中关于世界的结构体系。
      然而,要想真正描述一个世界,仅有结构式不行的。开始我们说过,世界观主要关注两个方面:一是世界是什么样子的(结构),另一个就是世界时如何演进的(运作)。现在,我们来讨论对象论中关于世界运作的理论。
      这里首先要指出一点,“对象论”是关于程序世界(即将一个软件系统看成一个世界)的世界观,而非关于现实世界的,所以,将对象论应用于现实世界时,往往会 有所偏颇。其实前面的某些地方已经体现出这一点,而在运作理论这里,会体现的尤其明显。但是为了直观起见,我依然会将对象论应用于现实世界去举例子,当然 我会非常谨慎和小心,并且会明确指出对象论应用于现实世界的偏颇在哪里。

6.2、世界本没有类

      对象论认为:世界的演进,是而且只是各种对象通过互相调用其他对象的公开服务而完成交互。

      注意,是对象交互,而不是类交互!没错,类之间是永远不可能交互的。因为不论是现实世界还是程序世界,从来不存在具体的类。类只是抽象思维作用于对象的产物,它帮助我们理解、记忆、分析和设计。类是抽象的概念,它“客观”存在,但不是“具体”的存在。
      例如,现实世界中,我们可以找出很多个“具体的苹果”对象,但是你能找出一个东西,说它是“苹果”这个类吗?你这一辈子吃的每一个苹果,都是一个具体的苹 果对象,从来没有具体的“苹果类”和你交互过。再上升一点,你一生交互过的所有东西,都是对象,而没有一个具体的类。“类”不过是你的抽象思维作用于对象 形成的帮助你理解认识世界的抽象概念罢了。“类们”从不曾和你真正交互。
      程序世界中也是一样,程序运行起来,从来都是具体对象之间的交互,类只是帮助你分析设计的概念工具罢了。
      认识到上面几点对于理解对象论的世界运行理论非常重要,时刻铭记,参与真正世界运行的,只有对象,没有类!对象在世界中,类在我们心中!
      这一小节的标题是“世界本没有类”,代表两个意思:一是世界“本来”没有类,二是世界“本质”没有类。
      你可能会问,在第五章“耦合”中,不是说依赖关系是“两个类因为可能交互而产生的关系”吗?其实,确切点说,应该是“两个类所能映射到的对象因为可能交互而产生的关系”,本质上,依赖本来是对象间的依赖,只不过在抽象时被同时抽象到类里面了。

6.3、程序世界——大同的和谐世界

      虽然在对象论里,现实世界和抽象世界的基本运作机理是一样的,但程序世界和现实世界在具体运作上有很大差别。首先,我要告诉你,程序世界时多么的大同和和谐!

      程序世界与现实世界第一点区别:现实世界的依赖以对象为单位,程序世界的依赖以类为单位。

      没明白这意味着什么?
      举个例子,在现实世界中,是不是关系很重要啊。为什么?因为你认识的人多,可依赖的人就多。例如你生病了,如果你有个医生朋友,看病就方便很多;如果你要 打官司,而你又恰巧认识律师朋友,是不是很爽呢;如果你想上清华大学,刚好清华大学校长是你亲戚,那一切就好办多了是吧。
      为什么会这样?究其本质,是因为现实世界中对象间的依赖是以对象为单位的,这种依赖关系不会随着泛化过程而被泛化到类里面去。例如,有一个人现在在北京航空航天大学上学,从这“一个人可”以泛化出“人”这个类,而北航可以泛化出“大学”这个类,但这个具体的人和北航的这种关系可没有被泛化到两个类中,也就是说,并不是每一个“人对象”都可以去任何一个“大学对象”去上学的。
      不过,如果是程序世界里,上面的推理是可行的,因为程序世界中对象间的依赖是以类为单位的,这种依赖关系会随着泛化过程而被泛化到类里面去。并且,只要两个类建立了依赖,那么两个类之间的所有对象都两两依赖了。换句话说,在程序世界里,只要有一个“人”和一个“大学”发生了联系,那么这种联系就被泛化到类中了,随后,所有的“人”都可以上“任何”的大学。


图6.1、两个世界中依赖的区别

      看图6.1,假设世界上只有三个人和三所大学。在现实世界中,小龙女考上了清华,不过这和其他人其他大学一点关系也没有,这种关系并没有体现在类上,看, 两个类没有任何联系。但在程序世界中,小龙女考上了清华,一下子人和大学两个类就关联起来了,接着,张无忌和郭靖这两个不好好学习的学生也沾了光,和三所 大学都联系起来了。(提示:其实这里和第四章讲到的OCP和LSP联系非常紧密,读者可以联系OCP和LSP两个原则自己思考一下为什么程序世界会这 样。)
      你知道了吧,在程序世界里,全世界的医生随你看,律师随你用,大学随你上,美食随你吃!多么和谐大同的美好世界!

      看了上面对程序世界的描述,你是不是已经垂涎三尺了?恨不得自己变成一段代码,跑到程序世界里。不过别着急,事情也许没有你想象的那么美好。下面我们来看另一个程序世界与现实世界的区别。

6.4、程序世界——封建的专制世界

      上文描述了程序世界是多么多么美好,不过如果有一天,你真的跑到里面去了,你可就惨了。不信看下面。话说你一进程序世界,就迫不及待想在程序世界里找个漂 亮的女朋友,可以吗?对不起,不成!你想吃法国大餐,对不起,不成!你想上最好的大学,对不起,不成!……搞什么!不是说程序世界什么都可以得到吗。没 错,除了选择权!

      程序世界里的对象没有选择权。

      为什么会这样?因为如果对象有选择权,就没法贯彻OCP了!你要是活在程序世界里,不但给你包办婚姻,连吃饭、上学……一切的一切,你都得服从包办,对象一点点选择权也没有。至于谁给你包办的,那是后话。
      看了这些,你还敢去程序世界吗?不过这还不是最恐怖的,告诉你更恐怖的一点:

      程序世界里的对象不认识对象。

      没错,良好的面向对象提倡对象不认识对象!很不可思议?其实,这就是所谓的“低耦合”,我们喊了那么多年的“低耦合”,到底什么是低耦合?所谓低耦合,就是先剥夺对象的选择权,再剥夺对象的感觉。对象间谁也不认识谁,只知道对象能提供什么服务。
      我们现在了解了程序世界是什么样子了,下面,我们讨论程序世界为什么要这样。

6.5、有奶就是娘

      中国有句俗语,叫“有奶就是娘”,往往用来讽刺那种六亲不认,两面三刀,谁给好处就跟谁的无耻小人。不过,面向对象可是非常提倡“有奶就是娘”的行为。如 果我们的程序都能做到“有奶就是娘”的地步,那就真是实现了“低耦合”这一教义了,套用梁朝伟的话,在程序世界里,有奶就是娘的行为“是美德”。

      要理解上述道理,我们要先抛却我们脑中的道德、廉耻等概念,从本质上看看“有奶就是娘”体现了什么哲学道理。
     “有奶就是娘”,纯从字面解释,是说任何一个人,只要能给奶喝,就当做自己亲娘。上升到哲学层面,是说这么一个意思:不以其他对象实体本身为交互准则,而以其他对象的行为作为交互准则,与一个对象是否进行交互纯粹是从其行为判断,而不对对象本体有任何概念。
      这种处事哲学,在现实生活中是最被人鄙夷的,但在程序世界里确是最提倡的。如果一个程序世界里,所有对象都能以“有奶就是娘”的哲学去处事,那么,这就是一个最美好运作方式。

6.6、接口横空出世

      上文说到,程序世界中提倡的运作方式是“有奶就是娘”的方式,但要真正实现这种方式,似乎还少点东西。我们回顾一下,世界本来只有对象,我们从对象中抽象出了类,这就是目前我们眼中的世界。这样,我们的交互,要么以对象为准则,要么以类为准则。
      以对象为准则,显然是不行的,因为我们说了,对象间根本互不认识。以类为准则,理论上可行,但这样有问题,就是类本身是对象“实体的抽象”,是为了更好记 忆、描述和认识世界而创建的对象,归根到底,还是“实体”范畴的概念,所以在哲学上还是和“以行为作为交互准则”向左。

      认识到以上困难,就能认识到,目前我们的世界还无法实现以行为为交互准则,于是,我们需要为世界再衍生一些内容。第二章说过,世界本身只有对象,而衍生其 他概念的基本方法是抽象。所以,这里我们当然要用抽象衍生一些概念出来。进一步,类是对象“实体”的抽象,而我们需要的是以行为为交互准则,很自然的,我 们完全可以创建一种新概念,这种概念是行为的抽象,这种新概念,就是接口(Interface)。

      接口(Interface):对象行为的抽象。

      这里要说明,接口和类虽然都是从对象上通过抽象衍生出的概念,但两者本质不同,是从对象的两个不同的哲学角度和动机,抽象出的不同概念,并形成世界两个完全不同的方面(Aspect)。至于两者具体有什么区别,下一小节详细讨论。

6.7、接口 vs 抽象类

      经常有朋友迷惑一件事情,抽象类和接口有什么区别?何时使用抽象类,何时使用接口?但从功能来讲,抽象类完全可以代替接口,那为什么还要有接口呢?这一小节来分析这些问题。

      这里附带说一个问题,产生这种疑惑的原因,大多是因为朋友们已经习惯了学习一个东西时,只看其什么样子?怎么用?而不习惯于弄清楚一个东西起源于哪?出现的动机是什么?其实,要想学好、用好任何一个东西,后两个问题更关键一些。
      举个例子,有人发明了吹风机,我们如果只搞清楚其是什么样子——“有个把手,有个吹风筒”,以及怎么用——“打开按钮能吹出热风,关闭按钮就停止了”。如 果我们只搞清楚这些,那么我们八成用不对这个东西,为什么?因为我们根本不知道这东西是怎么来的,它为什么要被发明出来。也许我们天天拿他吹脸取暖或吹衣 服,还一派洋洋得意以为用的很好的样子。殊不知这东西其实是用来吹头发帮助头发快点干起来的。
      不要笑,这种事经常发生在我们身上。因为在软件开发中,有太多的东西,我们只顾着学习其是什么样子,怎么个用法,也许就像吹风机一样,这些并不复杂,然后我们就把它用到不该用的地方,还以为自己用得很好。
      用不用得好吹风机,不在于是否熟练掌握开开关关,而在于是不是用它吹头发。同理,任何东西用得好不好,不在于是不是熟练掌握用法,而在于是不是用对了地方。而要想用对地方,就要弄清楚这个东西的“怎么出来的”和“出来是做什么用的”。

      说了挺多,我们回到接口和抽象类的话题上来。
      首先要说明一点,“抽象类(Abstract Class)”和“类(Class)”在哲学意义上没什么区别,其区别仅仅是实现层面上的,即抽象类只不过是一种特殊的类,编程环境强制不准这种类生成实 例,哲学意义上两者没有任何区别。所以,从哲学层面讨论“抽象类与接口对比”和讨论“类与接口对比”是等价的。

      类与接口的不同点有以下几点:
      I. 抽象范畴不同。类是对象“体征”的抽象,接口是对象行为的抽象。
      II. 抽象动机不同。抽象出类是为了帮助记忆、认识世界,抽象出接口是为了实现低耦合交互。
      III. 关注不同。类关注共同的体征,接口关注用来交互的行为。
      IV. 存在范畴不同。类存在于抽象层次树上,接口存在于接口网。
      V. 应用范畴不同。类应用于结构范畴,是静态概念,接口应用于运作范畴,是动态概念。

      上面的条目有点学术了,通俗说来,类是从对象实体的的体征范畴上抽象出来的,用来帮助我们记忆、分析世界不同的对象,主要表明对象“什么样子”;而接口是从对象交互时需要的行为中抽象出来的,关注对象交互时需要的行为。
      还是举个例子吧。
      例如,有一群具体的司机和好多辆具体的汽车,我们可以从司机中抽象出“司机”这个类,从汽车抽象出“汽车”这个类,这种抽象是“体征范畴”的,抽象的目的 仅仅是帮助记忆、认识,完全和交互没有关系。而当考虑到交互——司机需要驾驶汽车,于是抽象出一个“可驾驶”这个接口。注意,一但“可驾驶”这个接口被抽 象出来,就完全和司机以及汽车没有关系了,除了汽车,拖拉机、轮船、飞机都可以实现这个接口,而不一定是司机,会开车的任何人都可以通过“可驾驶”这个接 口去驾驶任何实现“可驾驶”接口的东西。这样一来,“驾驶”这种交互就完全取决于这个接口了,这就是“以行为为交互准则的意思”。

      如果明白了这一小节的内容,相信大家再也不会被“接口和类有什么区别?”、“何时使用抽象类,何时使用接口?”这样的问题迷惑了,而可以挥洒自如的在系统 中正确使用接口和类。一个方法:拿不准的时候问问自己,这个抽象是体征抽象还是行为抽象?是为了记忆、分析、设计还是为了交互需要?想明白,再下手。

6.8、依赖是如何被倒置的

      弄清楚了接口,下面可以谈一个有名的OO原则了:依赖倒置原则(DIP)。
      如上,我们先不说DIP是什么,而是搞清楚DIP的来龙去脉。到时,朋友们自然对DIP就有深刻理解了。我们开始!
      首先,我们要说明,依赖是有方向的,客户类依赖于服务类。什么是客户类?如果A类需要B类提供的服务,那么A类就依赖B类,反之不成立。在没有引入接口前,客户类“知道”服务类,而服务类“不知道”客户类,就像下面这个样子。


图6.2、没有接口的依赖

      我们看到,司机作为客户类,汽车作为服务类。依赖的方向是从司机到汽车,以为这里司机要使用汽车提供的“驾驶”方法操作汽车。这是我们不推荐的方式,因为不够“松耦合”。于是,我们将驾驶抽象成接口,依赖变成如下形式。


图6.3、引入接口后的依赖

      如图6.3所示,我们从这种交互关系中,抽象出了“可驾驶”这个接口。注意,此时两者谁也不依赖谁,或说谁也不知道谁了。那么为什么司机可以放心呢?因为 他知道可驾驶接口的存在,他要驾驶的东西一定实现了这个接口,甭管是什么,只要实现了这个接口,我就能驾驶。其实这里才体现出接口的哲学意义。

      接口的哲学意义:对客户类的保证,对服务类的约束。

      正是接口约束了服务类必须实现什么功能,客户类才可以在不知道具体服务类的情况下“放心”进行交互,因为接口对客户类提供了一种保证。希望各位能好好体会接口的这种哲学意义,这对于对象论的良好运行体质的理解非常重要。
      可是,这样还不够,我们还有一个非常重要的问题没有讨论:谁有权利定义接口?或者说服务类和客户类谁拥有接口?当然,理论上时谁拥有都可以,但却会对世界的运作产生巨大影响。我们先看服务类拥有接口的情形。


图6.4、服务类拥有接口

      如图6.4,由于服务类拥有制定接口的权利,所以各个服务类都定义了自己的接口,一般情况下他们的接口是不相容的。如图,司机可以驾驶汽车,但由于轮船、 飞机各自有自己的可驾驶接口,所以会开汽车未必会开飞机和轮船,如果要开飞机或轮船还要一个个学,现实世界中就是这样一种情况。所以,这种世界的运行其实 接口几乎没有起到作用,由于服务类是“大爷”,所以它们可以指定诸多霸王条款,而客户必须忍气吞声去迁就,所以,实际的依赖方向还是从客户类到服务类。
      下面在看看客户类拥有接口会是什么样子。


图6.5、客户类拥有接口

      看上图,客户终于翻身做主人了,现在客户拥有定义接口的权利,服务类必须无条件实现,这下好了,只要会开汽车,就会开轮船和飞机,因为客户有权利定义一个 统一的接口,服务类必须无条件实现!这样,三种交通工具的驾驶方法必须完全一致(虽然现实世界还没有这样),这回客户终于可以扬眉吐气,体会一把“顾客是 上帝”的感觉了。
      在图6.5的情况下,司机可以有权定义接口,他不必“知道”服务类,而服务类必须“知道”客户定义了什么接口,你有没有发现,依赖的方向已经悄悄倒置过来 了!变成服务类依赖客户类了(谁知道谁,谁就依赖谁)!这就是“依赖倒置”的由来。不必说,所谓依赖倒置原则就是让我们必须按图6.5的方式运行世界,而 不能按图6.2,6.3,6.4的方式。下面正式定义依赖倒置原则。

      依赖倒置原则(DIP):客户类和服务类都应该依赖于抽象(接口),并且客户类拥有接口。

      我想,看过上述来龙去脉,已经不用我再去解释这个原则了吧。

6.9、神秘的统治者

      到目前为止,我们基本已经搞清楚了对象世界的运行机制。但仍有一个疑问:我们曾经说过,程序世界里对象时没有选择权的,甚至不知道谁是谁,只知道接口,那么,谁来指定服务类呢?
      例如,上述司机可以制定接口,所以汽车、飞机、轮船等可驾驶的东西都要实现,于是司机可以按照自己制定的方式驾驶东西。但是,司机不能选择驾驶什么啊,他根本不知道自己驾驶的是什么,那么,谁制定他是驾驶飞机、汽车还是轮船呢?
      似乎冥冥中,这个世界存在一个统治者,它掌管所有对象之间谁和谁交互(只要不违反接口),否则,世界根本没法正常运行。没错,程序世界是有这么一个统治者,他就是大名鼎鼎的“依赖注入容器(DI)”,也有人叫做“控制反转容器(IoC)”。
      什么叫依赖注入?什么叫控制反转?如果你看了上面的文章,那太好理解了,依赖注入就是容器挑选符合接口的服务类为客户类提供服务。例如,上面司机要一个可驾驶的东西,容器就会根据既定规则选择一个,可能是飞机、可能是汽车、也可能是轮船,交给司机。司机驾驶就行了,不用管是什么,反正知道这东西肯定实现了“可驾驶”接口。

      让我们向这个伟大的统治者致敬吧,没有他,程序世界可真玩不转了(当然,如果某个程序世界不符合DIP甚至没接口,都是类之间依赖,那么就不需要依赖注入容器了,不过这么一来,可就是“高耦合”了,是OO所反对的)。

6.10、运作起来吧

      到了这里,根本不用我废话说程序世界时怎么运作的了,因为上面都已经说明白了。不过,我还是用短短几句话总结一下吧。

      一个符合OO原则的、低耦合的程序世界的运作形式是这样的:首先参与运作的本质只有对象,对象不直接依赖,没有选择权,互相不知道,而只知道各个接口。客户类制定接口,对象间通过接口交互,形成运作。世界的统治者依赖注入容器决定选择哪个服务类给客户类使用。

      好了,关于程序世界的运作哲理就讲到这里了,大家可以在脑子里描绘一下上述运作情景,加深印象

本文地址http://www.chengxuyuans.com/software_design/14.html


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值