posts - 48, comments - 95, trackbacks - 0, articles - 2
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

公告

面向接口设计思想

Posted on 2013-10-28 12:29 xuld 阅读(...) 评论(...) 编辑 收藏

无论之前你是否听说过面向接口,本文所描述的将是一个全新的设计思想叫面向接口。这里的接口并不是代码中的 interface 关键字,思想和语言是没有直接关系的,只不过有些语言实现一种思想比较方便而已。

在了解面向接口前,必须先了解面向对象,因为面向接口是从面向对象根据历史的经验衍生出来的一种思想。在面向对象中,一切都是对象,对象拥有独立性:它必须保持一个内部状态,并且避免一切外界干扰。所以面向对象要求大部分字段都应该私有的,然后提供有限的公开的接口去访问这些字段。如:

class 书 {
   
    private string 名字;

    public string get名字(){
        return this.名字;
    }

}

初学者经常会问:为什么不直接公开这个字段,一定要写个get名字的函数才能获取到书的名字。面向对象的专家会回答这是为了类的封装性(即独立性)。名字这个字段对外界来说是只读不可写的。

面向对象通过强调对象的独立性保证了高的代码重用率,降低了类的学习成本(无需关心内部细节)。但是它却忘了一个事实:世界万物都有联系。于是,当两个对象开始有联系时,麻烦的问题就出现了。比如现在新增一个人的类:

class 人 {
    
    public void 读书(书 a){
          读取(  a.get名字()  )
    }

}

人可以读书,但书也可以被读。那么读书这个操作是属于人还是属于书,在面向对象中是没有区分的。虽然书本身是一个独立的对象,但是人在读书时,却又不得不打破书的封装性----人需要读取书的内容和更多细节。抓着封装思想不放,就不得不为书增加API才能让书真正被读:一个对象提供哪些API是根据需求来决定的。如果需求很多,那么书的API会非常多,当其他人去读这份源码时,会发现本来一个很简单的对象,却有很多不知道干嘛用的API。很多API是为了某些需求而写的,作者将它们塞进这个对象,仅仅为了更方便访问私有字段。这种代码设计其实已经偏离了面向对象的初衷,但对作者而言,完成项目才是重点。所以他们选择这种折中的方式:API随便加,反正达到访问的目标即可,打破了封装又如何?所以也有人说:只有有专家才会在一开始就设计好有哪些对象和API,才能写出真正面向对象的代码,这些专家被称为软件构架师。面向对象拒而不谈对象之间的联系,导致一开始好好的代码最后变成互相引用的难以维护的代码。

关于更多面向对象的缺陷,可以见另一篇文章:《面向对象中的设计陷阱》

接下来介绍面向接口。面向接口中,同样的一切都是对象,但是它将对象分成两类:生物和非生物。非生物就是没有生命的对象:比如一本书,一个电脑。在程序中,非生物总是被动的----比如球自己是不会飞的,它只能被踢飞。生物则代表能力的拥有者,它可以处理非生物,可以记忆,可以和其它生物沟通。

比如现实场景:小明的电脑坏了,然后它交给小刚去修。这里其实有三个对象:小明、小刚、电脑。是人都知道:小明和小刚是生物,电脑是非生物。小明需要做这些事情:1. 记住他有一台电脑,这是他的私有财产。2. 通知小刚去修电脑,并且将他的私有财产转交给小刚。小刚需做这些事情:修电脑(不管是谁的电脑)。电脑是非生物,因此它不能做任何事情。那么面向接口中,如何将这个现实问题转换为代码表示呢?

原则一:所有的非生物不具备任何封装性。

以电脑为例,虽然我们总将电脑看成一个独立的整体,但确实是存在一些时刻,它的零件是打散的。电脑本身没有思考能力,它不能保证自己一定是处于完整的,能用的状态。因此对非生物来说,它不需要在内部维护一个状态。但是它可以有一些必要保护措施,来确保它不会被闲人弄坏,但这个措施不是强制的。就好像你弄坏了电脑,错在你,不是因为电脑质量差(当然好电脑是不会随便被搞坏的)。

原则二:所有的生物具有封装性。

比如小明拥有的电脑是他的私有财产,除非他愿意,否则没人可以使用他的电脑。如果小明主动忘记了他有一台电脑,那么这台电脑和小明将失去任何联系。

原则三:对事不对人。

比如小明的电脑坏了,他不一定就得交给小刚做,他只要交给一个会修电脑的人来做就行,只不过刚好小刚符合要求而已。

总结如上原则,上述现实问题描述成代码应该是这样的:

 

class 电脑{
    public bool 还的还是坏的;
}

class 小明{
   
   private 电脑 a;
  
   public void 去修电脑(会修电脑的人 b){
       b.修电脑(a);
   }

}

class 小刚 : 会修电脑的人{
   
    public void 修电脑(电脑 a) {
        a.还的还是坏的 = true;
    }

}

 

以上代码和面向对象代码的区别:

1. 电脑拥有 public 字段:因为电脑是非生物,不需要封装。

2. 去修电脑的参数是会修电脑的人,而不是小刚。

原则四:能力可以随时扩展。

以上三个原则其实并未体现面向接口的优势,真正的优势在于原则四。现实中,你还是你,但是你的能力是在不断成长的。面向对象中因为对象是独立封装的,即对象先天决定它有哪些能力。但是面向接口中,能力是可以后期扩展的。

比如原来会修电脑的人只能是小刚,后面小明自己也会修电脑了,那么,它甚至可以自己修电脑。而这个修改,并不需要在源码上进行。因为源码已经很好体现了这些现实逻辑,不管小明自己会不会修,原来的设计是不变的。一个对象可以拥有多个能力,被多方使用,能力可以是先天的,也可以是后期扩展的。对于上例代码,现在可以通过如下代码为小明增加修电脑的能力。

 

extend 小明: 会修电脑的人 {
   
    public void 修电脑(电脑 a){
          // 修电脑的逻辑
    }

}

 

讲到现在似乎都没有提到接口两个字。接口其实就是一份证书:用于约定一个生物具有哪些能力。上例中的 “会修电脑的人” 就是一种能力的约定-----即它是一个接口。理论上任何能力都可以用接口来描述,但这显然不现实(生活中不可能为任何能力都提供一份证书:会烧饭证书?会写字证书?)因此,我们使用同一类生物还借代:比如小刚是会修电脑的人,那么用小刚来借代所有会修电脑的人(就像平时说的邻居家的孩子来借代拥有各种能力的孩子)。

面向接口中,我们假设一切生物在起初是没有任何能力的,所有能力都是后期根据需要再提供的。但是这些能力和这个生物本身的资产是没有关系的。