組合式正規表示法
2009 年 7 月 24 日
撰寫可維護程式碼時,其中一個最強大的工具是將大型方法分解成命名良好的較小方法,這是一種肯特·貝克稱之為「組合式方法」的模式。
如果人們能詳細了解程式,然後將這些細節分塊成較高層級的結構,他們就能更快、更準確地閱讀程式。
-- 肯特·貝克
適用於方法的技巧通常也適用於其他事物。我遇到過幾次人們沒有這樣做的領域是正規表示法。
假設你有一個檔案,其中包含飯店連鎖店累積常客點數的規則。所有規則看起來都像
score 400 for 2 nights at Minas Tirith Airport
我們需要找出每一列的點數 (400)、晚數 (2) 和飯店名稱 (米那斯提力斯機場)。
這是一個正規表示法的明顯任務,我敢肯定你現在正在想,喔,是的,我們需要
const string pattern = @"^score\s+(\d+)\s+for\s+(\d+)\s+nights?\s+at\s+(.*)";
然後我們的三個值就會從群組中跳出來。
我不知道你是否能理解那個正規表示法是如何運作的,以及它是否正確。如果你像我一樣,你必須仔細查看這樣的正規表示法,才能找出它在說什麼。我經常發現自己必須計算括號,這樣我才能看到群組是如何排列的(在這種情況下其實不難,但我看過許多其他更困難的例子)。
你可能看過建議,可以採用這樣的模式並加上註解。(當你將其轉換成正規表示法時,通常需要切換。)這樣你就可以像這樣撰寫它。
protected override string GetPattern() { const string pattern = @"^score \s+ (\d+) # points \s+ for \s+ (\d+) # number of nights \s+ night s? #optional plural \s+ at \s+ (.*) # hotel name "; return pattern; } }
這比較容易理解,但註解從來無法完全滿足我。偶爾有人會指控我說註解很糟糕,而且不應該使用。這兩方面的說法都是錯的。註解並非不好,但通常有更好的選擇。我總是嘗試撰寫不需要註解的程式碼,通常透過良好的命名和結構。(我無法總是成功,但我覺得我成功的時候比失敗的時候多。)
人們通常不會嘗試為正規表示式建立結構,但我覺得這很有用。以下就是這樣做的一種方法。
const string scoreKeyword = @"^score\s+"; const string numberOfPoints = @"(\d+)"; const string forKeyword = @"\s+for\s+"; const string numberOfNights = @"(\d+)"; const string nightsAtKeyword = @"\s+nights?\s+at\s+"; const string hotelName = @"(.*)"; const string pattern = scoreKeyword + numberOfPoints + forKeyword + numberOfNights + nightsAtKeyword + hotelName;
我已將模式分解成邏輯區塊,並在最後將它們重新組合在一起。現在我可以查看最終的表達式,並了解表達式的基本區塊,深入了解每個正規表示式以查看詳細資訊。
這裡有另一個替代方案,旨在分隔空白,讓實際的正規表示式看起來更像是代幣。
const string space = @"\s+"; const string start = "^"; const string numberOfPoints = @"(\d+)"; const string numberOfNights = @"(\d+)"; const string nightsAtKeyword = @"nights?\s+at"; const string hotelName = @"(.*)"; const string pattern = start + "score" + space + numberOfPoints + space + "for" + space + numberOfNights + space + nightsAtKeyword + space + hotelName;
我發現這讓個別代幣更清楚,但所有這些空白變數讓整體結構更難理解。所以我比較喜歡前一個。
但這確實引發了一個問題。所有元素都以空白分隔,而在模式中放入大量的空白變數或 \s+
感覺很糟糕。將正規表示式分解成子字串的好處是,現在我可以使用程式設計邏輯來提出更符合我特定目的的抽象概念。我可以撰寫一個方法,這個方法會擷取子字串並使用空白將它們串接起來。
private String composePattern(params String[] arg) { return "^" + String.Join(@"\s+", arg); }
使用這個方法後,我便有了。
const string numberOfPoints = @"(\d+)"; const string numberOfNights = @"(\d+)"; const string hotelName = @"(.*)"; const string pattern = composePattern("score", numberOfPoints, "for", numberOfNights, "nights?", "at", hotelName);
您可能不會使用這些替代方案中的任何一個,但我強烈建議您思考如何讓正規表示式更清楚。程式碼不應該需要找出,而應該只是閱讀。
更新
在這個討論中,我已將組成正規表示式的元素設為局部變數。一種變化是擷取常用的正規表示式元素,並更廣泛地使用它們。這對於在許多地方需要使用的常用正規表示式來說很方便。我的同事卡洛斯·維萊拉評論說,需要注意的一件事是,如果這些片段沒有格式化良好,即在另一個片段中有一個已關閉的開括號。這可能會很難除錯。我沒有覺得有必要這樣做,所以沒有遇到這個問題。
有些人提到使用流暢介面(內部 DSL)作為更具可讀性的 正規表示式替代方案。我認為這是兩回事。如果正規表示式很小,它們不會困擾我,實際上我更喜歡使用小型的正規表示式,而不是等效的流暢介面。關鍵在於組合,你可以使用這兩種技術來進行組合。
有些人提到命名擷取群組。就像註解一樣,我發現這些比原始正規表示式更好,但仍然覺得組合結構更具可讀性。組合的重點在於將整體正規表示式分解成較小的部分,以便更容易理解。
重新發布於 2014 年 7 月 31 日