设计模式之策略模式详解
策略模式
在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如,出行旅游可以乘坐飞机、乘坐火车或自己开私家车等,超市促销可以釆用打折、送商品、送积分等方法。
在软件开发中也常常遇到类似的情况,当实现某一个功能存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等。
如果使用多重条件转移语句实现(即硬编码),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决这些问题。
1.策略模式的定义与特点
策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
简述:分别封装行为接口,实现算法族,超类里放行为接口对象,在子类里具体设定行为对象。
策略模式的主要优点如下。
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
其主要缺点如下。
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类。
2.鸭子问题
模拟鸭子项目,具体要求如下:
1)有各种鸭子(比如 绿头鸭、红头鸭,鸭子有各种行为,比如叫、游泳)
2)显示鸭子的信息
传统方案解决鸭子问题的分析和代码实现
1)传统的设计方案
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LReUNUvw-1608016555763)(C:\Users\dell\AppData\Roaming\Typora\typora-user-images\image-20201214135952353.png)]
2)代码实现
.java文件说明:
Duck.java:基类鸭子
GreenHeadDuck.java:绿头鸭
RedHeadDuck.java:红头鸭
SimulateDuck.java:模拟鸭子(main)
Duck.java
//抽象类:鸭子
public abstract class Duck {
public Duck(){
//子类的构造函数中可以定义行为
}
//在本抽象类中已经实现了
public void quack(){
System.out.println("~~嘎嘎叫~~");
}
//由子类实现
public abstract void display();
//在本抽象类中自己已经实现了
public void swim(){
System.out.println("~~我会游泳~~");
}
}
RedHeadDuck.java:
public class RedHeadDuck extends Duck{
@Override
public void display() {
System.out.println("我是独一无二的,我的头是红色的");
}}
GreenHeadDuck.java:
public class GreenHeadDuck extends Duck {
@Override
public void display() {
System.out.println("我和你们不一样,我的头是绿色的");
}
}
SimulateDuck.java:
GreenHeadDuck greenHeadDuck=new GreenHeadDuck();
RedHeadDuck redHeadDuck=new RedHeadDuck();
greenHeadDuck.display();
greenHeadDuck.quack();
greenHeadDuck.swim();
redHeadDuck.display();
redHeadDuck.quack();
redHeadDuck.swim();
我们已经实现了基本的项目需求了。模拟鸭子算是成功啦~!
但是项目添加了新的需求,添加会飞的鸭子(并不是所有的鸭子都会飞)
传统的方式实现的问题分析和解决方案
如果在基类写Fly()方法,其他鸭子,都继承了Duck类,所以fly让所有子类都会飞了,违背逻辑。
这个问题,是继承带来的问题,对类的局部改动,尤其超类的局部改动,会影响其他部分,会有溢出效应
为了改进问题,我们可以通过覆盖fly方法来解决,
问题又来了,如果我们有一个玩具鸭子,这样需要玩具鸭子去覆盖Duck的所有实现的方法 ,覆盖工作量特别大
解决方法:策略模式(strategy pattern)思路:继承是实现共性,减少代码的重复。接口是实现特性。
3.策略模式解决鸭子问题
策略模式解决鸭子问题的分析
1)设计方案
需要新的设计方式,应对项目的扩展性,降低复杂度:
a. 分析项目变化与不变部分,提取变化部分,抽象成接口+实现;(抽象是共性,接口是特性)
b. 鸭子那些功能是会根据新需求变化的?叫声、飞行
接口:
public interface FlyBehavior{
void fly();
}
public interface QuackBehavior{
void quack();
}
c. 好处:新增行为简单,行为类更好的复用,组合方便。既有继承带来的复用好处,没有挖坑
2)代码实现
.java文件说明:
Duck.java:基类鸭子
GreenHeadDuck.java:绿头鸭
RedHeadDuck.java:红头鸭
SimulateDuck.java:模拟鸭子(main)
FlyBehavior.java:(接口)特有的飞行行为
QuackBehavior.java:(接口)特有的叫喊行为
BadFlyBehavior.java:飞行行为的实现类
BadQuackBehavio.java:叫喊行为的实现类
Duck.java:
//抽象类:鸭子
public abstract class Duck {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
public Duck(){
//子类的构造函数中可以定义行为
}
//在本抽象类中已经实现了
public void quack(){
//System.out.println("~~嘎嘎叫~~");
quackBehavior.quack();
}
//由子类实现
public abstract void display();
//在本抽象类中自己已经实现了
public void swim(){
System.out.println("~~我会游泳~~");
}
//实例化对象时可以动态的改变对象的行为(比继承灵活性强)
public void SetFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
//实例化对象时可以动态的改变对象的行为
public void SetQuackBehavior(QuackBehavior qb) {
quackBehavior = qb;
}
public void fly(){
//System.out.println("飞");
flyBehavior.fly();
}
}
GreenHeadDuck.java:
public class GreenHeadDuck extends Duck {
public GreenHeadDuck(){
//行为轴展示具体的行为
flyBehavior = new BadFlyBehavior();
}
@Override
public void display() {
System.out.println("我和你们不一样,我的头是绿色的");
}
//覆盖超类
// public void fly(){
// System.out.println("我不会飞");
// }
}
RedHeadDuck.java:
public class RedHeadDuck extends Duck {
public RedHeadDuck(){
quackBehavior = new BadQuackBehavior();
}
@Override
public void display() {
System.out.println("我是独一无二的,我的头是红色的");
}}
SimulateDuck.java:
/**
* 主类:模拟鸭子
*/
public class SimulateDuck {
public static void main(String[] args) {
//父类为Duck,屏蔽了超类的差别性
Duck greenHeadDuck = new GreenHeadDuck();
Duck redHeadDuck=new RedHeadDuck();
// GreenHeadDuck greenHeadDuck = new GreenHeadDuck();
// RedHeadDuck redHeadDuck = new RedHeadDuck();
greenHeadDuck.display();
greenHeadDuck.fly();
greenHeadDuck.SetQuackBehavior(new QuackBehavior() {
@Override
public void quack() {
System.out.println("我会叫");
}
});
greenHeadDuck.swim();
redHeadDuck.display();
redHeadDuck.quack();
redHeadDuck.swim();
redHeadDuck.SetFlyBehavior(new FlyBehavior() {
@Override
public void fly() {
System.out.println("我会飞");
}
});
}
}
FlyBehavior.java:
public interface FlyBehavior {
void fly();
}
QuackBehavior.java:
public interface QuackBehavior {
void quack();
}
BadFlyBehavior.java:
public class BadFlyBehavior implements FlyBehavior{
@Override
public void fly() {
System.out.println("我不会飞");
}
}
BadQuackBehavio.java:
public class BadQuackBehavior implements QuackBehavior{
@Override
public void quack() {
System.out.println("我不会叫");
}
}
总结:运用设计模式中的策略模式,把变化的部分提取出来变为接口+实现。
Duck类中的SetQuackBehavoir()方法,灵活的让实例化对象灵活的改变对象的行为。比如,绿头鸭,使用了SetQuackBehavoir()方法,定制了自己的quck()方法。因为不是每只鸭都能叫的。叫的是当前鸭的特性。
4.设计原则
策略模式体现了几个设计原则
封装变化:把变化的代码从不变的代码中分离出来(找出应用中可能需要变化之处,把它们独立出来,不要和哪些不需要变化的代码混在一起。)
针对接口编程而不是具体类(定义了策略接口)
多用组合/聚合,少用继承(客户通过组合方式使用策略)
5.价格计算问题
以一个价格计算策略为背景
没有用策略模式
我们一般是下面的写法,直接写一个类,在类里面直接写策略算法(功能实现)
public class NoStrategy {
/**
* 传入客服等级类型获取相应的价格
* @param type 会员类型(等级)
* @param price 响应的价格
* @return
*/
public double getPrice(String type, double price) {
if ("普通客户小批量".equals(type)) {
System.out.println("[未采用设计模式] 不打折,原价");
return price;
} else if ("普通客户大批量".equals(type)) {
System.out.println("[未采用设计模式] 打九折");
return price * 0.9;
} else if ("老客户小批量".equals(type)) {
System.out.println("[未采用设计模式] 打八折");
return price * 0.8;
} else if ("老客户大批量".equals(type)) {
System.out.println("[未采用设计模式] 打七折");
return price * 0.7;
//拓展一种策略
// }else if("老客户特大批量".equals(type)){
// System.out.println("[未采用设计模式] 打六折");
// return price*0.6;
}
//乱传的也是当普通客户小批量(就是不打折)
return price;
}
}
- NoStrategy:没有策略的做法
- 实现起来比较容易,符合一般开发人员的思路
- 假如,类型特别多,算法比较复杂时,整个条件语句的代码就变得很长,难于维护。
- 如果有新增类型,就需要频繁的修改此处的代码!
- 不符合开闭原则!—对这个类的修改要关闭,就是这个类要是写好了就不要去改他了,对类的功能的拓展要开放,显然只有面向接口编程才满足,
- 所以应用策略模式Strategy这个接口
6.策略模式解决价格计算问题
1.写一个策略接口Strategy
- Strategy:策略接口
- 这个是对类NoStrategy改成面向接口的方式实现策略,不像NoStrategy一样,直接写死策略的实现,而是使用这个接口先定义策略
public interface Strategy {
/**
* 通过策略获取价格
* @param standardPrice
* @return
*/
double getPrice(double standardPrice);
}
2.面向接口,组合编程,少用继承(继承虽然可以复用代码,但是会造成耦合度增加,解决方式往往采用接口做类的属性),如下,这样所有实现Strategy 的各种策略实现类都”组合”到这个类里面了
- Context:策略模式上下文—策略接收器,专门接收策略实现的算法
- 负责和具体的策略类交互
- 这样的话,具体的算法和直接的客户端调用分离了,使得算法可以独立于客户端独立的变化。
- 如果使用spring的依赖注入功能,还可以通过配置文件,动态的注入不同策略对象,动态的切换不同的算法.
public class Context {
/**
* 当前采用的算法对象
* 面向接口,组合编程,少用继承
* 简言之复杂类型(类,接口等)做属性
*/
private Strategy strategy;
public Context(Strategy strategy) {
this.strategy = strategy;
}
public double getReultPrice(double price){
return this.strategy.getPrice(price);
}
}
3:既然是策略模式接口Strategy都明确了要做的事情是根据会员情况,返回价格,但是没有真正的实现,那么总有类来实现赛
策略实现类1 VIP0Strategy
/**
* VIP0Strategy:普通会员策略
*/
public class VIP0Strategy implements Strategy {
/**
* 输入一个价格,经过VIP0Strategy策略计算价格
* @param standardPrice
* @return
*/
@Override
public double getPrice(double standardPrice) {
System.out.println("[策略模式]普通会员 原价:"+standardPrice);
return standardPrice;
}
}
策略实现类2 VIP1Strategy
/**
* VIP1Strategy: 一级会员策略
*/
public class VIP1Strategy implements Strategy {
/**
* 输入一个价格,经过VIP1Strategy策略计算价格
* @param standardPrice
* @return
*/
@Override
public double getPrice(double standardPrice) {
System.out.println("[策略模式]一级会员 打九折:"+standardPrice * 0.9);
return standardPrice * 0.9;
}
}
策略实现类3 VIP2Strategy
/**
* VIP2Strategy:二级会员策略
*/
public class VIP2Strategy implements Strategy {
/**
* 输入一个价格,经过VIP2Strategy策略计算价格
* @param standardPrice
* @return
*/
@Override
public double getPrice(double standardPrice) {
System.out.println("[策略模式]二级会员八折:"+standardPrice*0.8);
return standardPrice*0.8;
}
}
策略实现类4 VIP3Strategy(新增加的需求)
public class VIP3Strategy implements Strategy {
@Override
public double getPrice(double standardPrice) {
System.out.println("[策略模式]老客户特大批量:"+standardPrice*0.6);
return standardPrice*0.6;
}
}
4.客户端:
- Client:策略模式客户端—Client 的main方法 可以想象成我们在使用别人写好的框架,我们有新的需求,对框架开发者来说就是需要对已有的
- 代码进行维护升级,比如此时我们修改NoStrategy类,那么修改完后新版本的框架NoStrategy类很有能是对已经在使用的客户机制上不兼容的,如果用户升级为新版框架,遇到使用NoStrategy类的会报错,各种不兼容就不符合开发者维护的版本的规范,所以修改已有的类是极其不科学的
import com.cx.price.NoStrategy;
import com.cx.price.VIP1Strategy;
public class Client {
public static void main(String[] args) {
System.out.println("未使用模式-----------------------------------------");
NoStrategy noStrategy = new NoStrategy();
double price = noStrategy.getPrice("普通客户大批量", 1000);
System.out.println(price);
System.out.println("\n测试策略------------------------------------------");
Context context0 = new Context(new VIP1Strategy());
double resultPrice = context0.getReultPrice(1000);
System.out.println(resultPrice);
//怎么体现策略模式呢?比如现在需求是增加一种会员机制, '老客户特大批量' ,那么显然打折力度更大,我们设置为6折,
// 分别在未使用策略模式和使用了策略模式的基础上拓展,看那个更加易于拓展,方便维护
//为了实现这么一个折扣计算功能,代码需要写4个if-else,如果需求再增多一个规则,代码还需重构if-else,这样在可维护性、可读性大大降低,而且修改容易出bug。
//如果运用策略模式,每个规则对应一个策略,根据符合的条件对应选择哪一种策略,这样整体代码逻辑清晰,而且不管新增或修改规则时,只需要新增或调整对应的规则策略,这样大大降低bug的风险,可维护性更高。
// //新增策略后未使用模式(会修该策略核心类)
// NoStrategy noStrategy1 = new NoStrategy();
// double price1 = noStrategy1.getPrice("老客户特大批量", 1000);
// System.out.println(price1);
//
//
// //新增策略后使用模式(不会修改策略接口,只是添加一个实现)
// Context context2 = new Context(new VIP3Strategy());
// double price2 = context2.getReultPrice(1000);
// System.out.println(price2);
}
}
结论: 修改服务器端已经写好了的类是极其不好的维护形式,因为这个类NoStrategy可能在别的类中作为依赖或者叫做别的类引用了该类,在不明确的情况下,可能牵一发动全身,是不好的维护方式,使用了策略模式,我们只是添加了一个策略接口的实现,低侵入式,不会对已有代码造成影响,低耦合
7.策略模式原理类图
说明:从上面这个图可以看出来客户context有成员变量strategy或者其他的策略接口,至于需要使用到哪个策略,我们可以在构造器中指定
策略模式的主要角色如下:
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
8.策略模式的总结
1)策略模式的关键是 :分析项目中变化部分与不变部分
2)策略模式的核心思想是 :多用组合/聚合,少用继承;用行为类组合,而不是行为的继承。更有弹性。
3)体现了“开闭原则”(对修改关闭,对扩展开放)。客户端增加行为不用修改原有代码,只要添加一种策略(或者行为)即可,避免了使用多重转移语句(if…else if … else);
4)提供了可以替换继承关心的办法 :策略模式将算法封装在独立的Strategy类中使得你可以独立于其Context改变它,使它易于切换、易于理解、易于扩展。
5)需要注意的是 :每添加一个策略就要增加一个类,当策略过多是会导致类数目庞大。
原文地址: https://blog.csdn.net/qq_43990485/article/details/111217043
本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
相关文章