設計模式之美 - 40 | 運用學過的設計原則和思想完善之前講的性能計數器項目(下)

這系列相關博客,參考 設計模式之美

設計模式之美 - 40 | 運用學過的設計原則和思想完善之前講的性能計數器項目(下)

上一節課中,我們針對版本 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() 函數只會被調用一次。但是,如果被多次調用,那就會存在問題。具體會有什麼問題呢?又該如何解決呢?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章