原型模式解析
本文是什么
在前段时间学习了一些设计模式,里面的一些内容还是需要去值得思考的。其中有一种创建型模式叫做原型模式。在之前看的一些文章中,单例模式写的和拷贝构造函数差不多。但经过几天的思考,感觉还是和拷贝构造函数有着明显的区别,故写下此文表达我的想法。如果有不同的思考,可以在评论区一起互动交换意见。
正文
什么是原型模式
我看过的一个说法是:原型模式是一种创建型设计模式,使你能够复制已有的对象,而无需使得代码依赖他们所属的类。
SubclassA可见,成员为公有变量的情况
现在让我们来思考。当我们看到有一个对象a,他所属的类是SubclassA。我们想要复制这个对象到新的对象aa的时候,该怎么做呢?其实,我们像下面的代码一样,只要看着SubclassA的构造,一个一个地复制对象中的成员变量即可。
SubclassA
{
public:
std::string name;
Subclass A()
{
this->name = "Subclass A";
}
}
int main(void)
{
SubclassA a;
SubclassA aa;
aa->name = a->name;
return 0;
}
SubclassA可见,成员为私有变量的情况
当成员为私有变量的时候,上面的代码就无法工作了。因为封装的特性,我们是无法直接访问私有变量的。这时有懂的小伙伴就会说:只要设置拷贝构造函数就可以了。没错,这个方法可行,拷贝构造函数作为成员方法,可以访问到类内到私有变量,这种场景最合适不过了。
SubclassA
{
private:
std::string name;
public:
Subclass A()
{
this->name = "Subclass A";
}
Subclass A(const SubclassA &a)
{
this->name = a.name;
}
}
int main(void)
{
SubclassA a;
SubclassA aa(a);
return 0;
}
SubclassA不可见的情况
当SubclassA对我们不可见的时候,上面的代码又失效了。因为我们现在没法实例化新的对象aa了,自然我们连拷贝构造函数也无法使用了。但其实也不是完全不可见,这里还需要假设我们知道这个类实现了什么样的接口,不然我们是真的没有办法了。这里就需要使用原型模式了(注意,这里原型模式是库的作者,也就是SubclassA需要提供的,而不是调用的客户)。原型模式需要SubclassA的作者实现它的可见API(也就是基类),拷贝构造函数和核心的clone函数。让我们来看看是如何实现的。
class Prototype
{
protected:
std::string name;
public:
Prototype()
{
name = "Prototype";
}
Prototype(Prototype &p)
{
name = p.name;
}
virtual void show()
{
std::cout << name << std::endl;
}
virtual Prototype *clone()
{
return new Prototype(*this);
}
};
class SubclassA : public Prototype
{
private:
std::string name;
public:
SubclassA()
{
name = "Subclass A";
}
SubclassA(SubclassA &p)
{
name = p.name;
}
virtual void show()
{
std::cout << name << std::endl;
}
virtual Prototype *clone()
{
return new SubclassA(*this);
}
};
int main(void)
{
// SubclassA are invisible to the client
SubclassA a = SubclassA();
// Prototype, pa and pb is visible to the client
Prototype *pa = &a;
Prototype p(*pa);
p.show();
Prototype *pp = pa->clone();
pp->show();
return 0;
}
输出以下信息
Prototype
Subclass A
在上面的例子中,我们按照我们的假设SubclassA类是不可见的。我们能看见的只有代表了可见API的基类Prototype类以及基类指针pa。
原型模式的设计很简单,第一是子类SubclassA要实现其拷贝构造函数,再实现一个clone函数,clone函数调用拷贝构造函数new一个新的对象出来返回即可,是不是很简单。那么为什么clone要包装拷贝构造函数?它和拷贝构造函数有什么区别呢?
最显著的区别就是,clone函数是一个虚函数!它利用了语言的动态绑定性质。如果直接使用基类的拷贝构造函数,那么只能得到一个基类的对象。上面代码中的p使用拷贝构造函数拷贝了pa指向的对象,但输出结果可以看到,只拷贝了基类,没有拷贝任何子类的信息。这不是我们想要的拷贝。
而如果使用clone函数,虽然拿到的是基类的指针,但执行的时候确实会调用到子类的功能(上面代码的输出都可以看到Prototype指针执行虚函数的时候输出的是子类虚函数的结果)。这样看来,我们确实在SubclassA不可见的情况下复制了它。
小结
当一个类是可见的情况下,我们可以直接通过复制对象成员或调用拷贝构造函数来获得一个与被拷贝对象一样的对象。
当一个类是不可见,但是能够通过基类知道子类实现了哪些API的情况下,我们可以通过原型模式来拷贝一个新对象,而且这个新对象和被拷贝的子类对象一样。
原型模式需要不可见类的设计者提供一个基类,实现子类的拷贝构造函数和clone虚函数。而不是客户端来实现,它只需要通过clone函数复制即可。