设计模式之美
王争
前Google工程师,《数据结构与算法之美》专栏作者
立即订阅
19995 人已学习
课程目录
已更新 46 讲 / 共 100 讲
0/6登录后,你可以任选6讲全文学习。
开篇词 (1讲)
开篇词 | 一对一的设计与编码集训,让你告别没有成长的烂代码!
免费
设计模式学习导读 (3讲)
01 | 为什么说每个程序员都要尽早地学习并掌握设计模式相关知识?
02 | 从哪些维度评判代码质量的好坏?如何具备写出高质量代码的能力?
03 | 面向对象、设计原则、设计模式、编程规范、重构,这五者有何关系?
设计原则与思想:面向对象 (11讲)
04 | 理论一:当谈论面向对象的时候,我们到底在谈论什么?
05 | 理论二:封装、抽象、继承、多态分别可以解决哪些编程问题?
06 | 理论三:面向对象相比面向过程有哪些优势?面向过程真的过时了吗?
07 | 理论四:哪些代码设计看似是面向对象,实际是面向过程的?
08 | 理论五:接口vs抽象类的区别?如何用普通的类模拟抽象类和接口?
09 | 理论六:为什么基于接口而非实现编程?有必要为每个类都定义接口吗?
10 | 理论七:为何说要多用组合少用继承?如何决定该用组合还是继承?
11 | 实战一(上):业务开发常用的基于贫血模型的MVC架构违背OOP吗?
12 | 实战一(下):如何利用基于充血模型的DDD开发一个虚拟钱包系统?
13 | 实战二(上):如何对接口鉴权这样一个功能开发做面向对象分析?
14 | 实战二(下):如何利用面向对象设计和编程开发接口鉴权功能?
设计原则与思想:设计原则 (12讲)
15 | 理论一:对于单一职责原则,如何判定某个类的职责是否够“单一”?
16 | 理论二:如何做到“对扩展开放、修改关闭”?扩展和修改各指什么?
17 | 理论三:里式替换(LSP)跟多态有何区别?哪些代码违背了LSP?
18 | 理论四:接口隔离原则有哪三种应用?原则中的“接口”该如何理解?
19 | 理论五:控制反转、依赖反转、依赖注入,这三者有何区别和联系?
20 | 理论六:我为何说KISS、YAGNI原则看似简单,却经常被用错?
21 | 理论七:重复的代码就一定违背DRY吗?如何提高代码的复用性?
22 | 理论八:如何用迪米特法则(LOD)实现“高内聚、松耦合”?
23 | 实战一(上):针对业务系统的开发,如何做需求分析和设计?
24 | 实战一(下):如何实现一个遵从设计原则的积分兑换系统?
25 | 实战二(上):针对非业务的通用框架开发,如何做需求分析和设计?
26 | 实战二(下):如何实现一个支持各种统计规则的性能计数器?
设计原则与思想:规范与重构 (11讲)
27 | 理论一:什么情况下要重构?到底重构什么?又该如何重构?
28 | 理论二:为了保证重构不出错,有哪些非常能落地的技术手段?
29 | 理论三:什么是代码的可测试性?如何写出可测试性好的代码?
30 | 理论四:如何通过封装、抽象、模块化、中间层等解耦代码?
31 | 理论五:让你最快速地改善代码质量的20条编程规范(上)
32 | 理论五:让你最快速地改善代码质量的20条编程规范(中)
33 | 理论五:让你最快速地改善代码质量的20条编程规范(下)
34 | 实战一(上):通过一段ID生成器代码,学习如何发现代码质量问题
35 | 实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”
36 | 实战二(上):程序出错该返回啥?NULL、异常、错误码、空对象?
37 | 实战二(下):重构ID生成器项目中各函数的异常处理代码
设计原则与思想:总结课 (3讲)
38 | 总结回顾面向对象、设计原则、编程规范、重构技巧等知识点
39 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(上)
40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下)
设计模式与范式:创建型 (2讲)
41 | 单例模式(上):为什么说支持懒加载的双重检测不比饿汉式更优?
42 | 单例模式(中):我为什么不推荐使用单例模式?又有何替代方案?
不定期加餐 (3讲)
加餐一 | 用一篇文章带你了解专栏中用到的所有Java语法
加餐二 | 设计模式、重构、编程规范等相关书籍推荐
春节特别加餐 | 王争:如何学习《设计模式之美》专栏?
免费
设计模式之美
登录|注册

40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下)

王争 2020-02-03
上一节课中,我们针对版本 1 存在的问题(特别是 Aggregator 类、ConsoleReporter 和 EmailReporter 类)进行了重构优化。经过重构之后,代码结构更加清晰、合理、有逻辑性。不过,在细节方面还是存在一些问题,比如 ConsoleReporter、EmailReporter 类仍然存在代码重复、可测试性差的问题。今天,我们就在版本 3 中持续重构这部分代码。
除此之外,在版本 3 中,我们还会继续完善框架的功能和非功能需求。比如,让原始数据的采集和存储异步执行,解决聚合统计在数据量大的情况下会导致内存吃紧问题,以及提高框架的易用性等,让它成为一个能用且好用的框架。
话不多说,让我们正式开始版本 3 的设计与实现吧!

代码重构优化

