這系列相關博客,參考 設計模式之美
上一節課中,我們針對版本 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 aggregato
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,
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, St
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、日誌文件、自定義輸出終端等),以方便查看。
經過整理拆解之後的需求列表如下所示:
接口統計信息:包括接口響應時間的統計信息,以及接口調用次數的統計信息等。
統計信息的類型:max、min、avg、percentile、count、tps 等。
統計信息顯示格式:JSON、HTML、自定義顯示格式。
統計信息顯示終端:Console、Email、HTTP 網頁、日誌、自定義顯示終端。
經過挖掘,我們還得到一些隱藏的需求,如下所示:
統計觸發方式:包括主動和被動兩種。主動表示以一定的頻率定時統計數據,並主動推
送到顯示終端,比如郵件推送。被動表示用戶觸發統計,比如用戶在網頁中選擇要統計
的時間區間,觸發統計,並將結果顯示給用戶。
統計時間區間:框架需要支持自定義統計時間區間,比如統計最近 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,
consoleReporter.startRepeatedReport(60, 60);
// 定時觸發統計並將結果輸出到郵件
EmailViewer emailViewer = new EmailViewer();
emailViewer.addToAddress("[email protected]");
EmailReporter emailReporter = new EmailReporter(storage, aggregator, emailV
emailReporter.startDailyReport();
// 收集接口訪問數據
MetricsCollector collector = new MetricsCollector(storage);
collector.recordRequest(new RequestInfo("register", 123, 10234));
collector.recordRequest(new RequestInfo("register", 223, 11234));
collector.recordRequest(new RequestInfo("register", 323, 12334));
collector.recordRequest(new RequestInfo("login", 23, 12434));
collector.recordRequest(new RequestInfo("login", 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,
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<String> emailToAddresses) {
this(new RedisMetricsStorage(), new Aggregator(), new EmailViewer(emailToAd
}
// 兼顧靈活性和代碼的可測試性,這個構造函數繼續保留
public EmailReporter(MetricsStorage metricsStorage, Aggregator aggregator, St
super(metricsStorage, aggregator, viewer);
}
// 省略其他代碼...
}
現在,我們再來看下框架如何來使用。具體使用示例如下所示。看起來是不是簡單多了呢?
public class PerfCounterTest {
public static void main(String[] args) {
ConsoleReporter consoleReporter = new ConsoleReporter();
consoleReporter.startRepeatedReport(60, 60);
List<String> emailToAddresses = new ArrayList<>();
emailToAddresses.add("[email protected]");
EmailReporter emailReporter = new EmailReporter(emailToAddresses);
emailReporter.startDailyReport();
MetricsCollector collector = new MetricsCollector();
collector.recordRequest(new RequestInfo("register", 123, 10234));
collector.recordRequest(new RequestInfo("register", 223, 11234));
collector.recordRequest(new RequestInfo("register", 323, 12334));
collector.recordRequest(new RequestInfo("login", 23, 12434));
collector.recordRequest(new RequestInfo("login", 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 threadNumToSaveDat
this.metricsStorage = metricsStorage;
this.eventBus = new AsyncEventBus(Executors.newFixedThreadPool(threadNumToS
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*percentile(count 表示總的數據個數,percentile 就是百分比,99 百分位就是 0.99)個數據,就是對應的 percentile 響應時間。
這裏我只給出了除了 percentile 之外的統計信息的計算代碼,如下所示。對於 percentile 的計算,因爲代碼量比較大,留給你自己實現。
public class ScheduleReporter {
private static final long MAX_STAT_DURATION_IN_MILLIS = 10 * 60 * 1000; // 10
protected MetricsStorage metricsStorage;
protected Aggregator aggregator;
protected StatViewer viewer;
public ScheduleReporter(MetricsStorage metricsStorage, Aggregator aggregator
this.metricsStorage = metricsStorage;
this.aggregator = aggregator;
this.viewer = viewer;
}
protected void doStatAndReport(long startTimeInMillis, long endTimeInMillis)
Map<String, RequestStat> stats = doStat(startTimeInMillis, endTimeInMillis)
viewer.output(stats, startTimeInMillis, endTimeInMillis);
}
private Map<String, RequestStat> doStat(long startTimeInMillis, long endTimeI
Map<String, List<RequestStat>> segmentStats = new HashMap<>();
long segmentStartTimeMillis = startTimeInMillis;
while (segmentStartTimeMillis < endTimeInMillis) {
long segmentEndTimeMillis = segmentStartTimeMillis + MAX_STAT_DURATION_IN
if (segmentEndTimeMillis > endTimeInMillis) {
segmentEndTimeMillis = endTimeInMillis;
}
Map<String, List<RequestInfo>> requestInfos =
metricsStorage.getRequestInfos(segmentStartTimeMillis, segmentEnd
if (requestInfos == null || requestInfos.isEmpty()) {
continue;
}
Map<String, RequestStat> segmentStat = aggregator.aggregate(
requestInfos, segmentEndTimeMillis - segmentStartTimeMillis);
addStat(segmentStats, segmentStat);
segmentStartTimeMillis += MAX_STAT_DURATION_IN_MILLIS;
}
long durationInMillis = endTimeInMillis - startTimeInMillis;
Map<String, RequestStat> aggregatedStats = aggregateStats(segmentStats, du
return aggregatedStats;
}
private void addStat(Map<String, List<RequestStat>> segmentStats, Map<String, RequestStat> segmentStat) {
for (Map.Entry<String, RequestStat> entry : segmentStat.entrySet()) {
String apiName = entry.getKey();
RequestStat stat = entry.getValue();
List<RequestStat> statList = segmentStats.putIfAbsent(apiName, new Array
statList.add(stat);
}
}
private Map<String, RequestStat> aggregateStats(Map<String, List<RequestStat> ,long durationInMillis) {
Map<String, RequestStat> aggregatedStats = new HashMap<>();
for (Map.Entry<String, List<RequestStat>> entry : segmentStats.entrySet())
String apiName = entry.getKey();
List<RequestStat> 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() > maxRespTime) maxRespTime = stat.getMaxR
if (stat.getMinResponseTime() < minRespTime) minRespTime = stat.getMinR
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() 函數只會被調用一次。但是,如果被多次調用,那就會存在問題。具體會有什麼問題呢?又該如何解決呢?