草庐IT

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

小能的博客 CanAngle's Blog 2023-03-28 原文

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

程序介绍

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

简单工厂模式优点

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

简单工厂模式缺点

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

扩展应用场景

参考资料

有关我的设计模式之旅、01 策略模式、简单工厂、反射的更多相关文章

  1. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  2. ruby-on-rails - Railstutorial : db:populate vs. 工厂女孩 - 2

    在railstutorial中,作者为什么选择使用这个(代码list10.25):http://ruby.railstutorial.org/chapters/updating-showing-and-deleting-usersnamespace:dbdodesc"Filldatabasewithsampledata"task:populate=>:environmentdoRake::Task['db:reset'].invokeUser.create!(:name=>"ExampleUser",:email=>"example@railstutorial.org",:passwo

  3. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  4. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  5. ruby-on-rails - 使用 rails 4 设计而不更新用户 - 2

    我将应用程序升级到Rails4,一切正常。我可以登录并转到我的编辑页面。也更新了观点。使用标准View时,用户会更新。但是当我添加例如字段:name时,它​​不会在表单中更新。使用devise3.1.1和gem'protected_attributes'我需要在设备或数据库上运行某种更新命令吗?我也搜索过这个地方,找到了许多不同的解决方案,但没有一个会更新我的用户字段。我没有添加任何自定义字段。 最佳答案 如果您想允许额外的参数,您可以在ApplicationController中使用beforefilter,因为Rails4将参数

  6. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  7. ruby - 简单获取法拉第超时 - 2

    有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url

  8. ruby - 我可以将我的 README.textile 以正确的格式放入我的 RDoc 中吗? - 2

    我喜欢使用Textile或Markdown为我的项目编写自述文件,但是当我生成RDoc时,自述文件被解释为RDoc并且看起来非常糟糕。有没有办法让RDoc通过RedCloth或BlueCloth而不是它自己的格式化程序运行文件?它可以配置为自动检测文件后缀的格式吗?(例如README.textile通过RedCloth运行,但README.mdown通过BlueCloth运行) 最佳答案 使用YARD直接代替RDoc将允许您包含Textile或Markdown文件,只要它们的文件后缀是合理的。我经常使用类似于以下Rake任务的东西:

  9. ruby - 用 Ruby 编写一个简单的网络服务器 - 2

    我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b

  10. jquery - 我的 jquery AJAX POST 请求无需发送 Authenticity Token (Rails) - 2

    rails中是否有任何规定允许站点的所有AJAXPOST请求在没有authenticity_token的情况下通过?我有一个调用Controller方法的JqueryPOSTajax调用,但我没有在其中放置任何真实性代码,但调用成功。我的ApplicationController确实有'request_forgery_protection'并且我已经改变了config.action_controller.consider_all_requests_local在我的environments/development.rb中为false我还搜索了我的代码以确保我没有重载ajaxSend来发送

随机推荐