C++ “接口”与“实现”分离(两种方法)

接口需求

在软件开发这个行业中,一个较大的软件项目,一般由几个小组共同开发完成,为了将小组之间的影响降低到最低,定义好接口势在必行,如若要求短时间开发完成,定义好接口更是如此。或者说你的客户要求为其提供实现某个功能的接口,然后再在这些接口的基础上进行二次开发,如何定义才能定义好的接口呢? 第一,接口名字和实际的功能相符合;第二、接口要对数据进行封装,不允许客户直接操作接口之下的数据,尤其是使用new和delete在堆上操作内存数据。因为客户很容易由于操作不当造成错误,误以为是设计的接口有问题。

接口与实现分离

c++中实现对接口与实现进行分离有两种方法,一种是将对象的实现细目隐藏于指针背后,简单的说就是将其分成两个类,一个类只提供接口,另一个负责实现该接口,这种设计手法常称为Pimpl Idiom(pointer to implementation)。 
另一种方法就是将接口定义为抽象类,接口全被定义为纯虚函数(纯虚函数没有具体的实现方法),派生类的成员函数负责实现这些接口。这种设计手法称为Object Interface。千万不要忘记把抽象接口类的析构函数定义为virtual函数,可能会造成内存泄漏。

Pimpl Idiom手法

下面举个简单的例子,要求实现一个Person接口,其要包含如下四个函数: 
string& getName() const; 
void setName(string& name); 
int getAge() const; 
void setAge(int age); 
它们的功能是设置获取名字和年龄。其声明在Person.h文件中,具体接口如下:

#include<string>
class PersonImpl;
using namespace std;

class Person {
public:
    Person(string& name, int age);
    virtual ~Person();

    string& getName() const;
    void setName(string& name);
    int getAge() const;
    void setAge(int age);

private:
    PersonImpl *mPersonImpl;
};

Person.cpp文件中定义了具体函数接口,其内容如下:

#include "Person.h"
#include "PersonImpl.h"

Person::Person(string& name, int age):
    mPersonImpl(new PersonImpl(name, age))
{
    std::cout << "construct Person" << std::endl;
}

Person::~Person() {
    delete mPersonImpl;
    std::cout << "deconstruct Person" << std::endl;
}

string& Person::getName() const {
    return mPersonImpl->getName();
}

void Person::setName(string& name) {
    mPersonImpl->setName(name);
}

int Person::getAge() const {
    return mPersonImpl->getAge();
}

void Person::setAge(int age) {
    mPersonImpl->setAge(age);
}

PersonImpl.h声明了实现接口背后所需细目的函数接口,其内容如下:

#include<string>
#include <iostream>
using namespace std;

class PersonImpl {
public:
    PersonImpl(string& name, int age);
    virtual ~PersonImpl();

    string& getName() const;
    void setName(string& name);
    int getAge() const;
    void setAge(int age);

private:
    string& mName;
    int mAge;
};

PersonImpl.cpp中负责实现这些接口背后的细目函数,其内容如下:

PersonImpl::PersonImpl(string& name, int age):
    mName(name),
    mAge(age)
{

}

PersonImpl::~PersonImpl() {

}

string& PersonImpl::getName() const {
    return mName;
}

void PersonImpl::setName(string& name) {
    mName = name;
}

int PersonImpl::getAge() const {
    return mAge;
}

void PersonImpl::setAge(int age) {
    mAge = age;
}

从上面的例子中可以发现,在对外提供的接口函数中,只包含操作背后细目数据的接口方法,致使客户无法直接操作接口背后的细目数据,因此最大限度地降低了客户错误使用的可能性。

Object Interface手法

同样我们参照上面那个例子,要求实现一个Animal接口,其由如下四个接口组成: 
string& getName() const; 
void setName(string& name); 
int getAge() const; 
void setAge(int age); 
它们的功能也是设置和获取名字和年龄,不同的是类不一样罢了,其声明在Animal.h文件中,具体接口如下:

#include <string>
using namespace std;

class Animal {
public:
    Animal(){};
    virtual ~Animal(){};

    virtual string& getName() const = 0;
    virtual void setName(string& name) = 0;
    virtual int getAge() const = 0;
    virtual void setAge(int age) = 0;
};

Animal* creat(string& name, int age);

真正实现Animal类声明的接口函数,声明在RealAnimal.h中,具体细节如下:

#include "Animal.h"

class RealAnimal: public Animal {
public:
    RealAnimal(string& name, int age);
    virtual ~RealAnimal();

    string& getName() const;
    void setName(string& name);
    int getAge() const;
    void setAge(int age);

private:
    friend Animal* creat(string& name, int age);

private:
    string& mName;
    int mAge;
};

在RealAnimal类中,除了继承的接口函数的声明之外,还多了一个友元函数,其有点类似于工厂函数,其作用就是实例化一个对象。下面看一下接口真正的实现细节,具体如下:

#include "RealAnimal.h"

RealAnimal::RealAnimal(string& name, int age):
    mName(name),
    mAge(age)
{

}

RealAnimal::~RealAnimal()
{

}

string& RealAnimal::getName() const {
    return mName;
}

void RealAnimal::setName(string& name){
    mName = name;
}

int RealAnimal::getAge() const{
    return mAge;
}

void RealAnimal::setAge(int age){
    mAge = age;
}

Animal* creat(string& name, int age) {
    return new RealAnimal(name, age);
}

如前面所说,Animal* creat(string& name, int age)确实只是实例化一个RealAnimal对象,返回的却是Animal接口对象,所以必须将类Animal 的析构函数声明为虚函数,不然会造成内存泄漏。

总结

无论是Impl Idiom手法,还是Object Interface手法都实现了同样的接口,而且它们有一个共同的目的,降低用户(被提供接口的小组也称为客户)直接操作数据造成不必要错误的可能性。其实它们有一个重要的优点就是将模块的依赖性降到了最低,举个例子吧,假如客户在使用这些接口的时候,如果这些接口内部的实现细目变更了,客户也不需要再重新编译自己的代码,因为客户只依赖接口声明的头文件。如果客户依赖接口的代码量非常大,那么,这个时候,这样定义接口就非常有必要了,毕竟客户在不修改自己代码的前提下,不需要重新编译自己的代码,这样可以提高客户的效率。 
其实,这样来设计接口还是有缺点的,虽然接口定义在一个类中,但是真正实例化接口类的过程中,编译器会自动替我们生成必需的成员函数(比如构造函数、拷贝构造函数等),显然Animal也不例外。虽然有这样的缺点,但还是瑕不掩瑜。

原文地址:https://blog.csdn.net/TAOKONG1017/article/details/79561856

猜你喜欢

转载自blog.csdn.net/weixin_39953289/article/details/81383142