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

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 等。为了支持更多的聚合统计规则,代码希望尽可能灵活、可扩展。
  • 显示:负责将统计数据以某种格式显示到终端,比如:输出到命令行、邮件、网页、自定义显示终端等。

猜你喜欢

转载自www.cnblogs.com/wod-Y/p/12785761.html