设计模式: 策略模式

博客主页

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

使用场景:

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

UML 类图

  1. Context 用来操作策略的上下文环境
  2. Strategy 策略的抽象
  3. ConcreteStrategyA、ConcreteStrategyB 具体的策略实现

通常如果一个问题有多种解决方案时,最简单的方式就是利用if-else或者switch-case方式根据不同的情景选择不同的解决方案。但这种简单的方案问题太多,例如耦合性太高、代码臃肿、难以维护等。

策略模式的简单实现

下面以在北京坐公共交通工具的费用计算来演示示例。例如:公交采用分段计价,也就是乘坐的距离越远,价格越高。公交车和地铁的价格计算方式不一样,但是在示例中是需要计算乘不同出行工具的成本,策略模式的第一个版本示例代码,

public class PriceCalculator {
    // 公交车类型
    private static final int BUS = 1;

    // 地铁类型
    private static final int METRO = 2;

    public static void main(String[] args) {
        PriceCalculator calculator = new PriceCalculator();
        System.out.println("坐16公里的公交车票价为:" + calculator.calculatorPrice(16, BUS));

    }

    private int calculatorPrice(int km, int type) {
        if (type == BUS) {
            return busPrice(km);
        } else if (type == METRO) {
            return metroPrice(km);
        }
        return 0;
    }

    /**
     * 公交车,10公里之内一元钱,超过10公里之后每加一元可以乘5公里
     */
    private int busPrice(int km) {
        // 超过10公里的总距离
        int extraTotal = km - 10;
        // 超过的距离是5公里的倍数
        int extraFactor = extraTotal / 5;
        // 超过的距离对5公里取余
        int fraction = extraFactor % 5;

        // 价格计算
        int price = 1 + extraFactor;

        return fraction > 0 ? ++price : price;
    }

    /**
     * 6公里(含)3元,6~12公里(含)4元,12~22公里(含)5元,22~32(含)6元
     */
    private int metroPrice(int km) {
        if (km <= 6) {
            return 3;
        } else if (km <= 12) {
            return 4;
        } else if (km <= 22) {
            return 5;
        } else if (km <= 32) {
            return 6;
        }
        return 7;
    }
}

这个版本很明显的问题就是并不是单一职责,首先是承担了计算公交车和地铁乘坐价格的职责,另一个是if-else的形式来判断使用哪种计算形式。当增加出租车出行方式,就要在PriceCalculator中增加一个方法来计算出租车出行的价格,并且在calculatorPrice函数中增加一个判断。

下面使用策略模式进行重构。

首先定义一个抽象的价格计算接口,如:CalculateStrategy

// 计算价格的接口
public interface CalculateStrategy {
    /**
     * 按距离来计算价格
     * @param km 公里
     * @return 返回价格
     */
    int calculatePrice(int km);
}

对于每一种出行方式都有一个独立的计算策略类,且实现CalculateStrategy接口
BusStrategy : 公交车价格计算策略

// 公交车价格计算策略
public class BusStrategy implements CalculateStrategy {
    // 10公里之内一元钱,超过10公里之后每加一元可以乘5公里
    @Override
    public int calculatePrice(int km) {
        // 超过10公里的总距离
        int extraTotal = km - 10;
        // 超过的距离是5公里的倍数
        int extraFactor = extraTotal / 5;
        // 超过的距离对5公里取余
        int fraction = extraFactor % 5;

        // 价格计算
        int price = 1 + extraFactor;

        return fraction > 0 ? ++price : price;
    }
}

MetroStrategy : 地铁价格计算策略

// 地铁价格计算策略
public class MetroStrategy implements CalculateStrategy {
    // 6公里(含)3元,6~12公里(含)4元,12~22公里(含)5元,22~32(含)6元
    @Override
    public int calculatePrice(int km) {
        if (km <= 6) {
            return 3;
        } else if (km <= 12) {
            return 4;
        } else if (km <= 22) {
            return 5;
        } else if (km <= 32) {
            return 6;
        }
        return 7;
    }
}

在创建一个扮演Context角色的类,如:TranficCalculator

// 公交出行价格计算器
public class TranficCalculator {
    public static void main(String[] args) {
        TranficCalculator calculator = new TranficCalculator();

        // 设置策略
        calculator.setStrategy(new BusStrategy());
        // 计算价格
        System.out.println("坐16公里的公交车票价为:" + calculator.calculatePrice(16));
    }

    private int calculatePrice(int km) {
        return strategy.calculatePrice(km);
    }

    private CalculateStrategy strategy;
    private void setStrategy(BusStrategy strategy) {
        this.strategy = strategy;
    }

}

这种方案隐藏实现的同时,可扩展性变的很强。通过建立抽象,将不同的策略构建成一个具体的策略实现,通过不同的策略实现算法替换,简化逻辑、结构的同时,增强了系统的可读性、稳定性、可扩展性,对于较为复杂的业务逻辑显得更为直观,扩展也更方便。

深入分析属性动画

在Android 3.0之前,Android 提供了几种动画类型:View Animation、Drawable Animation、Property Animation。View Animation只能支持简单的缩放、平移、旋转、透明度这几个基本的动画,且有一定的局限性。例如:希望View有一个颜色的切换动画,希望可以使用3D旋转动画,希望当动画停止时View的位置就是当前的位置,这些View Animation是无法做到。

Google在 Android 3.0提供了属性动画,为了兼容Android 3.0以下的动画库,在github上有一个开源的动画库NineOldAnimations,它通过判断系统版本来选择实现属性动画的方式。

属性动画体系的整体设计:

Animator 通过 PropertyValuesHolder 来更新对象的目标属性,如果用户没有设置目标属性的 Property 对象,那么会通过反射的形式调用目标属性的setter方法来更新属性值。否则,通过Property的set方法来设置属性值,这个属性值则通过 KeyframeSet 的计算得到,而KeyframeSet又是通过时间插值器和类型估值器来计算,在动画执行过程中不断地计算当前时刻目标属性的值,然后更新属性值来达到动画效果。

属性动画核心类介绍

  1. ValueAnimator:该类是Animator的子类,实现了动画的整个处理逻辑,也是属性动画最为核心的类
  2. ObjectAnimator: 对象属性动画的操作类,继承自ValueAnimator,通过该类使用动画的形式操作对象的属性
  3. TimeInterpolator:时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有线性插值器(LinearInterpolator)、加速减速插值器(AccelerateDecelerateInterpolator)、减速插值器(DecelerateInterpolator)等
  4. TypeEvaluator:类型估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有针对整型属性(IntEvaluator)、针对浮点型属性(FloatEvaluator)、针对Color属性(ArgbEvaluator)等
  5. Property:属性对象,主要定义了属性的set和get方法
  6. PropertyValuesHolder:持有目标属性Property、setter 和 getter方法,以及关键帧集合的类
  7. KeyframeSet:存储一个动画的关键帧集合

基本使用

示例一
改变一个对象(TextView)的 translationY 属性,让其沿着y轴向下平移一段距离,该动画在默认时间内完成,动画的完成时间是可以定义的,想要灵活的效果还可以定义插值器和估值算法。

ObjectAnimator.ofFloat(textview, "translationY", 100.f).start();

示例二
改变一个对象的背景色属性,如改变View的背景色。可以让背景色在3s内实现从0x22ff0000到0x2200ff00的渐变,并且动画会无限循环且会有反转的效果

ValueAnimator animator = ObjectAnimator.ofInt(textview, "backgroundColor", 0x22ff0000, 0x2200ff00);
animator.setDuration(3000); // 时间,默认300
animator.setEvaluator(new ArgbEvaluator()); // 类型估值器
animator.setRepeatCount(ValueAnimator.INFINITE); // 重复次数
animator.setRepeatMode(ValueAnimator.REVERSE); // 重复模式
animator.start();

示例三
动画集合,5s内对View的循转、平移、缩放和透明度都进行了改变

AnimatorSet set = new AnimatorSet();
set.playTogether(
        ObjectAnimator.ofFloat(textview, "rotationX", 0, 360),
        ObjectAnimator.ofFloat(textview, "rotationY", 0, 180),
        ObjectAnimator.ofFloat(textview, "rotation", 0, -90),
        ObjectAnimator.ofFloat(textview, "translationX", 0, 90),
        ObjectAnimator.ofFloat(textview, "translationY", 0, 90),
        ObjectAnimator.ofFloat(textview, "scaleX", 1, 1.5f),
        ObjectAnimator.ofFloat(textview, "scaleY", 1, 0.5f),
        ObjectAnimator.ofFloat(textview, "alpha", 1, 0.25f, 1)
);
set.setDuration(5 * 1000).start();

示例四
animate()方法是属性动画特有的,动画持续时间为2s,在y轴上旋转720度,且平移到(100,100)的位置

textview.animate().setDuration(2000).rotationYBy(720).x(100).y(100);

核心原理分析

下面以具体的示例作为分析的入口,如:要对某个View的小、轴缩放到原来的0.3倍,动画执行时间为1s

