18

データベースと対話するためのクラスを含む、PHP で記述されたデータベース対話モジュールについて考えてみましょう。クラスのコーディングを開始していないため、コード スニペットを提供することはできません。

以下で説明するように、データベース テーブルごとに 1 つのクラスがあります。

User - ユーザー テーブルと対話するためのクラス。このクラスには、createUser、updateUser などの関数が含まれています。

Locations - ロケーション テーブルを操作するためのクラス。このクラスには、searchLocation、createLocation、updateLocation などの関数が含まれています。

さらに、次のように別のクラスを作成することを考えています: -

DatabaseHelper : データベースへの接続を表すメンバーを持つクラス。このクラスには、executeQuery(query,parameters)、executeUpdate(query,parameters) などの SQL クエリを実行するための下位レベルのメソッドが含まれます。

この時点で、他のクラスで DatabaseHelper クラスを使用するための 2 つのオプションがあります。

  1. User クラスと Locations クラスは DatabaseHelper クラスを拡張して、継承された executeQuery メソッドと executeUpdate メソッドを DatabaseHelper で使用できるようにします。この場合、DatabaseHelper は、データベースへの接続のインスタンスが常に 1 つだけであることを保証します。
  2. DatabaseHelper クラスは、User および Location インスタンスを作成する Container クラスを介して User および Locations クラスに注入されます。この場合、コンテナーは、アプリケーション内に DatabaseHelper のインスタンスが常に 1 つだけ存在するようにします。

これらは、すぐに頭に浮かぶ2つのアプローチです。どのようなアプローチを取るべきか知りたいです。これらのアプローチは両方とも十分ではない可能性があります。その場合、データベース対話モジュールを実装するために使用できる他のアプローチを知りたいです。

編集:

Container クラスには、DatabaseHelper 型の静的メンバーが含まれることに注意してください。これには、既存の DatabaseHelper インスタンスを返すか、存在しない場合は新しい DatabaseHelper インスタンスを作成するプライベートな静的 getDatabaseHelper() 関数が含まれます。この場合、DatabaseHelper に接続オブジェクトが設定されます。Container には、DatabaseHelper を User と Locations にそれぞれ注入する makeUser と makeLocation という静的メソッドも含まれます。

いくつかの回答を読んだ後、最初の質問がほとんど回答されていることに気付きました。しかし、次のような最終的な答えを受け入れる前に、明確にする必要がある疑問がまだあります.

接続するデータベースが 1 つではなく複数ある場合の対処方法。DatabaseHelper クラスはこれをどのように組み込み、コンテナは User および Location オブジェクトに適切なデータベースの依存関係をどのように挿入しますか?

4

6 に答える 6

18

あなたの質問に上から下まで答えて、あなたの言うことに何を追加できるか見てみましょう.

以下で説明するように、データベース テーブルごとに 1 つのクラスがあります。

User - ユーザー テーブルと対話するためのクラス。このクラスには、createUser、updateUser などの関数が含まれています。

Locations - ロケーション テーブルを操作するためのクラス。このクラスには、searchLocation、createLocation、updateLocation などの関数が含まれています。

基本的に、ここで選択する必要があります。あなたが説明した方法は、アクティブレコードパターンと呼ばれます。オブジェクト自体は、それが格納されている方法と場所を認識しています。データベースとやり取りして作成/読み取り/更新/削除する単純なオブジェクトの場合、このパターンは非常に便利です。

データベース操作がより広範になり、理解するのが単純でなくなる場合は、多くの場合、データ マッパーを使用することをお勧めします (例:この実装)。これは、すべてのデータベース インタラクションを処理する 2 番目のオブジェクトですが、オブジェクト自体 (例: ユーザーまたは場所) はそのオブジェクトに固有の操作 (例: ログインまたは goToLocation) のみを処理します。オブジェクトの保存を可能にしたい場合は、新しいデータ マッパーを作成するだけで済みます。オブジェクトは、実装で何かが変更されたことさえ知りません。これにより、懸念事項のカプセル化と分離が強制されます。

