设计模式学习之策略模式

Posted 伯努力不努力

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了设计模式学习之策略模式相关的知识,希望对你有一定的参考价值。

写代码时总会出很多的if…else,或者case。如果在一个条件语句中又包含了多个条件语句就会使得代码变得臃肿,维护的成本也会加大,而策略模式就能较好的解决这个问题,本篇博客就带你详细了解策略模式。
策略模式的定义和使用场景

**定义:**策略模式定义了一系列的算法,并将每一个算法封装起来,而且使他们可以相互替换,让算法独立于使用它的客户而独立变化。

分析下定义,策略模式定义和封装了一系列的算法,它们是可以相互替换的,也就是说它们具有共性,而它们的共性就体现在策略接口的行为上,另外为了达到最后一句话的目的,也就是说让算法独立于使用它的客户而独立变化,我们需要让客户端依赖于策略接口。

策略模式的使用场景:

1.针对同一类型问题的多种处理方式,仅仅是具体行为有差别时;
2.需要安全地封装多种同一类型的操作时;
3.出现同一抽象类有多个子类,而又需要使用 if-else 或者 switch-case 来选择具体子类时。

UML类图

这个模式涉及到三个角色:

环境(Context)角色:持有一个Strategy的引用。

抽象策略(Strategy)角色:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。

具体策略(ConcreteStrategy)角色:包装了相关的算法或行为。

策略模式的典型代码如下:

抽象策略类

public interface Strategy 
    /**
     * 策略方法
     */
    public void strategyInterface();

具体策略类

public class ConcreteStrategyA implements Strategy 

    @Override
    public void strategyInterface() 
        //相关的业务
    


public class ConcreteStrategyB implements Strategy 

    @Override
    public void strategyInterface() 
        //相关的业务
    


环境角色类

public class Context 
    //持有一个具体策略的对象
    private Strategy strategy;
    /**
     * 构造函数,传入一个具体策略对象
     * @param strategy    具体策略对象
     */
    public Context(Strategy strategy)
        this.strategy = strategy;
    
    /**
     * 策略方法
     */
    public void contextInterface()
        
        strategy.strategyInterface();
    
    

策略模式例子

假设鹅厂推出了3种会员,分别为会员,超级会员以及金牌会员,还有就是普通玩家,针对不同类别的玩家,购买《王者农药》皮肤有不同的打折方式,并且一个顾客每消费10000就增加一个级别,那么我们就可以使用策略模式,因为策略模式描述的就是算法的不同,这里我们举例就采用最简单的,以上四种玩家分别采用原价(普通玩家),九折,八折和七价的收钱方式。

那么我们首先要有一个计算价格的策略接口

public interface CalPrice 
    //根据原价返回一个最终的价格
    Double calPrice(Double orgnicPrice);

下面是4种玩家的计算方式的实现

public class Orgnic implements CalPrice 

    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice;
    

public class Vip implements CalPrice 
    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice * 0.9;
    


public class SuperVip implements CalPrice 
    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice * 0.8;
    


public class GoldVip implements CalPrice 
    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice * 0.7;
    


我们看客户类,我们需要客户类帮我们完成玩家升级的功能。

public class Player 
    private Double totalAmount = 0D;//客户在鹅厂消费的总额
    private Double amount = 0D;//客户单次消费金额
    private CalPrice calPrice = new Orgnic();//每个客户都有一个计算价格的策略,初始都是普通计算,即原价

    //客户购买皮肤,就会增加它的总额
    public void buy(Double amount) 
        this.amount = amount;
        totalAmount += amount;
        if (totalAmount > 30000) //30000则改为金牌会员计算方式
            calPrice = new GoldVip();
         else if (totalAmount > 20000) //类似
            calPrice = new SuperVip();
         else if (totalAmount > 10000) //类似
            calPrice = new Vip();
        
    

    //计算客户最终要付的钱
    public Double calLastAmount() 
        return calPrice.calPrice(amount);
    

接下来是客户端调用,系统会帮我们自动调整收费策略。


public class Client 
    public static void main(String[] args) 
        Player player = new Player();
        player.buy(5000D);
        System.out.println("玩家需要付钱:" + player.calLastAmount());
        player.buy(12000D);
        System.out.println("玩家需要付钱:" + player.calLastAmount());
        player.buy(12000D);
        System.out.println("玩家需要付钱:" + player.calLastAmount());
        player.buy(12000D);
        System.out.println("玩家需要付钱:" + player.calLastAmount());
    