ValueAnimator animator = ObjectAnimator.ofFloat(textview, "scaleX", 0.3f);
animator.setDuration(1000);
animator.start();

它的背后是什么原理呢?首先从它的入口,即ObjectAnimator入手:

public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
    // 1. 构建动画对象
    ObjectAnimator anim = new ObjectAnimator(target, propertyName);
    // 2. 设置属性值
    anim.setFloatValues(values);
    return anim;
}

ObjectAnimator的ofFloat函数中会先构建属性动画对象,然后根据设置的属性值来初始化各个时间段对应的属性值,这个属性值就是values参数,它是一个可变参数,如果是一个参数,那么该函数为目标值;如果是两个参数,那么一个是起始值,另一个是目标值。先来看看setFloatValues函数实现

PropertyValuesHolder[] mValues; // mValues是动画的各个数值的集合
public void setFloatValues(float... values) {
    if (mValues == null || mValues.length == 0) {
        // No values yet - this animator is being constructed piecemeal. Init the values with
        // whatever the current propertyName is
        if (mProperty != null) {
            setValues(PropertyValuesHolder.ofFloat(mProperty, values));
        } else {
            setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
        }
    } else {
        super.setFloatValues(values);
    }
}

setFloatValues函数涉及到一个类PropertyValuesHolder,这个类是该动画库的一个核心类之一,它的作用就是保存属性的名称和它的setter、getter方法,以及它的目标值。

public class PropertyValuesHolder implements Cloneable {
    // 属性名称
    String mPropertyName;
    // 属性对象
    protected Property mProperty;
    // 属性setter方法
    Method mSetter = null;
    // 属性getter方法
    private Method mGetter = null;
    // 属性的类类型,如float、int等
    Class mValueType;
    // 这里是动画关键帧即可,即在duration时间内的动画帧集合,它保存的是在每个时刻该属性对应的值
    Keyframes mKeyframes = null;

    public static PropertyValuesHolder ofFloat(Property<?, Float> property, float... values) {
        // 构建的是FloatPropertyValuesHolder
        return new FloatPropertyValuesHolder(property, values);
    }
    
    // 内部类,Float类型的PropertyValuesHolder
    static class FloatPropertyValuesHolder extends PropertyValuesHolder {
        // float型的属性
        private FloatProperty mFloatProperty;
        // 动画的关键帧,这个是重点
        Keyframes.FloatKeyframes mFloatKeyframes;
        float mFloatAnimatedValue;

        // 构造函数
        public FloatPropertyValuesHolder(Property property, float... values) {
            super(property);
            // 设置目标属性值
            setFloatValues(values);
            if (property instanceof  FloatProperty) {
                mFloatProperty = (FloatProperty) mProperty;
            }
        }

        // 设置动画的目标值
        public void setFloatValues(float... values) {
            super.setFloatValues(values);
            // 获取动画关键帧
            mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
        }
        // 计算当前的动画帧
        void calculateValue(float fraction) {
            mFloatAnimatedValue = mFloatKeyframes.getFloatValue(fraction);
        }
        // ...省略
    }
}

该类是属性和属性值的辅助类,它保存了属性的名称、setter、getter,以及该属性在duration时间段内各个时刻对应属性数值(mKeyframeSet)。这样当执行动画时,动画库只需要根据动画的执行时间,到mKeyframeSet中查询这个时刻对应的属性值,然后修改执行动画的对象的目标属性值,连续这个过程即可达到动画的效果。

计算各个时刻的属性值的操作放在了父类(即PropertyValuesHolder)的setFloatValues的函数中,结下来看下setFloatValues实现

public void setFloatValues(float... values) {
    mValueType = float.class;
    mKeyframes = KeyframeSet.ofFloat(values);
}

该函数又调用了KeyframeSet的ofFloat方法

