【wwwmgm8001】依赖注入那些事儿

3.3 多态的活性与依赖注入

3.3.2 不同活性多态的依赖注入选择

一般来说,高活多态性适合使用Setter注入。因为Setter注入最灵活,也是唯一允许在同一客户类实例运行期间更改服务类的注入方式。并且这种注入一般由上下文环境通过Setter的参数指定服务类类型,方便灵活,适合频繁变化的高活多态性。

对于中活多态性,则适合使用Constructor注入。因为Constructor注入也是由上下文环境通过Construtor的参数指定服务类类型,但一点客户类实例化后,就不能进行再次注入,保证了其时间稳定性。

而对于低活多态性,则适合使用Dependency
Locate并配合文件配置进行依赖注入,或Setter、Constructor配合配置文件注入,因为依赖源来自文件,如果要更改服务类,则需要更改配置文件,一则确保了低活多态性的时间和空间稳定性,二是更改配置文件的方式方便于大规模服务类替换。(因为低活多态性一旦改变行为,往往规模很大,如替换整个数据访问层,如果使用Setter和Construtor传参,程序中需要改变的地方不计其数)

本质上,这种选择是因为不同的依赖注入类型有着不同的稳定性,大家可以细细体会“活性”、“稳定性”和“依赖注入类型”之间密切的关系。

3.1.2 构造注入

另外一种依赖注入方式,是通过客户类的构造函数,向客户类注入服务类实例。

构造注入(Constructor
Injection)是指在客户类中,设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。

wwwmgm8001 1

图3.3 构造注入示意

图3.3是构造注入的示意图,可以看出,与Setter注入很类似,只是注入点由Setter方法变成了构造方法。这里要注意,由于构造注入只能在实例化客户类时注入一次,所以一点注入,程序运行期间是没法改变一个客户类对象内的服务类实例的。

由于构造注入和Setter注入的IServiceClass,ServiceClassA和ServiceClassB是一样的,所以这里给出另外ClientClass类的示例代码。

wwwmgm8001 2wwwmgm8001 3

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConstructorInjection
{
    internal class ClientClass
    {
        private IServiceClass _serviceImpl;

        public ClientClass(IServiceClass serviceImpl)
        {
            this._serviceImpl = serviceImpl;
        }

        public void ShowInfo()
        {
            Console.WriteLine(_serviceImpl.ServiceInfo());
        }
    }
}

View Code

 

可以看到,唯一的变化就是构造函数取代了Set_ServiceImpl方法,成为了注入点。

4.3.2 Unity

wwwmgm8001 4

对于小型项目和讲求敏捷的团队,Spring.NET可能有点太重量级,那么可以选择轻量级的Unity。Unity是微软patterns
& practices团队推出的轻量级框架,非常好用,目前最新版本是1.2。

Unity的官方网站是:

参考文献

[1]  Shivprasad koirala, Design pattern – Inversion of control and
Dependency injection,

[2]  Martin Fowler, Inversion of Control Containers and the Dependency
Injection pattern,

[3]  Paul, IoC Types,

[4]  Eric Freeman, Elisabeth Freeman. Head First Design Patterns.
O’Reilly Media, 2004. ISBN 0596007142

[5]  Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design
Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley,

  1. ISBN 0201633612

[6]  Patrick Smacchia 著,施凡等 译,C#和.NET2.0
平台、语言与框架。2008.1,人民邮电出版

[7]  Jeffrey Rechter 著,CLR via C#(影印版)。2008.8,人民邮电出版

 

本文基于署名-非商业性使用
3.0许可协议发布,欢迎转载,演绎,但是必须保留本文的署名张洋(包含链接),且不得用于商业目的。如您有任何疑问或者授权方面的协商,请与我联系。

 

From

1.1 讨论会

话说有一个叫IGame的游戏公司,正在开发一款ARPG游戏(动作&角色扮演类游戏,如魔兽世界、梦幻西游这一类的游戏)。一般这类游戏都有一个基本的功能,就是打怪(玩家攻击怪物,借此获得经验、虚拟货币和虚拟装备),并且根据玩家角色所装备的武器不同,攻击效果也不同。这天,IGame公司的开发小组正在开会对打怪功能中的某一个功能点如何实现进行讨论,他们面前的大屏幕上是这样一份需求描述的ppt:

wwwmgm8001 5

图1.1 需求描述ppt

各个开发人员,面对这份需求,展开了热烈的讨论,下面我们看看讨论会上都发生了什么。

4.2 IoC Container的分类

前面曾经讨论了三种依赖注入方式,但是,想通过方式对IoC
Container进行分类很困难,因为现在IoC
Container都设计很完善,几乎支持所有依赖注入方式。不过,根据不同框架的特性和惯用法,还是可以讲IoC
Container分为两个大类。

3.2 反射与依赖注入