他にもオプションがありますが、これらの 2 つは、データベースの対話を実装する最もよく使用される方法です。

さらに、次のように別のクラスを作成することを考えています: -

DatabaseHelper : データベースへの接続を表す静的メンバーを持つクラス。このクラスには、executeQuery(query,parameters)、executeUpdate(query,parameters) などの SQL クエリを実行するための下位レベルのメソッドが含まれます。

ここで説明しているのはsingletonのように聞こえます。通常、これは本当に良い設計上の選択ではありません。2 番目のデータベースが存在しないことを本当に、本当に確信していますか? おそらくそうではないので、1 つのデータベース接続のみを許可する実装に限定しないでください。静的メンバーで DatabaseHelper を作成する代わりに、接続、切断、クエリの実行などを可能にするいくつかのメソッドを使用して Database オブジェクトを作成することをお勧めします。これにより、2 番目の接続が必要になった場合に再利用できます。

この時点で、他のクラスで DatabaseHelper クラスを使用するための 2 つのオプションがあります。

  1. User クラスと Locations クラスは DatabaseHelper クラスを拡張して、継承された executeQuery メソッドと executeUpdate メソッドを DatabaseHelper で使用できるようにします。この場合、DatabaseHelper は、データベースへの接続のインスタンスが常に 1 つだけであることを保証します。
  2. DatabaseHelper クラスは、User および Location インスタンスを作成する Container クラスを介して User および Locations クラスに注入されます。この場合、コンテナーは、アプリケーション内に DatabaseHelper のインスタンスが常に 1 つだけ存在するようにします。

これらは、すぐに頭に浮かぶ2つのアプローチです。どのようなアプローチを取るべきか知りたいです。これらのアプローチは両方とも十分ではない可能性があります。その場合、データベース対話モジュールを実装するために使用できる他のアプローチを知りたいです。

最初のオプションは実際には実行可能ではありません。継承の説明を読むと、継承は通常、既存のオブジェクトのサブタイプを作成するために使用されることがわかります。User は DatabaseHelper のサブタイプではなく、場所でもありません。MysqlDatabase は Database のサブタイプになるか、Admin は User のサブタイプになります。オブジェクト指向プログラミングのベスト プラクティスに従っていないため、このオプションはお勧めしません。

2 番目のオプションの方が適切です。アクティブ レコード メソッドを使用する場合は、データベースを User および Location オブジェクトに挿入する必要があります。もちろん、これは、これらすべての種類の相互作用を処理する何らかの第 3 のオブジェクトによって行われるべきです。おそらく、依存性注入制御の反転を見たいと思うでしょう。

それ以外の場合、データ マッパー メソッドを選択した場合は、データベースをデータ マッパーに挿入する必要があります。このようにして、すべての懸念事項を分離しながら、複数のデータベースを使用することができます。

アクティブ レコード パターンとデータ マッパー パターンの詳細については、Martin Fowler の著書Patterns of Enterprise Application Architectureを入手することをお勧めします。このようなパターンでいっぱいです。

これがお役に立てば幸いです (私はネイティブ スピーカーではありませんが、本当に下手な英語の文章が含まれている場合は申し訳ありません!)。

==編集==

データマッパーパターンのアクティブレコードパターンを使用すると、コードのテストにも役立ちます(Aurelが言ったように)。1 つのことだけを実行するためにすべてのコード ピースを分離すると、実際にこの 1 つのことを実行していることを確認しやすくなります。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 型の静的メンバーが含まれることに注意してください。これには、既存の DatabaseHelper インスタンスを返すか、存在しない場合は新しい DatabaseHelper インスタンスを作成するプライベートな静的 getDatabaseHelper() 関数が含まれます。この場合、DatabaseHelper に接続オブジェクトが設定されます。Container には、DatabaseHelper を User と Locations にそれぞれ注入する makeUser と makeLocation という静的メソッドも含まれます。

いくつかの回答を読んだ後、最初の質問がほとんど回答されていることに気付きました。しかし、次のような最終的な答えを受け入れる前に、明確にする必要がある疑問がまだあります.

