也许,你听过设计模式里的箴言,要针对接口编程,而不应该针对细节(具体)编程;或者你还听过,要减少代码间的依赖关系;甚至你还知道,修改一个模块时,要保证重新编译的文件依赖要最小化,而重新编译的时间最短化。当你问,How to?大神就会祭出嗯,你应该将接口与实现分离
的经文。
我们在使用面向对象语言编程时,或者更宽泛些 ,设计一个好的接口时,经常会把 接口与实现分离
这句话挂在嘴边。只是,真正弄明白这句话的含义怕是比听到这句话晚好几年。因为没有足够的项目经验和知识积累,你很难对这句话有真实的体会。
什么是接口
这个问题有些千人千面,如果你听过万物皆是对象,那么我可以告诉你,万物皆接口,嗯,有点拾人牙慧的意思,在计算机的世界里,你(coder)写的任何一个字符都是接口,它连接着计算机和现实世界;当然,太抽象了,等于没说。我们往大点说,接口就是一组代码和另一组代码的桥梁。再往大点说,接口是一个程序对另一个程序的连接点;继续大点说,接口是一个程序员和另一个程序员沟通的工具。再往大点说,接口就是,嗯,停下来好了。
定义一个好的接口是非常重要的,如果你的接口对象是计算机,你需要写出计算机能识别和良好构建(至少是编译器级别的)的代码;如果你的接口是另一组代码,那你需要做到良好的定义,接口可以是一个函数,一组api,一个类等;如果你的接口对象是程序员,那除了易读的代码本身外,你可能还需要一些说明文档。
本文把接口局限在代码层面,类或者函数层面。
什么是实现
限定了接口的范围,我们说说实现,实现从本质上说就是把承若的类或者函数给coding done了。作为程序员,我们大部分工作都是在实现。
接口和实现分离:
1.为何分离
然我们看一段代码:
假如我们需要实现一个学生类 ,它有学号、出生日期、寝室号等信息。生日日期我们使用Date类,寝室号我们使用DormNum类。
class Student
{
public:
Student(const Date& birthDate,const DormNum& bedroomNum,const std::string& name);
std::string birthDate() const;
std::string bedroomNum() const;
std::string name() const;
private:
Date birthDate_;
DormNum bedroomIn_;
std::string name_;
};
为了让这个类能通过编译,我们需要将使用到的类的定义通过include包含进来,就像这样:
#include <string> //包含类string
#include "date.h"//包含类Date
#include "dormNum.h"//包含类DormNum
这样看来,一切都很完美,可是,一旦Date或者DormNum类发生了改变,整个Student类以及调用这个类的相关部分全部需要重新编译,这真不是个好事情,毕竟时间宝贵啊。
因此,我们需要将编译依赖降低,使用接口和实现分离的方式来缩减需要重新编译的代码。
2.如何分离
这个问题其实很大,我这里只能给些思路和建议:
首先,我们主要是担心Date类和DormNum类所在的文件发生改变,那么我们就把这两个类单独拿出来好了。像现在这样:
#include <string>
class Date;
class DormNum;
class Student
{
public:
Student(const Date& birthDate,const DormNum& bedroomNum,const std::string& name);
std::string birthDate() const;
std::string bedroomNum() const;
std::string name() const;
private:
Date birthDate_;
DormNum bedroomIn_;
std::string name_;
也就是说,将使用的依赖类进行前置声明,避免编译时无法找到对应类的错误,而不用 Student类定义的文件中包含Date和DormNum的头文件。在C++中,前置声明一个类,然后使用这个类类型声明其他数据是可以的,只是,这样做还不够,因为当我们需要调用这个类的某个成员函数,到底还是需要类的成员函数的定义。因此,我们可以准备两个头文件,一个用于安放类的前置声明,比如studentFwd.h
;另一个则是具体的类的定义,就像最开始的date.h,dormNum.h之类的头文件。
比如放前置声明的studentFwd.h
class Date;
class DormNum;
接着,我们开始把实现剥离出来:
1.使用implement
接口定义好了,我们开始实现一个StudentImpl类,所有的Student类的具体工作都将在这个类中完成,
class StudentImpl
{
public:
StudentImpl(const Date& birthDate,const DormNum& bedroomNum,const std::string& name);
std::string birthDate() const;
std::string bedroomNum() const;
std::string name() const;
private:
Date birthDate_;
DormNum bedroomIn_;
std::string name_;
};
事实上StudetImpl拥有和原Student类完全一样的结构,修改改变的是Student类:
class Date;
class DormNum;
class Student
{
public:
Student(const Date& birthDate,const DormNum& bedroomNum,const std::string& name)
:spStuImp(new StudentImpl(birthDate,bedroomNum,name));
std::string birthDate() const{
return spStuImp->birthDate();
}
std::string bedroomNum() const{
return spStuImp->bedroomNum();
}
std::string name() const{
return spStuImp->name();
}
private:
shared_ptr<StudentImpl> spStuImp;
};
作为接口类Student,将它的具体执行全部调用StudentImpl类去完成,自己则保持对外稳定的接口形式,这是接口与实现分离的雏形。回到编译上,当外界依赖的类改变后,Student类不需要重新编译,唯一需要做的是StudentImpl的重新编译。
2.使用abstract class
不过,我们还可以使用真正意义上的接口类来完成这个目标,在java中有明确的interface声明,尽管C++中并没有该关键字,但是可以在形式上与之保持一致。
class Student
{
public:
virtual std::string birthDate() const = 0;
virtual std::string bedroomNum() const = 0;
virtual std::string name() const = 0;
virtual ~Student();
};
现在的Student被定义为纯虚类,我们使用一个具体的类来继承它,并在此类中实现具体的接口操作
class RealStudent:public Student
{
public:
RealStudent(const Date& birthDate,const DormNum& bedroomNum,
const std::string& name)
:birthDate_(birthDate),bedroomIn_(bedroomNum),name_(name)
{}
virtual ~RealStudent();
virtual std::string birthDate() const ;
virtual std::string bedroomNum() const ;
virtual std::string name() const ;
private:
Date birthDate_;
DormNum bedroomIn_;
std::string name_;
};
接口使用的时候,我们可以使用Student指针指向具体的子类对象,比如
Student *reStudent = new RealStudent;
或者更为合理的智能指针:
shared_ptr<Student> Student::create(const Date& birthDate,
const DormNum& bedroomNum,const std::string& name){
return shared_prt<Student>(new RealStudent(birthDate,bedroomNum,name));
}
enn,现在已经有点工厂模式的味道了。
接口与实现分离是整个设计模式大厦的最初目标,随着软件技术的不断发展,该理论的局限性也在缩小,该思路配合软件设计的分层理论,构建了现代软件开发的基石。