C# Command命令(行為型模式)+隊列 實現事務,帶異步命令重試機制和生命周期


一、簡介

耦合是軟件不能抵御變變化的根本性原因,不僅實體對象與實體對象之間有耦合關系(如創建性設計模式存在的原因),對象和行為之間也存在耦合關系.

 

二、實戰

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,或者不當之處,請在下面指正,感謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM