设计模式:如何仅在需要时创建数据库对象/连接? [英] Design Patterns: How to create database object/connection only when needed?

查看:30
本文介绍了设计模式:如何仅在需要时创建数据库对象/连接?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个简单的应用程序,假设它有一些类和一个处理数据库请求的额外"类.目前我每次使用应用程序时都会创建数据库对象,但在某些情况下不需要数据库连接.我是这样做的(PHP顺便说一句):

I've a simple application, say it has some classes and an "extra" one that handles database requests. Currently i'm creating the database object everytime the app is used, but in some cases there's no need for a database connection. I'm doing it like this (PHP btw):

$db = new Database();    
$foo = new Foo($db); // passing the db

但有时 $foo 对象不需要数据库访问,因为只调用没有数据库操作的方法.所以我的问题是:处理这种情况的专业方法是什么/如何仅在需要时创建数据库连接/对象?

But sometimes the $foo object does not need db access, as only methods without database actions are called. So my question is: What's the professional way to handle situations like this / how to create the db connection/object only when needed ?

我的目标是避免不必要的数据库连接.

推荐答案

注意:虽然直接回答操作问题,我什么时候可以只在需要时创建/连接到数据库,而不是在每个请求上"是在需要时注入它,简单地说这没有有帮助.我在此解释您实际上是如何正确执行此操作的,因为在非特定框架上下文中确实没有很多有用的信息可以在这方面提供帮助.

Note: Although the direct answer to ops question, "when can I only create / connect to the database when required and not on every request" is inject it when you need it, simply saying that is not helpful. I'm explaining here how you actually go about that correctly, as there really isn't a lot of useful information out there in a non-specific-framework context to help in this regard.

更新:此问题的旧"答案见下文.这鼓励了服务定位器模式,这是非常有争议的,对许多人来说是一种反模式".添加了我从研究中学到的新答案.请先阅读旧答案以了解进展情况.

Updated: The 'old' answer to this question can be see below. This encouraged the service locator pattern which is very controversial and to many an 'anti-pattern'. New answer added with what I've learned from researching. Please read the old answer first to see how this progressed.

新答案

在使用 pimple 一段时间后,我了解了很多关于它是如何工作的,以及它毕竟不是实际上那么神奇.它仍然很酷,但它只有 80 行代码的原因是因为它基本上允许创建一个闭包数组.Pimple 经常被用作服务定位器(因为它的实际功能非常有限),这是一种反模式".

New Answer

After using pimple for a while, I learned much about how it works, and how it's not actually that amazing after all. It's still pretty cool, but the reason it's only 80 lines of code is because it basically allows the creation of an array of closures. Pimple is used a lot as a service locator (because it's so limited in what it can actually do), and this is an "anti-pattern".

服务定位器模式是软件开发中使用的一种设计模式,用于用强抽象层封装获取服务所涉及的过程.此模式使用称为服务定位器"的中央注册表,根据请求返回执行特定任务所需的信息.

The service locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer. This pattern uses a central registry known as the "service locator" which on request returns the information necessary to perform a certain task.

我在引导程序中创建 pimple,定义依赖项,然后将此容器传递给我实例化的每个类.

I was creating pimple in the bootstrap, defining dependencies, and then passing this container to each and every single class I instantiated.

你说这有什么问题?主要问题是这种方法隐藏了类的依赖关系.因此,如果开发人员要更新这个类并且他们之前没有看到它,他们将看到一个包含未知数量对象的容器对象.此外,测试这个类将是一场噩梦.

What's the problem with this you say? The main problem is that this approach hides dependencies from the class. So if a developer is coming to update this class and they haven't seen it before, they're going to see a container object containing an unknown amount of objects. Also, testing this class is going to be a bit of a nightmare.

我最初为什么要这样做?因为我认为在控制器之后是您开始进行依赖注入的地方.这是错误的.您可以直接在控制器级别启动它.

Why did I do this originally? Because I thought that after the controller is where you start doing your dependency injection. This is wrong. You start it straight away at the controller level.

如果这就是我的应用程序中的工作方式:

If this is how things work in my application:

前端控制器 --> 自举 --> 路由器 --> 控制器/方法 -->模型[服务|域对象|映射器] --> 控制器 --> 视图 --> 模板

Front Controller --> Bootstrap --> Router --> Controller/Method --> Model [Services|Domain Objects|Mappers] --> Controller --> View --> Template

...那么依赖注入容器应该立即在第一个控制器级别开始工作.

...then the dependency injection container should start working right away at the first controller level.

