OOP实例-水果商人(一)

DinS          Written on 2017/11/1

在掌握了OOP的基本概念后,让我们尝试将FOP的水果商人改成OOP版。

一、设计类体系

沿用之前的分析结果,展现出来:

大图点这里

这是一个很简单的设计,只有两层,而且虚函数也只涉及两个,但是这对于目前的问题足够了。
有时候人们会对继承有某种不合理的执着,实际上,如果可能就做最简单的

二、实现这个类体系

简便起见所有代码都放在Fruit.h和.cpp中,实际情况应该分开在不同文件中。
首先是抽象基类:

声明自定义构造函数为了派生类调用。

既然是抽象基类,不会建立对象,那么赋值和拷贝构造是用不上的,但是为什么还要声明呢?这是为了方便派生类进行相应操作。

在这里有必要对比一下非虚函数、虚函数、纯虚函数的区别和联系,可以这样来理解和记忆:

1.声明纯虚函数的目的是让派生类只继承接口。GetFruitType()必须在派生类中重新声明并定义。“派生类,你必须提供一个GetFruitType()函数,但是我不干涉你怎么实现它”。

2.声明一般虚函数的目的是让派生类继承接口和缺省行为。派生类可自行选择要不要覆盖CalcTotalPrice()。“派生类,你必须支持一个CalcTotalPrice()函数,但是如果你不想自己写一个,可以使用基类提供的缺省版本”。

3.声明非虚函数的目的是让派生类继承接口和实现,且不能改变。GetPrice()在所有派生类中实现都是一样的。“派生类,你必须支持一个GetPrice()函数,并且不能动它”。无论什么情况下都不要重新定义一个继承而来的非虚函数。

希望这样可以方便读者理解。

抽象基类实现:

接下来看苹果类:

因为GetFruitType是纯虚函数,所以必须重新声明并定义。

苹果的实现:

之后的葡萄和桃子的代码类似,就是GetFruitType()返回的字符串不同,这里不赘述了。
另外为了查看智能指针的效果,这里派生类的析构函数统一输出调用对应析构函数的信息。

三、设计辅助类

这里我们使用一个Basket类来使用Fruit。
这个Basket在概念上可以理解为购物车,含有一个vector<Fruit>装各种水果,但是不能够真的存放Fruit对象,而是要存放智能指针,这是因为只有引用或指针才能够产生动态绑定。

实际上这个Basket类还是有许多亮点可以说的,先来看.h文件:

如果不熟悉智能指针,数据成员可能有些费解,实际上就是一个vector,内部装的类型是智能指针shared_ptr,这些指针指向的是Fruit类型。
为什么要用智能指针呢?一个原因是为了动态绑定必须用指针,另一个是智能指针可以自动调用析构函数。后面我们将看到实际的例子。
读文件专门写了一个私有函数。

不提供默认构造函数,没有意义。

这里显式删除了拷贝构造函数和赋值。因为我们不希望Basket类被拷贝。
实际上因为使用的是容器和智能指针,用默认的拷贝构造和赋值不会有问题。但是这里依然删除了。如果用不到,就删除。

再看看cpp:

大图点这里

ReadOrder部分是初始化的核心。

使用了异常来使程序更加robust,而且只能使用异常来使构造函数返回。(对异常不了解的可参考《c++异常机制》)

读文件这部分跟FOP差不多,毕竟文件格式决定了代码。

接下来这部分看起来很长,直觉上感觉有些问题。事实上如何构造对象的问题是设计模式探讨的主题之一。下文将使用工厂模式优化这部分代码。

if判断里发生了有趣的事,注释里已经写得很清楚了。
使用这种方法可以把实际上类型不同的对象放入同一个容器中,这是惯用手法了。

构造失败时还能体现出智能指针的另一个好处:即使在构造函数失败的时候也能够正确清理内存,后面有一个例子说明这个问题。

OutputResult负责计算并写文件。这个for是全部代码的核心。
我们这里遍历vector,调用Fruit类的虚函数CalcTotalPrice,在程序执行时实际调用的是派生类的CalcTotalPrice,因此无论之后计价方法如何变化,这里的代码是不会变的。

四、在主程序中写代码

有了Basket类就可以在main里写出非常简洁的代码。

这大概是现代c++程序入口处的通常写法:建立几个类,执行;外面套一个try-catch,先来看看失败时候的结果:

这个文件含有非法参数,应该会构造失败。把断点打在return 0;处,运行程序:

这里的看点是异常与智能指针的完美配合。
构造函数提前结束,对象并没有构造出来,所以析构函数并不会调用。
如果这里不使用智能指针,就会内存泄漏。使用了智能指针会及时释放内存。这也是为什么称为“智能”的原因。

再来看一个正常的例子,输入文件:

运行程序:

一方面程序成功释放了内存,另一方面成功进行了计算。

至此我们的水果商人OOP框架就搭好了,明显比FOP复杂些,效果是一样的?
别着急,这只是实现了基本功能。下面让我们重现遇到的需求变更,看看OOP是如何应对的,见《OOP实例-水果商人(二)》。