public static KeyframeSet ofFloat(float... values) {
    boolean badValue = false;
    int numKeyframes = values.length;
    FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
    if (numKeyframes == 1) {
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
        keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
        if (Float.isNaN(values[0])) {
            badValue = true;
        }
    } else {
        keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
        for (int i = 1; i < numKeyframes; ++i) {
            keyframes[i] =
                    (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
            if (Float.isNaN(values[i])) {
                badValue = true;
            }
        }
    }
    if (badValue) {
        Log.w("Animator", "Bad value (NaN) in float animator");
    }
    return new FloatKeyframeSet(keyframes);
}

关键帧的计算在函数ofFloat中实现。如果设置了一个目标值,那么这个值就是最终的值,它的起始值会被默认设置为0,如果用户设置了大于1个目标值,这些关键帧都会被存储到KeyframeSet对象中。设置完关键帧后,就会调用start()方法启动动画。

public void start() {
    super.start();
}

调用父类的start()方法,它的父类是ValueAnimator

public void start() {
    start(false);
}

private void start(boolean playBackwards) {
    // 判断Looper是否为空,这里的Looper是UI线程的Looper
    if (Looper.myLooper() == null) {
        throw new AndroidRuntimeException("Animators may only be run on Looper threads");
    }
    mReversing = playBackwards;
    mSelfPulse = !mSuppressSelfPulseRequested;
    // Special case: reversing from seek-to-0 should act as if not seeked at all.
    if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
        if (mRepeatCount == INFINITE) {
            // Calculate the fraction of the current iteration.
            float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
            mSeekFraction = 1 - fraction;
        } else {
            mSeekFraction = 1 + mRepeatCount - mSeekFraction;
        }
    }
    mStarted = true;
    mPaused = false;
    mRunning = false;
    mAnimationEndRequested = false;
    mLastFrameTime = -1;
    mFirstFrameTime = -1;
    mStartTime = -1;
    addAnimationCallback(0);
    // 是否延迟
    if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
        // 如果没有启动延迟,立即开始并通知监听器
        // 否则,推迟到开始延迟后的第一帧
        startAnimation();
        if (mSeekFraction == -1) {
            setCurrentPlayTime(0);
        } else {
            setCurrentFraction(mSeekFraction);
        }
    }
}

如果我的文章对您有帮助,不妨点个赞鼓励一下(^_^)

Image placeholder
YANGWW
未设置
  49人点赞

没有讨论,发表一下自己的看法吧

推荐文章
在 Laravel 中实现「福勒的货币设计模式」

“这个世界上有很大比例的计算机都在操纵金钱,因此,我一直感到困惑的是,金钱实际上并不是任何主流编程语言中的一流数据类型。缺乏类型会导致问题,这是最明显的周边货币。如果您所有的计算都是用一种货币完成的

Python 面向对象 高阶-描述符与设计模式笔记

描述符 当一个类中,包含了三个魔术方法(__get__,__set__,__delete__)之一,或者全部时,那么这个类就称为描述符类 作用 描述符的作用就是对一个类中的某个成员进行一个详细的

微博广告策略工程架构体系演进

概述 1.广告样式与场景 上图是微博广告目前商业场景流,“一屏四大流”。“一屏”指打开微博的Fashion,“四大流”指占据微博商业化的主体,包括关系信息流、热门流、评论流和热搜流。右图为广告投放的

Laravel 第八章学习——中间件以及策略

中间件 Laravel中间件(Middleware)为我们提供了一种非常棒的过滤机制来过滤进入应用的HTTP请求,例如,当我们使用Auth中间件来验证用户的身份时,如果用户未通过身份验证,则Auth中

Laravel 第八章学习——中间件以及策略

中间件Laravel中间件(Middleware) 为我们提供了一种非常棒的过滤机制来过滤进入应用的HTTP请求,例如,当我们使用Auth中间件来验证用户的身份时,如果用户未通过身份验证,则Auth中

制定机器学习训练数据策略的6个技巧

人工智能(AI)和机器学习(ML)如今已经十分常见。AI指的是机器模仿人类进行认知的概念,ML是一种用于构建AI的方法。如果AI是指计算机可以根据指令执行一组任务,那么ML就是机器从数据中摄取、解析和

面对网络风险 企业应当如何部署安全策略?

有关数据泄露和漏洞的突发层出不穷,这些数据泄露和漏洞对企业的财务和声誉有着非常大的影响。企业高管似乎无法摆脱预警头条的抨击,以及由专家组成的轰炸,这些专家就如何避免这些网络安全攻击提出了一些建议。尽管

vue加载优化策略有哪些?

方法一路由懒加载vue.js是一个比较流行的前端框架,与react.js、angular.js相比来说,vue.js入手曲线更加流畅,不管掌握多少都可以快速上手。但是单页面应用也都有其弊病,有时候首屏

vue源码解读(四)Vue中的异步更新策略

欢迎star我的github仓库,共同学习~目前vue源码学习系列已经更新了6篇啦~https://github.com/yisha0307/...快速跳转: Vue的双向绑定原理(已完成) 说说vu

如何设计 QQ、微信、微博、Github 等等,第三方账号登陆 ?(附表设计)

前言:多账户登陆1.创业初期用户名密码注册登陆手机号注册登陆2.数据库设计3.引入第三方账户方案4.数据库设计5.总结前言:多账户登陆互联网应用当中,我们的应用会使用多个第三方账号进行登录,比如:网易

