18

考虑一个用 PHP 编写的数据库交互模块,其中包含用于与数据库交互的类。我还没有开始编写课程,所以我无法提供代码片段。

每个数据库表将有一个类,如下所述。

User - 与用户表交互的类。该类包含createUser、updateUser等函数。

Locations - 用于与位置表交互的类。该类包含searchLocation、createLocation、updateLocation等函数。

另外,我正在考虑创建另一个类,如下所示:-

DatabaseHelper:一个类,该类将具有一个表示与数据库的连接的成员。此类将包含用于执行 SQL 查询的较低级别的方法,例如 executeQuery(query,parameters)、executeUpdate(query,parameters) 等。

此时,我有两个选项可以在其他类中使用 DatabaseHelper 类:-

  1. User 和 Locations 类将扩展 DatabaseHelper 类,以便它们可以在 DatabaseHelper 中使用继承的 executeQuery 和 executeUpdate 方法。在这种情况下,DatabaseHelper 将确保在任何给定时间只有一个与数据库的连接实例。
  2. DatabaseHelper 类将通过 Container 类注入到 User 和 Locations 类中,该 Container 类将创建 User 和 Location 实例。在这种情况下,容器将确保在任何给定时间应用程序中只有一个 DatabaseHelper 实例。

这是我很快想到的两种方法。我想知道采用哪种方法。可能这两种方法都不够好,在这种情况下,我想知道我可以用来实现数据库交互模块的任何其他方法。

编辑:

请注意,Container 类将包含一个类型为 DatabaseHelper 的静态成员。它将包含一个私有静态 getDatabaseHelper() 函数,该函数将返回现有的 DatabaseHelper 实例或创建一个新的 DatabaseHelper 实例(如果不存在),在这种情况下,它将填充 DatabaseHelper 中的连接对象。Container 还将包含名为 makeUser 和 makeLocation 的静态方法,它们将分别将 DatabaseHelper 注入到 User 和 Locations 中。

在阅读了几个答案后,我意识到最初的问题几乎已经得到了回答。但在我接受如下最终答案之前,仍有一个疑问需要澄清。

当我有多个数据库要连接而不是单个数据库时该怎么办。DatabaseHelper 类如何结合这一点以及容器如何在 User 和 Location 对象中注入适当的数据库依赖项?

4

6 回答 6

18

让我们从上到下回答您的问题,看看我可以在您所说的内容中添加什么。

每个数据库表将有一个类,如下所述。

User - 与用户表交互的类。该类包含createUser、updateUser等函数。

Locations - 用于与位置表交互的类。该类包含函数>如searchLocation、createLocation、updateLocation等。

基本上你必须在这里选择。您描述的方法称为活动记录模式。对象本身知道它的存储方式和存储位置。对于与数据库交互以创建/读取/更新/删除的简单对象,这种模式非常有用。

如果数据库操作变得更广泛且不那么容易理解,那么使用数据映射器(例如这个实现)通常是一个不错的选择。这是处理所有数据库交互的第二个对象,而对象本身(例如用户或位置)仅处理特定于该对象的操作(例如登录或 goToLocation)。如果你想存储你的对象,你只需要创建一个新的数据映射器。您的对象甚至不知道在实现中发生了某些变化。这加强了关注点的封装分离

还有其他选项,但这两种是实现数据库交互的最常用方法。

另外,我正在考虑创建另一个类,如下所示:-

DatabaseHelper :一个类,它有一个静态成员,表示与数据库的连接。此类将包含用于执行 SQL 查询的较低级别的方法,例如 executeQuery(query,parameters)、executeUpdate(query,parameters) 等。

您在此处描述的内容听起来像是单例。通常这不是一个好的设计选择。你真的,真的确定永远不会有第二个数据库吗?可能不会,因此您不应该将自己局限于只允许一个数据库连接的实现。除了使用静态成员创建 DatabaseHelper 之外,您还可以使用一些方法更好地创建 Database 对象,这些方法允许您连接、断开连接、执行查询等。这样,如果您需要第二个连接,您可以重用它。

此时,我有两个选项可以在其他类中使用 DatabaseHelper 类:-

  1. User 和 Locations 类将扩展 DatabaseHelper 类,以便它们可以在 DatabaseHelper 中使用继承的 executeQuery 和 executeUpdate 方法。在这种情况下,DatabaseHelper 将确保在任何给定时间只有一个与数据库的连接实例。
  2. DatabaseHelper 类将通过 Container 类注入到 User 和 Locations 类中,该 Container 类将创建 User 和 Location 实例。在这种情况下,容器将确保在任何给定时间应用程序中只有一个 DatabaseHelper 实例。