接続するデータベースが 1 つではなく複数ある場合の対処方法。DatabaseHelper クラスはこれをどのように組み込み、コンテナは User および Location オブジェクトに適切なデータベースの依存関係をどのように挿入しますか?

静的プロパティは必要ないと思いますし、コンテナには makeLocation メソッドの makeUser も必要ありません。アプリケーションのすべてのフローを制御するクラスを作成するアプリケーションのエントリ ポイントがあるとします。あなたはそれをコンテナと呼んでいるようですが、私はそれをコントローラーと呼んでいます。結局のところ、アプリケーションで何が起こるかを制御します。

$controller = new Controller();

コントローラーは、どのデータベースをロードする必要があるか、また、そのデータベースが 1 つなのか複数なのかを認識している必要があります。たとえば、あるデータベースにはユーザー データが含まれ、別のデータベースには位置データが含まれます。上記のアクティブ レコード 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 インスタンスをスタブ クラスに置き換えることができ、同じインターフェイスを実装しますが、実際にはアクセスしません。データベース。これにより、モデルの機能のテストが非常に簡単になります。

ところで、これを本当に便利にするには、他のクラス (User、Locations) が直接 DatabaseHelper に依存するのではなく、DatabaseHelperInterface に依存する必要があります。(これは、実装を切り替えることができるようにするために必要です)

于 2012-06-12T08:24:46.357 に答える
5

依存性注入と継承の問題は、少なくとも特定の例では、「is a」または「has a」に帰着します。

クラスfooはクラスバーの一種ですか? バーですか?もしそうなら、多分継承は行く方法です。

クラス foo はクラス bar のオブジェクトを使用しますか? あなたは今、依存性注入の領域にいます。

あなたの場合、データ アクセス オブジェクト (私のコード アプローチでは、これらは UserDAO と LocationDAO です) はデータベース ヘルパーのタイプではありません。たとえば、UserDAO を使用して別の DAO クラスへのデータベース アクセスを提供することはありません。代わりに、DAO クラスでデータベース ヘルパーの機能を使用します。これは、データベース ヘルパー クラスを拡張することによって、技術的にやりたいことを達成できなかったという意味ではありません。しかし、それは悪い設計であり、設計が進化するにつれて将来的に問題を引き起こすと思います.

それについて考える別の方法は、すべてのデータがデータベースから取得されるのかということです。将来どこかで、たとえば RSS フィードから位置データを取得したい場合はどうなるでしょうか。アプリケーションの残りの部分が位置データを取得する方法に関して、LocationDAO は基本的にインターフェイス (いわば「契約」) を定義します。しかし、LocationDAO を実装するために DatabaseHelper を拡張した場合は、行き詰まるでしょう。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と呼ばれます。

データベース テーブルへのゲートウェイとして機能するオブジェクト。1 つのインスタンスがテーブル内のすべての行を処理します。

一般に、継承よりも合成を優先し、インターフェイスに向けてプログラミングします。オブジェクトを組み立てるのに手間がかかるように思えるかもしれませんが、それを行うことは、長期的には保守とプログラムを変更する能力に利益をもたらします (そして、変更がプロジェクトで常に一定であることは誰もが知っています)。

ここで依存性注入を使用する最も明白な利点は、ゲートウェイの単体テストを行う場合です。継承を使用する場合、データベースへの接続を簡単にモックすることはできません。つまり、これらのテストには常にデータベース接続が必要です。Depedency Injection を使用すると、その接続をモックして、ゲートウェイがデータベース ヘルパーと正しく対話することをテストできます。

于 2012-06-12T08:56:16.460 に答える
3

ここでの他の回答は非常に優れていますが、 CakePHP ( MVCフレームワーク)を使用した経験から、他の考えを入れたいと思いました。基本的に、APIのリーフを 1 つまたは 2 つだけお見せします。主な理由は、私にとっては、明確に定義され、考え抜かれているように見えるからです (おそらく、私が毎日使用しているからです)。

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 に答える