控制反转和依赖注入的理解(通俗易懂)_啥?啥是控制反转,依赖注入啊!?

依赖倒置,控制反转,依赖注入及Google Guice

1. 依赖倒置

依赖
字面意思事物之间具有的一种关系。在面向对象编程中我将其理解为一种对象之间引用的持有关系。任何系统都无法避免依赖,如果一个类或接口在系统中没有被依赖,那么他们就不应该出现在系统之中。举个简单的例子说明依赖关系:师徒四人去西天取经。我们说说唐僧,他有个徒弟,其他的东西我们暂且忽略。如果把唐僧师徒一个具体类的话,他在收服了悟空之后应该有这样的依赖关系。

e4e08a2f1c75f3cc0d350dbed79763e7.png

我们从唐僧角度考虑的话,他应该是依赖于悟空的,没有他,他可取不到经。那么我们可以说唐僧依赖于他的徒弟。

在代码中,他们的关系如下:

public class TangSeng {
  WuKong wuKong = new WuKong();
}

有了徒弟,唐僧就可以跟他愉快地取经了。中途遇到妖怪,也可以让徒弟去打妖怪了。

public class TangSeng {

  WuKong wuKong = new WuKong();

  public void letsFight(String name) {
      wuKong.fight();
  }

}

那么问题了,唐僧在后面还会收徒弟。当他收齐之后情况应该是这样的。

b71f05a998e54f50d5888c5a28e08884.png

这里他就依赖于四个徒弟了,也就是说徒弟越多,他的依赖就越多。依赖多的话会产生什么影响呢?首先,遇到妖怪了,让谁出场是个问题了,总不能天天让一个人吃苦,这就不是一个好的领导了。所以,出战的方法也得修改。

public class TangSeng {

  WuKong wuKong = new WuKong();
  BaJie baJie = new BaJie();
  LaoSha laoSha = new LaoSha();
  XiaoBaiLong xiaoBaiLong = new XiaoBaiLong();

  public void letsFight(String name) {
    if (name.equals("wuKong")) {
      wuKong.fight();
    } else if (name.equals("baJie")) {
      baJie.fight();
    } else if (name.equals("laoSha")) {
      laoSha.fight();
    } else {
      runAway();
    }
  }

  private void runAway() {
    System.out.println("Bye Bye ~");
  }

}

这里代码量虽然只稍微上来一点,但是我们得看到更本质的东西。徒弟每增加一个徒弟,唐僧的依赖就会多一个,对应的代码可能就得修改一下。而且,唐僧直接依赖于具体的徒弟类,如果某个徒弟除了问题,那唐僧是不是也可能会出问题。因为他有一个具体的徒弟类,而且还会调用到具体的方法。这种耦合性比较高的代码,后期维护起来会比较糟糕,修改一个地方,其他地方可能也要跟着做很多更改。所以我们需要换个角度考虑一下。

依赖倒置

上面分析了问题出现在什么地方。主要是类之间直接的依赖关系导致的高耦合,那么要如何改变这种依赖关系呢?这就要改变我们思考的方式了,我们应该更多的依赖于接口(广泛概念上的接口)而不是具体的类,即要依赖于接口,而不是依赖于具体。无论是悟空,八戒还是沙僧,他们对于唐僧而言都是徒弟。我们可以将徒弟抽象成一个接口。如下:

d3f92e22943cc5925b6cbafc7022c620.png

这里,具体类之间的直接依赖关系就被改变了。由类与具体类之间的依赖,转换成了类与接口之间的依赖。即唐僧类依赖于TuDi接口,而四个徒弟也依赖于TuDi接口,他们实现了接口。从上往下的依赖关系,在TuDi与徒弟实现类这里发生了改变,成了徒弟向上依赖于TuDi接口。这种倒置,就是依赖倒置。

下面看看这种倒置对代码产生的影响:

public class TangSeng {

  List<TuDi> ts = new ArrayList<TuDi>();

  public TangSeng(List<TuDi> ts) {
    this.ts = ts;
  }

  public void letsFight(TuDi tudi) {
    tudi.fight();
  }

}

实例化的语句没有了,具体类和实例对象也没有了。TuDi的具体实现对于唐生而言已经无所谓了,能打就行了。