这是我很快想到的两种方法。我想知道采用哪种方法。可能这两种方法都不够好,在这种情况下,我想知道我可以用来实现数据库交互模块的任何其他方法。

第一种选择实际上并不可行。如果你阅读了继承的描述,你会看到继承通常用于创建现有对象的子类型。用户不是 DatabaseHelper 的子类型,也不是位置。MysqlDatabase 将是 Database 的子类型,或者 Admin 将是 User 的子类型。我建议不要使用此选项,因为它没有遵循面向对象编程的最佳实践。

第二种选择更好。如果您选择使用活动记录方法,您确实应该将数据库注入到用户和位置对象中。这当然应该由处理所有这些交互的第三个对象来完成。您可能想看看依赖注入控制反转

否则,如果您选择数据映射器方法,则应将数据库注入数据映射器。这样,仍然可以使用多个数据库,同时分离所有关注点。

有关活动记录模式和数据映射器模式的更多信息,我建议您获取Martin Fowler的企业应用程序架构模式一书。它充满了这些模式以及更多!

我希望这会有所帮助(如果那里有一些非常糟糕的英语句子,我很抱歉,我不是母语人士!)。

== 编辑 ==

使用数据映射器模式的活动记录模式也有助于测试您的代码(如 Aurel 所说)。如果您将所有代码分开来只做一件事,那么检查它是否真的在做这件事会更容易。通过使用PHPUnit(或其他一些测试框架)来检查您的代码是否正常工作,您可以非常确定每个代码单元中都不会出现错误。如果您混淆了这些问题(例如当您选择选项 1 时),这将变得更加困难。事情变得非常混乱,你很快就会得到一大堆意大利面条代码

== 编辑2 ==

活动记录模式的示例(非常懒惰,并不是真正的活动):

class Controller {
    public function main() {
        $database = new Database('host', 'username', 'password');
        $database->selectDatabase('database');
        
        $user = new User($database);
        $user->name = 'Test';
        
        $user->insert();
        
        $otherUser = new User($database, 5);
        $otherUser->delete();
    }
}

class Database {
    protected $connection = null;
    
    public function __construct($host, $username, $password) {
        // Connect to database and set $this->connection
    }
    
    public function selectDatabase($database) {
        // Set the database on the current connection
    }
    
    public function execute($query) {
        // Execute the given query
    }
}

class User {
    protected $database = null;
    
    protected $id = 0;
    protected $name = '';
    
    // Add database on creation and get the user with the given id
    public function __construct($database, $id = 0) {
        $this->database = $database;
        
        if ($id != 0) {
            $this->load($id);
        }
    }
    
    // Get the user with the given ID
    public function load($id) {
        $sql = 'SELECT * FROM users WHERE id = ' . $this->database->escape($id);
        $result = $this->database->execute($sql);
        
        $this->id = $result['id'];
        $this->name = $result['name'];
    }
    
    // Insert this user into the database
    public function insert() {
        $sql = 'INSERT INTO users (name) VALUES ("' . $this->database->escape($this->name) . '")';
        $this->database->execute($sql);
    }
    
    // Update this user
    public function update() {
        $sql = 'UPDATE users SET name = "' . $this->database->escape($this->name) . '" WHERE id = ' . $this->database->escape($this->id);
        $this->database->execute($sql);
    }
    
    // Delete this user
    public function delete() {
        $sql = 'DELETE FROM users WHERE id = ' . $this->database->escape($this->id);
        $this->database->execute($sql);
    }
    
    // Other method of this user
    public function login() {}
    public function logout() {}
}

以及数据映射器模式的示例:

class Controller {
    public function main() {
        $database = new Database('host', 'username', 'password');
        $database->selectDatabase('database');
        
        $userMapper = new UserMapper($database);
        
        $user = $userMapper->get(0);
        $user->name = 'Test';
        $userMapper->insert($user);
        
        $otherUser = UserMapper(5);
        $userMapper->delete($otherUser);
    }
}

class Database {
    protected $connection = null;
    
    public function __construct($host, $username, $password) {
        // Connect to database and set $this->connection
    }
    
    public function selectDatabase($database) {
        // Set the database on the current connection
    }
    
    public function execute($query) {
        // Execute the given query
    }
}

class UserMapper {
    protected $database = null;
    
    // Add database on creation
    public function __construct($database) {
        $this->database = $database;
    }
    
