利用编程做一个简易的商场收银系统,营业员根据客户购买的商品的单价与数量,向客户收费。
马上想到的解决思路:无非是提供两个输入,分别对应商品单价与数量,然后通过计算得到费用输出!
/**
* @create on 2020/5/19 22:02
* @description 收银客户端
* @author mrdonkey
*/
class Client {
companion object {
@JvmStatic
fun main(vararg arg: String) {
val scanner = Scanner(System.`in`)
println("请输入商品单价:")
val price = scanner.nextLine().toDouble()
println("请输入商品数量:")
val num = scanner.nextLine().toInt()
println("商品总价为:${price.times(num)}")
}
}
}
测试结果:
请输入商品单价:
38.5
请输入商品数量:
10
商品总价为:385.0
上面的代码看起来可用,可是如果商场对商品搞活动,所有的商品打八折该怎么办呢?
简单,那不是在总价再乘以0.8不就好了吗。
那难到商场活动结束了,你还要再改一遍吗,然后再用改好的程序去把所有机器都安装一遍吗?
嗯?那增加一个优惠方式不就行了,默认是原价,可以选择原价/打折
/**
* @create on 2020/5/19 22:24
* @description 收银客户端:增加打折功能
* @author mrdonkey
*/
class Client {
companion object {
@JvmStatic
fun main(vararg arg: String) {
val scanner = Scanner(System.`in`)
println("请输入商品单价:")
val price = scanner.nextLine().toDouble()
println("请输入商品数量:")
val num = scanner.nextLine().toInt()
println("请选择计算方式:")
println("1.原价")
println("2.打8折")
println("3.打7折")
val rebate = when (scanner.nextLine().toInt()) {
1 -> 1.0
2 -> 0.8
else -> 0.7
}
println("商品总价为:${price.times(num).times(rebate)}")
}
}
}
测试结果:
请输入商品单价:
38.5
请输入商品数量:
10
请选择计算方式:
1.原价
2.打8折
3.打7折
2
商品总价为:308.0
除了打折,商场又需要添加新的需求:满300返100的促销
简单!写一个基类,再继承它实现多个打折和返利类,利用多态结合简单工厂模式,完成代码
那你打算写几个类?
打八折、七折、五折、满300送100、满200送50…要几个写几个
真的有必要这样做吗?如果我要打三折,要满300送80,难道再去加子类?不想想看,哪些是相同的,哪些是不同的
呃…有道理!打折都是一样的,只是参数不同,满几送几,则需要两个参数来确定
面向对象编程,并不是越多类越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类
打一折和打九折只是形式不同,抽象分析出来,所有打折都是一样的,所以打折算法应该是一个类。
简单工厂模式的UML图:
CashSuper 现金收费抽象类
/**
* @create on 2020/5/19 23:11
* @description 现金收费抽象类
* @author mrdonkey
*/
abstract class CashSuper {
/**
* 收取现金抽象方法,返回为当前价
*/
abstract fun acceptCash(money: Double): Double
}
CashNormal 正常收费子类
/**
* @create on 2020/5/19 23:14
* @description 正常收费子类
* @author mrdonkey
*/
class CashNormal : CashSuper() {
/**
* 正常收费返回原价
*/
override fun acceptCash(money: Double): Double {
return money
}
}
CashReturn 返利收费子类
/**
* @create on 2020/5/19 23:19
* @description 返利收费子类
* @author mrdonkey
*/
class CashReturn(var moneyCondition: Double = 0.0, var moneyReturn: Double = 0.0) : CashSuper() {
/**
* 例如 满300返100
* [moneyCondition] 返利条件 300
* [moneyReturn] 返利 100
*/
override fun acceptCash(money: Double): Double {
return if (money >= moneyCondition)
money.minus(money.div(moneyCondition).toInt().times(moneyReturn))// 500-(500/300)*100
else money
}
}
CashRebate 打折收费子类
/**
* @create on 2020/5/19 23:16
* @description 打折收费子类
* @author mrdonkey
*/
class CashRebate(var moneyRebate: Double = 1.0) : CashSuper() {
/**
* 打折收费
*/
override fun acceptCash(money: Double): Double {
//moneyRebate为构造参数参入折扣率,如果是8折则传入0.8,默认则1.0
return money.times(moneyRebate)
}
}
CashAcceptType 收费类型的枚举类
/**
* @create on 2020/5/19 23:38
* @description 收费类型
* @author mrdonkey
*/
enum class CashAcceptType(vararg var arg: Double) {
NORMAL,//正常收费
RETURN300_100(300.0, 100.0),//满300返利100
REBATE(0.8);//打8折
companion object{
@JvmStatic
fun getTypeByOrdinal(ordinal: Int): CashAcceptType = values()[ordinal]
}
}
CashFactory 现金收费工厂
/**
* @create on 2020/5/19 23:35
* @description 现金收费工厂
* @author mrdonkey
*/
class CashFactory {
companion object {
@JvmStatic
fun createCashAccept(type: CashAcceptType): CashSuper {
return when (type) {
CashAcceptType.NORMAL -> CashNormal()
CashAcceptType.RETURN300_100 -> CashReturn(type.arg[0], type.arg[1])
CashAcceptType.REBATE -> CashRebate(type.arg[0])
}
}
}
}
Client 客户端代码
/**
* @create on 2020/5/19 23:46
* @description 客户端代码
* @author mrdonkey
*/
class Client {
companion object {
@JvmStatic
fun main(vararg args: String) {
val scanner = Scanner(System.`in`)
println("请输入商品单价:")
val price = scanner.nextLine().toDouble()
println("请输入商品数量:")
val num = scanner.nextLine().toInt()
println("请选择计算方式:")
println("0.原价")
println("1.满300返100")
println("2.打8折")
val ordinal = scanner.nextLine().toInt()
val cashSuper = CashFactory.createCashAccept(CashAcceptType.getTypeByOrdinal(ordinal))
val total = cashSuper.acceptCash(price.times(num))
println("总价:$total")
}
}
}
测试结果:
请输入商品单价:
38.5
请输入商品数量:
10
请选择计算方式:
0.原价
1.满300返100
2.打8折
1
总价:285.0
利用简单工厂,我只要选择对应的收费方式,工厂类就会生成一个收费基类(指向具体的收费实现类),调用基类引用的抽象方法,
即可得到最终的价格!
简单工厂模式虽然也能解决这个问题,但这是解决的对象创建问题,由于工厂本身包括所有收费方式,商场是经常性的更改打折额度和返利额度,每次维护或拓展收费都需要改动这个工厂,以至于代码需要重新编译部署,这是很糟糕的处理方式,所以用它不是最好的解决办法。
面对算法的时常改动,而不影响使用算法的客户端?有什么好方法呢?
策略模式: 它定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。
Strategy类,定义所有支持的算法的公共接口
/**
* @create on 2020/5/20 08:05
* @description 抽象算法类
* @author mrdonkey
*/
abstract class Strategy {
/**
* 算法方法
*/
abstract fun algorithmInterface()
}
ConcreteStrategy 具体算法类
/**
* @create on 2020/5/20 08:19
* @description 具体算法A
* @author mrdonkey
*/
class ConcreteStrategyA : Strategy() {
/**
* 算法A的实现方法
*/
override fun algorithmInterface() {
println("算法A的具体实现")
}
}
/**
* @create on 2020/5/20 08:19
* @description 具体算法B
* @author mrdonkey
*/
class ConcreteStrategyB : Strategy() {
/**
* 算法B的实现方法
*/
override fun algorithmInterface() {
println("算法A的具体实现")
}
}
/**
* @create on 2020/5/20 08:21
* @description 具体算法C
* @author mrdonkey
*/
class ConcreteStrategyC : Strategy() {
/**
* 算法C的实现方法
*/
override fun algorithmInterface() {
println("算法C的具体实现")
}
}
Context类:用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用
/**
* @create on 2020/5/20 08:23
* @description Context上下文,用一个ConcreteStrategy来配置,维护一个对Strategy对象的引用
* @author mrdonkey
*/
class Context constructor(private val strategy: Strategy) {
/**
* [strategy] 算法的一部分是作为参数传递的
* 根据具体的侧脸对象,调用其算法的方法
*/
fun contextInterface() {
strategy.algorithmInterface()
}
}
Client 客户端类
/**
* @create on 2020/5/20 08:26
* @description 客户端代码
* @author mrdonkey
*/
class Client {
companion object {
@JvmStatic
fun main(vararg args: String) {
//由于实例化不同的策略,所以最终结果在调用context.contextInterface时所获得的结果不尽相同
var cxt = Context(ConcreteStrategyA())
cxt.contextInterface()
cxt = Context(ConcreteStrategyB())
cxt.contextInterface()
cxt = Context(ConcreteStrategyC())
cxt.contextInterface()
}
}
}
测试结果:
算法A的具体实现
算法A的具体实现
算法C的具体实现
看上面的策略模式的基本代码示例,模仿写策略模式的代码,只需要增加一个CashContext类,改一下客户端即可
CashContext:收费上下文(策略与简单工厂的结合)
/**
* @create on 2020/5/20 08:32
* @description 收费上下文
* @author mrdonkey
*/
class CashContext constructor(type: CashAcceptType) {
private var cs: CashSuper = when (type) {
CashAcceptType.NORMAL -> CashNormal()
CashAcceptType.RETURN300_100 -> CashReturn(type.arg[0], type.arg[1])
CashAcceptType.REBATE -> CashRebate(type.arg[0])
}
/**
* 根据策略不同,获得计算结果
*/
fun getResult(money: Double): Double {
return cs.acceptCash(money)
}
}
Client 客户端类(策略与简单工厂的结合)
/**
* @create on 2020/5/20 08:34
* @description 客户端
* @author mrdonkey
*/
class Client {
companion object {
@JvmStatic
fun main(vararg args: String) {
val scanner = Scanner(System.`in`)
println("请输入商品单价:")
val price = scanner.nextLine().toDouble()
println("请输入商品数量:")
val num = scanner.nextLine().toInt()
println("请选择计算方式:")
println("0.原价")
println("1.满300返100")
println("2.打8折")
val ordinal = scanner.nextLine().toInt()
val cc = CashContext((CashAcceptType.getTypeByOrdinal(ordinal)))
val total = cc.getResult(price.times(num))
println("总价:$total")
}
}
}
简单工厂模式:
val cashSuper = CashFactory.createCashAccept(CashAcceptType.getTypeByOrdinal(ordinal))
val total = cashSuper.acceptCash(price.times(num))
策略模式与简单工厂结合的用法:
val cc = CashContext((CashAcceptType.getTypeByOrdinal(ordinal)))
val total = cc.getResult(price.times(num))
简单工厂模式:需要客户端认识两个类,CashSuper与CashFactory
策略模式结合简单工厂的用法:客户端只需要认识一个类CashContext即可。耦合性更低
策略模式: 是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合。
策略模式的Strategy类层次为Context定义了一系列的可供重用的算法或行为。继承有助于析取出这些算法中的公共功能(就是获得计算费用结果的getResult()方法),这使得算法间有了抽象的父类CashSuper。
策略模式简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。
在编程之初,用when条件分支,判断具体使用哪个算法,这是正常的。因为,当所有不同行为堆砌在同一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的Strategy类中,可以在使用这些行为的类中消除条件语句。在上述商场收银软件设计中,巧妙利用简单工厂模式将客户端的条件语句放到Context中,减轻了客户端的职责。
总的来说:
“策略模式封装了变化”,策略模式就是用来封装算法的,但在实际中,我们发现可以用它来封装几乎任何类型的规则,只要在分析过程中听到需要在不同时间应用不同的业务规则,就可以考虑使用策略模式来处理这种变化的可能性。
但我感觉,在基本的策略模式中,选择所有具体实现的职责由客户端对象承担,并转给策略模式的Context对象。 这本身没有解除客户端需要选择判断的压力,而策略模式与简单工厂模式结合后,选择具体的职责也可以由Context来承担了。
不足之处:
CashContext中还是到了条件语句,如果需要加一种算法,那么必须新增CashContext中的条件判断,让人真不爽!
能怎么办,任何需求的变更都是需要成本的!还有更好的方法吗?
利用反射技术!,在抽象工厂模式章节有对反射的讲解