职责驱动设计及状态模式的融会贯通

一、需求针对某通信产品,我们需要开发一个版本升级管理系统。该系统通过Java开发后台管理,由Telnet发起向前端基站设备的命令,以获取基站设备的版本信息,并在后台比较与当前最新版本的差异,以确定执行

软件架构被高估,清晰简单的设计被低估

软件架构最佳实践、企业架构模式以及系统描述的正式方法都是非常重要且实用的工具,总会有合适的场景让它们发挥作用。但在设计系统时,请从简单始、以简单终,尽可能避免一切会无谓提高复杂度的架构与正式工具。

Laravel 使用 CURD 之外- Domain,大中型 Laravel 项目架构设计

0x01面向领域的Laravel 人类分类思考,我们的代码应该映射这一点 首先说明,我没有提出这个术语『领域』-我从流行的开发模式DDD中学来的。引用牛津词典,『领域』可以描述为『一个特定范围的活

GoWeb教程_13.0. 如何设计一个 Web 框架

前面十二章介绍了如何通过Go来开发Web应用,介绍了很多基础知识、开发工具和开发技巧,那么我们这一章通过这些知识来实现一个简易的Web框架。通过Go语言来实现一个完整的框架设计,这框架中主要内容有第一

GoWeb教程_13.2. 自定义路由器设计

HTTP路由 HTTP路由组件负责将HTTP请求交到对应的函数处理(或者是一个struct的方法),如前面小节所描述的结构图,路由在框架中相当于一个事件处理器,而这个事件包括: 用户请求的路径(pat

GoWeb教程_13.3. controller 设计

传统的MVC框架大多数是基于Action设计的后缀式映射,然而,现在Web流行REST风格的架构。尽管使用Filter或者rewrite能够通过URL重写实现REST风格的URL,但是为什么不直接设计

GoWeb教程_13.4. 日志和配置设计

日志和配置的重要性 前面已经介绍过日志在我们程序开发中起着很重要的作用,通过日志我们可以记录调试我们的信息,当初介绍过一个日志系统seelog,根据不同的level输出不同的日志,这个对于程序开发和程

高并发设计笔记

基础篇 高并发系统:它的通用设计方法是什么? 高并发系统设计的三种通用方法:Scale-out、缓存和异步。 这三种方法可以在做方案设计时灵活地运用,但它不是具体实施的方案,而是三种思想,在实际运用中

S.O.L.I.D: PHP 面向对象设计的五个基准原则

S.O.L.I.D是首个5个面向对象设计(OOD)准则的首字母缩写,这些准则是由RobertC.Martin提出的,他更为人所熟知的名字是UncleBob。 这些准则使得开发出易扩展、可维护的软件变

高并发设计笔记

基础篇 高并发系统:它的通用设计方法是什么? 高并发系统设计的三种通用方法:Scale-out、缓存和异步。 这三种方法可以在做方案设计时灵活地运用,但它不是具体实施的方案,而是三种思想,在实际运用中

Spring WebFlux 的设计及工作原理剖析

前言 Spring5发布有两年了,随Spring5一起发布了一个和SpringWebMvc同级的SpringWebFlux。这是一个支持反应式编程模型的新框架体系。反应式模型区别于传统的MVC最大的不

算法题:设计和实现一个 LRU Cache 缓存机制

理论基础 LRU算法、Cache 实现LRUCache缓存机制 题目描述 设计和实现一个LRUCache缓存机制 解题思路 leastrecentlyused最近最少使用(被淘汰) Doubl

实践和思考的重要意义(论软件代码设计)

感触 最近这段时间,包括以前,经常听到,程序员们大谈设计模式,这个话题并不陌生,面试必问的问题,活了这么多年,我就一直没搞清楚,为啥面试官喜欢问这个问题。如果一个面试官喜欢问这种问题,我觉得也没啥意思

算法题:设计和实现一个 LRU Cache 缓存机制

题目来源于力扣 理论基础 LRU算法、Cache 实现LRUCache缓存机制题目描述 设计和实现一个LRUCache缓存机制 解题思路leastrecentlyused最近最少使用(被淘汰) Do

实践和思考的重要意义(论软件代码设计)

感触最近这段时间,包括以前,经常听到,程序员们大谈设计模式,这个话题并不陌生,面试必问的问题,活了这么多年,我就一直没搞清楚,为啥面试官喜欢问这个问题。如果一个面试官喜欢问这种问题,我觉得也没啥意思。