策略模式(Strategy Pattern)
定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换。
策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
策略模式是一种对象行为模式。
策略模式中的 3 个角色:
- Context(环境类)
- 环境类是使用算法的角色,它在解决某个问题时可以采用多种策略。
- 在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
- Strategy(抽象策略类)
- 它为所支持的算法声明了抽象方法,是所有策略类的父类。
- 它可以是抽象类或具体类,也可以是接口。
- 环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
- ConcreteStrategy(具体策略类)
- 它实现了抽象策略中声明的算法。
- 在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。
策略模式中,对环境类的理解十分重要,环境类是需要使用算法的类,环境类根据具体的环境上下文使用不同的算法。
环境类中维持一个对抽象策略的引用,在具体环境中使用不同的策略算法。
在客户端代码中表现为向环境类中注入一个具体策略对象。
条条大路通罗马,实现目的的途径不止一条,可以根据实际情况选择合适的途径。
当处理一个业务时,有多种处理方式,并且需要在运行时决定使用哪一种具体实现时,就可以使用策略模式。
策略模式基本上是实际开发过程中最常用、最实用的设计模式了。
策略模式能有效解决部分场景中大量的 if ... else 代码,提升代码的可读性和扩展性。
案例:
- 在支付业务中,有三种付款方式,程序运行时使用哪种方式由用户选择,根据用户选择执行不同的逻辑。
- 在云计算业务中,创建的虚拟机可以 OpenStack 也可以是 VMware,还可以是公有云。
- 在云原生业务中,Kubernetes 可以部署在虚拟机上,也可以部署在裸机上,甚至还有一体机模式。
下面就已多云管理平台 CMP 中,最常见的创建不同虚拟化平台的虚拟机为例,对比下使用与不使用策略设计代码的区别。
- 没有使用设计模式的代码
package main
import "fmt"
type VirtualMachine struct {
Name string
Type string // OpenStack | VMware | Aliyun
}
func (vm VirtualMachine) create() {
if vm.Type == "OpenStack" {
fmt.Println("do some before create OpenStack VirtualMachine")
fmt.Println("call OpenStack API create VirtualMachine")
fmt.Println()
} else if vm.Type == "VMware" {
fmt.Println("do some before create VMware VirtualMachine")
fmt.Println("call VMware API create VirtualMachine")
fmt.Println()
} else if vm.Type == "Aliyun" {
fmt.Println("do some before create Aliyun VirtualMachine")
fmt.Println("call Aliyun API create VirtualMachine")
fmt.Println()
}
}
func main() {
var virtualMachineType = "OpenStack"
vm := VirtualMachine{Name: "An Aliyun VirtualMachine", Type: virtualMachineType}
vm.create()
}
虽然,这种方式创建不同的虚拟机不需要修改创建的源码,只需要修改新建结构体时 type 参数。
但是,这种方式还是有一些缺陷。
- create() 方法代码量太大,各种类型虚拟机创建的代码都集中在这个方法中,不利于测试和维护。
- 违反开闭原则,后面新加其它类型虚拟机 create() 方法代码还会继续增加和修改,灵活性和扩展性不足。
- 代码复用度低,系统中其它地方需要复用这些算法,则只能通过源代码的复制和粘贴实现,无法单独重用其中的某个或某些算法。
- 使用设计模式的代码
package main
import "fmt"
type VirtualMachineOperator interface {
create()
}
type OpenStackVirtualMachine struct {
// VirtualMachine 通用属性
vm VirtualMachine
// OpenStack VirtualMachine 个性化属性
OpenStackFiled string
}
func (vm OpenStackVirtualMachine) create() {
fmt.Println("do some before create OpenStack VirtualMachine")
fmt.Println("call OpenStack API create VirtualMachine")
fmt.Println()
}
type VMwareVirtualMachine struct {
vm VirtualMachine
VMwareFiled string
}
func (vm VMwareVirtualMachine) create() {
fmt.Println("do some before create VMware VirtualMachine")
fmt.Println("call VMware API create VirtualMachine")
fmt.Println()
}
type AliyunVirtualMachine struct {
vm VirtualMachine
AliyunFiled string
}
func (vm AliyunVirtualMachine) create() {
fmt.Println("do some before create Aliyun VirtualMachine")
fmt.Println("call Aliyun API create VirtualMachine")
fmt.Println()
}
type VirtualMachine struct {
Name string
Type string // OpenStack | VMware | Aliyun
VMOperator VirtualMachineOperator
}
func (vm VirtualMachine) create() {
fmt.Println("do some before create VirtualMachine")
vm.VMOperator.create()
}
func main() {
var virtualMachineType = "OpenStack"
vm := VirtualMachine{Name: "An OpenStack VirtualMachine", Type: virtualMachineType, VMOperator: OpenStackVirtualMachine{}}
vm.create()
}
定义独立的类来封装不同的策略算法,每个类封装一种具体的策略算法。
为了保证这些不同策略算使用时的一致性,通常会提供一个抽象的策略接口来做规则的定义,每种算法则对应一个具体的策略类。
- Context(环境类)
- VirtualMachine
- Strategy(抽象策略类)
- VirtualMachineOperator
- ConcreteStrategy(具体策略类)
- OpenStackVirtualMachine
- VMwareVirtualMachine
- AliyunVirtualMachine
- Client(客户端)
- main
- 实际微服务开发过程中,客户端会是一个 Service 。
这种方式给 VirtualMachine 定义了统一的操作接口 VirtualMachineOperator,每种类型 VirtualMachine 有自己的结构体和接口方法的实现。
每种类型 VirtualMachine 结构体可以定义自己特有的属性,实现自己的操作方法。
使用方客户端(环境类)只需要维护好 VirtualMachine Entity 和 VirtualMachine Operator 之间的引用关系,即可创建对应类型的虚拟机。
OpenStackVirtualMachine、VMwareVirtualMachine、AliyunVirtualMachine 代码可以分文件独立编写,这样阅读起来也更加简洁。
总结
策略模式的核心是将算法的定义与使用分开,也就是将算法的行为和环境分开。
将算法的定义放在专门的策略类中,每个策略类封装了一个种实现算法。
使用算法的环境类根据上下文使用对应的策略类,符合依赖倒转原则。
在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。