代码改变世界

【C#|.NET】从控制反转(依赖注入)想到事件注入 (非AOP)

2011-12-21 15:16  熬夜的虫子  阅读(6966)  评论(9编辑  收藏  举报

前文

事件注入的想法是由依赖注入所联想到

依赖注入不算什么吸引人的话题 本篇就不详说了 不过有闲暇时间的机会不妨按照自己的兴趣去摸索、研究一些东西,也是一种乐子。

在抓虫系列里简单的描述一下依赖注入在项目中的应用场景抓虫(五) 浅谈依赖注入与控制反转

关于依赖注入推荐T2噬菌体同学的一篇文章 依赖注入那些事儿

关于事件注入已添加进我的设计模式 【系列索引】结合项目实例 回顾传统设计模式 打造属于自己的模式类系列


 概要

所谓事件注入是我一时兴起随便杜撰的词,其思想借鉴依赖注入。当然看到这个词很多同学会想到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>         
        /// <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>
    [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哈哈哈~...");
            }
        }

配置完成后,看下效果 


 本篇到此 希望对大家有帮助