我有一个简单的应用程序,说它有一些类和一个处理数据库请求的"额外".目前我每次使用应用程序时都会创建数据库对象,但在某些情况下,不需要数据库连接.我这样做(PHP顺便说一句):
$db = new Database(); $foo = new Foo($db); // passing the db
但有时该$foo
对象不需要db访问,因为只调用没有数据库操作的方法.所以我的问题是:处理这种情况的专业方法是什么/如何仅在需要时创建数据库连接/对象?
我的目标是避免不必要的数据库连接.
新答案注意:尽管ops 的直接答案是问题,"我什么时候才能在需要时创建/连接到数据库,而不是在每次请求时" 在需要时注入它,只是说这没有帮助.我在这里解释你是如何正确地解决这个问题的,因为在非特定框架环境中确实没有很多有用的信息来帮助这方面.
更新:此问题的"旧"答案如下所示.这鼓励了服务定位器模式,这是非常有争议的,并且很多都是"反模式".新的答案补充了我从研究中学到的东西.请先阅读旧答案,看看这是如何进展的.
使用疙瘩一段时间后,我学到了很多关于它是如何工作的,以及它如何并不是真正是惊人的毕竟.它仍然很酷,但它只有80行代码的原因是因为它基本上允许创建一个闭包数组.疙瘩被广泛用作服务定位器(因为它在实际上可以做到如此有限),这是一种"反模式".
服务定位器模式是软件开发中使用的设计模式,用于封装获得具有强抽象层的服务所涉及的过程.此模式使用称为"服务定位器"的中央注册表,根据请求返回执行特定任务所需的信息.
我在引导程序中创建了疙瘩,定义了依赖项,然后将此容器传递给我实例化的每个单独的类.
你说这有什么问题?主要问题是这种方法隐藏了类的依赖关系.因此,如果开发人员要更新此类并且之前没有看过它们,他们将会看到包含未知数量对象的容器对象.此外,测试这个课程将是一个噩梦.
我为什么最初这样做?因为我认为在控制器开始执行依赖注入之后.这是错的.你可以直接在控制器级别启动它.
如果这是我的应用程序中的工作方式:
前端控制器 - > Bootstrap - > 路由器 - > 控制器/方法 - > 模型[服务|域对象|映射器] - > 控制器 - > 视图 - > 模板
...然后依赖注入容器应该立即开始在第一个控制器级别工作.
所以,如果我仍然使用疙瘩,我将定义将要创建哪些控制器,以及它们需要什么.因此,您可以将视图和任何内容从模型层注入控制器,以便它可以使用它.这是控制反转,使测试更容易.来自Aurn wiki,(我将很快谈到):
在现实生活中,您不会通过将整个五金店(希望)运送到施工现场来建造房屋,以便您可以访问所需的任何部件.取而代之的是,工头(__construct())请求,将需要的特定部位(门窗),去了解他们采购.你的对象应该以相同的方式运作; 他们应该只询问完成工作所需的具体依赖性.让房子进入整个五金店是最糟糕的OOP风格,最糟糕的是可维护性的噩梦. - 来自Auryn Wiki
关于这一点,我想向您介绍一些精彩称为Auryn,以书面Rdlowrey,我被介绍给周末.
Auryn'自动连接'类依赖于基于类构造函数签名.这意味着,对于每个请求的类,Auryn发现它,计算出它的构造函数需要,创建它需要什么,然后再创建你问原来是类的一个实例.以下是它的工作原理:
Provider基于其构造方法签名中指定的参数type-hints递归地实例化类依赖项.
...如果你对PHP的反思有所了解,你会发现有些人称之为'慢'.所以这就是Auryn对此的看法:
你可能听说过"反思很慢".让我们清楚一点:如果你做错了,任何东西都可能"太慢".反射比磁盘访问快一个数量级,比从远程数据库检索信息(例如)快几个数量级.此外,如果您担心速度,每次反射都可以缓存结果.Auryn缓存它产生的任何反射,以最大限度地减少潜在的性能影响.
所以现在我们已经跳过了"反思很慢"的论点,这就是我一直在使用它的方式.
我让Auryn成为我自动加载器的一部分.这样,当一个类被要求时,Auryn可以离开并读取类和它的依赖项,它是依赖项的依赖项(等),并将它们全部返回到类中进行实例化.我创建了Auyrn对象.
$injector = new \Auryn\Provider(new \Auryn\ReflectionPool);
我在数据库类的构造函数中使用数据库接口作为要求.所以我告诉Auryn要使用哪个具体实现(如果要在代码中的单个点实例化不同类型的数据库,这是您更改的部分,并且它仍然可以工作).
$injector->alias('Library\Database\DatabaseInterface', 'Library\Database\MySQL');
如果我想改成MongoDB并且我为它编写了一个类,我只需要Library\Database\MySQL
改为Library\Database\MongoDB
.
然后,我将其传递$injector
到我的路由器,并在创建控制器/方法时,这是自动解决依赖关系的地方.
public function dispatch($injector) { // Make sure file / controller exists // Make sure method called exists // etc... // Create the controller with it's required dependencies $class = $injector->make($controller); // Call the method (action) in the controller $class->$action(); }
好了,所以使用这种技术,让我们说你有用户控制器,它要求用户服务(让我们说的usermodel),这需要访问数据库.
class UserController { protected $userModel; public function __construct(Model\UserModel $userModel) { $this->userModel = $userModel; } } class UserModel { protected $db; public function __construct(Library\DatabaseInterface $db) { $this->db = $db; } }
如果您使用路由器中的代码,Auryn将执行以下操作:
使用MySQL作为具体类(boostrap中的别名)创建Library\DatabaseInterface
创建'UserModel',并将先前创建的数据库注入其中
使用先前创建的UserModel注入其中来创建UserController
这就是那里的递归,这就是我之前谈到的'自动布线'.这解决了有机磷农药的问题,因为只有当类层次结构包含数据库对象的构造函数的要求是insantiated对象,不是在每次请求.
此外,每个类都具有在构造函数中运行所需的完全要求,因此没有像服务定位器模式那样的隐藏依赖项.
RE:如何使它在需要时调用connect方法.这很简单.
确保在Database类的构造函数中,不实例化对象,只需传入它的设置(主机,dbname,用户,密码).
new PDO()
使用类的设置有一个实际执行对象的connect方法.
class MySQL implements DatabaseInterface { private $host; // ... public function __construct($host, $db, $user, $pass) { $this->host = $host; // etc } public function connect() { // Return new PDO object with $this->host, $this->db etc } }
所以现在,您传递数据库的每个类都将具有此对象,但由于尚未调用connect(),因此将不具有连接.
在可以访问Database类的相关模型中,您可以调用$this->db->connect();
然后继续执行您想要执行的操作.
本质上,您仍然使用我之前描述的方法将数据库对象传递给需要它的类,但是为了决定何时在逐个方法的基础上执行连接,您只需在所需的方法中运行connect方法一.不,你不需要单身人士.你只需告诉它什么时候连接,当你不想告诉它连接时它就不会.
我将更深入地解释依赖注入容器,以及它们如何可以帮助您的情况.注意:理解"MVC"的原理将在这里有所帮助.
您想要创建一些对象,但只有某些对象需要访问数据库.你目前正在做的是在每个请求上创建数据库对象,这是完全没必要的,并且在使用DiC容器之类的东西之前也是完全常见的.
这是您可能想要创建的两个对象的示例.一个需要数据库访问,另一个不需要数据库访问.
/** * @note: This class requires database access */ class User { private $database; // Note you require the *interface* here, so that the database type // can be switched in the container and this will still work :) public function __construct(DatabaseInterface $database) { $this->database = $database; } } /** * @note This class doesn't require database access */ class Logger { // It doesn't matter what this one does, it just doesn't need DB access public function __construct() { } }
那么,创建这些对象并处理相关依赖项的最佳方法是什么,并且只将数据库对象传递给相关的类?好吧,幸运的是,这两个在使用依赖注入容器时协调一致.
Pimple是一个非常酷的依赖注入容器(由Symfony2框架的制造商提供),它使用PHP 5.3 +的闭包.
痘痘做的方式真的很酷 - 你想要的对象直到你直接要求它才会被实例化.因此,您可以设置一个新对象的负载,但在您要求它们之前,它们不会被创建!
这是一个非常简单的疙瘩示例,您可以在boostrap中创建:
// Create the container $container = new Pimple(); // Create the database - note this isn't *actually* created until you call for it $container['datastore'] = function() { return new Database('host','db','user','pass'); };
然后,在此处添加User对象和Logger对象.
// Create user object with database requirement // See how we're passing on the container, so we can use $container['datastore']? $container['User'] = function($container) { return new User($container['datastore']); }; // And your logger that doesn't need anything $container['Logger'] = function() { return new Logger(); };
好问题!因此,您已经在引导程序中创建了$container
对象,并设置了对象及其所需的依赖项.在路由机制中,将容器传递给控制器.
注意:示例基本代码
router->route('controller', 'method', $container);
在您的控制器,您访问$container
过的参数,当你问它从用户对象,你得到一个新的用户对象(工厂式),与数据库对象已经注入!
class HomeController extends Controller { /** * I'm guessing 'index' is your default action called * * @route /home/index * @note Dependant on .htaccess / routing mechanism */ public function index($container) { // So, I want a new User object with database access $user = $container['User']; // Say whaaat?! That's it? .. Yep. That's it. } }
所以,你现在已经用一块石头杀死了多只鸟(不仅仅是两只).
在每个请求上创建一个DB对象 - 不再是!它只是在你要求它时创建的,因为Pimple使用了闭包
从控制器中删除"新"关键字 - 是的,这是对的.您已将此责任交给容器.
注意:在继续之前,我想指出第二点是多么重要.如果没有这个容器,假设您在整个应用程序中创建了50个用户对象.然后有一天,您想要添加一个新参数.OMG - 您现在需要浏览整个应用程序并将此参数添加到每个应用程序中new User()
.然而,使用DiC - 如果你在$container['user']
任何地方使用,你只需将第三个参数添加到容器一次,就是这样.是的,这完全是真棒.
切换数据库的能力 - 你听说过我,这一点的重点是如果你想从MySQL改为PostgreSQL - 你改变容器中的代码以返回你编码的新的不同类型的数据库,并且只要这一切都归还同样的东西,那就是它!该换出具体的实现能力,每个人都始终竖琴上的.
这是使用容器的一种方式,它只是一个开始.有许多方法可以使这更好的-例如,而不是物品转交容器的每一个方法,你可以使用反射/某种映射来决定需要什么样的容器的部分.自动化,你是金色的.
我希望你发现这很有用.我在这里完成它的方式至少为我减少了大量的开发时间,启动它很有趣!