我的设计模式之旅、01 策略模式、简单工厂、反射
编程旅途是漫长遥远的,在不同时刻有不同的感悟,本文会一直更新下去。
程序介绍
本程序实现收银员对顾客收银时可以采用不同的促销策略,支持原价,按折扣促销,满多少返利多少三种策略。使用策略模式与简单工厂模式。简单工厂使用依赖注入方法,通过配置文件 config.json 能够动态实例化对象。
PS C:\Users\小能喵喵喵\Desktop\设计模式\策略模式_简单工厂_反射> go run .
商品数量 10
单价 100
当前商品总额¥700
--------------------------------
商品数量 30
单价 50
当前商品总额¥1700
--------------------------------
商品数量 -1
顾客需要支付¥1700
程序代码
typeRegister.go
package main
import (
"errors"
"reflect"
"runtime"
)
var TypeReg = make(TypeRegister)
func init() {
TypeReg.Set(Discount{})
TypeReg.Set(MoneyOff{})
TypeReg.Set(Normal{})
runtime.GC()
}
type TypeRegister map[string]reflect.Type
func (t TypeRegister) Set(i interface{}) {
t[reflect.TypeOf(i).Name()] = reflect.TypeOf(i)
}
func (t TypeRegister) Get(name string) (interface{}, error) {
if typ, ok := t[name]; ok {
return reflect.New(typ).Interface(), nil // ^ 新建对象获取指针并以空接口类型返回
}
return nil, errors.New("no one")
}
- 维护一个
TypeRegister
字典结构是为了实现依赖注入,什么是依赖注入?
var TypeReg = make(TypeRegister)
首先介绍一种设计思想,控制反转。正常情况下,对函数或方法的调用是调用方主动的行为,调用方清楚地知道被调的函数名是什么,参数有哪些类型直接主动调用,包括对象的初始化也是显式直接初始化。控制反转就是将主动行为变为间接行为,调用方需要通过框架代码进行间接调用和初始化。
这样的好处就是能够解耦调用方和被调方,调用者的代码不用写死,可以让控制反转的框架代码读取配置,动态构建对象。依赖注入是实现控制反转的一种方法,通过注入参数或实例的方式实现控制反转。通常这两者是同一个东西。
golang没有java的class.forName
动态生成类实例的方法。需要自行维护一套类型注册字典。该字典类型有添加类和生成类实例两大方法。init
函数会在main
函数之前运行,在函数体创建各个类型的实例来进行注册,使字典保存各个类型的类名和对应的reflect.Type
结构。reflect.Type
通过的New
函数创建一个新的实例并返回它的指针。这样我们可以实现依赖注入,控制反转(通过外部的 config.json
配置文件,动态生成实例)
- 为什么要返回空接口类型?
return reflect.New(typ).Interface(), nil
New出来的是reflect.Value类型,不是原有的具体类型,转换成空接口,该接口内部存放具体类型实例,可以使用接口类型查询去还原为具体类型。
jsonConfig.go
package main
// 加载 config.json 文件并创建维护策略实例的上下文实例对象
// by 小能喵喵喵 2022年9月8日
import (
"encoding/json"
"io/ioutil"
"log"
"strings"
)
const (
configPath = "./config.json" // 配置文件绝对路径
)
type Config struct {
Promotion string `json:"promotion"` // 从json字符串转换成结构体
}
func loadConfig() (c Context) {
config := getConfig(configPath)
params := strings.Split(config.Promotion, " ")
c.set(params[0], params[1:]) // 动态生成结构体实例并调用实例的config函数填入参数
return
}
func getConfig(path string) Config {
f, err := ioutil.ReadFile(path)
if err != nil {
log.Fatal("Error when opening file: ", err)
}
var config Config
err = json.Unmarshal(f, &config)
if err != nil {
log.Fatal("Error during Unmarshal(): ", err)
}
return config
}
strategy.go
package main
import (
"math"
"strconv"
)
// ^ 策略接口定义所有支持的算法的公共接口
type IStrategy interface {
acceptCash(money float64) float64
config(args []string)
}
type Normal struct{}
type Discount struct {
Percent float64
}
type MoneyOff struct {
Threshold float64
Back float64
}
func (d Normal) acceptCash(money float64) float64 {
return money
}
func (d *Normal) config(args []string) {}
func (d Discount) acceptCash(money float64) float64 {
return money * d.Percent
}
func (d *Discount) config(args []string) {
d.Percent = GetFloat(args[0])
}
func (m MoneyOff) acceptCash(money float64) float64 {
if money >= m.Threshold {
money -= math.Floor(money/m.Threshold) * m.Back
}
return money
}
func (m *MoneyOff) config(args []string) {
m.Threshold = GetFloat(args[0])
m.Back = GetFloat(args[1])
}
// ^ 字符串转float64
func GetFloat(s string) float64 {
f, _ := strconv.ParseFloat(s, 64)
return f
}
/* -------------------------------------------------------------------------- */
// ^ 上下文对象用于生成策略实例
type Context struct {
strategy IStrategy
}
// ^ 依赖注入生成策略实例
func (c *Context) set(str string, args []string) {
var strategy IStrategy
s, err := TypeReg.Get(str)
if err != nil {
return
}
strategy = s.(IStrategy)
strategy.config(args)
c.strategy = strategy
}
// ^ 上下文执行策略
func (c *Context) cal(f float64) float64 {
if c.strategy == nil {
return f
}
return c.strategy.acceptCash(f)
}
main.go
package main
// 策略模式_简单工厂_反射
// by 小能喵喵喵 2022年9月8日
import (
"fmt"
"strings"
)
var (
cost float64
quantity int
price float64
)
func main() {
c := loadConfig()
for {
fmt.Print("商品数量 ")
fmt.Scanln(&quantity)
if quantity <= 0 {
break
}
fmt.Print("单价 ")
fmt.Scanln(&price)
// ^ 使用策略
cost += c.cal(price * float64(quantity))
fmt.Printf("当前商品总额¥%v\n", cost)
fmt.Println(strings.Repeat("-", 32))
}
fmt.Printf("顾客需要支付¥%v\n", cost)
}
config.json
{
"promotion": "MoneyOff 300 100"
}
可以改成 Normal
,也可以改成 Discount 0.5
打五折
Console
PS C:\Users\小能喵喵喵\Desktop\设计模式\策略模式_简单工厂_反射> go run .
商品数量 10
单价 100
当前商品总额¥700
--------------------------------
商品数量 30
单价 50
当前商品总额¥1700
--------------------------------
商品数量 -1
顾客需要支付¥1700
思考总结
什么是策略模式
一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。
策略模式:定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。
可能有点抽象,晦涩难懂,用自己的话来说就是
策略模式(白话文):完成一件事有多种方法,比如刷碗可以人工刷也可以机器刷,做的都是刷碗的工作。把各个方法封装到类里面去,每个类都能完成同样的工作,我们可以抽象出行为共性,即接口,接口内有这个公共方法,各个子类实现这个接口。客户端(使用方)声明一个接口接收一个具体的子类方法实例,然后调用声明接口的公共方法(里氏替换原则)。如果未来需要添加新的方法,只需要添加子类,原来的客户端不会受到影响(开放-封闭原则)。如果需要修改原来的方法,只需要修改客户端new实例的地方(最小的改动)。
使用策略模式能够降低具体算法与使用者之间耦合程度。封装的算法完成的是同一份工作,只是实现不同。这些算法随时都可能相互替换的,策略模式封装了变化点。虽然严格定义上策略模式是用来封装算法的,但实践中可以用来封装任何类型的规则(需要在不同时间应用不同的业务规划)。
完成一个工作有多个方法,如果不用策略模式,而是直接在单个类中使用方法,如果每个方法的执行有一定的条件要求,那么肯定会导致方法在这个类的堆积(大量的switch,if判断),这既不灵活,也不好维护。如果有了新的方法,拓展了子类,却还要修改客户端的判断,这显然违背了开放-封闭原则
。
通过里氏代换原则
,子类必须能够替换父类而不影响代码的正常运行;迪米特法则
,如果两个类不直接通信,尽量让两个类之间保持松耦合。策略模式的设计,客户端使用context对象,该对象维护了一个策略实例,实际上变量声明的是抽象父类或抽象接口(里氏代换原则),用户通过context对象调用具体策略的方法,而不再通过各个分支判断new出具体策略实例调用方法。
基本策略模式优点
- 封装了变化点,消除客户端繁杂的条件语句。
- 符合里氏代换原则、迪米特法则。
- 提供了统一接口方法,每个子类都是一个策略,方便进行单元测试。
基本策略模式缺点
- 选择策略的职责依旧是客户端承担,将选择的策略转给Context对象。可以实现依赖注入。
- Context用switch来判断生成哪个子类实例,每添加一个子类就要修改Context,违反了
开放-封闭原则
。可以用反射解决。
策略模式为什么要context
有人说为啥要 context ,干脆在客户端声明接口然后new具体策略不就行了?既然要context肯定有它设计的原因。我认为主要有两点
- 可以在context做一些必要工作,难不成你客户端每次new具体策略前都要写一遍额外工作的重复代码?
- context用于实现简单工厂模式。将客户端判断分支的逻辑迁移到context中去,那么每次扩展策略类,只要修改context了。而这个判断分支的逻辑也能进一步用反射优化,通过反射动态实例化对象,去除分支判断(具体可以看上面的例子)
什么是简单工厂
简单工厂模式属于创建型模式的一种。创建型模式隐藏了这些类的实例是如何被创建和放在一起,整个系统关于这些对象所知道的是由抽象类所定义的接口。
案例程序中Context使用了改进后的简单工厂,客户端调用set函数,使用了反射技术和依赖注入,Context可以动态生成实例对象。
简单工厂模式优点
- 工厂类包含必要逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。
简单工厂模式缺点
- 不符合
开放-封闭原则
,每一次更改都要更改工厂类。
扩展应用场景
参考资料
- 《Go语言核心编程》李文塔
- 《Go语言高级编程》柴树彬、曹春辉
- 《大话设计模式》程杰
- 策略模式 | 菜鸟教程 (runoob.com)