OOP实例-水果商人(二)

DinS          Written on 2017/11/1

上文我们已经搭建好了OOP版水果商人的框架,这里看看如何应对需求变化。

增加水果种类:
这个需求在FOP里没提及,这里也仅仅简略说一下。
FOP里就是改变结构体中string字段,OOP中就是从Fruit新派生一个类。
使用FOP的这种方法,非常容易并且最终一定导致Spagetti code,维护起来让人抓狂。
OOP则不会,因为不需要改变原来的代码。当然用现在的代码我们在构造Basket时需要稍微修改一下,使用工厂模式能够将修改将至最低。
比如我们public一个Pear出来,然后在Basket的ReadOrder里加入Pear一项。现在就可以计算Pear了。

折扣策略:
这个是重头戏,让我们来实现OOP版的折扣策略。
实际上有三种思路来实现折扣策略,这也是OOP的魅力之处:设计思维。

先把需求拷贝下来方便查找:
比如说,买苹果的话10斤以上20斤以下打9折,20-27斤打8折,27斤以上打7.5折
买葡萄的话5-10斤打9.5折,10-18斤打8折,18斤以上打7折
买桃的话统一打9.5折
折扣券由第三方提供,如果有折扣券则按重量打折不适用

1.思路一:直接在派生类的虚函数CalcTotalPrice()里添加折扣代码

这个思路应该是最直接、最容易想到的。
我们直接利用OOP的设计架构来完成变化。实际上这也是我们一开始设计的初衷。排除掉输入输出引起的代码变化(这部分肯定是要变的因为数据内容变了),我们无需动其他部分的代码就可以实现折扣策略,而且不需要繁琐的判断。

让我们来尝试实现。
首先是在基类、派生类的初始化中增加折扣券相应内容,Basket中也要做相应修改,这部分改动无法避免。
因为改动数据项实际上意味着约定接口变化了,不管FOP还是OOP都不能不做出调整。这部分代码就不贴了,重点看CalcTotalPrice():

在苹果类的声明里解放虚函数的封印,然后实现如下:

这部分的逻辑跟FOP一样。这不奇怪,OOP架设的是体系,真正的代码逻辑该怎么写还要怎么写的,偷不了懒。

其他的水果也类似,这里不贴了。
然后就可以测试程序了,输入文件如下:

运行结果如下:

可见计算是正确的。
总感觉这里少了点什么:程序在运行时到底是如何的呢?
看下面这张图:

大图点这里

动态绑定在视觉上就是这样的。程序智能地调用了不同派生类的计算函数,所以算出了正确结果。
这样的好处显而易见:
如果以后再更换折扣策略,我们完全不需要改变原有的代码,哪个水果换策略直接在哪个水果的CalcTotalPrice()里更改即可。这样子我们就把变化与不变分离开来了。折扣策略显然是会经常变化的,而计算总价的概念是稳定的。通过分离变化,我们即保证了代码结构稳定,同时又能够适应变化。

2.思路二:进一步抽象

上述做法好不好呢?就目前的需求而言是没问题的,但是我们应该想得更远。
折扣券是新加入的一个概念,在上面的做法中我们直接把第三方提供的接口加到代码中,这样有隐患。因为第三方可能变化,一旦变化则接口也会变化,导致周围的代码也变化,“扩散波动”又出现了。

因此为了应对将来的变化,我们需要再次抽象,就是针对折扣券这个概念建立新的类体系。
水果-具体的各种水果
折扣券-不同提供方提供的折扣券
这个思路是相同的。

让我们来试试看。
第一步设计类体系。
折扣券有一点与水果的类体系不同,那就是我们只有接口而没有实现。因此在抽象时只能按照提供方来派生,而不是具体的折扣券种类。当然如果第三方有其他的方式,那么也可以建立不同的类体系。
这里以提供方来建模:

第二步实现类体系:

大图点这里

注意这个抽象基类没有数据成员,仅仅是单纯的接口约定。
看看一个派生类:

大图点这里

这个供应方提供了ApplyCoupon接口。

再看看另一个供应方:

这里除了折扣券类型外还增加了一个校验值,这个是模拟第三方各种情况:

大图点这里

这个供应方提供了另一个MaxMinus接口,传入参数不一样。

类体系好做,关键是如何把这个类体系有机融合到现有的类体系中。这是一个大问题。
需要辅助类吗?辅助类无疑会增加复杂度,如果没有必要则省去,比如我们可以在Fruit的类体系中加入这样的代码:

上述代码仅仅是示例。我们增加了供应方的概念,所以输入文件中也要增加对应的信息才行。
有了这个信息,在这里可以使用该信息建立对应的shared_ptr,然后调用接口即可
这个思路跟在Basket里建立Fruit的派生类一致。
这里为了简单仅仅是演示,没必要再调整输入文件了。You get the idea。

这样设计的话,不管接口如何变化或者替换,Fruit的派生类中的代码都不需要变化。代码有点偷懒,这是为了把重点放到思想上:进一步抽象。

如果我们把整个程序的类体系画出来会发现一些很有意思的东西:

这张图明显分成了几个层次,这些层次就是抽象层次。
最高层是Basket和Fruit基类,再具体一些是Fruit的派生类和Coupon基类,再具体是Coupon的派生类。
线的粗细也是不一样的,Basket和Fruit、Fruit派生类和Coupon之间是粗线,我想表达的意思是这些类之间有代码上的联系。

我们拥有了整个水果类体系,但是在更高抽象层级上Basket只使用了Fruit基类。(严格来讲现在的代码在创建对象时出现了派生类,我们接下来会干掉这些的)。换句话说,既然在Layer1的抽象层次上没有出现派生类的代码,那么我们不管如何修改派生类都不会动到Layer1上的代码,也就是OOP保证的应对变化的能力。同样的故事发生在Layer2和Layer3之间。

总结成规律为:某一抽象层次上的修改只会影响到之下的抽象层次,而不会影响到之上的抽象层次。这是OOP成功的原因。当然随之而来的问题就是如何设计抽象层次,这个问题会在之后单独探讨。

3.思路三:更加抽象

我们还可以更加抽象,怎么抽象?
其实你也应该意识到了,折扣策略本身即一种概念。
按照这个思路我们可以设计出如下类体系:

这是一个更加抽象的思路,而且在设计模式中有现成的策略模式。不过需要注意一点:抽象是以复杂度增加为代价的
可以设计很多方案,但是实现时选择最恰当的一个。
如果可以,就做最简单

4.进一步变化?

有了Fruit和Coupon类体系,我们可以应付很多变化了.
如果供应商换了,没关系,从Coupon派生出新的供应商,在派生类中调用其提供的接口,原来的代码不用动,
如果打着策略变化,没关系,在Fruit派生类的CalcTotalPrice里改即可,每一中水果都可以独立变化不会影响到其他代码。
如果卖水果卖的很成功,你开始卖其他商品了呢?
看起来是个很大的变化,但是不要紧,我们可以轻松的重构。类就像是积木,可以放到任何适用的地方去。比如这样调整:

我们抽象出一个商品基类,然后把整个Fruit体系连根拔起放到Commodity下面。
这样可以实现之前的所有功能,同时不需要改动原来的代码。
这种“搭积木”的方法也是OOP经常遇到的。


OOP小结:

OOP隔离了变化,所以可以很好地应对变化,这带来了极大的扩展性。
具体而言OOP建立在数据抽象、继承和动态绑定之上,常用的套路是设计出一个抽象基类,然后派生出一个类体系。之后设计一个辅助类使用基类完成功能。
这个套路可以迭代下去,整个程序呈现出多个抽象层次。

到这里你也应该想到了OOP的难点:设计与建模。
难点并不在编程与实现,能用的功能无非那几个,设计优秀才是OOP编程的皇冠。
关于如何设计是一个仁者见仁智者见智的事,接下来我们尝试着来探讨这个问题。

首先探讨设计模式,见《设计模式-创建型》、《设计模式-结构型》、《设计模式-行为型》。

其次进行哲学和体系上的探讨,见《OO建模》、《OOP若干原则》。