概要
所謂事件注入是我一時興起隨便杜撰的詞,其思想借鑒依賴注入。當然看到這個詞很多同學會想到AOP,這裡先不置可否。
依賴注入(Dependency Injection),是這樣一個過程:由於某客戶類只依賴於服務類的一個接口,而不依賴於具體服務類,所以客戶類只定義一個注入點。在程序運行過程中,客戶類不直接實例化具體服務類實例,而是客戶類的運行上下文環境或專門組件負責實例化服務類,然後將其注入到客戶類中,保證客戶類的正常運行。
也就是說依賴注入在我們的項目場景中充當一個解耦的角色。在項目結構中它可以解耦出一個模塊、一個服務。T2噬菌體同學在他的博文中描述了一個游戲打怪的例子來解釋依賴注入的作用。那麼我們同樣用打怪的例子來闡述下事件注入的場景。
詳解
關於原型的設計可以參考T2同學對游戲打怪的描述,我這裡直接略過看效果圖
下面我們撇開T2同學對依賴注入場景的設計。假設目前這個demo就是可行的。但是隨著游戲版本的更新,招式越來越多,效果越來越絢,規則越來越多。T2同學用依賴注入解決的重點是將Role和武器之間的依賴,封裝算法簇,讓它們之間可以互相替換,讓算法的變化獨立於使用算法的客戶類。
那麼浮現問題點,如果我要更新的並不是武器等因素,而是對流程的更新。例如玩dota的同學都知道,一個英雄他的技能前後搖擺的時間也是很重要的因素,好吧,我們給游戲添加技能前搖的設置,砍完怪的我還得獲得金幣,嗯,再添加一下攻擊後獲得金幣的內容。如何符合我們的OCP原則呢。於是,我們引入事件注入的概念。
首先我們來定義我們所需要的行為
/// <summary> /// 攻擊前事件 /// </summary> public static event EventHandler<EventArgs> BeforeAttackEvent; protected virtual void BeforeAttack(EventArgs e) { EventHandler<EventArgs> tmp = BeforeAttackEvent; if (tmp != null) tmp(this, e); } /// <summary> /// 攻擊後事件 /// </summary> public static event EventHandler<GameEventArgs> AttackedEvent; protected virtual void OnAttacked(GameEventArgs e) { EventHandler<GameEventArgs> tmp = AttackedEvent; if (tmp != null) tmp(this, e); }
這裡定義的僅僅是事件的句柄,如果在這裡就實現我們事件的實體也就違背了我們ocp的原則以及事件注入的概念。
這裡要提出說明的EventArgs 是包含事件數據的類的基類,如果說我們需要對注入的事件進行額外的信息處理,例如我需要獲得金幣,那麼金幣這個屬性需要在事件數據中說明
例如上述的攻擊後事件
/// <summary> /// 注入事件元素 /// </summary> public class GameEventArgs :EventArgs { public GameEventArgs() : this(0) { } public int Coin { get; set; } public GameEventArgs(int coin) { Coin = coin; } }
事件的框架有了,我們便在現有程序中找尋合適的注入點。這裡我選擇的是攻擊前後
/// <summary> /// 攻擊怪物 /// </summary> /// http://www.bianceng.cn/Programming/csharp/201410/45483.htm /// <param name="monster">被攻擊的怪物</param> public void Attack(Monster monster) { BeforeAttack(EventArgs.Empty); if (monster.HP <= 0) { Console.WriteLine("此怪物已死"); return; } if ("WoodSword" == WeaponTag) { monster.HP -= 20; if (monster.HP <= 0) { Console.WriteLine("攻擊成功!怪物" + monster.Name + "已死亡"); } else { Console.WriteLine("攻擊成功!怪物" + monster.Name + "損失20HP"); } } else if ("IronSword" == WeaponTag) { monster.HP -= 50; if (monster.HP <= 0) { Console.WriteLine("攻擊成功!怪物" + monster.Name + "已死亡"); } else { Console.WriteLine("攻擊成功!怪物" + monster.Name + "損失50HP"); } } else if ("MagicSword" == WeaponTag) { Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200; monster.HP -= loss; if (200 == loss) { Console.WriteLine("出現暴擊!!!"); } if (monster.HP <= 0) { Console.WriteLine("攻擊成功!怪物" + monster.Name + "已死亡"); } else { Console.WriteLine("攻擊成功!怪物" + monster.Name + "損失" + loss + "HP"); } } else { Console.WriteLine("角色手裡沒有武器,無法攻擊!"); } var e =new GameEventArgs(); OnAttacked(e); }
這些設計完成之後,我們需要的就是設計來注入些什麼事件。
[Extension("游戲規則_攻擊前", "1.0.0.0", "熬夜的蟲子")] public class GameRule { public GameRule() { Role.BeforeAttackEvent += BeforeAttack; } void BeforeAttack(object sender, EventArgs e) { Console.WriteLine("技能前搖 扭動身體..."); } }
[Extension("游戲規則_攻擊後", "1.0.0.0", "熬夜的蟲子")] public class GameRule2 { private readonly Random _random = new Random(); public GameRule2() { Role.AttackedEvent += Attacked; } void Attacked(object sender, EventArgs e) { var currentrole = sender as Role; int addcoin = _random.Next(1, 10); if (currentrole != null) { currentrole.Coin += addcoin; Console.WriteLine("本次攻擊獲得了..." + addcoin.ToString() + "個金幣,當前金幣為" + currentrole.Coin+"個"); } } }
事件定義完成後,我們接下來的步驟就是如何來注入到我們現有的框架中。
老道的同學可以發現在事件定義的過程中,我用了擴展屬性。沒錯,這個屬性就是實現注入環節的樞紐所在。
/// <summary> /// 事件注入實現 /// </summary> /// http://www.bianceng.cn/Programming/csharp/201410/45483.htm [AttributeUsage(AttributeTargets.Class)] public class ExtensionAttribute : Attribute { public ExtensionAttribute(string description, string version, string author) { _Description = description; _Version = version; _Author = author; } private readonly string _Description; public string Description { get { return _Description; } } private readonly string _Version; public string Version { get { return _Version; } } private readonly string _Author; public string Author { get { return _Author; } } }
如果想更深入的同學可以在設計一個事件注入管理類,添加一些是否可用,過期時間,版本,描述等等信息來管理注入事件。例如當管理類信息入庫,每次注入前check管理類的信息。這樣可以可視化並更方便管理注入的事件。
我們回到注入實現這個話題上來,如何利用這個擴展屬性,通過反射。
var di = new System.IO.DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); foreach (var item in di.GetFiles("*.dll", System.IO.SearchOption.TopDirectoryOnly)) { System.Reflection.Assembly assembly = System.Reflection.Assembly.LoadFrom(item.FullName); Type[] types = assembly.GetTypes(); foreach (Type type in types) { object[] attributes = type.GetCustomAttributes(typeof(Extension.ExtensionAttribute), false); foreach (object attribute in attributes) { assembly.CreateInstance(type.FullName); } } }
上面的程序更具我們定義的擴展屬性找到相關的注入事件方法類型,並生成實例。到此,一個簡單的注入流程就已經OK了。
我們來看一下效果。
注入事件的組件與源程序分開,源程序不依賴注入事件組件,可以任意的定義多個同類注入事件,將組件放入程序指定的目錄即可。
例如我們再新建一個注入事件組件
[Extension("游戲規則_攻擊後", "1.0.0.0", "熬夜的蟲子")] public class GameRule { public GameRule() { Role.AttackedEvent += Attacked; } void Attacked(object sender, EventArgs e) { Console.WriteLine("技能後擺 O(∩_∩)O哈哈哈~..."); } }
配置完成後,看下效果