C#

[设计模式笔记] No.1 策略模式(Strategy)

2018-01-17  本文已影响69人  SEVEN_PIG

策略模式——定义算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

算法族:一组行为,例如:下面案例中的鸭子的不同叫法,呱呱叫,吱吱叫等等,这些不同的叫法,就可看成是一组行为。

案例引导

需求(迭代一)

开发一套模拟鸭子的游戏:SimUDuck。游戏中会出现各种鸭子,一边游泳戏水,一边呱呱叫。此系统使用了标准的OO设计,设计了鸭子超类,并让各种鸭子集成此超类。

迭代二

鸭子会飞

方案一 父类实现

方案一问题

所有鸭子都会飞,比如橡皮鸭。对代码所做的局部修改,影响层面可不只是局部(会飞的橡皮鸭),当涉及到“维护”时,为了复用(reuse)目的而使用继承,结局并不完美。

方案二 继承,子类覆盖父类方法

但是这样又有问题了,每当有新的鸭子子类出现,就要被迫检查并且可能需要覆盖fly()和quark()。。。这就是无穷无尽的噩梦啊!

①代码在多个子类中重复。

②运行时的行为不容易改变。

③很难知道所有鸭子的全部行为。

④改变会牵一发动全身,造成其他鸭子不想要的改变。

明显,继承并不是答案。

方案三 接口

把fly()从超类中取出放进一个“Flyable接口”中,这么一来,只有会飞的鸭子才能实现此接口。同样的方式,也可以用来设计一个“Quackable接口”,因为不是所有的鸭子都会叫。

问题:这样一来,重复的代码就会变多,如果有100个Duck的子类,就都得要分别去实现接口中的方法。

把问题归零

现在我们知道,使用继承并不能很好地解决问题,因为鸭子的行为在子类里不断地改变,并且让所有的子类都有这些行为是不恰当的。Flyable和Quackable接口一开始似乎还挺不错,解决了问题(只有会飞的鸭子才实现Flyable),但是Java接口不具有实现代码,所以继承接口无法达到代码的复用。这意味着:无论何时,你需要修改某个行为,你必须得往下追踪并在每一个定义此行为的类中修改它,一不小心,可能会造成新的错误!

幸运的是,有一个设计原则,恰好适用于此状况。

设计原则一

找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

换一种思考方式:把会变化的部分取出并封装起来,以便以后可以轻易地改动或者扩充此部分,而不影响不需要变化的其他部分。这样,系统中的某部分改变不会影响其他部分,代码变化引起的不经意后果变少,系统变得更有弹性。

案例问题分析(分开变化和不会变化的部分)

就目前所知,除了fly()和quack()的问题外,Duck类还算一切正常,所以除了某些小改变之外,我们不打算对Duck类做太多处理。

为了分开”变化和不会变化的部分“,我们准备建立两组类(完全远离Duck类),一个是”fly“相关的,一个是”quack“相关的,每一组类将实现各自的动作。比方说,我们可能有一个类实现”呱呱叫“,另一个实现”吱吱叫“,还有一个类实现”安静“。

备注:每组行为,就称为一个算法族。

如何设计那组实现飞行和呱呱叫的行为的类呢?

我们希望一切能有弹性,毕竟正是因为一开始鸭子行为没有弹性,才让我们走上现在这条路。我们还想能”指定“行为到鸭子的实例,让鸭子的行为动态地改变。换句话说,我们应该在鸭子类中包含设定行为的方法,这样就可以在”运行时“动态地”改变“绿头鸭的飞行行为。

有了这些需求,接着看看第二个设计原则:

设计原则二

 针对接口编程,而不是针对实现编程。

解读:"针对接口编程"真正的意思是"针对超类型(supertype)编程"。

