什么是IOC容器?为什么需要IOC容器?

假设我们现在正在使用三层架构开发一个项目,其中有一个用户模块,包含登录、注册等功能。现在已经写好了User实体类和UserDao数据访问层:

public class User
{
    private Integer id;
    private String username;
    private String password;

    // 以下是getter和setter方法
}

public interface UserDao
{
    // 查找用户
    User get(String username, String password);

    // 插入用户
    void insert(User user);
}

public class UserDaoImpl implements UserDao
{
    @Override
    public User getByUsername(String username, String password)
    {
        ...
    }

    @Override
    public void insert(User user)
    {
        ...
    }
}

UserDao封装了对数据库中的用户表进行操作。现在,需要一个UserService来封装登录、注册这两个业务逻辑:

public interface UserService
{
    // 登录
    User login(String username, String password);

    // 注册
    void register(User user);
}

public class UserServiceImpl implements UserService
{
    @Override
    public User login(String username, String password)
    {
        User user = userDao.get(username, password); // userDao从哪里来?
        if (user == null)
        {
            // 用户名或密码错误
        }
        return user;
    }

    @Override
    public void register(User user)
    {
        userDao.insert(user); // userDao从哪里来?
    }
}

显然,UserServiceImpl需要一个UserDao的实例userDao来访问数据库,那么问题来了:这个userDao如何该获取呢?

很多人都会用如下代码来获取userDao

public class UserServiceImpl implements UserService
{
    private UserDao userDao = new UserDaoImpl();
    ...
}

直接在UserServiceImpl内部new一个UserDaoImpl,看起来很方便,也可以正常工作,但是它存在一些问题:

  1. 现在UserServiceImpl依赖于UserDaoImpl,如果这两个类是由两个不同的人开发的,则他们无法同时工作,因为在UserDaoImpl完成之前,UserServiceImpl无法通过编译
  2. UserServiceImpl无法被测试,因为它与某个特定的UserDao实现类绑定在了一起,我们不能把它替换成一个用于单元测试的MockUserDao
  3. 如果我们有多套数据库实现(即多个UserDao实现类),那么不能很方便地切换

为了解决上面几个问题,可以使用一种被称为依赖注入的技巧:

public class UserServiceImpl implements UserService
{
    private UserDao userDao;

    // 构造函数注入
    public UserServiceImpl(UserDao userDao)
    {
        this.userDao = userDao;
    }
    ...
}

// 外部程序
UserService userService = new UserServiceImpl(new UserDaoImpl());

现在,userDao不是由UserServiceImpl本身构造,而是让外部程序通过UserServiceImpl的构造函数传入进来,这种操作称为构造函数注入

还可以使用另一种注入方式——setter方法注入

public class UserServiceImpl implements UserService
{
    private UserDao userDao;

    // setter方法注入
    public void setUserDao(UserDao userDao)
    {
        this.userDao = userDao;
    }
    ...
}

// 外部程序
UserService userService = new UserServiceImpl();
userService.setUserDao(new UserDaoImpl());

不论哪种注入方式,其基本逻辑都是一样的:组件不负责创建自己依赖的组件,而是让外部程序创建依赖组件,然后通过构造函数或setter函数注入进来。其实,这里也蕴含着控制反转的思想,因为创建依赖组件的任务从组件内部转移到了外部程序

使用了依赖注入,前面的几个问题就迎刃而解了,因为UserServiceImpl不再依赖UserDao的具体实现类,我们可以轻松地替换UserDao的实现。

但是问题又来了:该由谁负责对象的组装呢?

答案是:应该由应用的最外层负责对象的组装。例如,在三层架构中,可以在controller层负责service类的组装;如果我们的程序有main函数,也可以在main函数中进行相关组件的组装。

public class UserController
{
    private UserService userService = new UserServiceImpl(new UserDaoImpl());

    public void handleLoginRequest(...)
    {
        userService.login(...);
        ...
    }
}

按照这种方式写程序,项目中的所有组件都按照依赖注入的方式管理自己的依赖,所有组件都由最外层统一组装,如果想替换掉某个组件的实现也很方便,看起来很美好。但是,当项目逐渐变得庞大,组件之间的依赖变多的时候,某个组件可能需要依赖于几十个大大小小的其它组件,创建这样的组件就成了一种折磨:

// 创建一个复杂的组件
Component1 c1 = new Component1(new Component2(new Component3()), new Component4(new Component5(), new Component6()), new Component7());

如果这个组件只需要被使用一次,看起来还是可以接受,但是如果这个组件在很多地方都要使用,那么在每个使用的地方都需要写一遍上面创建的代码,这将会产生大量的代码冗余:

public class A
{
    Component1 c1 = new Component1(new Component2(new Component3()), new Component4(new Component5(), new Component6()), new Component7());

    public void f1()
    {
        // 使用c1
        ...
    }
}

public class B
{
    Component1 c1 = new Component1(new Component2(new Component3()), new Component4(new Component5(), new Component6()), new Component7());

    public void f2()
    {
        // 使用c1
        ...
    }
}

public class C
{
    Component1 c1 = new Component1(new Component2(new Component3()), new Component4(new Component5(), new Component6()), new Component7());

    public void f3()
    {
        // 使用c1
        ...
    }
}

更糟糕的是,如果组件c1依赖的其中一个组件将要被替换,那么上面所有创建c1的代码都要修改,这简直是维护的噩梦!

为了避免这个问题,可以把系统中所有的组件放进一个“容器”中统一管理:

public class Container
{
    public static Component1 getComponent1()
    {
        ...
    }

    public static Component2 getComponent2()
    {
        ...
    }

    public static Component3 getComponent3()
    {
        ...
    }
    ...
}

然后,系统中所有需要使用组件的地方都通过Container类来获取:

public class A
{
    Component1 c1 = Container.getComponent1();

    public void f1()
    {
        // 使用c1
        ...
    }
}

public class B
{
    Component1 c1 = Container.getComponent1();

    public void f2()
    {
        // 使用c1
        ...
    }
}

public class C
{
    Component1 c1 = Container.getComponent1();

    public void f3()
    {
        // 使用c1
        ...
    }
}

使用这种方法,不论是获取组件还是替换组件都非常方便。但是,现在Container类是通过Java代码来实现的,如果系统中的组件有任何变动,就需要修改代码,然后重新编译项目。在某些场景下,我们可能需要在项目运行时动态地添加、移除或者替换组件。

为了实现组件的动态管理,可以将如何创建组件以及组件之间的依赖关系等信息写入配置文件中,然后项目启动时通过读取配置文件来动态创建所有组件,再放到Container中。这样就可以在项目运行时修改配置文件中的组件信息,而无需重新编译,甚至无需重启服务器:

// 创建Container
Container container = new ContainerFactory("container.xml").create();

// 获取Component1
Component1 c1 = (Component1) container.create("c1");

其实,上面的Container就是一个简单的IOC容器。IOC表示控制反转,意思是创建组件的工作不再由程序员控制,而是由IOC容器控制,程序员只负责告诉IOC容器如何创建某个组件,如果想要这个组件,直接从容器中取就是了,这就是IOC容器的基本逻辑。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,204评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,091评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,548评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,657评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,689评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,554评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,302评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,216评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,661评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,851评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,977评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,697评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,306评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,898评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,019评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,138评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,927评论 2 355

推荐阅读更多精彩内容