组合与继承.我的数据库交互库应该使用什么



考虑一个用PHP编写的数据库交互模块,该模块包含用于与数据库交互的类。我还没有开始给这个类编码,所以我不能给出代码片段。

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

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

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

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

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

在这一点上,我有两个选项可以在其他类中使用DatabaseHelper类:-

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

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

编辑:

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

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

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

让我们从上到下回答您的问题,看看我能为您所说的添加什么。

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

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

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

本质上,您必须在此处进行选择。您描述的方法称为活动记录模式。对象本身知道它是如何存储的以及存储在哪里的。对于与数据库交互以创建/读取/更新/删除的简单对象,这种模式非常有用。

如果数据库操作变得更加广泛,理解起来也不那么简单,那么使用数据映射器通常是一个不错的选择(例如,此实现)。这是第二个处理所有数据库交互的对象,而对象本身(例如User或Location)只处理特定于该对象的操作(例如login或goToLocation)。如果您想利用对象的存储空间,您只需要创建一个新的数据映射器。您的对象甚至不会知道实现中发生了什么变化。这加强了关注点的封装和分离。

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

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

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

您在这里描述的内容听起来像是一个单例。通常情况下,这不是一个很好的设计选择。你真的,真的确定永远不会有第二个数据库吗?可能不会,所以您不应该将自己限制在只允许一个数据库连接的实现中。与使用静态成员创建DatabaseHelper不同,您可以更好地使用一些方法创建数据库对象,这些方法允许您连接、断开连接、执行查询等。这样,如果您需要第二次连接,就可以重用它。

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

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

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

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

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

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

有关活动记录模式和数据映射器模式的更多信息,我建议您阅读MartinFowler的《企业应用程序架构模式》一书。它充满了这些模式,还有更多!

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

===编辑===

使用数据映射器模式的活动记录模式也有助于测试代码(正如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() {}
}

数据映射器模式的一个示例:

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实例或创建一个新的DatabaseHelpr实例。如果不存在,则在这种情况下,它将填充DatabaseHelper中的连接对象。容器还将包含名为makeUser和makeLocation的静态方法,它们将分别将DatabaseHelper注入User和Locations。

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

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

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

$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;
}
}

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

这能回答你悬而未决的问题吗?

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

顺便说一句,要使它真正有用,您的其他类(User、Locations)应该依赖于DatabaseHelperInterface,而不是直接依赖于DatabaseHelp。(这是能够切换实现所必需的)

依赖注入与继承的问题,至少在您的特定示例中可以归结为:"是a"或"有a"。

类foo是类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具有类似的接口,还有很多工作要做。)

您用User和Location类描述的内容称为表数据网关:

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

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

在这里使用依赖注入最明显的好处是当您想要对网关进行单元测试时。使用继承时,无法轻松模拟到数据库的连接。这意味着您必须始终拥有这些测试的数据库连接。使用Depedency注入可以模拟该连接,并测试网关与数据库助手的正确交互。

尽管这里的其他答案都很好,但我还是想从我使用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 }

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

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

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

通过这种方式,您的数据库设计和代码将是分开的。

最新更新