• 设计模式简记-实战二:针对非业务的通过框架开发,如何做需求分析设计


    3.11 针对非业务的通过框架开发,如何做需求分析和设计?

    3.11.1 需求分析

    对于非业务通用框架的开发,做需求分析的时候,除了功能性需求分析之外,还需要考虑框架的非功能性需求。

    • 易用性
    • 性能
    • 扩展性
    • 容错性
    • 通用性
    3.11.1.1 项目实例

    设计开发一个小的框架,能够获取接口调用的各种统计信息,比如,响应时间的最大值(max)、最小值(min)、平均值(avg)、百分位值(percentile)、接口调用次数(count)、频率(tps) 等,并且支持将统计结果以各种显示格式(比如:JSON 格式、网页格式、自定义显示格式等)输出到各种终端(Console 命令行、HTTP 网页、Email、日志文件、自定义输出终端等),以方便查看。

    • 功能性需求

      • 接口统计信息:包括接口响应时间的统计信息,以及接口调用次数的统计信息等。

      • 统计信息的类型:max、min、avg、percentile、count、tps 等。

      • 统计信息显示格式:Json、Html、自定义显示格式。

      • 统计信息显示终端:Console、Email、HTTP 网页、日志、自定义显示终端。

      • 线框图,原型设计

        image-20200427113755580

    • 隐藏性需求

      • 统计触发方式:包括主动和被动两种。主动表示以一定的频率定时统计数据,并主动推送到显示终端,比如邮件推送。被动表示用户触发统计,比如用户在网页中选择要统计的时间区间,触发统计,并将结果显示给用户。
      • 统计时间区间:框架需要支持自定义统计时间区间,比如统计最近 10 分钟的某接口的 tps、访问次数,或者统计 12 月 11 日 00 点到 12 月 12 日 00 点之间某接口响应时间的最大值、最小值、平均值等。
      • 统计时间间隔:对于主动触发统计,我们还要支持指定统计时间间隔,也就是多久触发一次统计显示。比如,每间隔 10s 统计一次接口信息并显示到命令行中,每间隔 24 小时发送一封统计信息邮件。
    • 非功能性需求分析

      • 易用性:易集成、易插拔、跟业务代码是否松耦合、提供的接口是否够灵活等等。
      • 性能:低延迟,统计代码不影响或很少影响接口本身的响应时间;希望框架本身对内存的消耗不能太大。
      • 扩展性:在不修改或尽量少修改代码的情况下添加新的功能。
      • 容错性:对框架可能存在的各种异常情况都考虑全面,对外暴露的接口抛出的所有运行时、非运行时异常都进行捕获处理。
      • 通用性:尽可能通用。除了接口统计这样一个需求,还可以适用到其他哪些场景中,比如是否还可以处理其他事件的统计信息,比如 SQL 请求时间的统计信息、业务统计信息(比如支付成功率)等。

    3.11.2 设计

    对于复杂框架的设计,几个小技巧,其中包括:

    • 画产品线框图
    • 聚焦简单应用场景
    • 设计实现最小原型
    • 画系统设计图等
    • 这些方法的目的都是为了让问题简化、具体、明确,提供一个迭代设计开发的基础,逐步推进。
    3.11.2.1 最小原型
    • 对于性能计数器这个框架的开发来说,可以先聚焦于一个非常具体、简单的应用场景,比如统计用户注册、登录这两个接口的响应时间的最大值和平均值、接口调用次数,并且将统计结果以 JSON 的格式输出到命令行中。
    //应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数
    public class UserController {
      public void register(UserVo user) {
        //...
      }
      
      public UserVo login(String telephone, String password) {
        //...
      }
    }
    
    • 要输出接口的响应时间的最大值、平均值和接口调用次数,我们首先要采集每次接口请求的响应时间,并且存储起来,然后按照某个时间间隔做聚合统计,最后才是将结果输出。

      public class Metrics {
        // Map的key是接口名称,value对应接口请求的响应时间或时间戳;
        private Map<String, List<Double>> responseTimes = new HashMap<>();
        private Map<String, List<Double>> timestamps = new HashMap<>();
        private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
      
        public void recordResponseTime(String apiName, double responseTime) {
          responseTimes.putIfAbsent(apiName, new ArrayList<>());
          responseTimes.get(apiName).add(responseTime);
        }
      
        public void recordTimestamp(String apiName, double timestamp) {
          timestamps.putIfAbsent(apiName, new ArrayList<>());
          timestamps.get(apiName).add(timestamp);
        }
      
        public void startRepeatedReport(long period, TimeUnit unit){
          executor.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
              Gson gson = new Gson();
              Map<String, Map<String, Double>> stats = new HashMap<>();
              for (Map.Entry<String, List<Double>> entry : responseTimes.entrySet()) {
                String apiName = entry.getKey();
                List<Double> apiRespTimes = entry.getValue();
                stats.putIfAbsent(apiName, new HashMap<>());
                stats.get(apiName).put("max", max(apiRespTimes));
                stats.get(apiName).put("avg", avg(apiRespTimes));
              }
        
              for (Map.Entry<String, List<Double>> entry : timestamps.entrySet()) {
                String apiName = entry.getKey();
                List<Double> apiTimestamps = entry.getValue();
                stats.putIfAbsent(apiName, new HashMap<>());
                stats.get(apiName).put("count", (double)apiTimestamps.size());
              }
              System.out.println(gson.toJson(stats));
            }
          }, 0, period, unit);
        }
      
        private double max(List<Double> dataset) {//省略代码实现}
        private double avg(List<Double> dataset) {//省略代码实现}
      }
      
    • 统计注册、登录接口的响应时间和访问次数

      //应用场景:统计下面两个接口(注册和登录)的响应时间和访问次数
      public class UserController {
        private Metrics metrics = new Metrics();
        
        public UserController() {
          metrics.startRepeatedReport(60, TimeUnit.SECONDS);
        }
      
        public void register(UserVo user) {
          long startTimestamp = System.currentTimeMillis();
          metrics.recordTimestamp("regsiter", startTimestamp);
          //...
          long respTime = System.currentTimeMillis() - startTimestamp;
          metrics.recordResponseTime("register", respTime);
        }
      
        public UserVo login(String telephone, String password) {
          long startTimestamp = System.currentTimeMillis();
          metrics.recordTimestamp("login", startTimestamp);
          //...
          long respTime = System.currentTimeMillis() - startTimestamp;
          metrics.recordResponseTime("login", respTime);
        }
      }
      
    3.11.2.2 系统设计图

    image-20200427115326349

    • 数据采集:负责打点采集原始数据,包括记录每次接口请求的响应时间和请求时间。数据采集过程要高度容错,不能影响到接口本身的可用性。除此之外,因为这部分功能是暴露给框架的使用者的,所以在设计数据采集 API 的时候,我们也要尽量考虑其易用性。
    • 存储:负责将采集的原始数据保存下来,以便后面做聚合统计。数据的存储方式有多种,比如:Redis、MySQL、HBase、日志、文件、内存等。数据存储比较耗时,为了尽量地减少对接口性能(比如响应时间)的影响,采集和存储的过程异步完成。
    • 聚合统计:负责将原始数据聚合为统计数据,比如:max、min、avg、pencentile、count、tps 等。为了支持更多的聚合统计规则,代码希望尽可能灵活、可扩展。
    • 显示:负责将统计数据以某种格式显示到终端,比如:输出到命令行、邮件、网页、自定义显示终端等。
  • 相关阅读:
    WEB引入Google思源黑体
    Linux安装最新版Node.js
    JS判断值是否是数字
    高效jQuery的奥秘
    一个Web前端工程师或程序员的发展方向,未来困境及穷途末路
    javascript 模块化 (切记:学习思想)
    学习 正则表达式 js java c# python 通用
    Promise如何解决回调地狱
    VSCode 开发插件 推荐
    js移动端自适应动态设置html的fontsize
  • 原文地址:https://www.cnblogs.com/wod-Y/p/12785761.html
Copyright © 2020-2023  润新知