回想上面Dependency Locate的例子,我们虽然使用了多态性和Abstract
Factory,但对OCP贯彻的不够彻底。在理解这点前,朋友们一定要注意潜在扩展在哪里,潜在会出现扩展的地方是“新的组件系列”而不是“组件种类”,也就是说,这里我们假设组件就三种,不会增加新的组件,但可能出现新的外观系列,如需要加一套Ubuntu风格的组件,我们可以新增UbuntuWindow、UbuntuButton、UbuntuTextBox和UbuntuFactory,并分别实现相应接口,这是符合OCP的,因为这是扩展。但我们除了修改配置文件,还要无可避免的修改FactoryContainer,需要加一个分支条件,这个地方破坏了OCP。依赖注入本身是没有能力解决这个问题的,但如果语言支持反射机制(Reflection),则这个问题就迎刃而解。

我们想想,现在的难点是出在这里:对象最终还是要通过“new”来实例化,而“new”只能实例化当前已有的类,如果未来有新类添加进来,必须修改代码。如果,我们能有一种方法,不是通过“new”,而是通过类的名字来实例化对象,那么我们只要将类的名字作为配置项,就可以实现在不修改代码的情况下,加载未来才出现的类。所以,反射给了语言“预见未来”的能力,使得多态性和依赖注入的威力大增。

下面是引入反射机制后,对上面例子的改进:

wwwmgm8001 6

图3.7 引入反射机制的Dependency Locate

可以看出,引入反射机制后,结构简单了很多,一个反射工厂代替了以前的一堆工厂,Factory
Container也不需要了。而且以后有新组件系列加入时,反射工厂是不用改变的,只需改变配置文件就可以完成。下面给出反射工厂和配置文件的代码。

 

wwwmgm8001 7wwwmgm8001 8View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Xml;
 
namespace DependencyLocate
{
    internal static class ReflectionFactory
    {
        private static String _windowType;
        private static String _buttonType;
        private static String _textBoxType;
 
        static ReflectionFactory()
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(“”);
            XmlNode xmlNode = xmlDoc.ChildNodes[1].ChildNodes[0];
 
            _windowType = xmlNode.ChildNodes[0].Value;
            _buttonType = xmlNode.ChildNodes[1].Value;
            _textBoxType = xmlNode.ChildNodes[2].Value;
        }
 
        public static IWindow MakeWindow()
        {
            return Assembly.Load(“DependencyLocate”).CreateInstance(“DependencyLocate.” + _windowType) as IWindow;
        }
 
        public static IButton MakeButton()
        {
            return Assembly.Load(“DependencyLocate”).CreateInstance(“DependencyLocate.” + _buttonType) as IButton;
        }
 
        public static ITextBox MakeTextBox()
        {
            return Assembly.Load(“DependencyLocate”).CreateInstance(“DependencyLocate.” + _textBoxType) as ITextBox;
        }
    }
}

 

配置文件如下:

 

wwwmgm8001 9wwwmgm8001 10View Code

<?xml version=”1.0″ encoding=”utf-8″ ?>
<config>
    <window>MacWindow</window>
    <button>MacButton</button>
    <textBox>MacTextBox</textBox>
</config>

 

反射不仅可以与Dependency Locate结合,也可以与Setter
Injection与Construtor
Injection结合。反射机制的引入,降低了依赖注入结构的复杂度,使得依赖注入彻底符合OCP,并为通用依赖注入框架(如Spring.NET中的IoC部分、Unity等)的设计提供了可能性。

【wwwmgm8001】依赖注入那些事儿。4.3.2 Unity

wwwmgm8001 4

对于小型项目和讲求敏捷的团队,Spring.NET可能有点太重量级,那么可以选择轻量级的Unity。Unity是微软patterns
& practices团队推出的轻量级框架,非常好用,目前最新版本是1.2。

Unity的官方网站是:

参考文献

[1]  Shivprasad koirala, Design pattern – Inversion of control and
Dependency injection,

[2]  Martin Fowler, Inversion of Control Containers and the Dependency
Injection pattern,

[3]  Paul, IoC Types,

[4]  Eric Freeman, Elisabeth Freeman. Head First Design Patterns.
O’Reilly Media, 2004. ISBN 0596007142

[5]  Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design
Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley,

  1. ISBN 0201633612

[6]  Patrick Smacchia 著,施凡等 译,C#和.NET2.0
平台、语言与框架。2008.1,人民邮电出版

[7]  Jeffrey Rechter 著,CLR via C#(影印版)。2008.8,人民邮电出版

【wwwmgm8001】依赖注入那些事儿。 

本文基于署名-非商业性使用
3.0许可协议发布,欢迎转载,演绎,但是必须保留本文的署名张洋(包含链接),且不得用于商业目的。如您有任何疑问或者授权方面的协商,请与我联系。

 

From

2 探究依赖注入

1.3 架构师的建议

小李阐述完自己的想法并演示了Demo后,项目组长Peter首先肯定了小李的思考能力、编程能力以及初步的面向对象分析与设计的思想,并承认小李的程序正确完成了需求中的功能。但同时,Peter也指出小李的设计存在一些问题,他请小于讲一下自己的看法。

小于是一名有五年软件架构经验的架构师,对软件架构、设计模式和面向对象思想有较深入的认识。他向Peter点了点头,发表了自己的看法:

