一、簡介
耦合是軟件不能抵御變變化的根本性原因,不僅實體對象與實體對象之間有耦合關系(如創建性設計模式存在的原因),對象和行為之間也存在耦合關系.
二、實戰
1、常規開發中,我們經常會在控制器中或者Main方法中調用多個對象,進行批量的操作(完成一次事務性的操作),像下面這樣:
/// <summary> /// 設計模式之Command命令模式 /// </summary> public class Program { public static void Main(string[] args) { //模擬持久化內容到文檔中 var doc = new Document(); var result=doc.WriteText("小超"); if (result) { //持久化成功,記錄日志 var log = new Log(); var logRes = log.WriteLog("小超寫入到文檔中成功"); if (logRes) { Console.WriteLine("事務性操作成功!"); } else { Console.WriteLine("事務性操作失敗!"); } } Console.ReadKey(); } } /// <summary> /// 模擬文檔對象 /// </summary> public class Document { public bool WriteText(string content) { //持久化到對應的數據容器 return true; } } /// <summary> /// 模擬日志對象 /// </summary> public class Log { public bool WriteLog(string logContent) { //持久化到對應的數據容器 return true; } }

ok,上面的硬編碼可以很好的完成需求,但是如果中間發生異常,上的代碼將無法支持撤銷和回滾.注:這里假設持久化到文檔和持久化到日志是一個事務操作(即他們兩個必須同時成功,這個操作才算完成,否則就需要回滾).關於事務,和數據庫操作一樣,使用過SqlTransaction對象的都知道下面這幾個方法:

