防止變異模式 | Protected Variations

這也是防止變異的一個案例,我們愛貓貓,無論是胖貓貓還是瘦貓貓,圖片來源
情境
高內聚、低耦合將日誌管理、資料存儲、通訊協定、商業邏輯模組化,這時出現一個問題,分離是分離了,但是如何串接呢 ?
是要直接另一個模組的類別嗎 ? 這樣哪天要換類別名稱、方法名稱我還要修改每個有關的模組 ?
舉個例子,日誌模組因為太多日誌,從原本打印在文件上到後面可能要串 ELK,我的類別名稱從 FileLogger 變成 ELKLogger;通訊協定用 HTTP 到後面為了加速微服務間通訊速度改成 grpc,相關的方法名都改動;又或者第三方資料提供有問題要換另一個資料源。
換不換呢 ? 換了影響範圍太大,不換又掛羊頭賣狗肉,而且每次改動模組內所有有調用的地方都要更改,哪來的低耦合 ?
防止變異
“ 識別或預計變化或不穩定的地方,分配職責用以在這些變化之外創建穩定介面。 ”
類別必須呈現具體功能,例如日誌紀錄是單純寫入文件,就會命名 FileLoggerWritter,如果命名 Logger 可能裡面就哪天多了很多不同的紀錄日誌方式。
而介面可以呈現高度抽象的概念,日誌異動可以是 Logger、資料存取可以是 Repository。模組提供高度抽象的介面給其他模組使用並規範行為,介面對於模組自身,具體方法會繼承後實作;對於使用該模組的其他模組,只在乎高度抽象的行為。
例如日誌紀錄就是 Logger.LogInfo、資料存取就是 Repository.Save、發佈事件 EventBus.Pulish。
以下是針對日誌功能防止變異的一種範例,根據以下範例,可以看到只要使用介面 ILogger,並且改變 LogManager 注入的類別就能替換,無須更改每個調用的地方。
// 定義高度抽象的介面 ILogger
public interface ILogger
{
void LogInfo(string message);
void LogError(string message);
void LogDebug(string message);
}
// FileLoggerWriter 負責具體的日誌記錄到文件中
public class FileLoggerWriter : ILogger
{
public void LogInfo(string message)
{
// 模擬將日誌寫入到文件中
Console.WriteLine($"[INFO]: {message}");
}
public void LogError(string message)
{
// 模擬將錯誤日誌寫入到文件中
Console.WriteLine($"[ERROR]: {message}");
}
public void LogDebug(string message)
{
// 模擬將除錯日誌寫入到文件中
Console.WriteLine($"[DEBUG]: {message}");
}
}
// DatabaseLoggerWriter 負責具體的日誌記錄到資料庫中
public class DatabaseLoggerWriter : ILogger
{
public void LogInfo(string message)
{
// 模擬將日誌寫入到資料庫中
Console.WriteLine($"Database Log [INFO]: {message}");
}
public void LogError(string message)
{
// 模擬將錯誤日誌寫入到資料庫中
Console.WriteLine($"Database Log [ERROR]: {message}");
}
public void LogDebug(string message)
{
// 模擬將除錯日誌寫入到資料庫中
Console.WriteLine($"Database Log [DEBUG]: {message}");
}
}
// 日誌管理模組
public class LogManager
{
private readonly ILogger _logger;
// 注入日誌具體的實作
public LogManager(ILogger logger)
{
_logger = logger;
}
public void LogInfo(string message)
{
_logger.LogInfo(message);
}
public void LogError(string message)
{
_logger.LogError(message);
}
public void LogDebug(string message)
{
_logger.LogDebug(message);
}
}
// 使用範例
public class Program
{
public static void Main()
{
// 使用文件日誌記錄
ILogger fileLogger = new FileLoggerWriter();
LogManager fileLogManager = new LogManager(fileLogger);
fileLogManager.LogInfo("這是一條普通資訊日誌");
fileLogManager.LogError("這是一條錯誤日誌");
fileLogManager.LogDebug("這是一條除錯日誌");
// 使用資料庫日誌記錄
ILogger dbLogger = new DatabaseLoggerWriter();
LogManager dbLogManager = new LogManager(dbLogger);
dbLogManager.LogInfo("這是一條資料庫普通資訊日誌");
dbLogManager.LogError("這是一條資料庫錯誤日誌");
dbLogManager.LogDebug("這是一條資料庫除錯日誌");
}
}
防止變異的概念是識別變化,並在這之上創建穩定的介面,穩定的介面防止了變化對於其它模組的影響,同時也保證了語意的正確性。
過度設計
“ 初學者傾向脆弱的設計。中等程度的開發者傾向過度想像的、靈活的、普遍的設計。專家級的開發者會理智地進行選擇。有時,簡單和脆弱的設計可能會與其變化所需的成本達成平衡。 ”
上面提供的案例,無論是日誌功能、資料存取會隨著業務發展而需要進行變化,那如果是不會變化呢?
Applying UML and Patterns 一書內針對變化點分成兩類
- 變化點:當前系統或需求中的變化,例如必須支持多個稅金計算接口、資料源。
- 進化點:預測將來可能會產生的變化,但並不存在現有的需求。
無論是變化點跟進化點,都可以使用防止變異,也就是建立穩定的介面避免變化,但是前提都是它會變化,才會需要防止變化。
或許是否會變化只有拉普拉斯的惡魔才知道,進化點未來可能變成變化點,就像總是會有黑天鵝飛出。但是抽象的成本,也就是易讀性與性能的犧牲如果比之後改動的成本要來的高,就有必要考慮是否需要防止變異。
在 Applying UML and Patterns 對於防止變異的警告有一段話。
“ 如果預測將來驗證或預測 ”複用” 的可能性十分不確定,則需要有克制和批判的態度。 ”
作者心得
在信息專家中,談論了分配職責的定義,控制器與創建者講了兩個有個具體職責的角色,高內聚與低耦合使我們將程式碼分成一個個優良的模塊,而防止變異是保持低耦合的關鍵,它讓模塊的介面保持穩定。
最近看完獵人動畫,然後在追我的英雄學院,還蠻好看的耶!