2. 控制反转(IOC)

继续用上面的例子分析。我们知道师徒四人会遇到妖怪,但是遇到的妖怪是哪个?跟谁打?这个在正常情况下我们可能没法确定,但是在代码实现时,如果需要指定他们要面对的妖怪,我们可能就要在类中实例化这个妖怪了。

public class TangSeng {

  List<TuDi> ts = new ArrayList<TuDi>();
  LionMonster lion = new LionMonster();

  public TangSeng(List<TuDi> ts) {
    this.ts = ts;
  }

  public void letsFight(TuDi tudi) {
    tudi.fight(lion);
  }

}

这里就等于写死了,他们只会跟狮子怪干架。妖怪是应用程序自己主动指定创建的。如果我们更改这种模式,他们跟哪个妖怪打架可以动态改变,由其他的配置控制。就是我们可以在需要的时候,将对象实例传递过去,这是被动的过程。这种由主动变成被动的方式,就是我理解中的反转控制。 具体的实现可能有多种方式,反射就是一种比较经典的实现。

public class TangSeng {

  List<TuDi> ts = new ArrayList<TuDi>();
  Property pro = new Property();

  public TangSeng(List<TuDi> ts) {
    this.ts = ts;
  }

    public Monster getMonster() {
        Class monsterClass = Class.forName(pro.getClassName());
        return monsterClass.newInstance();
    }

  public void letsFight(TuDi tudi) {
    tudi.fight(getMonster);
  }

}

pro.getClassName()返回的值,可以通过配置文件更改成指定的类。

3. 依赖注入(Dependency injection)

  • 注入
    我们再看看唐僧类中妖怪战斗的方法,圣僧是铁定上不了场的,这里我们是通过接口声明参数的,但是当真正调用方法的时候,这个地方肯定是要有个具体的徒弟实现类。所以问题就是这个徒弟怎么来。通过上面的讨论我们已经有两种方法可以实现了。其一,在代码中直接实例化好,然后传入对象,可以是通过工厂返回的对象。其二,通过配置文件指定徒弟类,在运行时动态生成徒弟类。后者变是反转控制的一种实现。反转控制是一个概念,他具体的实现方式可能有很多种。大部分场景是通过IOC容器,根据配置文件来实现注入。常用的框架有Spring,非常用的有Google Guice,因为Druid的依赖注入都是通过Google Guice实现的,所以这里简单介绍一下它。

4. Google Guice

Google Guice 是一款轻量级的依赖注入框架。

<dependency>
    <groupId>com.google.inject</groupId> 
    <artifactId>guice </artifactId>  
    <version>4.1.0</version>  
 </dependency>  

<dependency>
    <groupId>aopalliance</groupId>
    <artifactId>aopalliance</artifactId>
    <version>1.0</version>
</dependency>

4.1 使用实例

场景1:唐僧被妖怪抓走了,大师兄刚刚化缘回来。各位师兄弟的反应如下,沙僧:大师兄!师父被妖怪抓走了!!八戒:分行李吧!!悟空:我去看看是哪路妖怪,如此胆大!!于是,悟空出发了!!
以下是悟空类,悟空多了一个拯救师父的任务
public class Wukong implements Tudi {

    private String name = "wukong";
    private Skill skill;

    public void toSaveMaster(){
        //悟空使用它的技能(skill)对付妖怪
         skill.Effect();
    }

    @Override
    public void fight(Monster monster) {
        System.out.println("WuKong is fighting with " + monster);
    }
}

那我们知道悟空会很多的法术,妖怪也是千奇百怪,所以要对症下药,选择正确的技能才能在损失最小的情况下快速地救出师傅。那这里的技能就又要用我们上面的思想来处理了:

//把Skill定义为一个接口
public interface Skill {
    public void Effect();
}  

//下面是悟空的几个技能
class Fire implements Skill{
    @Override
    public void Effect() {
        System.out.println("悟空在喷火!!对敌方造成火属性伤害100");
    }
}


class Water implements Skill{
    @Override
    public void Effect() {
        System.out.println("哇!!悟空在喷水!!对敌方造成水属性伤害100");
    }
}

