抽象类与接口
作为面向对象的四大特性之一,抽象类和接口是经常被使用到的概念。那么抽象类和接口的区别是什么?什么时候用接口?什么时候用抽象类?抽象类和接口存在的意义是什么?
抽象类和接口的区别
有一个抽象类的使用例子(为便于展示将函数声明及定义写在一起):Logger
是一个记录日志的抽象类,FileLogger
和 MessageQueueLogger
继承 Logger
,分别实现两种不同的日志记录方式:记录日志到文件中和记录日志到消息队列中。FileLogger
和 MessageQueueLogger
两个子类复用了父类 Logger
中的name
、enabled
、minPermittedLevel
属性和 log()
方法,但因为这两个子类写日志的方式不同,它们又各自重写了父类中的 doLog()
方法。
#pragma once
#include<string>
typedef int level;
class Logger
{
private:
std::string name;
bool enabled;
level minPermittedLevel;
public:
Logger(std::string name_, bool enabled_, level minPermittedLevel_) :
name(name_), enabled(enabled_), minPermittedLevel(minPermittedLevel_)
{}
void log(level level_, std::string message)
{
bool loagable = enabled && (minPermittedLevel <= level_);
if (!loagable)
return;
doLog(level_, message);
}
virtual void doLog(level level_, std::string message) = 0;
};
class FileLogger : public Logger
{
public:
void doLog() {...}
};
class MessageQueueLogger : public Logger
{
public:
void doLog() {...}
};
以上这个例子使用纯虚函数实现了抽象类,可以得到抽象类的特征:
抽象类不允许被实例化,只能被继承。
抽象类可以包含属性和方法。方法既可以包含代码实现(比如 Logger
中的 log()
方法),也可以不包含代码实现(比如 Logger
中的 doLog()
方法)。不包含代码实现的方法叫作抽象方法。
子类继承抽象类,必须实现抽象类中的所有抽象方法。对应到例子代码中就是,所有继承 Logger
抽象类的子类,都必须重写 doLog()
方法。
而接口,对于java可能有更直观的体会,因为java中可以直接使用interferce 关键字定义接口。在Java中定义个接口,之后可以定义不同的类来实现接口,如果有个函数的参数为这个接口的话,就可以对各自的类做出不同的响应,这里使用一个简单的例子:
public interface animal
{
public void info();
}
public class dog implements animal
{
public void info()
{
System.out.println("dog class");
}
}
public class cat implements animal
{
public void info()
{
System.out.println("cat class");
}
}
public class func{
public void act(animal a)
{
a.info();
}
}
public class test{
public static void main(String []args)
{
dog d = new dog();
cat c = new cat();
func f = new func();
f.act(d);
f.act(c);
}
}
而在C++中,没有接口的定义,我们要定义抽象类来实现像java中的接口功能。例如:
#include <iostream>
using namespace std;
class animal
{
public:
virtual void info()=0;
};
class dog:public animal
{
void info()
{
cout << "dog class" << endl;
}
};
class cat:public animal
{
void info()
{
cout << "cat class" << endl;
}
};
void test(animal &a)
{
a.info();
}
int main()
{
dog d;
cat c;
test(d);
test(c);
return 0;
}
由上可以得到接口的特性有:
接口不能包含属性(也就是成员变量)。
接口只能声明方法,方法不能包含代码实现。
类实现接口的时候,必须实现接口中声明的所有方法。
由此可以得到,抽象类实际上就是类,只不过是一种特殊的类,这种类不能被实例化为对象,只能被子类继承。我们知道,继承关系是一种 is-a 的关系,那抽象类既然属于类,也表示一种 is-a 的关系。相对于抽象类的 is-a 关系来说,接口表示一种 has-a 关系,表示具有某些功能。对于接口,有一个更加形象的叫法,那就是协议。
为什么需要抽象类和接口?
为什么需要抽象类?
首先,抽象类不能实例化,只能被继承,因此抽象类是为了解决代码复用问题。当然,使用虚函数也是为了实现多态特性。若 Logger 类中没有设计定义纯虚函数,则子类中无法直接定义doLog()方法。除此之外,使用抽象类,编译器会强制要求子类重写 log() 方法,否则会报编译错误,在大型项目的开发中,方法很多的时候,这样的设计可以确保我们在子类中都会实现对应的log()方法。
为什么需要接口?
抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,联想类比 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。接口实现了约定和实现相分离,可以降低代码间的耦合性,提高代码的可扩展性。
当然前文已经讲了用c++实现接口,即构造满足接口特性(不包含属性、只能声明方法,方法不能包含代码实现)的抽象类。
如何决定使用抽象类还是接口?
如果我们要表示一种 is-a 的关系,并且是为了解决代码复用的问题,我们就用抽象类;如果我们要表示一种 has-a 关系,并且是为了解决抽象而非代码复用的问题,那我们就可以使用接口。