“小李的思考能力是不错的,有着基本的面向对象分析设计能力,并且程序正确完成了所需要的功能。不过,这里我想从架构角度,简要说一下我认为这个设计中存在的问题。

首先,小李设计的Role类的Attack方法很长,并且方法中有一个冗长的if…else结构,且每个分支的代码的业务逻辑很相似,只是很少的地方不同。

再者,我认为这个设计比较大的一个问题是,违反了OCP原则。在这个设计中,如果以后我们增加一个新的武器,如倚天剑,每次攻击损失500HP,那么,我们就要打开Role,修改Attack方法。而我们的代码应该是对修改关闭的,当有新武器加入的时候,应该使用扩展完成,避免修改已有代码。

一般来说,当一个方法里面出现冗长的if…else或switch…case结构,且每个分支代码业务相似时,往往预示这里应该引入多态性来解决问题。而这里,如果把不同武器攻击看成一个策略,那么引入策略模式(Strategy
Pattern)是明智的选择。

最后说一个小的问题,被攻击后,减HP、死亡判断等都是怪物的职责,这里放在Role中有些不当。”

【wwwmgm8001】依赖注入那些事儿。Tip:OCP原则,即开放关闭原则,指设计应该对扩展开放,对修改关闭。

Tip:策略模式,英文名Strategy
Pattern,指定义算法族,分别封装起来,让他们之间可以相互替换,此模式使得算法的变化独立于客户。

小于边说,边画了一幅UML类图,用于直观表示他的思想。

wwwmgm8001 12

图1.3 小于的设计

Peter让小李按照小于的设计重构Demo,小李看了看小于的设计图,很快完成。相关代码如下:

 

wwwmgm8001 13wwwmgm8001 14View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    internal interface IAttackStrategy
    {
        void AttackTarget(Monster monster);
    }
}

 

wwwmgm8001 15wwwmgm8001 16View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    internal sealed class WoodSword : IAttackStrategy
    {
        public void AttackTarget(Monster monster)
        {
            monster.Notify(20);
        }
    }
}

 

wwwmgm8001 17wwwmgm8001 18View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    internal sealed class IronSword : IAttackStrategy
    {
        public void AttackTarget(Monster monster)
        {
            monster.Notify(50);
        }
    }
}

 

wwwmgm8001 19wwwmgm8001 20View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    internal sealed class MagicSword : IAttackStrategy
    {
        private Random _random = new Random();
 
        public void AttackTarget(Monster monster)
        {
            Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
            if (200 == loss)
            {
                Console.WriteLine(“出现暴击!!!”);
            }
            monster.Notify(loss);
        }
    }
}

 

wwwmgm8001 21wwwmgm8001 22View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    /// <summary>
    /// 怪物
    /// </summary>
    internal sealed class Monster
    {
        /// <summary>
        /// 怪物的名字
        /// </summary>
        public String Name { get; set; }
 
        /// <summary>
        /// 怪物的生命值
        /// </summary>
        private Int32 HP { get; set; }
 
        public Monster(String name,Int32 hp)
        {
            this.Name = name;
            this.HP = hp;
        }
 
        /// <summary>
        /// 怪物被攻击时,被调用的方法,用来处理被攻击后的状态更改
        /// </summary>
        /// <param name=”loss”>此次攻击损失的HP</param>
        public void Notify(Int32 loss)
        {
            if (this.HP <= 0)
            {
                Console.WriteLine(“此怪物已死”);
                return;
            }
 
            this.HP -= loss;
            if (this.HP <= 0)
            {
                Console.WriteLine(“怪物” + this.Name + “被打死”);
            }
            else
            {
                Console.WriteLine(“怪物” + this.Name + “损失” + loss + “HP”);
            }
        }
    }
}

 

wwwmgm8001 23wwwmgm8001 24View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    /// <summary>
    /// 角色
    /// </summary>
    internal sealed class Role
    {
        /// <summary>
        /// 表示角色目前所持武器
        /// </summary>
        public IAttackStrategy Weapon { get; set; }
 
        /// <summary>
        /// 攻击怪物
        /// </summary>
        /// <param name=”monster”>被攻击的怪物</param>
        public void Attack(Monster monster)
        {
            this.Weapon.AttackTarget(monster);
        }
    }
}

 

wwwmgm8001 25wwwmgm8001 26View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLiAdv
{
    class Program
    {
        static void Main(string[] args)
        {
            //生成怪物
            Monster monster1 = new Monster(“小怪A”, 50);
            Monster monster2 = new Monster(“小怪B”, 50);
            Monster monster3 = new Monster(“关主”, 200);
            Monster monster4 = new Monster(“最终Boss”, 1000);
 
            //生成角色
            Role role = new Role();
 
            //木剑攻击
            role.Weapon = new WoodSword();
            role.Attack(monster1);
 
            //铁剑攻击
            role.Weapon = new IronSword();
            role.Attack(monster2);
            role.Attack(monster3);
 
            //魔剑攻击
            role.Weapon = new MagicSword();
            role.Attack(monster3);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
 
            Console.ReadLine();
        }
    }
}

 