class Wind implements Skill{
    @Override
    public void Effect() {
        System.out.println("悟空在刮风!!对敌方造成风属性伤害100");
    }
}

这里我们知道了悟空会法术(Skill接口),还知道了他的技能清单(Skill的实现类)。接下来就是根据地方选择正确的技能了。例如对面是白骨精,那我们就选择喷水技能打伤害吧(我也不知道为什么,感觉会很有效!)。那我们要做的就是把悟空的技能接口和接口的实现类Water绑定到一起。不使用框架的操作。

Wukong wukong = new Wukong();
     wukong.skill=new Water();

使用Guice,需要做的步骤如下:
1. 创建接口;
2. 接口实现;
3. 将接口和实现类绑定; 4. 创建接口实例。

前两步已经在上面的代码中完成了,接口为Skill,实现类就是喷火、喷水、刮风。接下来我们进行接口的绑定。
步骤三:将接口和实现类绑定

public class SkillModule implements Module {
    @Override
    public void configure(Binder binder) {
        binder.bind(Skill.class).to(Water.class);
    }
    //接口和实现类绑定的一种方式就是通过实现配置模块,实现其config方法来完成。这种绑定关系,我们也可以通过配置文件指定。

步骤四:创建接口的实例

public static void main(String[] args) {
         //将配置传入
        Injector injector = Guice.createInjector(new SkillModule());
        skill = injector.getInstance(Skill.class);
        Wukong wukong = new Wukong();
        wukong.skill=skill;
        wukong.toSaveMaster();
    } 

    运行结果如下:
    哇!!悟空在喷水!!对敌方造成水属性伤害100

4.2 Guice中的注解

@ImplementedBy:用于注解接口,直接指定接口的实现类而不需要在Module中实现接口的绑定;
Demo

//定义接口时即指定其实现类为Water
@ImplementedBy(Water.class)
public interface Skill {
    public void Effect();
}

在Main方法中的代码也做相应的更改:

public static void main(String[] args) {
        Injector injector = Guice.createInjector();
        skill = injector.getInstance(Skill.class);
        Wukong wukong = new Wukong();
        wukong.skill=skill;
        wukong.toSaveMaster();
    }

运行结果一样,但是整个代码工程中少了配置module的过程。但是谁又能在定义接口时就知道其实现类呢,我觉得用处不是特别大。

@Inject:使用该注解,可以将对象实例直接注入到一个对其依赖的类中。可以用在某个类的构造方法中:
Demo

public class Wukong implements Tudi {

    private static String name = "wukong";
    private static Skill skill;

    @Inject
    public Wukong(Skill skill) {
        this.skill = skill;
    }

    public void toSaveMaster(){
        skill.Effect();
    }

    @Override
    public void fightMonster() {
        System.out.println("WuKong is fighting !!");
    }
}

Main方法也变了

public static void main(String[] args) {
        Injector injector = Guice.createInjector();
        Wukong wukong = injector.getInstance(Wukong.class);
        wukong.toSaveMaster();
    }

运行结果一样。

@Singleton
用来注解类,可以确保调用injector.getInstance时创建的是一个单例类。

@Named:当一个接口实多个绑定时可以使用该注解区分。
改写SkillModule类

public class SkillModule implements Module {
    @Override
    public void configure(Binder binder) {
        binder.bind(Skill.class).annotatedWith(Names.named("Water")).to(Water.class);
        binder.bind(Skill.class).annotatedWith(Names.named("Fire")).to(Fire.class);
    }
}

在看看这个注解是如何发挥作用的

public static void main(String[] args) {
        Injector injector = Guice.createInjector(new SkillModule());
        @Named("Water") Skill waterSkill = injector.getInstance(Skill.class);
        Wukong wukong = new Wukong();
        wukong.skill = waterSkill;
        wukong.toSaveMaster();
        @Named("Fire") Skill fireSkill = injector.getInstance(Skill.class);
        wukong.skill = fireSkill;
        wukong.toSaveMaster();
    }

这样就可以把一个接口绑定到多个实现类上,根据不同的Name可以创建不同的实例。但是在实际中无法通过编译,还没有看出是什么问题,所以不建议使用该注解。
Guice很强大,这里只是简单记录。啥?啥是控制反转,依赖注入啊!?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值