平行變更
2014 年 5 月 13 日
對介面進行影響所有使用者的變更需要兩種思考模式:實作變更本身,然後更新所有使用情況。當您嘗試同時執行這兩項操作時,這可能會很困難,特別是當變更位於具有多個或外部客戶端的 PublishedInterface 上時。
平行變更,也稱為擴充和收縮,是一種以安全的方式實作介面向後不相容變更的模式,方法是將變更分解為三個不同的階段:擴充、遷移和收縮。
為了瞭解此模式,我們使用一個簡單的 Grid
類別範例,它使用一對 x
和 y
整數座標儲存和提供其儲存格的資訊。儲存格在內部儲存在一個二維陣列中,客戶端可以使用 addCell()
、fetchCell()
和 isEmpty()
方法與網格互動。
class Grid { private Cell[][] cells; … public void addCell(int x, int y, Cell cell) { cells[x][y] = cell; } public Cell fetchCell(int x, int y) { return cells[x][y]; } public boolean isEmpty(int x, int y) { return cells[x][y] == null; } }
作為重構的一部分,我們偵測到 x
和 y
是 DataClump,並決定引入一個新的 Coordinate
類別。但是,這將會是 Grid
類別客戶端的向後不相容變更。我們決定應用平行變更模式,而不是一次變更所有方法和內部資料結構。
在擴充階段,您擴充介面以支援舊版本和新版本。在我們的範例中,我們引入一個新的 Map<Coordinate, Cell>
資料結構和新的方法,這些方法可以在不變更現有程式碼的情況下接收 Coordinate
執行個體。
class Grid { private Cell[][] cells; private Map<Coordinate, Cell> newCells; … public void addCell(int x, int y, Cell cell) { cells[x][y] = cell; } public void addCell(Coordinate coordinate, Cell cell) { newCells.put(coordinate, cell); } public Cell fetchCell(int x, int y) { return cells[x][y]; } public Cell fetchCell(Coordinate coordinate) { return newCells.get(coordinate); } public boolean isEmpty(int x, int y) { return cells[x][y] == null; } public boolean isEmpty(Coordinate coordinate) { return !newCells.containsKey(coordinate); } }
現有的客戶端將繼續使用舊版本,而新的變更可以逐步引入,而不會影響它們。
在遷移階段,您將使用舊版本的所有客戶端更新為新版本。這可以逐步進行,對於外部客戶端來說,這將是最長的階段。
一旦所有用法都已轉移到新版本,您執行合約階段以移除舊版本並變更介面,使其僅支援新版本。
在我們的範例中,由於在刪除舊方法後不再使用內部二維陣列,我們可以安全地移除該資料結構,並將 newCells
重新命名回 cells
。
class Grid { private Map<Coordinate, Cell> cells; … public void addCell(Coordinate coordinate, Cell cell) { cells.put(coordinate, cell); } public Cell fetchCell(Coordinate coordinate) { return cells.get(coordinate); } public boolean isEmpty(Coordinate coordinate) { return !cells.containsKey(coordinate); } }
這個模式在實作 持續傳遞 時特別有用,因為它允許您的程式碼在這些三個階段中的任何一個階段發佈。它也透過允許您轉移客戶端並逐步測試新版本來降低變更風險。
即使您控制介面的所有用法,遵循這個模式仍然有用,因為它可以防止您一次在整個程式碼庫中散佈中斷。轉移階段可以很短,但它是依賴編譯器找出所有需要修正用法的替代方案。
這個模式的一些範例應用是
- 重構:在變更方法或函式簽章時,特別是在進行 長期重構 或變更 已發佈介面 時。在重構期間,這個模式的一種變體實作是根據新的 API 實作舊方法,並使用 內聯方法 一次更新所有用法。將舊方法委派給新方法也是將轉移階段分解成更小且更安全的步驟的一種方式,允許您在變更公開 API 給客戶端之前先變更內部實作。當轉移階段較長時,這很有用,因此您不必維護兩個獨立的實作。
- 資料庫重構:這是 演化式資料庫設計 的關鍵組成部分。大多數資料庫重構遵循平行變更模式,其中轉移階段是原始架構和新架構之間的過渡期,直到所有資料庫存取程式碼都已更新為使用新架構。
- 部署:部署技術,例如金絲雀發布和 藍綠部署,是並行變更模式的應用,在該模式中,舊版和新版程式碼並排部署,並逐步將使用者從一個版本遷移到另一個版本,從而降低變更風險。在 微服務 架構中,它還可以消除由於服務之間的版本依賴關係而導致的複雜部署編排的需要。
- 遠端 API 演進:當您無法以向後相容的方式進行變更時,可以使用並行變更來演進遠端 API(例如 REST 網路服務)。這是使用公開 API 中的明確版本的一種替代方法。當對特定端點上 API 接受或傳回的有效負載進行變更時,您可以套用此模式,或者您可以引入新的端點來區分舊版和新版。在同一端點中使用並行變更的情況下,遵循 波斯特定律 是一種避免消費者在負載擴充時中斷的良好技術。
在遷移階段,可以使用 功能標記 來控制使用哪個版本的介面。客戶端上的功能切換允許它與供應商的新版本向前相容,這將供應商的發布與客戶端解耦。
實作 抽象分支 時,並行變更是一種在客戶端和供應商之間引入抽象層的良好方法。它也是在不將抽象層作為供應商側替換的接縫處引入大規模變更的另一種方法。但是,當您有大量客戶端時,使用抽象分支是縮小變更範圍並減少遷移階段混淆的更好策略。
使用並行變更的缺點是,在遷移階段,供應商必須支援兩個不同的版本,並且客戶端可能會對哪個版本是新版本或舊版本感到困惑。如果您沒有執行合約階段,您最終可能會陷入比開始時更糟的狀態,因此您需要有紀律才能成功完成轉換。新增棄用註解、文件或 TODO 註解可能有助於告知客戶端和其他處理相同程式碼庫的開發人員正在更換哪個版本。
進一步閱讀
Industrial Logic 的 重構相簿 編寫文件並示範執行平行變更的範例。
致謝
此技術最初由 Joshua Kerievsky 於 2006 年記錄為重構策略,並於 2010 年精實軟體與系統研討會中發表他的演講 The Limited Red Society。
感謝 Joshua Kerievsky 對本文章初稿提供回饋。此外,感謝許多 Thoughtworks 同事提供回饋:Greg Dutcher、Badrinath Janakiraman、Praful Todkar、Rick Carragher、Filipe Esperandio、Jason Yip、Tushar Madhukar、Pete Hodgson 和 Kief Morris。