Skip to content

Tea 与面向接口

xuld edited this page Jan 13, 2014 · 3 revisions

Tea 语言并不是面向过程,也不是面向对象的语言,它是面向接口的语言。 本文将为你解释何为面向接口。

编程思想

编程思想就是编程来解决问题的思路。现在比较有名的两种思想是面向过程和面向对象。

面向过程

假如工厂要生产一个罐头,需要经过这些流水线:

进货 => 加工 => 装罐头 => 装箱

流水线上的每个环节可以接受上一环节的产出物,并继续传递给下一环节。 这思路是非常清晰的,但是这意味着需要有一个人去管理整个流程,并不能允许任何环节出错。

面向过程编程就像是在创造一个流水线:程序本身就是在描述有哪些生产环节(即具体有哪些函数)。

面向过程中,每个环节都只能处理上一个环节的数据。 如果任何环节出现错误,将直接引发后续环节的错误。 为了避免这个问题,我们将每个环节作一次独立封装:每个环节只处理它规定的数据,它不再依赖于其它环节。

一个环节和它规定的数据一起将组成一个对象。 最后的产品是依次交给这些对象来生成的。 这就是面向对象的本质:将各个功能点分成若干对象,然后依次通过这些对象完成任务。

面向对象

面向对象编程就像是社会中有不同的人,当我们需要完成一个任务时,分别通知不同的人来完成。 每个人可以有私有财产(数据,字段),能力(函数,方法),通知他人的能力(消息,事件),以及传授财产和能力(继承,多态)。

通过面向对象,我们得到这些好处:

  1. 对象之间是独立的,这意味着可以分开开发。
  2. 很多对象的功能可以在不同项目反复使用。
  3. 通过对象的继承,可以创造一个具有更多能力的人。并最终完成各项任务。

面向接口

面向对象强调将不同的功能封装为一个独立的对象。每个对象都可以处理其他对象。 理论上,代码是可以完成任何功能的。 但是现实中会有这么一个现象:同样一个功能,却有两个对象完成(比如两个作者都写了同样的类,但是编译在了不同的程序里)。 就像现实中会有两个人拥有相同的水平。然而当我们要处理这两个人的数据时, 却因为是两个独立的对象而需要分别写代码处理。

就像同样生成罐头,却有不同的品牌和厂商,而不同厂商只能使用自己的机器,而拒绝外来机器,即使他们使用的机器功能是一样的。

所以面向对象的代码往往特别繁琐。

面向接口编程强调是对象的能力,而非对象本身。每个对象处理的是拥有指定能力的对象,而非特定的对象。

云里雾里?没事,通过下面这段代码可以加深映像。

例子

需求:用户可以输入一个格式为 “数字 + 数字” 或 “数字 - 数字” 的表达式,程序负责输出这个表达式的值。 例如用户输入 1 + 4 ,则输出 5 。 例如果用户输入 4 - 4, 则输出 0 。

面向过程:

void calcPlus(int x, int y) {
    return x + y;
}

void calcMinus(int x, int y) {
    return x - y;
}

void main(){
    
    int x = readInt();
    char c = readChar();
    int y = readInt();
    
    int result;

    // 程序仅仅在描述具体的过程。
    if(c == '+'){
        result = calcPlus(x, y);
    } else if(c == '-'){
        result = calcMinus(x, y);
    }
    
    writeInt(result);
}

面向对象:

class AddCalucator {
    int calc(int x, int y){
        return x + y;
    }
}

class MinusCalucator {
    int calc(int x, int y){
        return x - y;
    }
}

void main(){
    
    int x = readInt();
    char c = readChar();
    int y = readInt();
    
    int result;

    // 根据不同的功能,调用不同的类实现。
    if(c == '+'){
        result = new AddCalucator().calc(x, y);
    } else if(c == '-'){
        result = new MinusCalucator().calc(x, y);
    }
    
    writeInt(result);
}

面向对象中,我们在试图将计算功能封装为对象,不同的对象有不同的计算功能。

面向接口:

class AddCalucator {
    int calc(int x, int y){
        return x + y;
    }
}

class MinusCalucator {
    int calc(int x, int y){
        return x - y;
    }
}

void main(){
    
    int x = readInt();
    char c = readChar();
    int y = readInt();
    
    // 这里,我们并不强调 calucator 是具体哪一类对象。
    var calucator;
    int result;

    if(c == '+'){
        calucator = new AddCalucator();
    } else if(c == '-'){
        calucator = new MinusCalucator();
    }
    
    // 我们并不知道 calucator 具体是哪个对象,
    // 但是我们知道它们都拥有 calc 的能力,就可以直接调用。
    // 即使 AddCalucator 和 MinusCalucator 没有任何关系,也可以使用同样的代码调用。
    result = calucator.calc(x, y);
    
    writeInt(result);
}

仔细看,这里和面向对象中的多态的作用异曲同工:只需调用同一个函数,而具体的实现根据类型来定。 以上的代码在面向对象中,需要定义一个公共的父类。 从而达到程序只写一个代码来处理父类的目标。因此,如果代码已经编译过,想再提取父类是不可能的。 于是大量的面向对象代码选择完全自己重写,而不是重用已存在的代码。

如果程序处理的是拥有指定能力的对象,而非特定的对象, 那么我们不需要为未来写代码。无论这个类是在什么时候创建的,只要它有这个功能,就可以直接被已有的代码处理。这将大大减少为了面向对象所带来的代码量。

为了描述方便,我们将一个或多个能力命名为接口,比如 IEnumeratable 接口,即表示一个对象拥有循环遍历的能力。接口也可以是匿名的,就如上述例子中,其实就隐藏了这么一个匿名的接口:

  interface {
       int calc(int x, int y);
  }

区分类和接口

类和接口的主要区别在于它们描述的主体是否是抽象的。 比如冰箱、电视和电器三个概念中,因为冰箱和电视都是实际存在的主体,而电器是抽象的概念,因此冰箱和电视都是类,而电器则是一个接口。

当我们需要定义类冰箱电视时,为了节约代码量,我们必须提取公共部分统一处理。 在面向对象中,我们需要提取一个公共基类(abstract class),然后对这个基类操作, 就等效于对所有子类操作。 而面向接口中,它强调的是两个对象同符合同样的约定, 因此我们需要定义一个公共接口(interface),然后根据接口操作。 这两者有显然的区别:公共基类只能是一个,而接口是可以有无限个的。

接口的应用场景

比如现在已经定义了一个电视类,并且完成了它应有的功能。 后来,需要添加这样一个功能:定义一个显示器类。 我们会发现显示器的很多功能都和电视重复。 但如果让显示器继承电视,显然是不合常理的。 因此我们可以把电视看成一个接口----用个别借代整体,然后让显示器去实现电视的功能。

最后代码如下:

class 电视 {}

class 显示器:电视 {}

注意这里显示器不是继承电视的意思。而是用电视来代替一个接口,因此下面的代码是不对的。

电视 a = new 显示器();  // 错误: 显示器不是电视。