编译运行以上代码,得到的运行结果与上一版本代码基本一致。

3.3 多态的活性与依赖注入

3.3.2 不同活性多态的依赖注入选择

一般来说,高活多态性适合使用Setter注入。因为Setter注入最灵活,也是唯一允许在同一客户类实例运行期间更改服务类的注入方式。并且这种注入一般由上下文环境通过Setter的参数指定服务类类型,方便灵活,适合频繁变化的高活多态性。

对于中活多态性,则适合使用Constructor注入。因为Constructor注入也是由上下文环境通过Construtor的参数指定服务类类型,但一点客户类实例化后,就不能进行再次注入,保证了其时间稳定性。

而对于低活多态性,则适合使用Dependency
Locate并配合文件配置进行依赖注入,或Setter、Constructor配合配置文件注入,因为依赖源来自文件,如果要更改服务类,则需要更改配置文件,一则确保了低活多态性的时间和空间稳定性,二是更改配置文件的方式方便于大规模服务类替换。(因为低活多态性一旦改变行为,往往规模很大,如替换整个数据访问层,如果使用Setter和Construtor传参,程序中需要改变的地方不计其数)

本质上,这种选择是因为不同的依赖注入类型有着不同的稳定性,大家可以细细体会“活性”、“稳定性”和“依赖注入类型”之间密切的关系。

1.1 讨论会

话说有一个叫IGame的游戏公司,正在开发一款ARPG游戏(动作&角色扮演类游戏,如魔兽世界、梦幻西游这一类的游戏)。一般这类游戏都有一个基本的功能,就是打怪(玩家攻击怪物,借此获得经验、虚拟货币和虚拟装备),并且根据玩家角色所装备的武器不同,攻击效果也不同。这天,IGame公司的开发小组正在开会对打怪功能中的某一个功能点如何实现进行讨论,他们面前的大屏幕上是这样一份需求描述的ppt:

wwwmgm8001 5

图1.1 需求描述ppt

各个开发人员,面对这份需求,展开了热烈的讨论,下面我们看看讨论会上都发生了什么。

3.2 反射与依赖注入

回想上面Dependency Locate的例子,我们虽然使用了多态性和Abstract
Factory,但对OCP贯彻的不够彻底。在理解这点前,朋友们一定要注意潜在扩展在哪里,潜在会出现扩展的地方是“新的组件系列”而不是“组件种类”,也就是说,这里我们假设组件就三种,不会增加新的组件,但可能出现新的外观系列,如需要加一套Ubuntu风格的组件,我们可以新增UbuntuWindow、UbuntuButton、UbuntuTextBox和UbuntuFactory,并分别实现相应接口,这是符合OCP的,因为这是扩展。但我们除了修改配置文件,还要无可避免的修改FactoryContainer,需要加一个分支条件,这个地方破坏了OCP。依赖注入本身是没有能力解决这个问题的,但如果语言支持反射机制(Reflection),则这个问题就迎刃而解。

我们想想,现在的难点是出在这里:对象最终还是要通过“new”来实例化,而“new”只能实例化当前已有的类,如果未来有新类添加进来,必须修改代码。如果,我们能有一种方法,不是通过“new”,而是通过类的名字来实例化对象,那么我们只要将类的名字作为配置项,就可以实现在不修改代码的情况下,加载未来才出现的类。所以,反射给了语言“预见未来”的能力,使得多态性和依赖注入的威力大增。

下面是引入反射机制后,对上面例子的改进:

wwwmgm8001 6

图3.7 引入反射机制的Dependency Locate

可以看出,引入反射机制后,结构简单了很多,一个反射工厂代替了以前的一堆工厂,Factory
Container也不需要了。而且以后有新组件系列加入时,反射工厂是不用改变的,只需改变配置文件就可以完成。下面给出反射工厂和配置文件的代码。

 

wwwmgm8001 29wwwmgm8001 30View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Xml;
 
namespace DependencyLocate
{
    internal static class ReflectionFactory
    {
        private static String _windowType;
        private static String _buttonType;
        private static String _textBoxType;
 
        static ReflectionFactory()
        {
            XmlDocument xmlDoc = new XmlDocument();
            xmlDoc.Load(“”);
            XmlNode xmlNode = xmlDoc.ChildNodes[1].ChildNodes[0];
 
            _windowType = xmlNode.ChildNodes[0].Value;
            _buttonType = xmlNode.ChildNodes[1].Value;
            _textBoxType = xmlNode.ChildNodes[2].Value;
        }
 
        public static IWindow MakeWindow()
        {
            return Assembly.Load(“DependencyLocate”).CreateInstance(“DependencyLocate.” + _windowType) as IWindow;
        }
 
        public static IButton MakeButton()
        {
            return Assembly.Load(“DependencyLocate”).CreateInstance(“DependencyLocate.” + _buttonType) as IButton;
        }
 
        public static ITextBox MakeTextBox()
        {
            return Assembly.Load(“DependencyLocate”).CreateInstance(“DependencyLocate.” + _textBoxType) as ITextBox;
        }
    }
}

 

配置文件如下:

 

