通俗易懂的理解什么是依赖注入、控制反转和 IoC 容器(附源码)

好久没写技术文了,今天跟大家简单聊聊什么是面向对象编程中的依赖注入、控制反转和 IoC 容器。

依赖注入

开始之前我们先来看两段代码:

  1. 没有使用依赖注入:
class Car
{
    public $engine;

    public function __construct()
    {
        $this->engine = new BMWEngine(); // 通过 new 来实例化对象
    }
}
  1. 使用了依赖注入:
class Car
{
    public $engine;

    public function __construct(BMWEngine $engine)
    {
        $this->engine = $engine; // 通过注入来传递对象
    }
}

通过上面的代码我们可以看到,依赖注入仅仅从动作上来看它就只是改变了类的传递方式

如果仅仅只是这样,你可能会觉得这也没什么特别的。

但是如果我们把这个注入类换成一个抽象的接口呢?再来看看下面这段代码:

// 声明一个抽象的接口(引擎的标准)
interface Engine
{
    public function start();
}

// BMWEngine 实现引擎标准
class BMWEngine implements Engine
{
    public function start()
    {
        echo 'BMW Engine' . PHP_EOL;
    }
}

// BenzEngine 实现引擎标准
class BenzEngine implements Engine
{
    public function start()
    {
        echo 'Benz Engine' . PHP_EOL;
    }
}

class Car
{
    public $engine;

    // 只接受实现引擎标准的类
    public function __construct(Engine $engine)
    {
        $this->engine = $engine;
    }
}

可以看到,只要是实现了这个抽象接口的类就可以注入到 Car 类中去使用了。

汽车和引擎之间的关系就变成了组合关系。我们不再依赖于具体的实现类,而是依赖于抽象的接口(引擎的标准)。

如果有其他的汽车零件商想要提供汽车引擎,那他只要实现了这个抽象接口(引擎的标准)就可以了。

通过注入抽象的接口,我们就可以实现依赖倒置 (Dependency Inversion Principle, DIP),即高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象。

写到这里,我突然有了一点小小的感悟:像我们程序员其实也一样,不应该依赖于特定的公司或业务,而应该依赖于掌握通用的解决方案或底层能力。

控制反转

说完依赖注入,我们再来看看控制反转。那什么是控制反转呢?

维基百科的解释是:控制反转(Inversion of Control,IoC),是面向对象编程中的一种设计原则,可以用来降低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,DI),还有一种方式叫“依赖查找”(Dependency Lookup)。

所以这里重点说一下:控制反转其实是一种设计思想,并不是具体实现。它强调的是将对象的控制权从代码内部转移到外部容器或框架中,从而实现模块间的解耦。

IoC 容器

明白了这点,我们再来说下 IoC 容器(控制反转容器)。

大家可能好奇,既然依赖注入已经可以做到控制反转了,那为什么我们还需要 IoC 容器呢?

同样,我们还是先来看段代码:

class Car
{
    public $engine;

    public function __construct(Engine $engine)
    {
        $this->engine = $engine;
    }
}

// A 业务中的代码
$engineObj = new BMWEngine();
$carObj = new Car($engineObj);
$carObj->engine->start(); // BMW Engine

// B 业务中的代码
$engineObj = new BMWEngine();
$carObj = new Car($engineObj);
$carObj->engine->start(); // BMW Engine

// C 业务中的代码
$engineObj = new BMWEngine();
$carObj = new Car($engineObj);
$carObj->engine->start(); // BMW Engine

可以看到,我们在很多个业务模块中都使用到了 BMWEngine 这个引擎类。

如果现在老板需要将引擎更换为 BenzEngine,那我们该怎么办?是不是要修改所有使用 BMWEngine 的业务代码?

所以,为了解决这个问题,我们还需要一个 IoC 容器来帮助我们管理对象的创建和依赖注入。

我们再来看看修改后的代码:

// ======================== 1. 定义接口与实现类 ========================
interface Engine
{
    public function start();
}

class BMWEngine implements Engine
{
    public function start()
    {
        return "BMW Engine." . PHP_EOL;
    }
}

class BenzEngine implements Engine
{
    public function start()
    {
        return "Benz Engine." . PHP_EOL;
    }
}

// ======================== 2. 实现简易 IoC 容器 ========================
class Container
{
    private $bindings = [];
    private $instances = [];

    // 绑定抽象到具体实现(单例绑定/普通绑定)
    // 单例绑定示例(整个应用生命周期共享实例,如:数据库连接、配置管理等)
    // 普通绑定示例(每次解析时都会创建新的实例,如:邮件发送器、请求相关服务等)
    public function bind(string $abstract, $concrete, bool $singleton = false)
    {
        $this->bindings[$abstract] = [
            'concrete' => $concrete,
            'singleton' => $singleton
        ];
    }