我们知道,继承能解决代码重复的问题。我们可以将 ConsoleReporter 和 EmailReporter 中的相同代码逻辑,提取到父类 ScheduledReporter 中,以解决代码重复问题。按照这个思路,重构之后的代码如下所示:
public abstract class ScheduledReporter {
protected MetricsStorage metricsStorage;
protected Aggregator aggregator;
protected StatViewer viewer;
public ScheduledReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) {
this.metricsStorage = metricsStorage;
this.aggregator = aggregator;
this.viewer = viewer;
}
protected void doStatAndReport(long startTimeInMillis, long endTimeInMillis) {
long durationInMillis = endTimeInMillis - startTimeInMillis;
Map<String, List<RequestInfo>> requestInfos =
metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis);
Map<String, RequestStat> requestStats = aggregator.aggregate(requestInfos, durationInMillis);
viewer.output(requestStats, startTimeInMillis, endTimeInMillis);
}
}
ConsoleReporter 和 EmailReporter 代码重复的问题解决了,那我们再来看一下代码的可测试性问题。因为 ConsoleReporter 和 EmailReporter 的代码比较相似,且 EmailReporter 的代码更复杂些,所以,关于如何重构来提高其可测试性,我们拿 EmailReporter 来举例说明。将重复代码提取到父类 ScheduledReporter 之后,EmailReporter 代码如下所示:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《设计模式之美》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(10)

  • 辣么大
    思考题:startRepeatedReport()多次调用,会启动多个线程,每个线程都会执行统计和输出工作。
    想了一种简单的实现方式,将runnable做为成员变量,第一次调用startRepeatedReport()时初始化,若多次调用,判空,返回。
      public void startRepeatedReport(long periodInSeconds, long durationInSeconds) {
        if (runnable != null) {
          System.out.println("duplicate calls!");
          return;
        }
        runnable = () -> {
          long durationInMillis = durationInSeconds * 1000;
          long endTimeInMillis = System.currentTimeMillis();
          long startTimeInMillis = endTimeInMillis - durationInMillis;
          doReport(startTimeInMillis, endTimeInMillis);
        };
        executor.scheduleAtFixedRate(runnable, 0, periodInSeconds, TimeUnit.SECONDS);
      }
    代码放在了:https://github.com/gdhucoder/Algorithms4/tree/master/designpattern/u40
    2020-02-03
    3
  • Jxin
    先回答问题:
    1.会导致多余线程做多余的统计和展示。因为每次调用都会起一个异步线程输出统计数据到控制台。这样既会带来额外的性能开销,又会导致统计信息不易阅读。

    2.在ConsoleReporter内部维护一个可视字段 started。然后在方法执行时,优先判断该字段是否已经变为true。如果是则不再往下执行。也算是保证该函数的幂等性。

    个人疑问:
    1.怎么做到这样分步展示重构过程的?我现在写,基本一边写就一边重构,停手也就差不多到合适的质量了。刻意要展示重构手法,展示的知识点会有很多疏漏,并无法做到这样一步一步的展示(下意识一步到位,并不知道怎么退到不好的代码结构)。

    2.能理解栏主尽量不依赖任何框架的初衷。但对于java,spring其实才是标准,感觉是不是基于spring框架来写demo还好点? 我现在比较喜欢让代码依赖spring框架来实现,感觉这样会显得优雅一些。栏主怎么看?

    2020-02-03
    4
    2
  • undefined
    深入浅出,过瘾。
    2020-02-03
    1
  • 小晏子
    课后思考:如果 startRepeatedReport()被多次调用,那么会生成多个线程以fixed rate去请求然后输出结果到console上,一方面导致输出结果混乱,另一方面增加了系统的负担。
    要解决该问题有个办法是再重构一下代码,示意如下(未测试),
    ```
    private Future<?> future;
    //避免创建多个线程,也可以放在其他地方,如构造函数里
    private Runnable runnable = new Runnable () {
           @Override
           public void run() {
               ....
            }
    };

    public void startRepeatedReport(long periodInSeconds, long durationInSeconds) {
        future.cancel(true); //每次调用就取消上一次的调用
        future = service.scheduleAtFixedRate(runnable, 0L, periodInSeconds, TimeUnit.SECONDS); //重新开始
    }
    ```
    2020-02-03
    1
  • 高源
    老师39,40课完整源代码可以提供下吗,我准备好好研究学习下
    2020-02-03
    1
  • 守拙
    课堂讨论:



    正常情况下,ConsoleReporter 的 startRepeatedReport() 函数只会被调用一次。但是,如果被多次调用,那就会存在问题。具体会有什么问题呢?又该如何解决呢?



    startRepeatedReport()多次调用会导致SingleThreadExecutor排队执行任务. 从性能上, 浪费系统资源, 从需求上, 不符合方法设计的初衷. 解决方案之一是使用免锁容器存储唯一值, 作为任务已开始调度的flag, 在startRepeatedReport()方法判断: 如果任务已开始调度, 则直接return.
    2020-02-04
  • Andy
    老师能提供课程代码吗?
    2020-02-04
  • javaadu
    课堂讨论,使用一个标记flag作为该函数被调用国的标记,并给这个函数加锁,解决并发问题
    2020-02-03
  • 平风造雨
    调用多次可以通过多线程共享的状态变量来解决,CAS或者加锁进行状态的变更。
    2020-02-03
  • Jeff.Smile
    沙发,打卡!一路跟进!
    2020-02-03
收起评论
10
返回
顶部