wwwmgm8001 31wwwmgm8001 32View Code

<?xml version=”1.0″ encoding=”utf-8″ ?>
<config>
    <window>MacWindow</window>
    <button>MacButton</button>
    <textBox>MacTextBox</textBox>
</config>

 

反射不仅可以与Dependency Locate结合,也可以与Setter
Injection与Construtor
Injection结合。反射机制的引入,降低了依赖注入结构的复杂度,使得依赖注入彻底符合OCP,并为通用依赖注入框架(如Spring.NET中的IoC部分、Unity等)的设计提供了可能性。

4 IoC Container

2.1 故事的启迪

我们现在静下心来,再回味一下刚才的故事。因为,这个故事里面隐藏着依赖注入的出现原因。我说过不只一次,想真正认清一个事物,不能只看“它是什么?什么样子?”,而应该先弄清楚“它是怎么来的?是什么样的需求和背景促使了它的诞生?它被创造出来是做什么用的?”。

回想上面的故事。刚开始,主要需求是一个打怪的功能。小李做了一个初步面向对象的设计:抽取领域场景中的实体(怪物、角色等),封装成类,并为各个类赋予属性与方法,最后通过类的交互完成打怪功能,这应该算是面向对象设计的初级阶段。

在小李的设计基础上,架构师小于指出了几点不足,如不符合OCP,职责划分不明确等等,并根据情况引入策略模式。这是更高层次的面向对象设计。其实就核心来说,小于只做了一件事:利用多态性,隔离变化。它清楚认识到,这个打怪功能中,有些业务逻辑是不变的,如角色攻击怪物,怪物减少HP,减到0怪物就会死;而变化的仅仅是不同的角色持有不同武器时,每次攻击的效用不一样。于是他的架构,本质就是把变化的部分和不变的部分隔离开,使得变化部分发生变化时,不变部分不受影响。

我们再仔细看看小于的设计图,这样设计后,有个基本的问题需要解决:现在Role不依赖具体武器,而仅仅依赖一个IAttackStrategy接口,接口是不能实例化的,虽然Role的Weapon成员类型定义为IAttackStrategy,但最终还是会被赋予一个实现了IAttackStrategy接口的具体武器,并且随着程序进展,一个角色会装备不同的武器,从而产生不同的效用。赋予武器的职责,在Demo中是放在了测试代码里。

这里,测试代码实例化一个具体的武器,并赋给Role的Weapon成员的过程,就是依赖注入!这里要清楚,依赖注入其实是一个过程的称谓!

目录

目录

1 IGame游戏公司的故事

    1.1 讨论会

    1.2 实习生小李的实现方法

    1.3 架构师的建议

    1.4 小李的小结

2 探究依赖注入

    2.1 故事的启迪

    2.2 正式定义依赖注入

3 依赖注入那些事儿

    3.1 依赖注入的类别

        3.1.1 Setter注入

        3.1.2 Construtor注入

        3.1.3 依赖获取

    3.2 反射与依赖注入

    3.3 多态的活性与依赖注入

        3.3.1 多态性的活性

        3.3.2 不同活性多态性依赖注入的选择

4 IoC Container

    4.1 IoC Container出现的必然性

    4.2 IoC Container的分类

        4.2.1 重量级IoC Container

        4.2.2 轻量级IoC Container

    4.3 .NET平台上典型IoC Container推介

        4.3.1 Spring.NET

        4.3.2 Unity

参考文献

1.3 架构师的建议

小李阐述完自己的想法并演示了Demo后,项目组长Peter首先肯定了小李的思考能力、编程能力以及初步的面向对象分析与设计的思想,并承认小李的程序正确完成了需求中的功能。但同时,Peter也指出小李的设计存在一些问题,他请小于讲一下自己的看法。

小于是一名有五年软件架构经验的架构师,对软件架构、设计模式和面向对象思想有较深入的认识。他向Peter点了点头,发表了自己的看法:

“小李的思考能力是不错的,有着基本的面向对象分析设计能力,并且程序正确完成了所需要的功能。不过,这里我想从架构角度,简要说一下我认为这个设计中存在的问题。

首先,小李设计的Role类的Attack方法很长,并且方法中有一个冗长的if…else结构,且每个分支的代码的业务逻辑很相似,只是很少的地方不同。

再者,我认为这个设计比较大的一个问题是,违反了OCP原则。在这个设计中,如果以后我们增加一个新的武器,如倚天剑,每次攻击损失500HP,那么,我们就要打开Role,修改Attack方法。而我们的代码应该是对修改关闭的,当有新武器加入的时候,应该使用扩展完成,避免修改已有代码。

一般来说,当一个方法里面出现冗长的if…else或switch…case结构,且每个分支代码业务相似时,往往预示这里应该引入多态性来解决问题。而这里,如果把不同武器攻击看成一个策略,那么引入策略模式(Strategy
Pattern)是明智的选择。

最后说一个小的问题,被攻击后,减HP、死亡判断等都是怪物的职责,这里放在Role中有些不当。”

Tip:OCP原则,即开放关闭原则,指设计应该对扩展开放,对修改关闭。