    // Get the user with the given ID
    public function get($id) {
        $user = new User();
        
        if ($id != 0) {
            $sql = 'SELECT * FROM users WHERE id = ' . $this->database->escape($id);
            $result = $this->database->execute($sql);
            
            $user->id = $result['id'];
            $user->name = $result['name'];
        }
        
        return $user;
    }
    
    // Insert the given user
    public function insert($user) {
        $sql = 'INSERT INTO users (name) VALUES ("' . $this->database->escape($user->name) . '")';
        $this->database->execute($sql);
    }
    
    // Update the given user
    public function update($user) {
        $sql = 'UPDATE users SET name = "' . $this->database->escape($user->name) . '" WHERE id = ' . $this->database->escape($user->id);
        $this->database->execute($sql);
    }
    
    // Delete the given user
    public function delete($user) {
        $sql = 'DELETE FROM users WHERE id = ' . $this->database->escape($user->id);
        $this->database->execute($sql);
    }
}

class User {
    public $id = 0;
    public $name = '';
    
    // Other method of this user
    public function login() {}
    public function logout() {}
}

== 编辑 3:由机器人编辑后 ==

请注意,Container 类将包含一个类型为 DatabaseHelper 的静态成员。它将包含一个私有静态 getDatabaseHelper() 函数,该函数将返回现有的 DatabaseHelper 实例或创建一个新的 DatabaseHelper 实例(如果不存在),在这种情况下,它将填充 DatabaseHelper 中的连接对象。Container 还将包含名为 makeUser 和 makeLocation 的静态方法,它们将分别将 DatabaseHelper 注入到 User 和 Locations 中。

在阅读了几个答案后,我意识到最初的问题几乎已经得到了回答。但在我接受如下最终答案之前,仍有一个疑问需要澄清。

当我有多个数据库要连接而不是单个数据库时该怎么办。DatabaseHelper 类如何结合这一点以及容器如何在 User 和 Location 对象中注入适当的数据库依赖项?

我认为不需要任何静态属性,Container 也不需要那些 makeUser 的 makeLocation 方法。让我们假设您有一些应用程序的入口点,您可以在其中创建一个类来控制应用程序中的所有流程。你似乎称它为容器,我更喜欢称它为控制器。毕竟,它控制着你的应用程序中发生的事情。

$controller = new Controller();

控制器必须知道它必须加载哪个数据库,以及是单个数据库还是多个数据库。例如,一个数据库包含用户数据,另一个数据库包含位置数据。如果给出了上面的活动记录 User 和类似的 Location 类,那么控制器可能如下所示:

class Controller {
    protected $databases = array();
    
    public function __construct() {
        $this->database['first_db'] = new Database('first_host', 'first_username', 'first_password');
        $this->database['first_db']->selectDatabase('first_database');
        
        $this->database['second_db'] = new Database('second_host', 'second_username', 'second_password');
        $this->database['second_db']->selectDatabase('second_database');
    }
    
    public function showUserAndLocation() {
        $user = new User($this->databases['first_database'], 3);
        $location = $user->getLocation($this->databases['second_database']);
        
        echo 'User ' . $user->name . ' is at location ' . $location->name;
    }
    
    public function showLocation() {
        $location = new Location($this->database['second_database'], 5);
        
        echo 'The location ' . $location->name . ' is ' . $location->description;
    }
}

将所有回声移动到 View 类或其他东西可能会很好。如果您有多个控制器类,则可能有一个不同的入口点来创建所有数据库并将它们推送到控制器中。例如,您可以将其称为前端控制器或入口控制器。

这是否回答了您开放的问题?

于 2012-06-12T08:51:01.113 回答
8

我会使用依赖注入,原因如下:如果在某些时候你想为你的应用程序编写测试,它会允许你用一个存根类替换 DatabaseHelper 实例,实现相同的接口,但并不真正访问一个数据库。这将使测试模型功能变得更加容易。

顺便说一句,为了真正有用,您的其他类(用户、位置)应该依赖于 DatabaseHelperInterface 而不是直接依赖于 DatabaseHelper。(这是能够切换实现所必需的)

于 2012-06-12T08:24:46.357 回答
5

至少在您的具体示例中,依赖注入与继承的问题归结为以下几点:“是”或“有”。

class foo 是 class bar 的一种吗?是酒吧吗?如果是这样,也许继承是要走的路。

foo 类是否使用类 bar 的对象?您现在处于依赖注入领域。

在您的情况下,您的数据访问对象(在我的代码方法中这些是 UserDAO 和 LocationDAO)不是数据库助手的类型。例如,您不会使用 UserDAO 来提供对另一个 DAO 类的数据库访问。相反,您在 DAO 类中使用数据库助手的功能。现在,这并不意味着从技术上讲,您无法通过扩展数据库助手类来实现您想要做的事情。但我认为这将是一个糟糕的设计,并且会随着您的设计发展而导致麻烦。

