• 接口与实现分离


    也许,你听过设计模式里的箴言,要针对接口编程,而不应该针对细节(具体)编程;或者你还听过,要减少代码间的依赖关系;甚至你还知道,修改一个模块时,要保证重新编译的文件依赖要最小化,而重新编译的时间最短化。当你问,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,现在已经有点工厂模式的味道了。

    接口与实现分离是整个设计模式大厦的最初目标,随着软件技术的不断发展,该理论的局限性也在缩小,该思路配合软件设计的分层理论,构建了现代软件开发的基石。

  • 相关阅读:
    bzoj3007: 拯救小云公主
    bzoj4316: 小C的独立集
    PostgreSQL Replication之第三章 理解即时恢复(3)
    PostgreSQL Replication之第三章 理解即时恢复(2)
    PostgreSQL Replication之第三章 理解即时恢复(1)
    PostgreSQL Replication之第二章 理解PostgreSQL的事务日志(5)
    PostgreSQL Replication之第二章 理解PostgreSQL的事务日志(4)
    PostgreSQL Replication之第二章 理解PostgreSQL的事务日志(3)
    PostgreSQL Replication之第二章 理解PostgreSQL的事务日志(2)
    PostgreSQL Replication之第二章 理解PostgreSQL的事务日志(1)
  • 原文地址:https://www.cnblogs.com/Stultz-Lee/p/10099261.html
Copyright © 2020-2023  润新知