Tip:策略模式,英文名Strategy
Pattern,指定义算法族,分别封装起来,让他们之间可以相互替换,此模式使得算法的变化独立于客户。

小于边说,边画了一幅UML类图,用于直观表示他的思想。

wwwmgm8001 12

图1.3 小于的设计

Peter让小李按照小于的设计重构Demo,小李看了看小于的设计图,很快完成。相关代码如下:

wwwmgm8001 34wwwmgm8001 35

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    internal interface IAttackStrategy
    {
        void AttackTarget(Monster monster);
    }
}

View Code

wwwmgm8001 36wwwmgm8001 37

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    internal sealed class WoodSword : IAttackStrategy
    {
        public void AttackTarget(Monster monster)
        {
            monster.Notify(20);
        }
    }
}

View Code

wwwmgm8001 38wwwmgm8001 39

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    internal sealed class IronSword : IAttackStrategy
    {
        public void AttackTarget(Monster monster)
        {
            monster.Notify(50);
        }
    }
}

View Code

wwwmgm8001 40wwwmgm8001 41

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    internal sealed class MagicSword : IAttackStrategy
    {
        private Random _random = new Random();

        public void AttackTarget(Monster monster)
        {
            Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
            if (200 == loss)
            {
                Console.WriteLine("出现暴击!!!");
            }
            monster.Notify(loss);
        }
    }
}

View Code

wwwmgm8001 42wwwmgm8001 43

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    /// <summary>
    /// 怪物
    /// </summary>
    internal sealed class Monster
    {
        /// <summary>
        /// 怪物的名字
        /// </summary>
        public String Name { get; set; }

        /// <summary>
        /// 怪物的生命值
        /// </summary>
        private Int32 HP { get; set; }

        public Monster(String name,Int32 hp)
        {
            this.Name = name;
            this.HP = hp;
        }

        /// <summary>
        /// 怪物被攻击时,被调用的方法,用来处理被攻击后的状态更改
        /// </summary>
        /// <param name="loss">此次攻击损失的HP</param>
        public void Notify(Int32 loss)
        {
            if (this.HP <= 0)
            {
                Console.WriteLine("此怪物已死");
                return;
            }

            this.HP -= loss;
            if (this.HP <= 0)
            {
                Console.WriteLine("怪物" + this.Name + "被打死");
            }
            else
            {
                Console.WriteLine("怪物" + this.Name + "损失" + loss + "HP");
            }
        }
    }
}

View Code

wwwmgm8001 44wwwmgm8001 45

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    /// <summary>
    /// 角色
    /// </summary>
    internal sealed class Role
    {
        /// <summary>
        /// 表示角色目前所持武器
        /// </summary>
        public IAttackStrategy Weapon { get; set; }

        /// <summary>
        /// 攻击怪物
        /// </summary>
        /// <param name="monster">被攻击的怪物</param>
        public void Attack(Monster monster)
        {
            this.Weapon.AttackTarget(monster);
        }
    }
}

View Code

wwwmgm8001 46wwwmgm8001 47

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace IGameLiAdv
{
    class Program
    {
        static void Main(string[] args)
        {
            //生成怪物
            Monster monster1 = new Monster("小怪A", 50);
            Monster monster2 = new Monster("小怪B", 50);
            Monster monster3 = new Monster("关主", 200);
            Monster monster4 = new Monster("最终Boss", 1000);

            //生成角色
            Role role = new Role();

            //木剑攻击
            role.Weapon = new WoodSword();
            role.Attack(monster1);

            //铁剑攻击
            role.Weapon = new IronSword();
            role.Attack(monster2);
            role.Attack(monster3);

            //魔剑攻击
            role.Weapon = new MagicSword();
            role.Attack(monster3);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);

            Console.ReadLine();
        }
    }
}

View Code 

编译运行以上代码,得到的运行结果与上一版本代码基本一致。

1.2 实习生小李的实现方式

在经过一番讨论后,项目组长Peter觉得有必要整理一下各方的意见,他首先询问小李的看法。小李是某学校计算机系大三学生,对游戏开发特别感兴趣,目前是IGame公司的一名实习生。

经过短暂的思考,小李阐述了自己的意见:

“我认为,这个需求可以这么实现。HP当然是怪物的一个属性成员,而武器是角色的一个属性成员,类型可以使字符串,用于描述目前角色所装备的武器。角色类有一个攻击方法,以被攻击怪物为参数,当实施一次攻击时,攻击方法被调用,而这个方法首先判断当前角色装备了什么武器,然后据此对被攻击怪物的HP进行操作,以产生不同效果。”

而在阐述完后,小李也飞快的在自己的电脑上写了一个Demo,来演示他的想法,Demo代码如下。

 

wwwmgm8001 48wwwmgm8001 49View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLi
{
    /// <summary>
    /// 怪物
    /// </summary>
    internal sealed class Monster
    {
        /// <summary>
        /// 怪物的名字
        /// </summary>
        public String Name { get; set; }
 
        /// <summary>
        /// 怪物的生命值
        /// </summary>
        public Int32 HP { get; set; }
 
        public Monster(String name,Int32 hp)
        {
            this.Name = name;
            this.HP = hp;
        }
    }
}

 