所以说真的,如果我仍然使用 pimple,我会定义将要创建的控制器,以及它们需要什么.因此,您可以将视图和模型层中的任何内容注入控制器,以便它可以使用它.这是控制反转,使测试更加容易.来自 Aurn wiki,(我很快就会谈到):

So really, if I were to still use pimple, I would be defining what controllers are going to be created, and what they need. So you would inject the view and anything from the model layer into the controller so it can use it. This is Inversion Of Control and makes testing much easier. From the Aurn wiki, (which I'll talk about soon):

在现实生活中,您不会通过将整个五金店(希望如此)运送到建筑工地来建造房屋,这样您就可以访问所需的任何部件.相反,工头 (__construct()) 会询问需要的特定部件(门和窗)并着手采购它们.您的对象应该以相同的方式运行;他们应该只询问完成工作所需的特定依赖项.让 House 访问整个硬件商店充其量是糟糕的 OOP 风格,最糟糕的是可维护性的噩梦.- 来自 Auryn Wiki

In real life you wouldn't build a house by transporting the entire hardware store (hopefully) to the construction site so you can access any parts you need. Instead, the foreman (__construct()) asks for the specific parts that will be needed (Door and Window) and goes about procuring them. Your objects should function in the same way; they should ask only for the specific dependencies required to do their jobs. Giving the House access to the entire hardware store is at best poor OOP style and at worst a maintainability nightmare. - From the Auryn Wiki

进入奥林

关于这一点,我想向您介绍一个名为 Auryn 的精彩内容,作者是Rdlowrey,我在周末被介绍了.

Enter Auryn

On that note, I'd like to introduce you to something brilliant called Auryn, written by Rdlowrey that I was introduced to over the weekend.

Auryn 根据类构造函数签名自动连接"类依赖项.这意味着,对于每个请求的类,Auryn 会找到它,在构造函数中找出它需要什么,首先创建它需要的东西,然后创建您最初要求的类的实例.这是它的工作原理:

Auryn 'auto-wires' class dependencies based on the class constructor signature. What this means that, for each class requested, Auryn finds it, figures out what it needs in the constructor, creates what it needs first and then creates an instance of the class you asked for originally. Here's how it works:

Provider 根据其构造函数方法签名中指定的参数类型提示递归地实例化类依赖项.

The Provider recursively instantiates class dependencies based on the parameter type-hints specified in their constructor method signatures.

...如果您对 PHP 的反射有所了解,您'会知道有些人称之为'慢'.所以这就是 Auryn 的做法:

...and if you know anything about PHP's reflection, you'll know some people call it 'slow'. So here's what Auryn does about that:

您可能听说过反射很慢".让我们澄清一些事情:如果你做错了,任何事情都可能太慢".反射比磁盘访问快一个数量级,比从远程数据库检索信息(例如)快几个数量级.此外,如果您担心速度,每个反射都提供了缓存结果的机会.Auryn 会缓存它生成的任何反射,以尽量减少潜在的性能影响.

You may have heard that "reflection is slow". Let's clear something up: anything can be "too slow" if you're doing it wrong. Reflection is an order of magnitude faster than disk access and several orders of magnitude faster than retrieving information (for example) from a remote database. Additionally, each reflection offers the opportunity to cache the results if you're worried about speed. Auryn caches any reflections it generates to minimize the potential performance impact.

所以现在我们已经跳过了反射很慢"的说法,这是我一直在使用它的方式.

So now we've skipped the "reflection is slow" argument, here's how I've been using it.

  • 我让我的自动加载器的 Auryn 部分.这样,当需要一个类时,Auryn 可以离开并读取该类及其依赖项,以及它的依赖项的依赖项(等),并将它们全部返回到类中以进行实例化.我创建了 Auyrn 对象.

  • I make Auryn part of my autoloader. This is so that when a class is asked for, Auryn can go away and read the class and it's dependencies, and it's dependencies' dependencies (etc), and return them all into the class for instantiation. I create the Auyrn object.

$injector = new AurynProvider(new AurynReflectionPool);

  • 我使用数据库接口作为数据库类构造函数的要求.所以我告诉 Auryn 使用哪个具体实现(如果你想在代码中的一个点实例化不同类型的数据库,这是你需要改变的部分,它仍然可以工作).

  • I use a Database Interface as a requirement in the constructor of my database class. So I tell Auryn which concrete implementation to use (this is the part you change if you want to instantiate a different type of database, at a single point in your code, and it'll all still work).

    $injector->alias('LibraryDatabaseDatabaseInterface', 'LibraryDatabaseMySQL');
    

  • 如果我想改用 MongoDB 并且我为它编写了一个类,我只需将 LibraryDatabaseMySQL 更改为 LibraryDatabaseMongoDB.

    If I wanted to change to MongoDB and I'd written a class for it, I'd simple change LibraryDatabaseMySQL to LibraryDatabaseMongoDB.

    • 然后,我将$injector 传递到我的路由器,并在创建控制器/方法时,这是自动解析依赖项的地方.

    • Then, I pass the $injector into my router, and when creating the controller / method, this is where the dependencies are automatically resolved.

    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();
    }
    

    好的,所以使用这种技术,假设您有需要访问数据库的用户服务(假设用户模型)的用户控制器.

    Okay, so using this technique, let's say you have the User controller which requires the User Service (let's say UserModel) which requires Database access.

    class UserController
    {
        protected $userModel;
    
        public function __construct(ModelUserModel $userModel)
        {
            $this->userModel = $userModel;
        }
    }
    
    class UserModel
    {
        protected $db;
    
        public function __construct(LibraryDatabaseInterface $db)
        {
            $this->db = $db;
        }
    }
    

    如果你使用路由器中的代码,Auryn 将执行以下操作:

    If you use the code in the router, Auryn will do the following:

    • 创建 LibraryDatabaseInterface,使用 MySQL 作为具体类(在 boostrap 中别名)
    • 创建UserModel"并将之前创建的数据库注入其中
    • 创建 UserController 并将之前创建的 UserModel 注入其中

    这就是递归,这就是我之前所说的自动布线".这解决了 OP 问题,因为只有当类层次结构包含数据库对象作为构造函数要求时 对象才被实例化,不是在每次请求时.

    That's the recursion right there, and this is the 'auto-wiring' I was talking about earlier. And this solves OPs problem, because only when the class hierarchy contains the database object as a constructor requirement is the object insantiated, not upon every request.

    此外,每个类都有它们在构造函数中运行所需的确切要求,因此与服务定位器模式一样,没有隐藏的依赖.

    Also, each class has exactly the requirements they need to function in the constructor, so there are no hidden dependencies like there were with the service locator pattern.

    RE:如何在需要时调用 connect 方法.这真的很简单.

    RE: How to make it so that the connect method is called when required. This is really simple.

    1. 确保在您的 Database 类的构造函数中,您没有实例化对象,而只是传入它的设置(主机、数据库名称、用户名、密码).
    2. 有一个实际执行 new PDO() 对象的连接方法,使用类的设置.

    1. Make sure that in the constructor of your Database class, you don't instantiate the object, you just pass in it's settings (host, dbname, user, password).
    2. Have a connect method which actually performs the new PDO() object, using the classes' settings.

    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() 还没有被调用.

  • So now, every class you pass the database to will have this object, but will not have the connection yet because connect() hasn't been called.

    本质上,您仍然使用我之前描述的方法将数据库对象传递给需要它的类,但要决定何时在方法对方法的基础上执行连接,您只需在所需的方法中运行连接方法.不,你不需要单身人士.你只需要在你想要的时候告诉它什么时候连接,当你不告诉它连接时它不会.

    In essence, you still pass your database object to the classes that require it, using the methods I have described previously, but to decide when to perform the connection on a method-by-method basis, you just run the connect method in the required one. No you don't need a singleton. You just tell it when to connect when you want it to, and it doesn't when you don't tell it to connect.

    我将更深入地解释依赖注入容器,以及它们如何可以帮助您的情况.注意:理解MVC"的原则在这里会有很大帮助.

    I'm going to explain a little more in-depth about Dependency Injection Containers, and how they can may help your situation. Note: Understanding the principles of 'MVC' will help significantly here.

    问题

    您想创建一些对象,但只有某些对象需要访问数据库.您当前所做的是在每个请求上创建数据库对象,这完全没有必要,而且在使用 DiC 容器之类的东西之前也很常见.

    The Problem

    You want to create some objects, but only certain ones need access to the database. What you're currently doing is creating the database object on each request, which is totally unnecessary, and also totally common before using things like DiC containers.

    以下是您可能想要创建的两个对象的示例.一个需要数据库访问权限,另一个不需要数据库访问权限.

    Here's an example of two objects that you may want to create. One needs database access, another doesn't need database access.

    /**
     * @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() { }
    }
    

    那么,创建这些对象并处理它们的相关依赖项,并将数据库对象仅传递给相关类的最佳方法是什么?好吧,我们很幸运,当使用依赖注入容器时,这两者和谐地协同工作.

    So, what's the best way to create these objects and handle their relevant dependencies, and also pass in a database object only to the relevant class? Well, lucky for us, these two work together in harmony when using a Dependency Injection Container.

    Pimple 是一个非常酷的依赖注入容器(由 Symfony2 框架的制造商开发),它利用了 PHP 5.3+ 的闭包.

    Pimple is a really cool dependency injection container (by the makers of the Symfony2 framework) that utilises PHP 5.3+'s closures.

    pimple 的做法真的很酷——你想要的对象在你直接请求它之前不会被实例化.因此,您可以设置大量新对象,但在您请求它们之前,它们不会被创建!

    The way that pimple does it is really cool - the object you want isn't instantiated until you ask for it directly. So you can set up a load of new objects, but until you ask for them, they aren't created!

    这是一个非常简单的痘痘示例,您在 boostrap 中创建:

    Here's a really simple pimple example, that you create in your 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 对象.

    Then, you add your User object and your Logger object here.

    // 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 对象?

    好问题!因此,您已经在引导程序中创建了$container 对象,并设置了对象及其所需的依赖项.在路由机制中,您将容器传递给控制器​​.

    Awesome! So.. how do I actually use the $container object?

    Good question! So you've already created the $container object in your bootstrap and set up the objects and their required dependencies. In your routing mechanism, you pass the container to your controller.

    注意:示例基本代码

    router->route('controller', 'method', $container);
    

    在你的控制器中,你访问传入的 $container 参数,当你从中请求用户对象时,你会得到一个新的用户对象(工厂风格),带有数据库对象已经注入!

    In your controller, you access the $container parameter passed in, and when you ask for the user object from it, you get back a new User object (factory-style), with the database object already injected!

    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.
        }
    }
    

    你解决了什么

    所以,你现在用一颗石头杀死了多只鸟(不仅仅是两只).

    What you've solved

    So, you've now killed multiple birds (not just two) with one stone.

    • 针对每个请求创建一个数据库对象 - 不再有!由于 Pimple 使用的闭包,它仅在您要求时才创建
    • 从控制器中删除新"关键字 - 是的,没错.你已经把这个责任交给了容器.
    • Creating a DB object on each request - Not any more! It's only created when you ask for it because of the closures Pimple uses
    • Removing 'new' keywords from your controller - Yep, that's right. You've handed this responsibility over to the container.

    注意: 在我继续之前,我想指出要点二的重要性.如果没有这个容器,假设您在整个应用程序中创建了 50 个用户对象.然后有一天,您想添加一个新参数.OMG - 您现在需要检查整个应用程序并将此参数添加到每个 new User().但是,对于 DiC - 如果您在任何地方都使用 $container['user'],您只需将第三个参数添加到容器中一次,就是这样.是的,这真是太棒了.

    Note: Before I continue, I want to point out how significant bullet point two is. Without this container, let's say you created 50 user objects throughout your application. Then one day, you want to add a new parameter. OMG - you now need to go through your whole application and add this parameter to every new User(). However, with the DiC - if you're using $container['user'] everywhere, you just add this third param to the container once, and that's it. Yes, that totally is awesome.

    • 切换数据库的能力 - 你听到我说的,重点是如果你想从 MySQL 更改为 PostgreSQL - 你可以更改容器中的代码以返回一个新的不同您编码的数据库类型,只要它都返回相同类型的东西,就是这样!更换具体实现的能力,这是每个人都津津乐道的事情.
    • The ability to switch out databases - You heard me, the whole point of this is that if you wanted to change from MySQL to PostgreSQL - you change the code in your container to return a new different type of database you've coded, and as long as it all returns the same sort of stuff, that's it! The ability to swap out concrete implementations that everyone always harps on about.

    这是使用容器的一种方式,而这只是一个开始.有很多方法可以使它变得更好 - 例如,您可以使用反射/某种映射来决定需要容器的哪些部分,而不是将容器交给每个方法.自动执行此操作,您就大功告成了.

    This is one way of using the container, and it's just a start. There are many ways to make this better - for example, instead of handing the container over to every method, you could use reflection / some sort of mapping to decide what parts of the container are required. Automate this and you're golden.

    我希望你觉得这很有用.我在这里完成的方式至少为我减少了大量的开发时间,而且启动起来很有趣!

    I hope you found this useful. The way I've done it here has at least cut significant amounts of development time for me, and it's good fun to boot!

    这篇关于设计模式:如何仅在需要时创建数据库对象/连接?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

  • 查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