    // 解析依赖
    public function make(string $abstract)
    {
        // 单例直接返回已有实例
        if (isset($this->instances[$abstract])) {
            return $this->instances[$abstract];
        }

        $concrete = $this->bindings[$abstract]['concrete'] ?? $abstract;

        // 如果是闭包,直接执行
        if ($concrete instanceof \Closure) {
            $object = $concrete($this);
        } 
        // 如果是类名,递归解析依赖
        else if (is_string($concrete)) {
            $object = $this->build($concrete);
        }

        // 如果是单例则保存实例
        if (($this->bindings[$abstract]['singleton'] ?? false) && isset($object)) {
            $this->instances[$abstract] = $object;
        }

        return $object ?? null;
    }

    // 递归构建对象及其依赖
    private function build(string $className)
    {
        $reflector = new \ReflectionClass($className);

        // 检查是否可实例化
        if (!$reflector->isInstantiable()) {
            throw new \Exception("无法实例化 {$className}");
        }

        // 获取构造函数
        $constructor = $reflector->getConstructor();
        if (is_null($constructor)) {
            return new $className();
        }

        // 解析构造函数参数
        $parameters = $constructor->getParameters();
        $dependencies = [];
        foreach ($parameters as $parameter) {
            $dependency = $parameter->getType()->getName();
            $dependencies[] = $this->make($dependency);
        }

        return $reflector->newInstanceArgs($dependencies);
    }
}

// ======================== 3. 业务类定义 ========================
class Car
{
    private $engine;

    // 依赖通过构造函数注入
    public function __construct(Engine $engine)
    {
        $this->engine = $engine;
    }

    public function getEngine()
    {
        return $this->engine;
    }

    public function start()
    {
        return $this->engine->start();
    }
}

// ======================== 4. 使用示例 ========================
$container = new Container();

// 绑定接口到默认实现(普通绑定)
$container->bind(Engine::class, BMWEngine::class, false);
$car1 = $container->make(Car::class);
echo $car1->start(); // 输出: BMW Engine.

// 动态切换引擎实现
$container->bind(Engine::class, BenzEngine::class);
$car2 = $container->make(Car::class);
echo $car2->start(); // 输出: Benz Engine.

// 绑定接口到默认实现(单例模式)
$container->bind(Engine::class, BMWEngine::class, true);

// 获取实例(单例验证)
$car1 = $container->make(Car::class);

// 再次获取实例(单例验证)
$car2 = $container->make(Car::class);
echo ($car1->getEngine() === $car2->getEngine()) ? "单例验证成功" . PHP_EOL : "单例验证失败" . PHP_EOL;

// ======================== 输出结果 ========================
// BMW Engine.
// Benz Engine.
// 单例验证成功

可以看到我们仅仅通过修改 bind 方法绑定新的实现就可让所有相关功能自动生效。

看到这里我们应该就明白了,所谓的 IoC 容器其实就是通过读取配置文件,将接口与具体的实现类进行绑定关联,当需要实例化接口时,IoC 容器就会自动注入具体的实现类。

所以如果我想要切换另一个实现类时,只需要对配置文件进行修改即可,而我完全不需要动我的业务代码。

这就好比我有一个登记表,提前登记好我需要的标准(抽象接口)和实现该标准的服务提供商(具体实现)。

当我需要使用某个服务时,我只需要查看登记表,并找到对应的服务提供商。

同样,我要更换服务提供商时,只需要在登记表中修改即可,而不需要去修改每个使用到这个服务的地方。

下面我们再通过几个实际应用场景来直观的感受下 IoC 容器的好处吧。

场景1:多环境配置

// 根据环境切换实现
public function register() {
    $this->app->bind(Cache::class, function () {
        return app()->environment('production') 
            ? new RedisCache(config('cache.redis'))
            : new ArrayCache();
    });
}

场景2:A/B 测试

// 随机分配实验组
$this->app->bind(RecommendationEngine::class, function () {
    return rand(0, 1) 
        ? new AlgorithmA()
        : new AlgorithmB();
});

场景3:单元测试

// 测试用例中覆盖绑定
public function test_order_processing()
{
    $this->app->instance(PaymentGateway::class, new FakePayment());

    $service = $this->app->make(OrderService::class);
    // 测试逻辑...
}

好了,到了这里,我想大家应该就能明白什么是依赖注入、控制反转和 IoC 容器了吧。

简单总结下:

  • 通过依赖注入,我们解耦了类与具体的实现;
  • 通过控制反转,我们将对象的控制权交给外部容器;
  • 而 IoC 容器则像是一位“智能管家”,自动管理依赖关系,让代码更灵活更能适应变化。

如果大家还有什么疑问欢迎评论区留言交流。

写在最后

正如帕累托法则所说:80% 的结果往往来自 20% 的关键努力

在日常开发中,我们也应该将更多的时间和精力用在学习优雅的底层设计或技术原理上。

如果本文对您有所帮助或者有所启发,请帮忙扫描下方二维码或微信搜索 「自在牛马」 关注一下我的公众号,您的支持是我最大的写作动力。感谢~
自在牛马

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!