wwwmgm8001 50wwwmgm8001 51View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLi
{
    /// <summary>
    /// 角色
    /// </summary>
    internal sealed class Role
    {
        private Random _random = new Random();
 
        /// <summary>
        /// 表示角色目前所持武器的字符串
        /// </summary>
        public String WeaponTag { get; set; }
 
        /// <summary>
        /// 攻击怪物
        /// </summary>
        /// <param name=”monster”>被攻击的怪物</param>
        public void Attack(Monster monster)
        {
            if (monster.HP <= 0)
            {
                Console.WriteLine(“此怪物已死”);
                return;
            }
 
            if (“WoodSword” == this.WeaponTag)
            {
                monster.HP -= 20;
                if (monster.HP <= 0)
                {
                    Console.WriteLine(“攻击成功!怪物” + monster.Name + “已死亡”);
                }
                else
                {
                    Console.WriteLine(“攻击成功!怪物” + monster.Name + “损失20HP”);
                }
            }
            else if (“IronSword” == this.WeaponTag)
            {
                monster.HP -= 50;
                if (monster.HP <= 0)
                {
                    Console.WriteLine(“攻击成功!怪物” + monster.Name + “已死亡”);
                }
                else
                {
                    Console.WriteLine(“攻击成功!怪物” + monster.Name + “损失50HP”);
                }
            }
            else if (“MagicSword” == this.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(“角色手里没有武器,无法攻击!”);
            }
        }
    }
}

 

wwwmgm8001 52wwwmgm8001 53View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace IGameLi
{
    class Program
    {
        static void Main(string[] args)
        {
            //生成怪物
            Monster monster1 = new Monster(“小怪A”, 50);
            Monster monster2 = new Monster(“小怪B”, 50);
            Monster monster3 = new Monster(“关主”, 200);
            Monster monster4 = new Monster(“最终Boss”, 1000);
 
            //生成角色
            Role role = new Role();
 
            //木剑攻击
            role.WeaponTag = “WoodSword”;
            role.Attack(monster1);
 
            //铁剑攻击
            role.WeaponTag = “IronSword”;
            role.Attack(monster2);
            role.Attack(monster3);
 
            //魔剑攻击
            role.WeaponTag = “MagicSword”;
            role.Attack(monster3);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
            role.Attack(monster4);
 
            Console.ReadLine();
        }
    }
}

 

程序运行结果如下:

wwwmgm8001 54

图1.2 小李程序的运行结果

 

2.2 正式定义依赖注入

下面,用稍微正式一点的语言,定义依赖注入产生的背景缘由和依赖注入的含义。在读的过程中,读者可以结合上面的例子进行理解。

依赖注入产生的背景:

随着面向对象分析与设计的发展,一个良好的设计,核心原则之一就是将变化隔离,使得变化部分发生变化时,不变部分不受影响(这也是OCP的目的)。为了做到这一点,要利用面向对象中的多态性,使用多态性后,客户类不再直接依赖服务类,而是依赖于一个抽象的接口,这样,客户类就不能在内部直接实例化具体的服务类。但是,客户类在运作中又客观需要具体的服务类提供服务,因为接口是不能实例化去提供服务的。就产生了“客户类不准实例化具体服务类”和“客户类需要具体服务类”这样一对矛盾。为了解决这个矛盾,开发人员提出了一种模式:客户类(如上例中的Role)定义一个注入点(Public成员Weapon),用于服务类(实现IAttackStrategy的具体类,如WoodSword、IronSword和MagicSword,也包括以后加进来的所有实现IAttackStrategy的新类)的注入,而客户类的客户类(Program,即测试代码)负责根据情况,实例化服务类,注入到客户类中,从而解决了这个矛盾。

依赖注入的正式定义:

