一个菜鸟的设计模式之旅,文章可能会有不对的地方,恳请大佬指出错误。
编程旅途是漫长遥远的,在不同时刻有不同的感悟,本文会一直更新下去。
本程序实现收银员对顾客收银时可以采用不同的促销策略,支持原价,按折扣促销,满多少返利多少三种策略。使用策略模式与简单工厂模式。简单工厂使用依赖注入方法,通过配置文件 config.json 能够动态实例化对象。
PS C:\Users\小能喵喵喵\Desktop\设计模式\策略模式_简单工厂_反射> go run . 商品数量 10 单价 100 当前商品总额¥700 -------------------------------- 商品数量 30 单价 50 当前商品总额¥1700 -------------------------------- 商品数量 -1 顾客需要支付¥1700
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类型,不是原有的具体类型,转换成空接口,该接口内部存放具体类型实例,可以使用接口类型查询去还原为具体类型。
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 }
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) }
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) }
{ "promotion": "MoneyOff 300 100" }
可以改成 Normal
,也可以改成 Discount 0.5
打五折
PS C:\Users\小能喵喵喵\Desktop\设计模式\策略模式_简单工厂_反射> go run . 商品数量 10 单价 100 当前商品总额¥700 -------------------------------- 商品数量 30 单价 50 当前商品总额¥1700 -------------------------------- 商品数量 -1 顾客需要支付¥1700
策略模式:定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。
可能有点抽象,晦涩难懂,用自己的话来说就是
策略模式(白话文):完成一件事有多种方法,比如刷碗可以人工刷也可以机器刷,做的都是刷碗的工作。把各个方法封装到类里面去,每个类都能完成同样的工作,我们可以抽象出行为共性,即接口,接口内有这个公共方法,各个子类实现这个接口。客户端(使用方)声明一个接口接收一个具体的子类方法实例,然后调用声明接口的公共方法(里氏替换原则)。如果未来需要添加新的方法,只需要添加子类,原来的客户端不会受到影响(开放-封闭原则)。如果需要修改原来的方法,只需要修改客户端new实例的地方(最小的改动)。
使用策略模式能够降低具体算法与使用者之间耦合程度。封装的算法完成的是同一份工作,只是实现不同。这些算法随时都可能相互替换的,策略模式封装了变化点。虽然严格定义上策略模式是用来封装算法的,但实践中可以用来封装任何类型的规则(需要在不同时间应用不同的业务规划)。
完成一个工作有多个方法,如果不用策略模式,而是直接在单个类中使用方法,如果每个方法的执行有一定的条件要求,那么肯定会导致方法在这个类的堆积(大量的switch,if判断),这既不灵活,也不好维护。如果有了新的方法,拓展了子类,却还要修改客户端的判断,这显然违背了开放-封闭原则
。
通过里氏代换原则
,子类必须能够替换父类而不影响代码的正常运行;迪米特法则
,如果两个类不直接通信,尽量让两个类之间保持松耦合。策略模式的设计,客户端使用context对象,该对象维护了一个策略实例,实际上变量声明的是抽象父类或抽象接口(里氏代换原则),用户通过context对象调用具体策略的方法,而不再通过各个分支判断new出具体策略实例调用方法。
基本策略模式优点
基本策略模式缺点
开放-封闭原则
。可以用反射解决。有人说为啥要 context ,干脆在客户端声明接口然后new具体策略不就行了?既然要context肯定有它设计的原因。我认为主要有两点
简单工厂模式属于创建型模式的一种。创建型模式隐藏了这些类的实例是如何被创建和放在一起,整个系统关于这些对象所知道的是由抽象类所定义的接口。
案例程序中Context使用了改进后的简单工厂,客户端调用set函数,使用了反射技术和依赖注入,Context可以动态生成实例对象。
简单工厂模式优点
简单工厂模式缺点
开放-封闭原则
,每一次更改都要更改工厂类。