Java教程

我的设计模式之旅 ① 策略模式、简单工厂、反射

本文主要是介绍我的设计模式之旅 ① 策略模式、简单工厂、反射,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一个菜鸟的设计模式之旅,文章可能会有不对的地方,恳请大佬指出错误。

编程旅途是漫长遥远的,在不同时刻有不同的感悟,本文会一直更新下去。

程序介绍

image-20220908185122153

本程序实现收银员对顾客收银时可以采用不同的促销策略,支持原价,按折扣促销,满多少返利多少三种策略。使用策略模式与简单工厂模式。简单工厂使用依赖注入方法,通过配置文件 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

思考总结

什么是策略模式

Strategy scheme

策略模式:定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。

可能有点抽象,晦涩难懂,用自己的话来说就是

策略模式(白话文):完成一件事有多种方法,比如刷碗可以人工刷也可以机器刷,做的都是刷碗的工作。把各个方法封装到类里面去,每个类都能完成同样的工作,我们可以抽象出行为共性,即接口,接口内有这个公共方法,各个子类实现这个接口。客户端(使用方)声明一个接口接收一个具体的子类方法实例,然后调用声明接口的公共方法(里氏替换原则)。如果未来需要添加新的方法,只需要添加子类,原来的客户端不会受到影响(开放-封闭原则)。如果需要修改原来的方法,只需要修改客户端new实例的地方(最小的改动)。

使用策略模式能够降低具体算法与使用者之间耦合程度。封装的算法完成的是同一份工作,只是实现不同。这些算法随时都可能相互替换的,策略模式封装了变化点。虽然严格定义上策略模式是用来封装算法的,但实践中可以用来封装任何类型的规则(需要在不同时间应用不同的业务规划)。

完成一个工作有多个方法,如果不用策略模式,而是直接在单个类中使用方法,如果每个方法的执行有一定的条件要求,那么肯定会导致方法在这个类的堆积(大量的switch,if判断),这既不灵活,也不好维护。如果有了新的方法,拓展了子类,却还要修改客户端的判断,这显然违背了开放-封闭原则

通过里氏代换原则,子类必须能够替换父类而不影响代码的正常运行;迪米特法则,如果两个类不直接通信,尽量让两个类之间保持松耦合。策略模式的设计,客户端使用context对象,该对象维护了一个策略实例,实际上变量声明的是抽象父类或抽象接口(里氏代换原则),用户通过context对象调用具体策略的方法,而不再通过各个分支判断new出具体策略实例调用方法。

基本策略模式优点

  • 封装了变化点,消除客户端繁杂的条件语句。
  • 符合里氏代换原则、迪米特法则。
  • 提供了统一接口方法,每个子类都是一个策略,方便进行单元测试。

基本策略模式缺点

  • 选择策略的职责依旧是客户端承担,将选择的策略转给Context对象。可以实现依赖注入。
  • Context用switch来判断生成哪个子类实例,每添加一个子类就要修改Context,违反了开放-封闭原则可以用反射解决。

策略模式为什么要context

有人说为啥要 context ,干脆在客户端声明接口然后new具体策略不就行了?既然要context肯定有它设计的原因。我认为主要有两点

  • 可以在context做一些必要工作,难不成你客户端每次new具体策略前都要写一遍额外工作的重复代码?
  • context用于实现简单工厂模式。将客户端判断分支的逻辑迁移到context中去,那么每次扩展策略类,只要修改context了。而这个判断分支的逻辑也能进一步用反射优化,通过反射动态实例化对象,去除分支判断(具体可以看上面的例子

什么是简单工厂

简单工厂模式属于创建型模式的一种。创建型模式隐藏了这些类的实例是如何被创建和放在一起,整个系统关于这些对象所知道的是由抽象类所定义的接口。

案例程序中Context使用了改进后的简单工厂,客户端调用set函数,使用了反射技术和依赖注入,Context可以动态生成实例对象。

简单工厂模式优点

  • 工厂类包含必要逻辑判断,根据客户端的选择条件动态实例化相关的类,对于客户端来说,去除了与具体产品的依赖。

简单工厂模式缺点

  • 不符合开放-封闭原则,每一次更改都要更改工厂类。

扩展应用场景

image-20220908194453935

这篇关于我的设计模式之旅 ① 策略模式、简单工厂、反射的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!