如果我們傳入的批量操作Sql(一般只用於增刪改,查可以忽略)中有一個發生異常,那么我們就可以調用Dispose方法(釋放資源)和Rollback方法,來對事務進行回滾.但是我們上面中的示例明顯不支持,所以這個時候我們就需要引入Command命令模式,將兩個操作合並為一個操作.在進行最終的提交,失敗則回滾,如果涉及非托管資源,不論成功如否都需要釋放資源.所以升級代碼如下:
/// <summary> /// 設計模式之Command命令模式 /// </summary> public class Program { public static void Main(string[] args) { var document = new Document("小超1"); var command = new DocumentCommand(document); var document_3 = new Document("小超3"); var command_3 = new DocumentCommand(document_3); var document_1 = new Document("小超"); var command_1 = new DocumentCommand(document_1); var log = new Log("日志內容"); var command_2 = new LogCommand(log); var manager = new CommandManager(); manager.Commands.Enqueue(command_3); manager.Commands.Enqueue(command); manager.Commands.Enqueue(command_1); manager.Commands.Enqueue(command_2); manager.Execute(); Console.ReadKey(); } } /// <summary> /// 模擬文檔對象 /// </summary> public class Document { private Document() { } public string Content { get; } public Document(string content) { Content = content; } public bool WriteText(string content) { //持久化到對應的數據容器 if (content == "小超") throw new Exception("寫入文檔異常"); else return true; } } /// <summary> /// 模擬日志對象 /// </summary> public class Log { private Log() { } public string Content { get; set; } public Log(string logContent) { Content = logContent; } public bool WriteLog() { //持久化到對應的數據容器 return true; } } /// <summary> /// 命令約束 /// </summary> public interface ICommand { void Execute(); void Undo(); void Redo(); } /// <summary> /// 命令基類 /// </summary> /// <typeparam name="T"></typeparam> public class Command<T> { /// <summary> /// 命令Id,方便回回滾數據 /// </summary> protected Guid CommandId { get; set; } = Guid.NewGuid(); } /// <summary> /// 文檔操作命令對象 /// </summary> public class DocumentCommand : Command<Guid>,ICommand { /// <summary> /// 模擬文檔內容數據容器 /// </summary> public Dictionary<Guid, Document> DocumentContents { get; set; } = new Dictionary<Guid, Document>(); private DocumentCommand() {} private Document _document; public DocumentCommand(Document document) { _document = document; } public void Execute() { //模擬持久化到數據容器中 try { Console.WriteLine("當前命令Id:{0},參數內容:{1}", CommandId, JsonConvert.SerializeObject(_document)); _document.WriteText(_document.Content); DocumentContents.Add(CommandId, _document); Console.WriteLine("當前命令執行成功,命令Id:{0},參數內容:{1}", CommandId, JsonConvert.SerializeObject(_document)); } catch (Exception ex) { Console.WriteLine("當前命令執行失敗,命令Id:{0},參數內容:{1},異常信息:{2}", CommandId, JsonConvert.SerializeObject(_document),ex.Message); throw ex; } } public void Redo() { //重新執行Execute方法 Execute(); } /// <summary> /// 事物操作,如果后面的操作發生異常,這里也需要回滾 /// </summary> public void Undo() { var value = default(Document); if (DocumentContents.ContainsKey(CommandId)) { value = DocumentContents[CommandId]; } else { Console.WriteLine("文檔命令執行發生異常,當前命令Id:{0},當前文檔信息:{1}", CommandId, JsonConvert.SerializeObject(_document));//記錄日志 } if (!DocumentContents.Remove(CommandId)) Console.WriteLine("文檔命令執行發生異常,當前命令Id:{0},當前文檔信息:{1}", CommandId, JsonConvert.SerializeObject(_document));//記錄日志 else Console.WriteLine("事物回滾,將插入到文檔中的內容刪除,被刪除的對象是:{0}", JsonConvert.SerializeObject(_document));//記錄日志 } } /// <summary> /// 日志操作命令 /// </summary> public class LogCommand: Command<Guid>, ICommand { /// <summary> /// 模擬文檔內容數據容器 /// </summary> public Dictionary<Guid, string> LogContents { get; set; } = new Dictionary<Guid, string>(); private LogCommand() { } private Log _log; public LogCommand(Log log) { _log = log; } public void Execute() { //模擬持久化到數據容器中 try { _log.WriteLog(); LogContents.Add(CommandId, _log.Content); } catch (Exception ex) { throw ex; } } public void Redo() { //重新執行Execute方法 Execute(); } /// <summary> /// 事物操作,如果后面的操作發生異常,這里也需要回滾 /// </summary> public void Undo() { var value = ""; if (LogContents.ContainsKey(CommandId)) { value = LogContents[CommandId]; } else { Console.WriteLine("日志命令執行發生異常,當前命令Id:{0},當前日志信息:{1}", CommandId, JsonConvert.SerializeObject(_log));//記錄日志 } if (!LogContents.Remove(CommandId)) Console.WriteLine("日志命令執行發生異常,當前命令Id:{0},當前日志信息:{1}", CommandId, JsonConvert.SerializeObject(_log));//記錄日志 else Console.WriteLine("事物回滾,將插入到日志中的內容刪除,被刪除的內容是:{0}", value);//記錄日志 } } /// <summary> /// 命令管理器 /// </summary> public class CommandManager { public Queue<ICommand> Commands = new Queue<ICommand>(); public Queue<ICommand> UndoCommands = new Queue<ICommand>(); public Queue<ICommand> SuccessCommands = new Queue<ICommand>(); /// <summary> /// 命令執行 /// </summary> public void Execute() { foreach (var command in Commands) { try { Console.WriteLine("命令開始執行,當前命令名稱:{0}", command.GetType().Name);//記錄日志 command.Execute(); Console.WriteLine("命令執行結束,當前命令名稱:{0}", command.GetType().Name);//記錄日志 Console.WriteLine(); SuccessCommands.Enqueue(command); } catch { Console.WriteLine("命令執行結束,當前命令名稱:{0}", command.GetType().Name);//記錄日志 Undo(command); Redo(); RollBack(); break; } } } public void Undo(ICommand command) { if (CanUndo) { UndoCommands.Enqueue(command); } else { Console.WriteLine("當前命令隊列沒有排隊的命令!");//記錄日志 } } /// <summary> /// 命令重試 /// </summary> public void Redo() { //這個最大重試次數,建議讀取配置文件 var tryCount = 3; var time = 0; if (CanRedo) { var command = UndoCommands.Dequeue(); //開啟一個新線程進行重試操作,重試3次,失敗則發送郵件通知,或者記錄日志 Task.Run(() => { var index = 1; while (true) { Interlocked.Add(ref time, index); try { command.Redo(); } catch (Exception ex) { if (time == tryCount) { Console.WriteLine("當前命令:{0},重試{1}次后執行失敗,請檢查原因!異常信息如下:{2}", typeof(DocumentCommand).Name, tryCount, ex.Message); break; } } } }); } } /// <summary> /// 事務回滾 /// </summary> public void RollBack() { Console.WriteLine(); if (SuccessCommands.Count > 0) { Console.WriteLine("事物發生異常,記錄開始回滾!"); foreach (var command in SuccessCommands) { command.Undo(); } Console.WriteLine("事物回滾結束"); } else { Console.WriteLine("當前沒有需要回滾的操作!"); } Console.WriteLine(); } private bool CanUndo { get { return Commands.Count > 0; } } private bool CanRedo { get { return UndoCommands.Count > 0; } } }

注:上面所有的Console.WriteLine都需要改成異步日志功能.異步重試中的Concosole.WriteLine因為Ms做了同步處理,所以輸出可能會異常.所以異步寫日志比較合理.
這里在提一點,如果需要實現多個命令組成一個復合命令,可以使用Composite組合模式將多個命令組成一個復合命令,來實現.后續的隨筆中我會介紹.
文章中的代碼有bug,或者不當之處,請在下面指正,感謝!
