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

时间:2012-06-12 08:17:49

标签: php inheritance dependency-injection

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

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

用户 - 用于与用户表进行交互的类。该类包含诸如createUser,updateUser等函数。

地点 - 用于与locations表进行交互的类。该类包含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实例。
  3. 这两种方法很快就会浮现在我的脑海中。我想知道采用哪种方法。这两种方法都可能不够好,在这种情况下,我想知道我可以用来实现数据库交互模块的任何其他方法。

    修改

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

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

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

6 个答案:

答案 0 :(得分:18)

让我们自上而下回答你的问题,看看我能为你说的内容添加什么。

  

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

     

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

     

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

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

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

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

  

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

     

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

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

  

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

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

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

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

第二种选择更好。如果选择使用活动记录方法,则应将数据库注入User和Location对象。当然,这应该由处理所有这些交互的第三个对象来完成。您可能需要查看dependency injectioninversion of control

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

有关活动记录模式和数据映射器模式的更多信息,我建议您获取Martin Fowler的Patterns of Enterprise Application Architecture书。它充满了这些模式,甚至更多!

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

==编辑==

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

== 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:经过bot ==

编辑后
  

请注意,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类或其他东西可能会很好。如果您有多个控制器类,则可能需要使用不同的入口点来创建所有数据库并将其推送到控制器中。例如,您可以将其称为前端控制器或入口控制器。

这是否能解答您的问题?

答案 1 :(得分:8)

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

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

答案 2 :(得分: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拥有类似的界面,还有很多工作要做。)

答案 3 :(得分:3)

您在用户和位置类中所描述的内容称为Table Data Gateway

  

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

通常,您想要favor Composition over Inheritanceprogramm towards an interface。虽然组装对象似乎需要付出更多努力,但从长远来看,这样做有利于维护和更改程序的能力(而且我们都知道变更是项目中唯一的常量)。

在此处使用依赖注入最明显的好处是,您希望对网关进行单元测试。使用继承时,您无法轻易地模拟与数据库的连接。这意味着您将始终必须为这些测试建立数据库连接。使用Depedency Injection可以模拟该连接,只需测试网关与数据库助手正确交互。

答案 4 :(得分:3)

尽管这里的其他答案非常好,但我想从使用CakePHPMVC框架)的经验中提出一些其他想法。基本上,我只会向你展示他们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 }

答案 5 :(得分:2)

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

您的类用户和位置听起来更像是与数据库交互的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);
   }

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