寫程式碼難免出現失誤。在對某些已經發布的庫進行升級或者審查的時候,就有可能會發現一些介面名稱需要變更。比如,早期命名不符合特定規範,或者出現了難以發現的拼寫錯誤等。有錯當然是要改的,但是直接更名會影響到已釋出的介面。粗暴的名稱變更本質上是刪除了舊介面,建立了新介面,對 API 使用者來說極具破壞性 —— 使用者會發現所有用到這些介面的地方都編譯不過,或者不能執行了,這簡直就是一場災難。
本文主要以 C# 為例介紹對庫介面更名的處理 —— 在 Assembly 內部,直接使用“重新命名”重構方法,藉助 IDE/Editor 的能力就可以完成變更;但是對於開放出去的介面,就必須保持介面相容性,並宣告過期時間。
本文以 C# 為例,但是處理方式和重構思想是語言無關的!
處理不符合規範的類屬性命名
這一節主要是解決屬性更名的問題。方法更名和屬性更名是同樣的道理,後面不再贅述。
假設有如下定義:
public class ActionResult : IActionResult {
public int code { get; set; }
public string message { get; set; }
}
這個定義命名不符合“公開屬性使用 Pascal Case 命名規則”的規範。但作為一個已經被廣泛使用的庫,直接重新命名將產生巨大的破壞,所以這裡應該按正確的名稱新增屬性,並將舊的屬性宣告為過期。
public class ActionResult : IActionResult {
public int Code { get; set; }
public string Message { get; set; }
[Obsolete("因規範命名,將從 v3.4 版本中刪除,請使用 Code 代替")]
public int code {
get => Code;
set => Code = value;
}
[Obsolete("因規範命名,將從 v3.4 版本中刪除,請使用 Message 代替")]
public string message {
get => Message;
set => Message = value;
}
}
不過修改後的 ActionResult
會在序列化的時候出現問題。庫中使用了 Newtonsoft JSON 來得到 JSON,按專案中的 JSON 規範,鍵名會使用 Camel Case 命名規則,所以在配置中新增了 CamelCasePropertyNamesContractResolver
例項。那麼 ActionResult
的 Code
和 code
屬性都會被處理為 "code"
這個鍵名,於是發生了重名的衝突。
為了解決這個問題,可以為重名屬性中的一個新增 Newtonsoft JSON 的 [JsonIgnore]
特性宣告。為了保持相容性,應該在新加的 Code
和 Message
屬性上新增這一宣告,並在 Obsolete
描述和相關文件、發行說明中充分說明這一情況,告訴使用者這一變更可能產生序列化問題。
也許你已經發現了,在這個問題上,我們作為庫的發行方始終不能保持完全的相容,畢竟使用者使用什麼樣的序列化方式不是我們能決定的。也許使用者會序列化成 XML,也許使用者不使用 Newtonsoft JSON 而是使用別的 JSON 序列化工具,也許使用者是需要遍歷所有屬性來實現某種邏輯 —— 作為庫的發行方,我們除了通知和警告,無能為力 —— 這也是很多知名大庫會把一些奇怪的介面保留很久的原因之一。
處理不符合規範的介面屬性
注意到,上面示例的類實現了 IActionResult
介面。code
和 message
的命名規範問題其實是從介面引入的,所以我們還需要處理介面屬性的命名。處理介面屬性會更為繁瑣,但好在 C# 8.0 引入了介面預設實現的特性。假設之前 IActionResult
介面定義如下
public interface IActionResult {
int code { get; set; }
string message { get; set; }
}
使用預設實現新增 Code
和 Message
並宣告原來的屬性即將失效
public interface IActionResult {
int Code { get => code; set => code == value; }
string Message { get => message; set => message = value; }
[Obsolete("因規範命名,將從 v4.0 版本中刪除,請改為實現 Code 屬性")]
int code { get; set; }
[Obsolete("因規範命名,將從 v4.0 版本中刪除,請改為實現 Message 屬性")]
string message { get; set; }
}
變更之後,前述 ActionResult
規範化前後的程式碼都可以通過編譯。
這裡特別需要注意的是,介面變更影響會比實現(類)的影響更大,應該給予使用者足夠的修改時間來處理,通常會在下個大版本或者越過幾個大版本之後才實際刪除宣告為 Obsolete 的介面,進行破壞性的升級。
處理類名中的拼寫錯誤
在程式碼審查的過程中,發現類 ApiErrorActioin
的名字中出現了拼寫錯誤(追究為什麼會讓這樣的錯誤出現在已釋出的庫中是有必要的,因為這通常是因為過程管理中存在漏洞,但不是本文的研究內容)。
直接更改類名同樣是極具破壞性的,我們可以使用“重新命名”重構方法更正名稱,再新增一個與之存在繼承關係,但保持原來錯誤名稱的類來處理。
先使用重新命名重構方法將 ApiErrorActioin
更正名稱為 ApiErrorAction
。現在所有 Assembly 內的錯誤名稱都更正了。然後再定義一個繼承自 ApiErrorAction
的 ApiErrorActioin
(注意要實現與原來相同簽名的所有建構函式過載),並新增廢棄宣告。
[Obsolete("因為拼寫錯誤將從 v3.5 版本中刪除,請使用正確命名的 ApiErrorAction")]
class ApiErrorActioin : ApiErrorAction {
// 建構函式示例
public ApiErrorActioin(string name): base(name) { }
}
這裡需要特別注意的是,為保持相容性而存在的 ApiErrorActioin
除了保持原來的非私有建構函式介面之外,其他介面都可以直接從修正後的 ApiErrorAction
繼承而來。而且,這個要廢棄的 ApiErrorActioin
中不應該包含任何邏輯程式碼。
對介面更名也可以採用類似的方法。但要注意的是,這種處理方式新增了繼承層次。雖然一般情況下不會造成使用者的困擾,但是如果使用者的“反射”程式碼有涉及到繼承層次的邏輯,就有可能出現問題。因此這樣的變更同樣需要在文件中註明併發出警告。
小錯誤,大問題
對於人類來說,大腦的自動修正能力非常強(真正的人工智慧!)所以一個小小的拼寫錯誤或者大小字錯誤並不是什麼大問題。但對計算機來說,只差一個字元,那就是完全不同的兩個標識。更正拼寫本身是個小事,但對於公共庫介面的更名,可能會對使用者產生巨大的影響,需要非常謹慎地處理。
然而,即使我們非常謹慎的處理了能想到的各種相容性問題,差異仍然是不可避免的。你永遠不知道使用者會怎麼使用這個庫,所以也不知道使用者會因為變更遇到什麼奇怪的問題 —— 所以請重視文件和發行說明中的詳細描述和警告。問題是藏不住的,公佈它才是正確的選擇。