运行以后会发现,第一次是原价,第二次是九折,第三次是八折,最后一次则是七价。这样设计的好处是,客户不再依赖于具体的收费策略,依赖于抽象永远是正确的。

在上面的基础上,我们可以使用简单工厂来稍微进行优化

public class CalPriceFactory 
    private CalPriceFactory()
    //根据客户的总金额产生相应的策略
    public static CalPrice createCalPrice(Player customer)
        if (customer.getTotalAmount() > 30000) //3000则改为金牌会员计算方式
            return new GoldVip();
        else if (customer.getTotalAmount() > 20000) //类似
            return new SuperVip();
        else if (customer.getTotalAmount() > 10000) //类似
            return new Vip();
        else 
            return new Orgnic();
        
    

这样就将制定策略的功能从客户类分离了出来,我们的客户类可以变成这样。

public class Player 
    private Double totalAmount = 0D;//客户在鹅厂消费的总额
    private Double amount = 0D;//客户单次消费金额
    private CalPrice calPrice = new Orgnic();//每个客户都有一个计算价格的策略,初始都是普通计算,即原价

    //客户购买皮肤,就会增加它的总额
    public void buy(Double amount) 
        this.amount = amount;
        totalAmount += amount;
        /* 变化点,我们将策略的制定转移给了策略工厂,将这部分责任分离出去 */
        calPrice = CalPriceFactory.createCalPrice(this);
    

    //计算客户最终要付的钱
    public Double calLastAmount() 
        return calPrice.calPrice(amount);
    

    public Double getTotalAmount() 
        return totalAmount;
    

虽然结合简单工厂模式,我们的策略模式灵活了一些,但不免发现在工厂中多了if-else判断,也就是如果增加一个会员类别,我又得增加一个else-if语句,这是简单工厂的缺点,对修改开放。

那有什么方法,可以较好的解决这个问题呢?那就是使用注解, 所以我们需要给注解加入属性上限和下限,用来表示策略生效的区间,用来解决总金额判断的问题。

1.首先我们做一个注解,这个注解是用来给策略添加的,当中可以设置它的上下限

//这是有效价格区间注解,可以给策略添加有效区间的设置
@Target(ElementType.TYPE)//表示只能给类添加该注解
@Retention(RetentionPolicy.RUNTIME)//这个必须要将注解保留在运行时
public @interface PriceRegion 
    int max() default Integer.MAX_VALUE;
    int min() default Integer.MIN_VALUE;


可以看到,我们只是使用这个注解来声明每一个策略的生效区间,于是对策略进行修改

@PriceRegion(max = 10000)
public class Orgnic implements CalPrice 

    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice;
    


@PriceRegion(max=20000)
public class Vip implements CalPrice 
    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice * 0.9;
    

@PriceRegion(min=20000,max=30000)
public class SuperVip implements CalPrice 
    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice * 0.8;
    


@PriceRegion(min=3000)
public class GoldVip implements CalPrice 
    @Override
    public Double calPrice(Double orgnicPrice) 
        return orgnicPrice * 0.7;
    

接下来就是在策略工厂中去处理注解

