策略模式
背景:需要一个鸭子类,包括绿头鸭,红头鸭,橡皮鸭等等,他们具有叫声,飞行,外貌长相等特性。
1. 先做一个不那么恰当的例子
//1.先做一个Duck class
public abstract class SimUDuck
{
void quack()
{
Console.WriteLine("鸭子都会嘎嘎叫");
}
void swim()
{
Console.WriteLine("鸭子都会游泳");
}
public abstract void display();//每个鸭子类型看起来都不一样,所以做成抽象方法
//这样写在父类里面,那么不能飞的鸭子也继承了飞的行为,如下写是不对的:
//void fly()
//{
// Console.WriteLine("新增的需求:让某些鸭子能飞");
//}
public abstract void fly();//让子类自己去判断能不能飞,但是这样代码量会变大,因为每一个继承它的子类都要复写一遍这个方法
}
class MallardDuck : SimUDuck
{
public override void display()
{
Console.WriteLine("看起来像绿头鸭");
}
public override void fly()
{
Console.WriteLine("绿头鸭会飞");
}
}
class RedHeadDuck : SimUDuck
{
public override void display()
{
Console.WriteLine("看起来像红头鸭");
}
public override void fly()
{
Console.WriteLine("红头鸭会飞");
}
}
class RubberDuck : SimUDuck
{
public override void display()
{
Console.WriteLine("看起来像橡皮鸭");
}
public override void fly()
{
Console.WriteLine("橡皮鸭会飞的方法写成空,表示橡皮鸭不能飞");//代码量会变大,因为每一个继承它的子类都要复写一遍这个方法,这样也是不行的
}
}
在上面的例子里面:
- 鸭子的叫声和游泳可能也是变化的,在父类中写定了是不行的
- 如果在父类中写定一个飞的实例方法,子类继承后,那么本来不应该会飞的子类也有了会飞的特性了。
- 如果父类方法全部做成抽象类,每一次子类的继承就要去重写一遍父类方法,就算有些鸭子不会飞,那也要重写为不会飞,这也是很低效的。
2. 用接口和算法簇实现
//1.先做一个Duck class
public abstract class SimUDuck
{
//声明接口的引用关联关系
public IFlyBehavior flyBehavior;
public IQuackBehavior quackBehavior;
public void performFly()//父类不具体实现飞的方法,委托给接口
{
flyBehavior.fly();
}
public void performQuack() //父类不具体实现叫的方法,委托给接口
{
quackBehavior.quack();
}
void swim()//所有父类和子类都一致的方法
{
Console.WriteLine("鸭子都会游泳");
}
public void setFlyBehavior(IFlyBehavior fb)//这两个方法用来动态实现具体的方法,而不是像原来一样写在构造函数里
{
flyBehavior = fb;
}
public void setQuackBehavior(IQuackBehavior qb)
{
quackBehavior = qb;
}
public abstract void display();//每个子类的都有的方法,但是每个子类的表现却不一样,所以做成抽象方法子类重写
}
class MallardDuck : SimUDuck
{
public MallardDuck()
{
base.quackBehavior = new Quack();//使用父类的字段
base.flyBehavior = new FlyWithWings();
}
public override void display()
{
Console.WriteLine("看起来像绿头鸭");
}
}
class RedHeadDuck : SimUDuck
{
public override void display()
{
Console.WriteLine("看起来像红头鸭");
}
}
class RubberDuck : SimUDuck
{
public override void display()
{
Console.WriteLine("看起来像橡皮鸭");
}
}
class DecoyDuck : SimUDuck
{
public override void display()
{
Console.WriteLine("看起来像诱饵鸭");
}
}
class FlyWithWings:IFlyBehavior
{
public void fly()
{
Console.WriteLine("有翅膀可以飞");
}
}
class FlyNoWay : IFlyBehavior
{
public void fly()
{
Console.WriteLine("没有翅膀不能飞");
}
}
class Quack: IQuackBehavior
{
public void quack()
{
Console.WriteLine("嘎嘎叫");
}
}
class Squeack: IQuackBehavior
{
public void quack()
{
Console.WriteLine("吱吱叫");
}
}
class MuteQuack: IQuackBehavior
{
public void quack()
{
Console.WriteLine("不会叫");
}
}
public interface IFlyBehavior//把变化的飞行行为做成接口
{
void fly();
}
public interface IQuackBehavior//把变化的叫声行为做成接口
{
void quack();
}
public static void main(String[] args)
{
SimUDuck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
}
- 实现写在具体的类里面
- 父类关联具体的接口引用,实现算法的具体类关联接口
- 策略模式
数据流向如下:
//MallarDuck的实现(在主函数中)
SimUDuck mallard = new MallardDuck();
mallard.performFly();
mallard.performQuack();
//1.去MallarDuck()的构造函数,构造函数里面把Mallarduck()实际用到的算法类关联给了父类的接口引用变量
public MallardDuck()
{
base.quackBehavior = new Quack();//使用父类的字段
base.flyBehavior = new FlyWithWings();
}
//2.子类继承的performFly()和performQuack()方法直接进入了父类的该方法,父类对该方法做了如下委托
public void performFly()//父类不具体实现飞的方法,委托给接口
{
flyBehavior.fly();
}
public void performQuack() //父类不具体实现叫的方法,委托给接口
{
quackBehavior.quack();
}
//3.因为先前在子类构造函数中已经关联了实际算法类,performFly()和performQuack()则会直接去到对应的算法类实例中:
class Quack: IQuackBehavior
{
public void quack()
{
Console.WriteLine("嘎嘎叫");
}
}
class FlyWithWings:IFlyBehavior
{
public void fly()
{
Console.WriteLine("有翅膀可以飞");
}
}
//4. 执行算法类中的算法实例
>>> 嘎嘎叫
>>> 有翅膀可以飞
3. 总结
- 在设计模式的时候,原则上把会变化的部分提取并封装,这样以后就可以只修改或者拓展这个部分,而不需要引起其他不必要变化的部分改变。
- 为同类行为提供一个固定的接口,针对接口编程,而不是针对具体的实例对象编程。
- 如上面的例子,使用算法的组合好处优于不断的继承。
- 学会看类图:
- - - |>,表示接口(实现implements)
;————|>,表示继承(拓展)
;——————>,表示存在(has-a)
- 在上面的例子中为什么倾向于使用
SimUDuck mallard = new MallardDuck()
而不是MallardDuck mallard = new MallardDuck()
,理由如下:
1. 多态性的体现:
使用父类的引用就可以利用多态性,在运行时可以把变量引用给任何父类的子类,使得代码的灵活性和拓展性更好,如下:
Duck myDuck;
myDuck = new MallardDuck();
myDuck = new RubberDuck();
2. 接口编程
当使用父类或者接口类型作为变量类型时,可以轻易地实现接口编程,这意味着代码将更关注鸭子的行为(公共方法和属性),而不是具体的实现,有助于减少对实现的依赖,提高代码的可维护性,如下:
public MallardDuck()
{
base.quackBehavior = new Quack();//使用父类的字段
base.flyBehavior = new FlyWithWings();
}
3. 依赖倒置原则
高层模块不应该依赖于低层模块,而应该依赖于抽象。这种情况下,Duck是一个抽象层次,而MallardDuck是一个具体的实现层次。通过依赖Duck而不是MallardDuck,可以更轻松的更改或者拓展实现而不改变其他使用Duck的对象的代码。
4. 在设计模式上来说,依赖这一类多态和抽象类型,可以更灵活。