💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
#依赖注入 依赖注入(DI)的目的是使类免除获取其操作所需的对象的责任(这些对象被称为服务)。 要在他们的实例化上传递这些服务。 我们在这章学到以下几点: * 依赖注入的原理是什么。 * 如何创建动态和静态DI容器。 * 如何延迟加载服务。 什么是依赖注入? 依赖注入(DI)是没有什么神秘或令人费解的。 它可以被理解为一个自私的句子:“不要寻求任何东西,让别人做它”。现在让我们把它翻译成程序员的演讲。 我们有一个博客里面有一个Article类: ~~~ class Article { public $id; public $title; public $content; function save() { // 我们将数据保存到数据库中 } } ~~~ 我们可以这样使用这个类 ~~~ $article = new Article; $article->title = '10 Things You Need to Know About Losing Weight'; $article->content = 'Every year millions of people in ...'; $article->save(); ~~~ save()方法是将要把文章保存到articles数据表中,它使用Nette\Database很容易实现。但是怎样Article得到数据库连接。 即使用Connection类。 嗯,我们可以将它放入一些全局变量中,如$ GLOBALS ['connection']或类的一些静态成员。 但是你没有听说过使用全局变量是错误的吗? 这是真的,全局变量是邪恶的,静态成员也是一样的。 那么在我们怎样得到数据库连接? DI有答案:“不要寻找任何东西,让别人做它。”换句话说,如果我需要一个数据库,有人给我,这不是我的工作。 哈,它狡猾,亲爱的DI! 我们开始做吧: ~~~ class Article { public $id; public $title; public $content; private $connection; function __construct(Nette\Database\Connection $connection) { $this->connection = $connection; } function save() { $this->connection->query('INSERT INTO articles', [ 'title' => $this->title, 'content' => $this->content, ]); } } ~~~ 使用Article类将稍有改变: ~~~ $article = new Article($connection); $article->title = ... $article->content = ... $article->save(); ~~~ 你在问,这个代码需要$connection? DI给出一个直接的答案:“让别人做它。”数据库连接将由调用代码的人提供。 等等,等等。 当然你说,不可能使责任的外交。 必须有一个零点。 你是对的。 开始有一个创造者,他没有委托任何东西,他创造了对象。 我们称之为系统容器。 他有一个单独的章节。 以前学过对象编程的人不难看懂,但是没有学过的话有点难了。实际上上面意思是做一个 Article对象,他有一个保存数据方法,但是在保存数据之前要连接数据,但是要怎样连接数据呢,所以我们做一个$connection默认对象,如果有人要使用Article类,那他就要实例化它,程序就会自动注入$connection默认对象。有些象做一个电脑配置单,然后按配置单采购配件,再组装起来,系统就是依赖注入,这样才能成为一台完整电脑出来,但可以按配置单组很多台电脑,每台电脑都有系统。 # 为什么全局变量是错误的呢? 好问题。 Article类无论如何都需要数据库连接。 但从第一个例子,没有显示出来,从哪里和如何得到它。 这样的代码的用户可能会感到惊讶,文章真的节省了,他问:“它在哪里保存?”第二个例子使用DI,代码是自我解释。 想象一下,你正在探索一些支付网关,你写一个例子: ~~~ $cc = new CreditCard('4461510140804839', 12, 2013); $cc->pay(1000, CreditCard::DOLLARS); ~~~ 您运行的代码,与您的卡号,后来你会发现,它真的从您的帐户扣了钱! 震惊你盯着列表和哀叹:“我的钱在哪里,怎么会发生,我没有配对它任何支付网关!”类CreditCard自己做了,发现一个在一些全局变量中词,神秘,如Article得到了数据库连接。 这样的事情你不是从代码中推导出来的,你甚至不知道如何将网关改为另一个,比如测试。 # DI意味着更多的写作 你可以实例化对象,使用DI意味着更多的写作,那就是创建一个Article的实例,你必须处理数据库连接等等。 这是真的,但不要忘记最后一次,当“少写作”花费你$ 1000! 不,我们不想放松。 异常是正确的,我们将添加一个更大:当我们发现需要缓存一些数据,与DI协调,它将需要一个参数与缓存存储库。 这意味着在许多地方调整应用程序:至少在任何地方,其中Article要被实例化。 现在怎么办? 事情有一个解决方案:而不是手动创建Article对象,我们做一个工厂,即。 函数创建这些文章对象。 当Article更改构造函数时,只有工厂必须更新,没有更多。 和在哪里得到工厂在我们的代码? 你知道...让别人做。 :-) # DI容器和服务 术语“DI容器”是指工厂。 更确切地说,它是一个对象,包含任何数量的工厂,每个服务一个。 什么是服务? 普通对象,比如说Connection实例。 只是用DI容器我们称之为服务。 也许一些顾问发明它使DI看起来很复杂,所以他们可以咨询。 示例可以是创建Article对象的容器,也可以是所需的数据库连接:: ~~~ class Container { function createConnection() { return new Nette\Database\Connection('mysql:', 'root', '***'); } function createArticle() { return new Article($this->createConnection()); } } ~~~ 用法如下: ~~~ $container = new Container; $article = $container->createArticle(); ~~~ 优点是显而易见的,我们不需要关心这篇文章究竟是如何实例化的,这是工厂的工作。 无论如何,解决方案仍有两个缺点。 首先,有条目数据连接到代码,所以我们将它们分离成一个变量: ~~~ class Container { private $parameters; function __construct(array $parameters) { $this->parameters = $parameters; } function createConnection() { return new Nette\Database\Connection( $this->parameters['dsn'], $this->parameters['user'], $this->parameters['password'] ); } function createArticle() { return new Article($this->createConnection()); } } $container = new Container([ 'dsn' => 'mysql:', 'user' => 'root', 'password' => '***', ]); $article = $container->createArticle(); ~~~ 更重要的缺点是,总是,当我们要求一个Article创建时,将要开始新的数据库连接。 这需要避免。 我们将添加方法getConnection,它将保留一次创建的服务以供下次使用: ~~~ class Container { private $parameters; private $services = []; function __construct(array $parameters) { $this->parameters = $parameters; } function createConnection() { return new Nette\Database\Connection( $this->parameters['dsn'], $this->parameters['user'], $this->parameters['password'] ); } function getConnection() { if (!isset($this->services['connection'])) { $this->services['connection'] = $this->createConnection(); } return $this->services['connection']; } function createArticle() { return new Article($this->getConnection()); } } ~~~ 现在我们有全功能DI容器。 如你所见,编写它并不复杂。 值得注意的是,服务单独不知道,这是由一些容器创建的,因此可以创建任何PHP对象的方式。 不用触摸自己的源代码。 # Nette\DI\Container Nette \ DI \ Container类是通用DI容器的灵活实现。 它自动确保该服务实例只创建一次。 我们可以使我们自己的容器静态,即。 继承这个类,或者当我们添加工厂作为闭包或回调时,它是动态的。 ## 静态容器 工厂方法的名称遵循统一的约定,它们包括前缀createService +从首字母大写开始的服务的名称。 如果他们不应该从外部访问,可以降低其受保护的可见性。 请注意,容器已经为用户参数定义了$parameters字段。 ~~~ class MyContainer extends Nette\DI\Container { protected function createServiceConnection() { return new Nette\Database\Connection( $this->parameters['dsn'], $this->parameters['user'], $this->parameters['password'] ); } protected function createServiceArticle() { return new Article($this->getService('connection')); } } ~~~ 现在我们创建一个容器的实例并传递参数: ~~~ $container = new MyContainer([ 'dsn' => 'mysql:', 'user' => 'root', 'password' => '***', ]); ~~~ 我们通过调用getService方法获取服务: ~~~ $article = $container->getService('article'); ~~~ 如前所述,所有服务都只在一个容器中创建一次,但是如果容器总是创建一个Article的新实例,这将更有用。 它可以很容易地实现:而不是服务文章的工厂,我们将创建一个普通的方法createArticle: ~~~ class MyContainer extends Nette\DI\Container { function createServiceConnection() { return new Nette\Database\Connection( $this->parameters['dsn'], $this->parameters['user'], $this->parameters['password'] ); } function createArticle() { return new Article($this->getService('connection')); } } $container = new MyContainer(...); $article = $container->createArticle(); ~~~ 从$ container-> createArticle()的调用很明显,一个新的对象总是被创建。 这是一个程序员的约定。 ## 动态容器 我们可以使用addService方法将服务添加到Nette \ DI \ Container甚至运行时。 工厂可以写成PHP回调或闭包。 请注意,容器本身作为参数传递给它们,以便容易地访问参数和其他服务。 ~~~ $container = new Nette\DI\Container; $container->addService('connection', function($container) { return new Nette\Database\Connection( $container->parameters['dsn'], $container->parameters['user'], $container->parameters['password'] ); }); ~~~ 当服务创建只包括简单的实例化时,可以直接传递类名作为addService方法的第二个参数。 当我们已经创建了对象时,我们甚至可以传递它。 除了addService,我们还有hasService用于服务存在检查,removeService用于删除它