构成与继承.我应该将什么用于我的数据库交互库?

CKi*_*ing 18 php inheritance dependency-injection

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

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

User - 用于与用户表交互的类.该类包含createUser,updateUser等函数.

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

另外,我想创建另一个类如下: -

DatabaseHelper:具有表示与数据库的连接的成员的类.该类将包含执行SQL查询的低级方法,如executeQuery(查询,参数),executeUpdate(查询,参数)等.

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

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

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

编辑:

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

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

当我有多个数据库连接到而不是单个数据库时该怎么办.DatabaseHelper类如何包含它以及容器如何在User和Location对象中注入适当的数据库依赖项?

Tha*_*ton 18

让我们从上到下回答你的问题,看看我可以添加到你说的内容.

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

User - 用于与用户表交互的类.该类包含createUser,updateUser等函数.

位置 - 用于与位置表交互的类.该类包含函数>,例如searchLocation,createLocation,updateLocation等.

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

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

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

另外,我想创建另一个类如下: -

DatabaseHelper:一个具有静态成员的类,该成员表示与数据库的连接.该类将包含执行SQL查询的低级方法,如executeQuery(查询,参数),executeUpdate(查询,参数)等.

你在这里描述的听起来像一个单身人士.通常这不是一个好的设计选择.你真的,非常确定永远不会有第二个数据库吗?可能不是,因此您不应将自己局限于仅允许一个数据库连接的实现.您可以使用一些允许连接,断开连接,执行查询等方法来创建数据库对象,而不是使用静态成员创建DatabaseHelper.这样,如果您需要第二个连接,则可以重复使用它.

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

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

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

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

第二种选择更好.如果选择使用活动记录方法,则应将数据库注入User和Location对象.当然,这应该由处理所有这些交互的第三个对象来完成.您可能希望了解依赖注入控制反转.

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

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

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

==编辑==

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

== EDIT2 ==

活动记录模式的一个示例(非常懒惰,而不是非常活跃):

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() {}
}
Run Code Online (Sandbox Code Playgroud)

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

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() {}
}
Run Code Online (Sandbox Code Playgroud)

==编辑3:经过bot ==编辑

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

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

当我有多个数据库连接到而不是单个数据库时该怎么办.DatabaseHelper类如何包含它以及容器如何在User和Location对象中注入适当的数据库依赖项?

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

$controller = new Controller();
Run Code Online (Sandbox Code Playgroud)

控制器必须知道它必须加载什么数据库,以及是否有一个数据库或多个数据库.例如,一个数据库包含用户数据,另一个数据库包含位置数据.如果给出了来自上面的活动记录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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

这是否能解答您的问题?


小智 8

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

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


Mar*_*rvo 5

至少在您的具体示例中,依赖注入与继承的问题可归结为以下内容:"是"或"具有".

class foo是一种类吧?这是酒吧吗?如果是这样,也许继承是可行的方法.

class 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拥有类似的接口,还有很多工作要做.)