这里所谓的”接口“有多个含义,接口是一个”概念“,也是一种Java的interface构造。你可以在不涉及Java interface的情况下,"针对接口编程",关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际情况执行到真正的行为,不会被绑死在超类型的行为上。"针对超类型编程"这句话,可以更明确地说成——变量的声明类型应该是超类型,通常是一个抽象类或者是一个接口,如此,只要是具体实现此超类型的类所产生的的对象,都可以指定给这个变量。这就意味着,声明类时不用理会以后执行时的真正对象类型。

例子:假设有一个抽象类Animal,有两个具体的实现(Dog与Cat)继承Animal

我们利用接口代表每个行为,比方说,FlyBehavior与QuackBehavior,而行为的每个实现都将实现其中的一个接口。

所以这次鸭子类不会负责Flying与Quacking接口,反而是由我们制造一组其他类专门实现FlyBehavior与QuackBehavior,这就成为”行为“类,由行为类而不是Duck来实现行为接口。

在我们新的设计中,鸭子的子类将使用接口(FlyBehavior与QuackBehavior)所表示的行为,所以实际的”实现“不会被绑死在鸭子的子类中,换句话说,特定的具体行为编写在实现了FlyBehavior与QuackBehavior的类中。

从现在开始,鸭子的行为将被放在分开的类中,此类专门提供某行为接口的实现。这样,鸭子类就不需要知道行为的实现细节。

实现鸭子的行为  

这样的设计,可以让飞行和呱呱叫的动作被其他的对象复用,因为这些行为已经与鸭子类无关了,而我们可以新增一些行为,不会影响到既有的行为类,也不会影响”使用“到飞行行为的鸭子类。

这样一来,有了继承的”复用“好处,却没有继承所带来的的包袱。

终极解决方案

①首先,在Duck类中“加入两个变量”,分别是“FlyBehavior”与“QuackBehavior”,声明为接口类型(而不是具体的实现类型),每个鸭子对象都会动态地设置这些变量以在运行时引用正确的行为类型。还有,用两个相似的方法performFly()与performQuack()取代Duck类中的fly()与quack()。

②为了能够在鸭子的子类中通过“设定方法(setter method)”来设定鸭子的行为,而不是在鸭子的构造器内实例化。可以在Duck类中,加入两个新的方法:

③接着实现performQuack()和performfly()

④现在来关心“如何设定mFlyBehavior”,看看 绿头鸭 MallardDuck类

⑤测试,在运行时想改变鸭子的行为,只需要调用鸭子的setter方法就可以了。

问题:我们说过,将不对具体实现编程,但是第④步中,我们在构造器里确实加入了具体的实现。这个问题后面学习更多的设计模式之后,会得到解决的。虽然不是很完美,但是目前的做法还是很有弹性的,只是初始化实例变量的做法不够弹性罢了。

组合的威力

“有一个”可能比“是一个”更好。
“有一个”关系:每一只鸭子都有一个FlyBehavior和一个QuackBehavior,好将飞行和呱呱叫行为委托给它们代为处理。

当你将两个类结合起来使用,就如同本例一般,这就是组合(composition)。这种做法和“继承”不同的地方在于,鸭子的行为不是继承而来的,而是和适当的行为对象“组合”来的。

这是一个很重要的技巧——第三个设计原则

设计原则三

        多用组合,少用继承

如你所见,使用组合建立系统具有很大的弹性,不仅可将算法族封装成类,更可以“在运行时动态地改变行为”,只要组合的行为对象符合正确的接口标准即可。

好了,在你辛苦地看了2500+个字后,你终于和策略模式进行了亲密交流,你也对它有了基本的认识。以后在实践中,多加训练,方能加强。

策略模式 定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

最后回顾一下,我们的设计工具箱中的工具

1、OO基础
①抽象
②封装
③多态
④继承

2、OO原则
①封装变化
②多用组合,少用继承
③针对接口编程,不针对实现编程

3、OO模式
①策略模式——定义算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

感谢阅读!

前言 为何要使用设计模式

上一篇下一篇

猜你喜欢

热点阅读