前几天跟同事brainstorm,讨论一个关于纯虚类的使用问题,挺有意思。回来心中久久不能平静,写出来一吐为快。
不论在C++中还是C#中,纯虚类都是不能实例化的,这是因为纯虚类其实是一个对业务类型的一种高度抽象,本质上是不存在这种东西的,所以也就不能实例化它。对于C++中只要类中含有一个纯虚函数就是纯虚类,而C#中是abstract修饰的类就是纯虚类,即使类中没有虚方法也可以是纯虚类,在这里我觉得C#的纯虚类没有C++的严谨,如果纯虚类中没有纯虚方法的话,那有何意义。
明白了纯虚类的原理,那纯虚类应该肯定需要被别的类继承的,因为如果不继承的话,它自己本身也不能实例化,就没有存在的意义了,所以它肯定是需要被继承的。那它需不需要含有一个或者多个纯虚方法呢?答案是肯定的,因为既然纯虚类是需要被继承的,那没有纯虚方法又有何意义。没有纯虚方法它就应该是可以实例化的,那就没有必要将其设定为纯虚类的,这也是我吐槽C#的abstract的一个原因。
有了纯虚类一定要有纯虚方法的这个基础,那就可以想象纯虚类的业务场景应该是下面这个样子的
abstract class Goods
{
public int GetPrice()
{
int price = 0;
//do some standard things
price = this.CaculatePrice();
//do some other steps
return price;
}
protected abstract int CaculatePrice();
}
我们有一类商品,它们都有一些统一的步骤需要做,先预处理,然后计算价格,这个计算价格是每种商品不一样的,最后还要统一再处理下,对一个售货员来说它其实并不关心这是什么商品有什么特性,她只关心这个商品的价格,如果购物车中有多个商品,计算出所有商品的总价格。所以一般会构建一个商品,它没有任何意义,只是将所有商品标准化东西统一起来,将不同的东西抽象为纯虚方法,这样不同的商品就可以只关心它是如何计算价格的,其它不同的部分交给虚基类来处理。这个是比较经典的业务场景,这里虚基类中可能会有多个public的方法,这些方法可以调用纯虚方法也是可以的。总之到最后Goods的使用者是直接调用Goods索引,而不是操作具体的商品类。这种情况一般纯虚类中的纯虚方法一般都是private或者protected(在C++中纯虚函数可以是private、protected和public,但是在C#中纯虚函数只能是protected和public),因为Goods的使用者一般不会直接操作商品内部的CaculatePrice的,这个应该是需要对使用者隐藏的。设成protected是有些时候需要对Goods再进行抽象,这类商品有它特定的使用者,这个时候它又需要调用Goods的方法,这种情况就开发成为protected,比如有一类商品,它们需要提供另外一种方法,计算另外一种类型的价格,但是这种计算方法又是依赖商品的CaculatePrice的结果,继承类需要再写一种方法调用基类的CaculatePrice的方法,如果是private,继承类就没有办法访问,此时就需要将其修改成为protected。
看样子纯虚类中的纯虚方法是不能为public的,一般是private或者protected的,那是否可以是public呢?答案是可以的。针对上面的例子,假设每种商品没有统一的步骤,计算方法各不一样,没有统一的步骤,那这个时候就需要将GetPrice改成纯虚函数,但是这里需要考虑的另外一个问题就是当一个纯虚类中有一个public的纯虚方法,要考虑这个纯虚方法是否是一种interface(这个是C#才有C++中没有的,这个是一个亮点,将is a和支持的方法隔离开来,因为is a的关系太强了),在这个例子我觉得还是纯虚类比较合适,interface不大合适。interface更多的像是一种技能,现实生活中不好想象这样的例子,在基础类库中的IEnumerator这个接口,这个是linq实现的基础,它就是一个典型的接口而不应该实现为纯虚类,list,array和dict它们没有公共关系,不好抽象出来一个基类来实现is
a的关系,但是都需要支持遍历数据结构中所有元素的方法,所以都实现IEnumerator的接口,所以interface更像一种技能。
在实际开发过程中需求往往是经常渐进,一开始我们并不知道要设计一个纯虚类,更多的情况是一开始只有一种商品,设计了Goods,然后有另外一种商品,它们有很大的共性,于是将共性提出来,让第二个商品继承第一个商品,其实此时就应该考虑纯虚类的,如果未来看得出来有更多同类的商品出现的话,应该重构一个纯虚基类出来,但是暂时没有让商品2继承商品1没问题,因为我觉得增加一个纯虚基类出来,而且未来还不知道能不能用上,没有必要,因为增加纯虚基类意味着强类型,将来很难再refactor,第二类太多对代码的维护成本也是很高的,基于done
is enough的原则,上面直接将商品2继承商品1没有问题,即使看上去两个不应该有继承关系,所以此时为了更好的解决这个问题,可以考虑将商品1改个名字,让它们的继承关系合法化。当商品越来越多的时候大多时候是不会重构出纯虚基类的,因为随着代码的不断增加,维护的成本会指数级增加,而且是修改一个基类,impact会比较大,一般也不会refactor一个纯虚基类。除非整个代码需要重构,可以考虑这么做,如果没有的话,这个代码结构不应该进行重构。所以纯虚基类一般都是代码整体重构的产物,当然如果在需求提出的时候就已经明确那是更好的。
在实际代码中我们会经常遇到一个虚方法是一个空函数的,那什么情况下会将一个虚方法设定为一个空函数呢?
空函数存在的价值是它是有意义的才可以,比如针对上面Goods类,假设它不是纯虚类,只是一个基类,如果有种商品就是没有价格,比如赠品,CalculatePrice直接返回0,没有任何计算,它的子类商品都有自己的计算价格方法,可以重写这个方法。所以说针对空函数的话,如果有意义才可以,一般不需要将其设成空函数,没有意义就可以直接将其改成纯虚方法。
总之:
纯虚类中是一定要有纯虚函数的,如果是私有纯虚方法那这个方法肯定是被纯虚类中其它public调用的,如果纯虚方法是public的时候是语义上更是is a的关系决定的,但是这个时候需要看看这个方法是否也算是has的方法,如果更多的是has的方法,需要考虑将这个方法改成interface,这个只有c#中才有这种功能,c++中是没有的,只是一个纯虚接口类而已。