Tea-与面向接口
Tea 语言并不是面向过程,也不是面向对象的语言,它是面向接口的语言。 本文将为你解释何为面向接口。
编程思想
编程思想就是编程来解决问题的思路。现在比较有名的两种思想是面向过程和面向对象。
面向过程
假如工厂要生产一个罐头,需要经过这些流水线:
进货 => 加工 => 装罐头 => 装箱
流水线上的每个环节可以接受上一环节的产出物,并继续传递给下一环节。 这思路是非常清晰的,但是这意味着需要有一个人去管理整个流程,并不能允许任何环节出错。
面向过程编程就像是在创造一个流水线:程序本身就是在描述有哪些生产环节(即具体有哪些函数)。
面向过程中,每个环节都只能处理上一个环节的数据。 如果任何环节出现错误,将直接引发后续环节的错误。 为了避免这个问题,我们将每个环节作一次独立封装:每个环节只处理它规定的数据,它不再依赖于其它环节。
一个环节和它规定的数据一起将组成一个对象。 最后的产品是依次交给这些对象来生成的。 这就是面向对象的本质:将各个功能点分成若干对象,然后依次通过这些对象完成任务。
面向对象
面向对象编程就像是社会中有不同的人,当我们需要完成一个任务时,分别通知不同的人来完成。 每个人可以有私有财产(数据,字段),能力(函数,方法),通知他人的能力(消息,事件),以及传授财产和能力(继承,多态)。
通过面向对象,我们得到这些好处:
- 对象之间是独立的,这意味着可以分开开发。
- 很多对象的功能可以在不同项目反复使用。
- 通过对象的继承,可以创造一个具有更多能力的人。并最终完成各项任务。
面向接口
面向对象强调将不同的功能封装为一个独立的对象。每个对象都可以处理其他对象。 理论上,代码是可以完成任何功能的。 但是现实中会有这么一个现象:同样一个功能,却有两个对象完成(比如两个作者都写了同样的类,但是编译在了不同的程序里)。 就像现实中会有两个人拥有相同的水平。然而当我们要处理这两个人的数据时, 却因为是两个独立的对象而需要分别写代码处理。
就像同样生成罐头,却有不同的品牌和厂商,而不同厂商只能使用自己的机器,而拒绝外来机器,即使他们使用的机器功能是一样的。
所以面向对象的代码往往特别繁琐。
面向接口编程强调是对象的能力,而非对象本身。每个对象处理的是拥有指定能力的对象,而非特定的对象。
云里雾里?没事,通过下面这段代码可以加深映像。
例子
需求:用户可以输入一个格式为 “数字 + 数字” 或 “数字 - 数字” 的表达式,程序负责输出这个表达式的值。 例如用户输入 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 显示器(); // 错误: 显示器不是电视。