另一种思考方式是,您的所有数据都将来自数据库吗?如果在路上的某个地方,您想从 RSS 提要中提取一些位置数据,该怎么办。你的 LocationDAO 本质上定义了你的接口——可以说是你的“合同”——关于你的应用程序的其余部分如何获取位置数据。但是如果你扩展了 DatabaseHelper 来实现你的 LocationDAO,你现在会被卡住。没有办法让您的 LocationDAO 使用不同的数据源。但是,如果 DatabaseHelper 和您的 RSSHelper 都有一个通用接口,您可以将 RSSHelper 直接插入您的 DAO,并且 LocationDAO 甚至根本不需要更改。*

如果您已将 LocationDAO 设为 DatabaseHandler 的类型,则更改数据源将需要更改 LocationDAO 的类型。这意味着不仅 LocationDAO 必须更改,而且您使用 LocationDAO 的所有代码都必须更改。如果您从一开始就将数据源注入到 DAO 类中,那么无论数据源如何,LocationDAO 接口都将保持不变。

(* 只是一个理论示例。要让 DatabaseHelper 和 RSSHelper 拥有类似的界面,还有很多工作要做。)

于 2012-06-13T18:51:55.533 回答
3

您使用 User 和 Location 类描述的内容称为Table Data Gateway

充当数据库表网关的对象。一个实例处理表中的所有行。

通常,您希望组合优于继承,而编程则倾向于接口。虽然组装对象似乎需要付出更多努力,但这样做将有利于维护和从长远来看更改程序的能力(我们都知道更改是项目中唯一不变的)。

在这里使用依赖注入最明显的好处是当您想要对网关进行单元测试时。使用继承时,您不能轻易地模拟与数据库的连接。这意味着您将始终需要为这些测试建立数据库连接。使用依赖注入允许您模拟该连接并仅测试网关与数据库助手的正确交互。

于 2012-06-12T08:56:16.460 回答
3

尽管这里的其他答案非常好,但我想从我使用CakePHP(一个MVC框架)的经验中提出一些其他想法。基本上,我只会向您展示他们的API中的一两页;主要是因为 - 对我来说 - 它似乎定义明确且经过深思熟虑(可能是因为我每天都使用它)。

class DATABASE_CONFIG { // define various database connection details here (default/test/externalapi/etc) }

// Data access layer
class DataSource extends Object { // base for all places where data comes from (DB/CSV/SOAP/etc) }
// - Database
class DboSource extends DataSource { // base for all DB-specific datasources (find/count/query/etc) }
class Mysql extends DboSource { // MySQL DB-specific datasource }
// - Web service
class SoapSource extends DataSource { // web services, etc don't extend DboSource }
class AcmeApi extends SoapSource { // some non-standard SOAP API to wrestle with, etc }

// Business logic layer
class Model extends Object { // inject a datasource (definitions are in DATABASE_CONFIG) }
// - Your models
class User extends Model { // createUser, updateUser (can influence datasource injected above) }
class Location extends Model { // searchLocation, createLocation, updateLocation (same as above) }

// Flow control layer
class Controller extends Object { // web browser controls: render view, redirect, error404, etc }
// - Your controllers
class UsersController extends Controller { // inject the User model here, implement CRUD, this is where your URLs map to (eg. /users/view/123) }
class LocationsController extends Controller { // more CRUD, eg. $this->Location->search() }

// Presentation layer
class View extends Object { // load php template, insert data, wrap in design }
// - Non-HTML output
class XmlView extends View { // expose data as XML }
class JsonView extends View { // expose data as JSON }
于 2012-06-13T18:32:40.233 回答
2

如果您有不同类型的服务,并且一个服务想要使用另一个服务,则首选依赖注入。

您的类 User 和 Locations 听起来更像是与数据库交互的 DAO (DataAccessObject) 层,因此对于您的给定情况,您应该使用 In Inheritance。继承可以通过扩展类或实现接口来完成

public interface DatabaseHelperInterface {
  public executeQuery(....);
}

public class DatabaseHelperImpl implemnets DatabaseHelperInterface {
  public executeQuery(....) {
     //some code
  }

public Class UserDaoInterface extends DatabaseHelperInterface {
   public createUser(....);
}

public Class UserDaoImpl extends DatabaseHelperImpl {
   public createUser(....) {
    executeQuery(create user query);
   }

这样,您的数据库设计和代码将是分开的。

于 2012-06-12T09:01:50.190 回答