public class CalPriceFactory 
    private static final String CAL_PRICE_PACKAGE = "com.example.stragedemo";//这里是一个常量,表示我们扫描策略的包

    private ClassLoader classLoader = getClass().getClassLoader();

    private List<Class<? extends CalPrice>> calPriceList;//策略列表

    //根据玩家的总金额产生相应的策略
    public CalPrice createCalPrice(Player player) 
        //在策略列表查找策略
        for (Class<? extends CalPrice> clazz : calPriceList) 
            PriceRegion validRegion = handleAnnotation(clazz);//获取该策略的注解
            //判断金额是否在注解的区间
            if (player.getTotalAmount() > validRegion.min() && player.getTotalAmount() < validRegion.max()) 
                try 
                    //是的话我们返回一个当前策略的实例
                    return clazz.newInstance();
                 catch (Exception e) 
                    throw new RuntimeException("策略获得失败");
                
            
        
        throw new RuntimeException("策略获得失败");
    

    //处理注解,我们传入一个策略类,返回它的注解
    private PriceRegion handleAnnotation(Class<? extends CalPrice> clazz) 
        Annotation[] annotations = clazz.getDeclaredAnnotations();
        if (annotations == null || annotations.length == 0) 
            return null;
        
        for (int i = 0; i < annotations.length; i++) 
            if (annotations[i] instanceof PriceRegion) 
                return (PriceRegion) annotations[i];
            
        
        return null;
    

    //单例
    private CalPriceFactory() 
        init();
    

    //在工厂初始化时要初始化策略列表
    private void init() 
        calPriceList = new ArrayList<Class<? extends CalPrice>>();
        File[] resources = getResources();//获取到包下所有的class文件
        Class<CalPrice> calPriceClazz = null;
        try 
            calPriceClazz = (Class<CalPrice>) classLoader.loadClass(CalPrice.class.getName());//使用相同的加载器加载策略接口
         catch (ClassNotFoundException e1) 
            throw new RuntimeException("未找到策略接口");
        
        for (int i = 0; i < resources.length; i++) 
            try 
                //载入包下的类
                Class<?> clazz = classLoader.loadClass(CAL_PRICE_PACKAGE + "." + resources[i].getName().replace(".class", ""));
                //判断是否是CalPrice的实现类并且不是CalPrice它本身,满足的话加入到策略列表
                if (CalPrice.class.isAssignableFrom(clazz) && clazz != calPriceClazz) 
                    calPriceList.add((Class<? extends CalPrice>) clazz);
                
             catch (ClassNotFoundException e) 
                e.printStackTrace();
            
        
    

    //获取扫描的包下面所有的class文件
    private File[] getResources() 
        try 
            File file = new File(classLoader.getResource(CAL_PRICE_PACKAGE.replace(".", "/")).toURI());
            return file.listFiles(new FileFilter() 
                public boolean accept(File pathname) 
                    if (pathname.getName().endsWith(".class")) //我们只扫描class文件
                        return true;
                    
                    return false;
                
            );
         catch (URISyntaxException e) 
            throw new RuntimeException("未找到策略资源");
        
    

    public static CalPriceFactory getInstance() 
        return CalPriceFactoryInstance.instance;
    

    private static class CalPriceFactoryInstance 

        private static CalPriceFactory instance = new CalPriceFactory();
    


虽然工厂里的逻辑增加了,但是解耦的效果达到了,现在我们随便加入一个策略,并设置好它的生效区间,策略工厂就可以帮我们自动找到适应的策略。

android源码中的策略模式

Android的源码中,策略模式最典型的就是属性动画中的应用.
我们使用属性动画的时候,可以通过set方法对插值器进行设置.可以看到内部维持了一个时间插值器的引用,并设置了getter和setter方法,默认情况下是先加速后减速的插值器,set方法如果传入的是null,则是线性插值器。而时间插值器TimeInterpolator是个接口,有一个接口继承了该接口,就是Interpolator这个接口,其作用是为了保持兼容

public interface TimeInterpolator 

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);


平时我们使用的时候,通过设置不同的插值器,实现不同的动画速率变换效果,比如线性变换,回弹,自由落体等等。这些都是插值器接口的具体实现,也就是具体的插值器策略。

@HasNativeInterpolator
public class AccelerateDecelerateInterpolator extends BaseInterpolator
        implements NativeInterpolatorFactory 
    public AccelerateDecelerateInterpolator() 
    

    @SuppressWarnings("UnusedDeclaration")
    public AccelerateDecelerateInterpolator(Context context, AttributeSet attrs) 
    

    public float getInterpolation(float input) 
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
    

    /** @hide */
    @Override
    public long createNativeInterpolator() 
        return NativeInterpolatorFactoryHelper.createAccelerateDecelerateInterpolator();
    


内部使用的时候直接调用getInterpolation方法就可以返回对应的值了,也就是属性值改变的百分比。

属性动画中另外一个应用策略模式的地方就是估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值。该属性和插值器是类似的,有几个默认的实现。其中TypeEvaluator是一个接口。

public interface TypeEvaluator<T> 

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * <code>fraction</code> representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: <code>result = x0 + t * (x1 - x0)</code>,
     * where <code>x0</code> is <code>startValue</code>, <code>x1</code> is <code>endValue</code>,
     * and <code>t</code> is <code>fraction</code>.
     *
     * @param fraction   The fraction from the starting to the ending values
     * @param startValue The start value.
     * @param endValue   The end value.
     * @return A linear interpolation between the start and end values, given the
     *         <code>fraction</code> parameter.
     */
    public T evaluate(float fraction, T startValue, T endValue);


TypeEvaluator的实现

