CategoryResourceRepost/极客时间专栏/设计模式之美/设计原则与思想:总结课/40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下).md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

486 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<audio id="audio" title="40 | 运用学过的设计原则和思想完善之前讲的性能计数器项目(下)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/bb/26/bbc5a1f54141775a37bd22d87caaf426.mp3"></audio>
上一节课中我们针对版本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&lt;String, List&lt;RequestInfo&gt;&gt; requestInfos =
metricsStorage.getRequestInfos(startTimeInMillis, endTimeInMillis);
Map&lt;String, RequestStat&gt; requestStats = aggregator.aggregate(requestInfos, durationInMillis);
viewer.output(requestStats, startTimeInMillis, endTimeInMillis);
}
}
```
ConsoleReporter和EmailReporter代码重复的问题解决了那我们再来看一下代码的可测试性问题。因为ConsoleReporter和EmailReporter的代码比较相似且EmailReporter的代码更复杂些所以关于如何重构来提高其可测试性我们拿EmailReporter来举例说明。将重复代码提取到父类ScheduledReporter之后EmailReporter代码如下所示
```
public class EmailReporter extends ScheduledReporter {
private static final Long DAY_HOURS_IN_SECONDS = 86400L;
private MetricsStorage metricsStorage;
private Aggregator aggregator;
private StatViewer viewer;
public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) {
this.metricsStorage = metricsStorage;
this.aggregator = aggregator;
this.viewer = viewer;
}
public void startDailyReport() {
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DATE, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Date firstTime = calendar.getTime();
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
long durationInMillis = DAY_HOURS_IN_SECONDS * 1000;
long endTimeInMillis = System.currentTimeMillis();
long startTimeInMillis = endTimeInMillis - durationInMillis;
doStatAndReport(startTimeInMillis, endTimeInMillis);
}
}, firstTime, DAY_HOURS_IN_SECONDS * 1000);
}
}
```
前面提到之所以EmailReporter可测试性不好一方面是因为用到了线程定时器也相当于多线程另一方面是因为涉及时间的计算逻辑。
实际上在经过上一步的重构之后EmailReporter中的startDailyReport()函数的核心逻辑已经被抽离出去了较复杂的、容易出bug的就只剩下计算firstTime的那部分代码了。我们可以将这部分代码继续抽离出来封装成一个函数然后单独针对这个函数写单元测试。重构之后的代码如下所示
```
public class EmailReporter extends ScheduledReporter {
// 省略其他代码...
public void startDailyReport() {
Date firstTime = trimTimeFieldsToZeroOfNextDay();
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 省略其他代码...
}
}, firstTime, DAY_HOURS_IN_SECONDS * 1000);
}
// 设置成protected而非private是为了方便写单元测试
@VisibleForTesting
protected Date trimTimeFieldsToZeroOfNextDay() {
Calendar calendar = Calendar.getInstance(); // 这里可以获取当前时间
calendar.add(Calendar.DATE, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
}
```
简单的代码抽离成trimTimeFieldsToZeroOfNextDay()函数之后虽然代码更加清晰了一眼就能从名字上知道这段代码的意图获取当前时间的下一天的0点时间但我们发现这个函数的可测试性仍然不好因为它强依赖当前的系统时间。实际上这个问题挺普遍的。一般的解决方法是将强依赖的部分通过参数传递进来这有点类似我们之前讲的依赖注入。按照这个思路我们再对trimTimeFieldsToZeroOfNextDay()函数进行重构。重构之后的代码如下所示:
```
public class EmailReporter extends ScheduledReporter {
// 省略其他代码...
public void startDailyReport() {
// new Date()可以获取当前时间
Date firstTime = trimTimeFieldsToZeroOfNextDay(new Date());
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
// 省略其他代码...
}
}, firstTime, DAY_HOURS_IN_SECONDS * 1000);
}
protected Date trimTimeFieldsToZeroOfNextDay(Date date) {
Calendar calendar = Calendar.getInstance(); // 这里可以获取当前时间
calendar.setTime(date); // 重新设置时间
calendar.add(Calendar.DATE, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
}
```
经过这次重构之后trimTimeFieldsToZeroOfNextDay()函数不再强依赖当前的系统时间,所以非常容易对其编写单元测试。你可以把它作为练习,写一下这个函数的单元测试。
不过EmailReporter类中startDailyReport()还是涉及多线程针对这个函数该如何写单元测试呢我的看法是这个函数不需要写单元测试。为什么这么说呢我们可以回到写单元测试的初衷来分析这个问题。单元测试是为了提高代码质量减少bug。如果代码足够简单简单到bug无处隐藏那我们就没必要为了写单元测试而写单元测试或者为了追求单元测试覆盖率而写单元测试。经过多次代码重构之后startDailyReport()函数里面已经没有多少代码逻辑了,所以,完全没必要对它写单元测试了。
## 功能需求完善
经过了多个版本的迭代、重构我们现在来重新Review一下目前的设计与实现是否已经完全满足第25讲中最初的功能需求了。
最初的功能需求描述是下面这个样子的,我们来重新看一下。
>
我们希望设计开发一个小的框架能够获取接口调用的各种统计信息比如响应时间的最大值max、最小值min、平均值avg、百分位值percentile接口调用次数count、频率tps并且支持将统计结果以各种显示格式比如JSON格式、网页格式、自定义显示格式等输出到各种终端Console命令行、HTTP网页、Email、日志文件、自定义输出终端等以方便查看。
经过整理拆解之后的需求列表如下所示:
>
<p>接口统计信息:包括接口响应时间的统计信息,以及接口调用次数的统计信息等。<br>
统计信息的类型max、min、avg、percentile、count、tps等。<br>
统计信息显示格式JSON、HTML、自定义显示格式。<br>
统计信息显示终端Console、Email、HTTP网页、日志、自定义显示终端。</p>
经过挖掘,我们还得到一些隐藏的需求,如下所示:
>
统计触发方式:包括主动和被动两种。主动表示以一定的频率定时统计数据,并主动推送到显示终端,比如邮件推送。被动表示用户触发统计,比如用户在网页中选择要统计的时间区间,触发统计,并将结果显示给用户。
>
统计时间区间框架需要支持自定义统计时间区间比如统计最近10分钟的某接口的tps、访问次数或者统计12月11日00点到12月12日00点之间某接口响应时间的最大值、最小值、平均值等。
>
统计时间间隔对于主动触发统计我们还要支持指定统计时间间隔也就是多久触发一次统计显示。比如每间隔10s统计一次接口信息并显示到命令行中每间隔24小时发送一封统计信息邮件。
版本3已经实现了大部分的功能还有以下几个小的功能点没有实现。你可以将这些还没有实现的功能自己实现一下继续迭代出框架的第4个版本。
- 被动触发统计的方式,也就是需求中提到的通过网页展示统计信息。实际上,这部分代码的实现也并不难。我们可以复用框架现在的代码,编写一些展示页面和提供获取统计数据的接口即可。
- 对于自定义显示终端,比如显示数据到自己开发的监控平台,这就有点类似通过网页来显示数据,不过更加简单些,只需要提供一些获取统计数据的接口,监控平台通过这些接口拉取数据来显示即可。
- 自定义显示格式。在框架现在的代码实现中显示格式和显示终端比如Console、Email是紧密耦合在一起的比如Console只能通过JSON格式来显示统计数据Email只能通过某种固定的HTML格式显示数据这样的设计还不够灵活。我们可以将显示格式设计成独立的类将显示终端和显示格式的代码分离让显示终端支持配置不同的显示格式。具体的代码实现留给你自己思考我这里就不多说了。
## 非功能需求完善
Review完了功能需求的完善程度现在我们再来看版本3的非功能性需求的完善程度。在第25讲中我们提到针对这个框架的开发我们需要考虑的非功能性需求包括易用性、性能、扩展性、容错性、通用性。我们现在就依次来看一下这几个方面。
### 1.易用性
所谓的易用性顾名思义就是框架是否好用。框架的使用者将框架集成到自己的系统中时主要用到MetricsCollector和EmailReporter、ConsoleReporter这几个类。通过MetricsCollector类来采集数据通过EmailReporter、ConsoleReporter类来触发主动统计数据、显示统计结果。示例代码如下所示
```
public class PerfCounterTest {
public static void main(String[] args) {
MetricsStorage storage = new RedisMetricsStorage();
Aggregator aggregator = new Aggregator();
// 定时触发统计并将结果显示到终端
ConsoleViewer consoleViewer = new ConsoleViewer();
ConsoleReporter consoleReporter = new ConsoleReporter(storage, aggregator, consoleViewer);
consoleReporter.startRepeatedReport(60, 60);
// 定时触发统计并将结果输出到邮件
EmailViewer emailViewer = new EmailViewer();
emailViewer.addToAddress(&quot;wangzheng@xzg.com&quot;);
EmailReporter emailReporter = new EmailReporter(storage, aggregator, emailViewer);
emailReporter.startDailyReport();
// 收集接口访问数据
MetricsCollector collector = new MetricsCollector(storage);
collector.recordRequest(new RequestInfo(&quot;register&quot;, 123, 10234));
collector.recordRequest(new RequestInfo(&quot;register&quot;, 223, 11234));
collector.recordRequest(new RequestInfo(&quot;register&quot;, 323, 12334));
collector.recordRequest(new RequestInfo(&quot;login&quot;, 23, 12434));
collector.recordRequest(new RequestInfo(&quot;login&quot;, 1223, 14234));
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
从上面的使用示例中我们可以看出框架用起来还是稍微有些复杂的需要组装各种类比如需要创建MetricsStorage对象、Aggregator对象、ConsoleViewer对象然后注入到ConsoleReporter中才能使用ConsoleReporter。除此之外还有可能存在误用的情况比如把EmailViewer传递进了ConsoleReporter中。总体上来讲框架的使用方式暴露了太多细节给用户过于灵活也带来了易用性的降低。
为了让框架用起来更加简单能将组装的细节封装在框架中不暴露给框架使用者又不失灵活性可以自由组装不同的MetricsStorage实现类、StatViewer实现类到ConsoleReporter或EmailReporter也不降低代码的可测试性通过依赖注入来组装类方便在单元测试中mock我们可以额外地提供一些封装了默认依赖的构造函数让使用者自主选择使用哪种构造函数来构造对象。这段话理解起来有点复杂我把按照这个思路重构之后的代码放到了下面你可以结合着一块看一下。
```
public class MetricsCollector {
private MetricsStorage metricsStorage;
// 兼顾代码的易用性,新增一个封装了默认依赖的构造函数
public MetricsCollectorB() {
this(new RedisMetricsStorage());
}
// 兼顾灵活性和代码的可测试性,这个构造函数继续保留
public MetricsCollectorB(MetricsStorage metricsStorage) {
this.metricsStorage = metricsStorage;
}
// 省略其他代码...
}
public class ConsoleReporter extends ScheduledReporter {
private ScheduledExecutorService executor;
// 兼顾代码的易用性,新增一个封装了默认依赖的构造函数
public ConsoleReporter() {
this(new RedisMetricsStorage(), new Aggregator(), new ConsoleViewer());
}
// 兼顾灵活性和代码的可测试性,这个构造函数继续保留
public ConsoleReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) {
super(metricsStorage, aggregator, viewer);
this.executor = Executors.newSingleThreadScheduledExecutor();
}
// 省略其他代码...
}
public class EmailReporter extends ScheduledReporter {
private static final Long DAY_HOURS_IN_SECONDS = 86400L;
// 兼顾代码的易用性,新增一个封装了默认依赖的构造函数
public EmailReporter(List&lt;String&gt; emailToAddresses) {
this(new RedisMetricsStorage(), new Aggregator(), new EmailViewer(emailToAddresses));
}
// 兼顾灵活性和代码的可测试性,这个构造函数继续保留
public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) {
super(metricsStorage, aggregator, viewer);
}
// 省略其他代码...
}
```
现在,我们再来看下框架如何来使用。具体使用示例如下所示。看起来是不是简单多了呢?
```
public class PerfCounterTest {
public static void main(String[] args) {
ConsoleReporter consoleReporter = new ConsoleReporter();
consoleReporter.startRepeatedReport(60, 60);
List&lt;String&gt; emailToAddresses = new ArrayList&lt;&gt;();
emailToAddresses.add(&quot;wangzheng@xzg.com&quot;);
EmailReporter emailReporter = new EmailReporter(emailToAddresses);
emailReporter.startDailyReport();
MetricsCollector collector = new MetricsCollector();
collector.recordRequest(new RequestInfo(&quot;register&quot;, 123, 10234));
collector.recordRequest(new RequestInfo(&quot;register&quot;, 223, 11234));
collector.recordRequest(new RequestInfo(&quot;register&quot;, 323, 12334));
collector.recordRequest(new RequestInfo(&quot;login&quot;, 23, 12434));
collector.recordRequest(new RequestInfo(&quot;login&quot;, 1223, 14234));
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
如果你足够细心可能已经发现RedisMeticsStorage和EmailViewer还需要另外一些配置信息才能构建成功比如Redis的地址Email邮箱的POP3服务器地址、发送地址。这些配置并没有在刚刚代码中体现到那我们该如何获取呢
我们可以将这些配置信息放到配置文件中在框架启动的时候读取配置文件中的配置信息到一个Configuration单例类。RedisMetricsStorage类和EmailViewer类都可以从这个Configuration类中获取需要的配置信息来构建自己。
### 2.性能
对于需要集成到业务系统的框架来说,我们不希望框架本身代码的执行效率,对业务系统有太多性能上的影响。对于性能计数器这个框架来说,一方面,我们希望它是低延迟的,也就是说,统计代码不影响或很少影响接口本身的响应时间;另一方面,我们希望框架本身对内存的消耗不能太大。
对于性能这一点落实到具体的代码层面需要解决两个问题也是我们之前提到过的一个是采集和存储要异步来执行因为存储基于外部存储比如Redis会比较慢异步存储可以降低对接口响应时间的影响。另一个是当需要聚合统计的数据量比较大的时候一次性加载太多的数据到内存有可能会导致内存吃紧甚至内存溢出这样整个系统都会瘫痪掉。
针对第一个问题我们通过在MetricsCollector中引入Google Guava EventBus来解决。实际上我们可以把EventBus看作一个“生产者-消费者”模型或者“发布-订阅”模型采集的数据先放入内存共享队列中另一个线程读取共享队列中的数据写入到外部存储比如Redis中。具体的代码实现如下所示
```
public class MetricsCollector {
private static final int DEFAULT_STORAGE_THREAD_POOL_SIZE = 20;
private MetricsStorage metricsStorage;
private EventBus eventBus;
public MetricsCollector(MetricsStorage metricsStorage) {
this(metricsStorage, DEFAULT_STORAGE_THREAD_POOL_SIZE);
}
public MetricsCollector(MetricsStorage metricsStorage, int threadNumToSaveData) {
this.metricsStorage = metricsStorage;
this.eventBus = new AsyncEventBus(Executors.newFixedThreadPool(threadNumToSaveData));
this.eventBus.register(new EventListener());
}
public void recordRequest(RequestInfo requestInfo) {
if (requestInfo == null || StringUtils.isBlank(requestInfo.getApiName())) {
return;
}
eventBus.post(requestInfo);
}
public class EventListener {
@Subscribe
public void saveRequestInfo(RequestInfo requestInfo) {
metricsStorage.saveRequestInfo(requestInfo);
}
}
}
```
针对第二个问题解决的思路比较简单但代码实现稍微有点复杂。当统计的时间间隔较大的时候需要统计的数据量就会比较大。我们可以将其划分为一些小的时间区间比如10分钟作为一个统计单元针对每个小的时间区间分别进行统计然后将统计得到的结果再进行聚合得到最终整个时间区间的统计结果。不过这个思路只适合响应时间的max、min、avg及其接口请求count、tps的统计对于响应时间的percentile的统计并不适用。
对于percentile的统计要稍微复杂一些具体的解决思路是这样子的我们分批从Redis中读取数据然后存储到文件中再根据响应时间从小到大利用外部排序算法来进行排序具体的实现方式可以看一下《数据结构与算法之美》专栏。排序完成之后再从文件中读取第count*percentilecount表示总的数据个数percentile就是百分比99百分位就是0.99个数据就是对应的percentile响应时间。
这里我只给出了除了percentile之外的统计信息的计算代码如下所示。对于percentile的计算因为代码量比较大留给你自己实现。
```
public class ScheduleReporter {
private static final long MAX_STAT_DURATION_IN_MILLIS = 10 * 60 * 1000; // 10minutes
protected MetricsStorage metricsStorage;
protected Aggregator aggregator;
protected StatViewer viewer;
public ScheduleReporter(MetricsStorage metricsStorage, Aggregator aggregator, StatViewer viewer) {
this.metricsStorage = metricsStorage;
this.aggregator = aggregator;
this.viewer = viewer;
}
protected void doStatAndReport(long startTimeInMillis, long endTimeInMillis) {
Map&lt;String, RequestStat&gt; stats = doStat(startTimeInMillis, endTimeInMillis);
viewer.output(stats, startTimeInMillis, endTimeInMillis);
}
private Map&lt;String, RequestStat&gt; doStat(long startTimeInMillis, long endTimeInMillis) {
Map&lt;String, List&lt;RequestStat&gt;&gt; segmentStats = new HashMap&lt;&gt;();
long segmentStartTimeMillis = startTimeInMillis;
while (segmentStartTimeMillis &lt; endTimeInMillis) {
long segmentEndTimeMillis = segmentStartTimeMillis + MAX_STAT_DURATION_IN_MILLIS;
if (segmentEndTimeMillis &gt; endTimeInMillis) {
segmentEndTimeMillis = endTimeInMillis;
}
Map&lt;String, List&lt;RequestInfo&gt;&gt; requestInfos =
metricsStorage.getRequestInfos(segmentStartTimeMillis, segmentEndTimeMillis);
if (requestInfos == null || requestInfos.isEmpty()) {
continue;
}
Map&lt;String, RequestStat&gt; segmentStat = aggregator.aggregate(
requestInfos, segmentEndTimeMillis - segmentStartTimeMillis);
addStat(segmentStats, segmentStat);
segmentStartTimeMillis += MAX_STAT_DURATION_IN_MILLIS;
}
long durationInMillis = endTimeInMillis - startTimeInMillis;
Map&lt;String, RequestStat&gt; aggregatedStats = aggregateStats(segmentStats, durationInMillis);
return aggregatedStats;
}
private void addStat(Map&lt;String, List&lt;RequestStat&gt;&gt; segmentStats,
Map&lt;String, RequestStat&gt; segmentStat) {
for (Map.Entry&lt;String, RequestStat&gt; entry : segmentStat.entrySet()) {
String apiName = entry.getKey();
RequestStat stat = entry.getValue();
List&lt;RequestStat&gt; statList = segmentStats.putIfAbsent(apiName, new ArrayList&lt;&gt;());
statList.add(stat);
}
}
private Map&lt;String, RequestStat&gt; aggregateStats(Map&lt;String, List&lt;RequestStat&gt;&gt; segmentStats,
long durationInMillis) {
Map&lt;String, RequestStat&gt; aggregatedStats = new HashMap&lt;&gt;();
for (Map.Entry&lt;String, List&lt;RequestStat&gt;&gt; entry : segmentStats.entrySet()) {
String apiName = entry.getKey();
List&lt;RequestStat&gt; apiStats = entry.getValue();
double maxRespTime = Double.MIN_VALUE;
double minRespTime = Double.MAX_VALUE;
long count = 0;
double sumRespTime = 0;
for (RequestStat stat : apiStats) {
if (stat.getMaxResponseTime() &gt; maxRespTime) maxRespTime = stat.getMaxResponseTime();
if (stat.getMinResponseTime() &lt; minRespTime) minRespTime = stat.getMinResponseTime();
count += stat.getCount();
sumRespTime += (stat.getCount() * stat.getAvgResponseTime());
}
RequestStat aggregatedStat = new RequestStat();
aggregatedStat.setMaxResponseTime(maxRespTime);
aggregatedStat.setMinResponseTime(minRespTime);
aggregatedStat.setAvgResponseTime(sumRespTime / count);
aggregatedStat.setCount(count);
aggregatedStat.setTps(count / durationInMillis * 1000);
aggregatedStats.put(apiName, aggregatedStat);
}
return aggregatedStats;
}
}
```
### 3.扩展性
前面我们提到,框架的扩展性有别于代码的扩展性,是从使用者的角度来讲的,特指使用者可以在不修改框架源码,甚至不拿到框架源码的情况下,为框架扩展新的功能。
在刚刚讲到框架的易用性的时候我们给出了框架如何使用的代码示例。从示例中我们可以发现框架在兼顾易用性的同时也可以灵活地替换各种类对象比如MetricsStorage、StatViewer。举个例子来说如果我们要让框架基于HBase来存储原始数据而非Redis那我们只需要设计一个实现MetricsStorage接口的HBaseMetricsStorage类传递给MetricsCollector和ConsoleReporter、EmailReporter类即可。
### 4.容错性
容错性这一点也非常重要。对于这个框架来说,不能因为框架本身的异常导致接口请求出错。所以,对框架可能存在的各种异常情况,我们都要考虑全面。
在现在的框架设计与实现中采集和存储是异步执行即便Redis挂掉或者写入超时也不会影响到接口的正常响应。除此之外Redis异常可能会影响到数据统计显示也就是ConsoleReporter、EmailReporter负责的工作但并不会影响到接口的正常响应。
### 5.通用性
为了提高框架的复用性能够灵活应用到各种场景中框架在设计的时候要尽可能通用。我们要多去思考一下除了接口统计这样一个需求这个框架还可以适用到其他哪些场景中。比如是否还可以处理其他事件的统计信息比如SQL请求时间的统计、业务统计比如支付成功率等。关于这一点我们在现在的版本3中暂时没有考虑到你可以自己思考一下。
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要掌握的重点内容。
还记得吗在第25、26讲中我们提到针对性能计数器这个框架的开发要想一下子实现我们罗列的所有功能对任何人来说都是比较有挑战的。而经过这几个版本的迭代之后我们不知不觉地就完成了几乎所有的需求包括功能性和非功能性的需求。
在第25讲中我们实现了一个最小原型虽然非常简陋所有的代码都塞在一个类中但它帮我们梳理清楚了需求。在第26讲中我们实现了框架的第1个版本这个版本只包含最基本的功能并且初步利用面向对象的设计方法把不同功能的代码划分到了不同的类中。
在第39讲中我们实现了框架的第2个版本这个版本对第1个版本的代码结构进行了比较大的调整让整体代码结构更加合理、清晰、有逻辑性。
在第40讲中我们实现了框架的第3个版本对第2个版本遗留的细节问题进行了重构并且重点解决了框架的易用性和性能问题。
从上面的迭代过程我们可以发现大部分情况下我们都是针对问题解决问题每个版本都聚焦一小部分问题所以整个过程也没有感觉到有太大难度。尽管我们迭代了3个版本但目前的设计和实现还有很多值得进一步优化和完善的地方但限于专栏的篇幅继续优化的工作留给你自己来完成。
最后,我希望你不仅仅关注这个框架本身的设计和实现,更重要的是学会这个逐步优化的方法,以及其中涉及的一些编程技巧、设计思路,能够举一反三地用在其他项目中。
## 课堂讨论
最后,还是给你留一道课堂讨论题。
正常情况下ConsoleReporter的startRepeatedReport()函数只会被调用一次。但是,如果被多次调用,那就会存在问题。具体会有什么问题呢?又该如何解决呢?
欢迎在留言区写下你的答案,和同学一起交流和分享。如果有收获,也欢迎你把这篇文章分享给你的朋友。
[<img src="https://static001.geekbang.org/resource/image/8b/43/8b7e755ddb44e490848a052c5dc11043.jpg" alt="">](https://jinshuju.net/f/cZuRmd)