【架构篇】OCP和依赖注入
描述
本篇文章主要讲解 :
(1)OO七大设计原则之OCP原则;
(2)依赖注入引入
(3)依赖注入分析
1 内容区
1.1 IOC背景
(1)Ralph E. Johnson & Brian Foote 论文 《Designing Reusable Classes》
早在1988年,Ralph E. Johnson & Brian Foote在论文Designing Reusable Classes中写到:
《OO面向对象设计七大原则,》,请参照我另外一篇文章 OO面向对象设计七大原则 .
2 OCP分析
OCP原则(Open Close Principle),核心思想是封闭修改(隔离变化),支持扩展(继承,目的是复用)。
为了分析清楚OCP,我们这里以人为研究对象,即把人当作超类。
2.1 定义超类(People类)
在定义一个类时,主要关心类的特性(Class 中的属性)和行为(Class 中的方法),这里,我们假设超类People中存在如下属性和方法:
a.属性:头,嘴
b.方法:Eat(),Sleep(),WalkPosture()
1 public abstract class People 2 { 3 private string Head;//头 4 private string Mouse;//嘴 5 6 public void Eat() //吃饭 7 { 8 //...... 9 } 10 public void Sleep() //睡觉 11 { 12 //...... 13 } 14 15 public abstract void WalkPosture(); //每个人的走路姿势不一样 16 17 18 }
UML类图如下:
(1)我们向People类中添加Speak()方法,使其能够说汉语(普通话),则People类变为如下:
1 public class People 2 { 3 private string Head;//头 4 private string Mouse;//嘴 5 6 public void Eat() //吃饭 7 { 8 //...... 9 } 10 public void Sleep() //睡觉 11 { 12 //...... 13 } 14 15 public abstract void WalkPosture();//每个人的走路姿势不一样 16
17 18 public string SpeakLanguage() //说话 19 { 20 //普通话 21 } 22 23 }
此时,UML类图变为如下:
(2)具体的某个人,继承People类即可。
1 public class XiaoMing : People 2 { 3 //...... 4 }
UML图如下:
2.2 对People类分析
People类UML图如下:
分析:
假设这样一个情景:即People类中不仅仅是中国人,还有其他231个国家的人(每个国家的语言并不完全相同),我们在本程序中,加入英国人,俄罗斯人,即People类中只有中国人,英国人,俄罗斯人三个国家的人。
做法一:
在People类中改写SpeakLanguage()方法。
1 public class People 2 { 3 private string Head;//头 4 private string Mouse;//嘴 5 6 public void Eat() //吃饭 7 { 8 //...... 9 } 10 public void Sleep() //睡觉 11 { 12 //...... 13 } 14 15 public abstract WalkPosture();//每个人的走路姿势不一样 16 17 18 public Language SpeakLanguage( Language language) //说话 19 { 20 if (language=="Chinese") 21 { 22 //普通话 23 } 24 if (language=="English") 25 { 26 //English 27 } 28 else 31 { 29 //Russian 30 } 31 32 } 33 34 }
我们来分析一下做法一的
问题:
Q1:由于直接修改超类People中的方法,违背了OO软件设计开闭原则(Open Close Principle,简称OCP);
Q2:如果再把其他国家加进来,那么SpeakLanguage() 方法体 会有很多 if…..else…..,不利于代码维护;
方法二:
根据OCP原则,对修改关闭,对扩展开放;在超类People中:
(1)属性Head,Mouse,每个人都具有;
(2)方法Eat(),Sleep(),每个人都具有;
(3)方法WalkPosture(),每个人走路的姿势不一样,可以用抽象方法来实现;
(4)方法SpeakLanguage(Language language),每个国籍的人,说话的语言不一定相同,这是类中变化的部分,需要独立开来;
因此,可以改写为如下:
定义一个Language类
Language类
1 public class Language 2 { 3 //To add Language business codes 4 }
People类
1 public class People 2 { 3 private string Head;//头 4 private string Mouse;//嘴 5 6 public void Eat() //吃饭 7 { 8 //...... 9 } 10 public void Sleep() //睡觉 11 { 12 //...... 13 } 14 15 public abstract WalkPosture();//每个人的走路姿势不一样 16 17 }
接口 ILanguage
1 public interface ILanguage 2 { 3 Language SpeakLanguage(Language language); 4 }
中国人
1 public class Chinese : People,ILanguage 2 { 3 // 继承People 4 // WalkPostrue 5 // 实现接口方法 Language SpeakLanguage(Language language); 6 }
英国人
1 public class English : People,ILanguage 2 { 3 // 继承People 4 // WalkPostrue 5 // 实现接口方法 Language SpeakLanguage(Language language); 6 }
俄罗斯人
1 public class Chinese : People,ILanguage 2 { 3 // 继承People 4 // WalkPostrue 5 // 实现接口方法 Language SpeakLanguage(Language language); 6 }
UML关系图如下:
如果你能将本OCP例子改为依赖注入,那么你不必往下看了,因为你已经会了。
3 依赖注入
在对依赖注入简要概述和对OCP简要分析之后,我们来研究依赖注入。
3.1 例子:(引用)
一个叫IGame的游戏公司,正在开发一款ARPG游戏(动作&角色扮演类游戏,如魔兽世界、梦幻西游这一类的游戏)。一般这类游戏都有一个基本的功能,就是打怪(玩家攻击怪物,借此获得经验、虚拟货币和虚拟装备),并且根据玩家角色所装备的武器不同,攻击效果也不同.打怪功能中的某一个功能:
(1)、角色可向怪物实施攻击,一次攻击后,怪物掉部分HP,HP掉完后,怪物死亡。
(2)、角色可装配不同武器,有木剑、铁剑、魔剑。
(3)、木剑每次攻击,怪物掉20PH,铁剑掉50HP,魔剑掉100PH。
IAttackStrategy接口
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace IGameLiAdv 7 { 8 internal interface IAttackStrategy 9 { 10 void AttackTarget(Monster monster); 11 } 12 }
WoodSword类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace IGameLiAdv 7 { 8 internal sealed class WoodSword : IAttackStrategy 9 { 10 public void AttackTarget(Monster monster) 11 { 12 monster.Notify(20); 13 } 14 } 15 }
IronSword类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace IGameLiAdv 7 { 8 internal sealed class IronSword : IAttackStrategy 9 { 10 public void AttackTarget(Monster monster) 11 { 12 monster.Notify(50); 13 } 14 } 15 }
MagicSword类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace IGameLiAdv 7 { 8 internal sealed class MagicSword : IAttackStrategy 9 { 10 private Random _random = new Random(); 11 12 public void AttackTarget(Monster monster) 13 { 14 Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200; 15 if (200 == loss) 16 { 17 Console.WriteLine("出现暴击!!!"); 18 } 19 monster.Notify(loss); 20 } 21 } 22 }
Monster类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace IGameLiAdv 7 { 8 /// <summary> 9 /// 怪物 10 /// </summary> 11 internal sealed class Monster 12 { 13 /// <summary> 14 /// 怪物的名字 15 /// </summary> 16 public String Name { get; set; } 17 18 /// <summary> 19 /// 怪物的生命值 20 /// </summary> 21 private Int32 HP { get; set; } 22 23 public Monster(String name,Int32 hp) 24 { 25 this.Name = name; 26 this.HP = hp; 27 } 28 29 /// <summary> 30 /// 怪物被攻击时,被调用的方法,用来处理被攻击后的状态更改 31 /// </summary> 32 /// <param name="loss">此次攻击损失的HP</param> 33 public void Notify(Int32 loss) 34 { 35 if (this.HP <= 0) 36 { 37 Console.WriteLine("此怪物已死"); 38 return; 39 } 40 41 this.HP -= loss; 42 if (this.HP <= 0) 43 { 44 Console.WriteLine("怪物" + this.Name + "被打死"); 45 } 46 else 47 { 48 Console.WriteLine("怪物" + this.Name + "损失" + loss + "HP"); 49 } 50 } 51 } 52 }
Role类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 6 namespace IGameLiAdv 7 { 8 /// <summary> 9 /// 角色 10 /// </summary> 11 internal sealed class Role 12 { 13 /// <summary> 14 /// 表示角色目前所持武器 15 /// </summary> 16 public IAttackStrategy Weapon { get; set; } 17 18 /// <summary> 19 /// 攻击怪物 20 /// </summary> 21 /// <param name="monster">被攻击的怪物</param> 22 public void Attack(Monster monster) 23 { 24 this.Weapon.AttackTarget(monster); 25 } 26 } 27 }
Program类
1 namespace IGameLiAdv 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 //生成怪物 8 Monster monster1 = new Monster("小怪A", 50); 9 Monster monster2 = new Monster("小怪B", 50); 10 Monster monster3 = new Monster("关主", 200); 11 Monster monster4 = new Monster("最终Boss", 1000); 12 13 //生成角色 14 Role role = new Role(); 15 16 //木剑攻击 17 role.Weapon = new WoodSword(); 18 role.Attack(monster1); 19 20 //铁剑攻击 21 role.Weapon = new IronSword(); 22 role.Attack(monster2); 23 role.Attack(monster3); 24 25 //魔剑攻击 26 role.Weapon = new MagicSword(); 27 role.Attack(monster3); 28 role.Attack(monster4); 29 role.Attack(monster4); 30 role.Attack(monster4); 31 role.Attack(monster4); 32 role.Attack(monster4); 33 34 Console.ReadLine(); 35 } 36 } 37 }
UML关系图
3.2 分析:
引入Strategy模式后,不但消除了重复性代码,更重要的是,使得设计符合了OCP。如果以后要加一个新武器,只要新建一个类,实现IAttackStrategy接口,当角色需要装备这个新武器时,客户代码只要实例化一个新武器类,并赋给Role的Weapon成员就可以了,已有的Role和Monster代码都不用改动。这样就实现了对扩展开发,对修改关闭。
上面例子的第二种实现中,Role不依赖具体武器,而仅仅依赖一个IAttackStrategy接口,接口是不能实例化的,虽然Role的Weapon成员类型定义为IAttackStrategy,但最终还是会被赋予一个实现了IAttackStrategy接口的具体武器,并且随着程序进展,一个角色会装备不同的武器,从而产生不同的效用。赋予武器的职责,在Demo中是放在了测试代码里。
这里,测试代码实例化一个具体的武器,并赋给Role的Weapon成员的过程,就是依赖注入!这里要清楚,依赖注入其实是一个过程的称谓!
依赖注入产生的背景:
随着面向对象分析与设计的发展,一个良好的设计,核心原则之一就是将变化隔离,使得变化部分发生变化时,不变部分不受影响(这也是OCP的目的)。为了做到这一点,要利用面向对象中的多态性,使用多态性后,客户类不再直接依赖服务类,而是依赖于一个抽象的接口,这样,客户类就不能在内部直接实例化具体的服务类。但是,客户类在运作中又客观需要具体的服务类提供服务,因为接口是不能实例化去提供服务的。就产生了“客户类不准实例化具体服务类”和“客户类需要具体服务类”这样一对矛盾。为了解决这个矛盾,开发人员提出了一种模式:客户类(如上例中的Role)定义一个注入点(Public成员Weapon),用于服务类(实现IAttackStrategy的具体类,如WoodSword、IronSword和MagicSword,也包括以后加进来的所有实现IAttackStrategy的新类)的注入,而客户类的客户类(Program,即测试代码)负责根据情况,实例化服务类,注入到客户类中,从而解决了这个矛盾。
3.3 依赖注入的正式定义:
依赖注入(Dependency Injection),是这样一个过程:由于某客户类只依赖于服务类的一个接口,而不依赖于具体服务类,所以客户类只定义一个注入点。在程序运行过程中,客户类不直接实例化具体服务类实例,而是客户类的运行上下文环境或专门组件负责实例化服务类,然后将其注入到客户类中,保证客户类的正常运行。
3.4 依赖注入总结
(1)组成要素
a.接口及其实现(剥离变化)
b.客户类和服务类
(2)核心思想
a.延迟注入服务,并不是一开始就注入服务,即在用到时,才通过接口形式注入服务;
4 依赖注入的种类
依赖注入大致可分为如下种类:
5 参考文献
【01】https://segmentfault.com/a/1190000010456858
【02】Head First设计模式
6 版权区
- 感谢您的阅读,若有不足之处,欢迎指教,共同学习、共同进步。
- 博主网址:http://www.cnblogs.com/wangjiming/。
- 极少部分文章利用读书、参考、引用、抄袭、复制和粘贴等多种方式整合而成的,大部分为原创。
- 如您喜欢,麻烦推荐一下;如您有新想法,欢迎提出,邮箱:2016177728@qq.com。
- 可以转载该博客,但必须著名博客来源。