public class ArgbEvaluator implements TypeEvaluator 
    private static final ArgbEvaluator sInstance = new ArgbEvaluator();

    /**
     * Returns an instance of <code>ArgbEvaluator</code> that may be used in
     * @link ValueAnimator#setEvaluator(TypeEvaluator). The same instance may
     * be used in multiple <code>Animator</code>s because it holds no state.
     * @return An instance of <code>ArgbEvalutor</code>.
     *
     * @hide
     */
    public static ArgbEvaluator getInstance() 
        return sInstance;
    

    /**
     * This function returns the calculated in-between value for a color
     * given integers that represent the start and end values in the four
     * bytes of the 32-bit int. Each channel is separately linearly interpolated
     * and the resulting calculated values are recombined into the return value.
     *
     * @param fraction The fraction from the starting to the ending values
     * @param startValue A 32-bit int value representing colors in the
     * separate bytes of the parameter
     * @param endValue A 32-bit int value representing colors in the
     * separate bytes of the parameter
     * @return A value that is calculated to be the linearly interpolated
     * result, derived by separating the start and end values into separate
     * color channels and interpolating each one separately, recombining the
     * resulting values in the same way.
     */
    public Object evaluate(float fraction, Object startValue, Object endValue) 
        int startInt = (Integer) startValue;
        int startA = (startInt >> 24) & 0xff;
        int startR = (startInt >> 16) & 0xff;
        int startG = (startInt >> 8) & 0xff;
        int startB = startInt & 0xff;

        int endInt = (Integer) endValue;
        int endA = (endInt >> 24) & 0xff;
        int endR = (endInt >> 16) & 0xff;
        int endG = (endInt >> 8) & 0xff;
        int endB = endInt & 0xff;

        return (int)((startA + (int)(fraction * (endA - startA))) << 24) |
                (int)((startR + (int)(fraction * (endR - startR))) << 16) |
                (int)((startG + (int)(fraction * (endG - startG))) << 8) |
                (int)((startB + (int)(fraction * (endB - startB))));
    

在Volley框架中,同样使用到了策略模式,其中定义了一个缓存接口

/**
 * An interface for a cache keyed by a String with a byte array as data.
 */
public interface Cache 
    /**
     * Retrieves an entry from the cache.
     * @param key Cache key
     * @return An @link Entry or null in the event of a cache miss
     */
    public Entry get(String key);

    /**
     * Adds or replaces an entry to the cache.
     * @param key Cache key
     * @param entry Data to store and metadata for cache coherency, TTL, etc.
     */
    public void put(String key, Entry entry);

    /**
     * Performs any potentially long-running actions needed to initialize the cache;
     * will be called from a worker thread.
     */
    public void initialize();

    /**
     * Invalidates an entry in the cache.
     * @param key Cache key
     * @param fullExpire True to fully expire the entry, false to soft expire
     */
    public void invalidate(String key, boolean fullExpire);

    /**
     * Removes an entry from the cache.
     * @param key Cache key
     */
    public void remove(String key);

    /**
     * Empties the cache.
     */
    public void clear();

    /**
     * Data and metadata for an entry returned by the cache.
     */
    public static class Entry 
        /** The data returned from cache. */
        public byte[] data;

        /** ETag for cache coherency. */
        public String etag;

        /** Date of this response as reported by the server. */
        public long serverDate;

        /** The last modified date for the requested object. */
        public long lastModified;

        /** TTL for this record. */
        public long ttl;

        /** Soft TTL for this record. */
        public long softTtl;

        /** Immutable response headers as received from server; must be non-null. */
        public Map<String, String> responseHeaders = Collections.emptyMap();

        /** True if the entry is expired. */
        public boolean isExpired() 
            return this.ttl < System.currentTimeMillis();
        

        /** True if a refresh is needed from the original data source. */
        public boolean refreshNeeded() 
            return this.softTtl < System.currentTimeMillis();
        
    


它有两个实现类NoCache和DiskBasedCache,使用的时候设置对应的缓存策略。

最后,推送一下自己的微信公众号,喜欢的同学可以关注。

阅读世界,共赴山海 423全民读书节,邀你共读

以上是关于设计模式学习之策略模式的主要内容,如果未能解决你的问题,请参考以下文章

设计模式学习之策略模式

设计模式学习之策略模式

《JavaScript设计模式与开发实践》学习之使用策略模式计算奖金

编程学习之简单工厂模式与策略模式

策略模式的学习之道

从零开始单排学设计模式「策略模式」黑铁 II