依赖注入(Dependency
Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

4.2.2 轻量级IoC Container

还有一种IoC
Container,一般不依赖外部配置文件,而主要使用传参的Setter或Construtor注入,这种IoC
Container叫做轻量级IoC
Container。这种框架很灵活,使用方便,但往往不稳定,而且依赖点都是程序中的字符串参数,所以,不适合需要大规模替换和相对稳定的低活多态性,而对于高活多态性,有很好的效果。

Unity是一个典型的轻量级IoC Container。

4.3 .NET平台上典型IoC Container推介

2.1 故事的启迪

我们现在静下心来,再回味一下刚才的故事。因为,这个故事里面隐藏着依赖注入的出现原因。我说过不只一次,想真正认清一个事物,不能只看“它是什么?什么样子?”,而应该先弄清楚“它是怎么来的?是什么样的需求和背景促使了它的诞生?它被创造出来是做什么用的?”。

回想上面的故事。刚开始,主要需求是一个打怪的功能。小李做了一个初步面向对象的设计:抽取领域场景中的实体(怪物、角色等),封装成类,并为各个类赋予属性与方法,最后通过类的交互完成打怪功能,这应该算是面向对象设计的初级阶段。

在小李的设计基础上,架构师小于指出了几点不足,如不符合OCP,职责划分不明确等等,并根据情况引入策略模式。这是更高层次的面向对象设计。其实就核心来说,小于只做了一件事:利用多态性,隔离变化。它清楚认识到,这个打怪功能中,有些业务逻辑是不变的,如角色攻击怪物,怪物减少HP,减到0怪物就会死;而变化的仅仅是不同的角色持有不同武器时,每次攻击的效用不一样。于是他的架构,本质就是把变化的部分和不变的部分隔离开,使得变化部分发生变化时,不变部分不受影响。

我们再仔细看看小于的设计图,这样设计后,有个基本的问题需要解决:现在Role不依赖具体武器,而仅仅依赖一个IAttackStrategy接口,接口是不能实例化的,虽然Role的Weapon成员类型定义为IAttackStrategy,但最终还是会被赋予一个实现了IAttackStrategy接口的具体武器,并且随着程序进展,一个角色会装备不同的武器,从而产生不同的效用。赋予武器的职责,在Demo中是放在了测试代码里。

这里,测试代码实例化一个具体的武器,并赋给Role的Weapon成员的过程,就是依赖注入!这里要清楚,依赖注入其实是一个过程的称谓!

2.2 正式定义依赖注入

下面,用稍微正式一点的语言,定义依赖注入产生的背景缘由和依赖注入的含义。在读的过程中,读者可以结合上面的例子进行理解。

依赖注入产生的背景:

随着面向对象分析与设计的发展,一个良好的设计,核心原则之一就是将变化隔离,使得变化部分发生变化时,不变部分不受影响(这也是OCP的目的)。为了做到这一点,要利用面向对象中的多态性,使用多态性后,客户类不再直接依赖服务类,而是依赖于一个抽象的接口,这样,客户类就不能在内部直接实例化具体的服务类。但是,客户类在运作中又客观需要具体的服务类提供服务,因为接口是不能实例化去提供服务的。就产生了“客户类不准实例化具体服务类”和“客户类需要具体服务类”这样一对矛盾。为了解决这个矛盾,开发人员提出了一种模式:客户类(如上例中的Role)定义一个注入点(Public成员Weapon),用于服务类(实现IAttackStrategy的具体类,如WoodSword、IronSword和MagicSword,也包括以后加进来的所有实现IAttackStrategy的新类)的注入,而客户类的客户类(Program,即测试代码)负责根据情况,实例化服务类,注入到客户类中,从而解决了这个矛盾。

依赖注入的正式定义:

依赖注入(Dependency
Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。

4 IoC Container

3.1.2 构造注入

另外一种依赖注入方式,是通过客户类的构造函数,向客户类注入服务类实例。

构造注入(Constructor
Injection)是指在客户类中,设置一个服务类接口类型的数据成员,并以构造函数为注入点,这个构造函数接受一个具体的服务类实例为参数,并将它赋给服务类接口类型的数据成员。

wwwmgm8001 1

图3.3 构造注入示意

图3.3是构造注入的示意图,可以看出,与Setter注入很类似,只是注入点由Setter方法变成了构造方法。这里要注意,由于构造注入只能在实例化客户类时注入一次,所以一点注入,程序运行期间是没法改变一个客户类对象内的服务类实例的。

由于构造注入和Setter注入的IServiceClass,ServiceClassA和ServiceClassB是一样的,所以这里给出另外ClientClass类的示例代码。

 

wwwmgm8001 56wwwmgm8001 57View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConstructorInjection
{
    internal class ClientClass
    {
        private IServiceClass _serviceImpl;
 
        public ClientClass(IServiceClass serviceImpl)
        {
            this._serviceImpl = serviceImpl;
        }
 
        public void ShowInfo()
        {
            Console.WriteLine(_serviceImpl.ServiceInfo());
        }
    }
}

 

可以看到,唯一的变化就是构造函数取代了Set_ServiceImpl方法,成为了注入点。

4.3.2 Unity

wwwmgm8001 4

对于小型项目和讲求敏捷的团队,Spring.NET可能有点太重量级,那么可以选择轻量级的Unity。Unity是微软patterns
& practices团队推出的轻量级框架,非常好用,目前最新版本是1.2。

Unity的官方网站是:

参考文献

[1]  Shivprasad koirala, Design pattern – Inversion of control and
Dependency injection, 

[2]  Martin Fowler, Inversion of Control Containers and the Dependency
Injection pattern, 

[3]  Paul, IoC
Types, 

[4]  Eric Freeman, Elisabeth Freeman. Head First Design Patterns.
O’Reilly Media, 2004. ISBN 0596007142

[5]  Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides. Design
Patterns: Elements of Reusable Object-Oriented Software. Addison-Wesley,

  1. ISBN 0201633612

[6]  Patrick Smacchia 著,施凡等 译,C#和.NET2.0
平台、语言与框架。2008.1,人民邮电出版

[7]  Jeffrey Rechter 著,CLR via C#(影印版)。2008.8,人民邮电出版

 

原文作者:张洋(包含链接)