策略模式
问题引入
当我们有一堆鸭子,各个鸭子都有相同的游泳(swim),自我描述(display)方法,因为它们都会游泳和自我描述;但是不同的鸭子又有自己特有的飞行(fly),叫声(quack),这时如何使用设计模式来解决这样的问题呢?
首先会想到的方法是,有一个鸭子超类,swim和diplay作为公有的方法,不同鸭子的子类去覆盖实现各自的fly和quack方法,貌似就可以解决这个问题了
不过,每当新的鸭子类型出现,就需要重新去实现fly和quack方法;甚至如果多个鸭子类型拥有相同的fly或quack方法,代码就不能很好地复用了。
在这里,鸭子的行为在子类里不断地改变,并且让所有子类都拥有这些行为是不恰当的。继承并不能很好地解决问题
设计原则
设计模式中的一个设计原则是:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。
换句话说,如果每次新的需求一来,都会使某方面的代码发生变化,那么你就可以确定,这部分的代码需要被抽出来,和其他稳定地代码有所区分。
这个原则的另一个思考方式是,把会变的部分取出来并封装起来,以便以后可以轻易地改动或扩充此部分,而不影响不需要变化的其他部分。
这样的概念很简单,几乎是每个设计模式背后的精神所在。所有的模式都提供了一套方法让系统中的某部分改变不会影响其他部分。
重新设计
是时候把鸭子的行为抽取出来了
-
分开变化和不变化的部分
- 变化的部分:quack,fly
- 不变化的部分:swim display
因此,将quack和fly行为抽取出来:
type basicDuck interface {
quackBehaviour
flyBehaviour
display()
swim()
}
type duck struct {
quackBehaviour
flyBehaviour
name string
}
func (d *duck) display() {
fmt.Printf("I am %+v\n", d.name)
}
func (d *duck) swim() {
fmt.Printf("I am %+v, I can swim\n", d.name)
}
可以看到,quack和fly作为两个接口被抽离了出来,而display和swim仍然作为鸭子固定不变的部分
看下quack和fly的部分
type quackBehaviour interface {
quack()
}
type flyBehaviour interface {
fly()
}
我们利用接口代表每个行为,比方说quackBehaviour和flyBehaviour,而行为的每个实现都将实现其中的一个接口;实际的实现就不会被绑死在鸭子的子类中了,即具体的行为编写在实现了quackBehaviour和flyBehaviour的子类中;
这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子无关了;而我们可以新增一些行为,不会影响到既有的行为类,也不会影响到现有的鸭子子类。
现在实现了以下几个飞行行为类:
type flyWithWings struct {
}
func (f *flyWithWings) fly() {
fmt.Println("fly with wings!")
}
type cantFly struct {
}
func (f *cantFly) fly() {
fmt.Println("cant fly...")
}
type flyWithRocketPower struct {
}
func (f *flyWithRocketPower) fly() {
fmt.Println("fly with rocket power!")
}
以及以下几个呱呱叫行为类:
type quackWithGuaGua struct {
}
func (q *quackWithGuaGua) quack() {
fmt.Println("quack with guagua!")
}
type quackWithGuGu struct {
}
func (q *quackWithGuGu) quack() {
fmt.Println("quack with gugu!")
}
type quackWithEngine struct {
}
func (q *quackWithEngine) quack() {
fmt.Println("quack with engine!")
}
这时,是时候整合鸭子的行为了。例如我们有一个火箭动力鸭,该如何实现这个鸭子呢?
type rocketDuck struct {
*duck
}
func main() {
d := rocketDuck{
&duck{
&quackWithEngine{},
&flyWithRocketPower{},
"rocket duck",
},
}
doAction(d)
}
func doAction(d basicDuck) {
d.display()
d.swim()
d.quack()
d.fly()
}
运行之,看效果:
I am rocket duck
I am rocket duck, I can swim
quack with engine!
fly with rocket power!
更进一步
如果我们想动态改变鸭子的行为,而不是将其绑死在鸭子类中,应该怎么做呢?
我们给鸭子父类添加set行为的方法
func (d *duck) setQuackBehaviour(qb quackBehaviour) {
d.quackBehaviour = qb
}
func (d *duck) setFlyBehaviour(fb flyBehaviour) {
d.flyBehaviour = fb
}
我们的火箭鸭新增了音速火箭动力飞行!
type flyWithSonicRocketPower struct {
}
func (f *flyWithSonicRocketPower) fly() {
fmt.Println("fly with sonic rocket power!")
}
动态改变:
func main() {
d := rocketDuck{
&duck{
&quackWithEngine{},
&flyWithRocketPower{},
"rocket duck",
},
}
d.setFlyBehaviour(&flyWithSonicRocketPower{})
doAction(d)
}
func doAction(d basicDuck) {
d.display()
d.swim()
d.quack()
d.fly()
}
运行之:
I am rocket duck
I am rocket duck, I can swim
quack with engine!
fly with sonic rocket power!
tips:golang方法的值接收和指针接收,最大的区别在于指针接收可以改变改结构体的属性;其他区别不大
匿名字段,例如rocketDuck有匿名字段*duck,因此相当于火箭鸭拥有了duck的所有方法,即实现了basicDuck接口;但是由于golang没有继承的概念,不能认为火箭鸭继承了duck,是duck(is-a);golang是通过interface实现多态,因此basicDuck interface类型可以作为火箭鸭的接收者。而如果duck不是匿名字段,而是一个火箭鸭的具名字段,如 d duck,火箭鸭对象不能直接调用duck的所有方法,需要通过例如rocketDuck.d.display()来调用,此时也不能认为火箭鸭实现了basicDuck interface,basicDuck无法作为火箭鸭的接收者。
总结
这就是策略模式,即:定义了算法族,分别封装起来,让它们之前可以相互替换,此模式让算法的变化独立于使用算法的客户
参考文章
《head first设计模式》