Подтвердить что ты не робот

Состав против наследования. Что я должен использовать для своей библиотеки взаимодействия с базами данных?

Рассмотрим модуль взаимодействия с базой данных, написанный на PHP, содержащий классы для взаимодействия с базой данных. Я не начал кодировать класс, поэтому я не буду давать фрагменты кода.

В таблице базы данных будет указан один класс, как описано ниже.

Пользователь. Класс для взаимодействия с пользовательской таблицей. Класс содержит такие функции, как createUser, updateUser и т.д.

Местоположение. Класс для взаимодействия с таблицей местоположений. Класс содержит такие функции, как searchLocation, createLocation, updateLocation и т.д.

Кроме того, я думаю о создании другого класса следующим образом: -

DatabaseHelper: класс, который будет иметь член, который представляет соединение с базой данных. Этот класс будет содержать методы нижнего уровня для выполнения SQL-запросов, таких как executeQuery (запрос, параметры), executeUpdate (запрос, параметры) и т.д.

На этом этапе у меня есть два варианта использования класса DatabaseHelper в других классах: -

  • Класс User and Locations расширяет класс DatabaseHelper, чтобы они могли использовать унаследованные методы executeQuery и executeUpdate в DatabaseHelper. В этом случае DatabaseHelper гарантирует, что в любой момент времени будет только один экземпляр подключения к базе данных.
  • Класс DatabaseHelper будет введен в класс User and Locations через класс Container, который будет создавать экземпляры пользователя и местоположения. В этом случае контейнер будет проверять наличие только одного экземпляра DatabaseHelper в приложении в любой момент времени.

Это два подхода, которые быстро приходят мне в голову. Я хочу знать, с каким подходом идти. Возможно, оба этих подхода недостаточно хороши, и в этом случае я хочу знать любой другой подход, который я могу использовать для реализации модуля взаимодействия с базой данных.

Edit:

Обратите внимание, что класс Container будет содержать статический член типа DatabaseHelper. Он будет содержать частную статическую функцию getDatabaseHelper(), которая вернет существующий экземпляр DatabaseHelper или создаст новый экземпляр DatabaseHelper, если он не существует, и в этом случае он заполнит объект соединения в DatabaseHelper. Контейнер также будет содержать статические методы, называемые makeUser и makeLocation, которые будут вводить DatabaseHelper в User и Locations соответственно.

Прочитав несколько ответов, я понимаю, что на начальный вопрос почти был дан ответ. Но все же есть сомнения, что нужно уточнить, прежде чем я смогу принять окончательный ответ, который заключается в следующем.

Что делать, когда у меня есть несколько баз данных для подключения, а не одна база данных. Как класс DatabaseHelper включает это и как контейнер вставляет соответствующие зависимости базы данных в объекты User и Location?

4b9b3361

Ответ 1

Давайте ответим на ваши вопросы сверху донизу и посмотрим, что я могу добавить к тому, что вы говорите.

В таблице базы данных будет указан один класс, как описано ниже.

Пользователь - класс для взаимодействия с пользовательской таблицей. Класс содержит такие функции, как createUser, updateUser и т.д.

Местоположение - класс для взаимодействия с таблицей местоположений. Класс содержит функции > такие как searchLocation, createLocation, updateLocation и т.д.

По существу у вас есть выбор здесь. Описанный метод называется активная запись. Сам объект знает, как и где он хранится. Для простых объектов, которые взаимодействуют с базой данных для создания/чтения/обновления/удаления, этот шаблон действительно полезен.

Если операции с базой данных становятся более обширными и менее понятными, часто бывает неплохо пойти с устройством сопоставления данных (например, эта реализация). Это второй объект, который обрабатывает все взаимодействия с базами данных, в то время как сам объект (например, пользователь или местоположение) обрабатывает только те операции, которые относятся к этому объекту (например, login или goToLocation). Если вы когда-нибудь захотите разместить свои объекты, вам нужно будет создать новый сопоставитель данных. Ваш объект даже не знает, что что-то изменилось в реализации. Это обеспечивает encapsulation и разделение проблем.

Существуют и другие варианты, но эти два являются наиболее распространенными способами реализации взаимодействия с базами данных.

Кроме того, я думаю о создании другого класса следующим образом: -

DatabaseHelper: класс, который будет иметь статический член, который представляет соединение с базой данных. Этот класс будет содержать методы нижнего уровня для выполнения SQL-запросов, таких как executeQuery (запрос, параметры), executeUpdate (запрос, параметры) и т.д.

То, что вы здесь описываете, выглядит как singleton. Обычно это не очень хороший дизайн. Вы действительно уверены, что никогда не будет второй базы данных? Вероятно, нет, поэтому вы не должны ограничиваться реализацией, которая допускает только одно подключение к базе данных. Вместо создания DatabaseHelper со статическими членами вы можете лучше создать объект базы данных с помощью некоторых методов, которые позволят вам подключиться, отключить, выполнить запрос и т.д. Таким образом, вы можете повторно использовать его, если вам когда-либо понадобится второе соединение.

На этом этапе у меня есть два варианта использования класса DatabaseHelper в других классах: -

  • Класс User and Locations расширяет класс DatabaseHelper, чтобы они могли использовать унаследованные методы executeQuery и executeUpdate в DatabaseHelper. В этом случае DatabaseHelper будет гарантировать, что есть только один экземпляр подключения к базе данных в любой момент времени.
  • Класс DatabaseHelper будет введен в класс User and Locations через класс Container, который будет создавать экземпляры пользователя и местоположения. В этом случае контейнер будет следить за тем, чтобы в приложении было только один экземпляр DatabaseHelper в любое время.

Это два подхода, которые быстро приходят мне в голову. Я хочу знать, с каким подходом идти. Возможно, оба этих подхода недостаточно хороши, и в этом случае я хочу знать любой другой подход, который я могу использовать для реализации модуля взаимодействия с базой данных.

Первый вариант не является жизнеспособным. Если вы прочитали описание наследования, вы увидите, что наследование обычно используется для создания подтипа существующего объекта. Пользователь не является подтипом DatabaseHelper и не является местоположением. MysqlDatabase будет подтипом базы данных, или Admin будет подтипом Пользователя. Я бы посоветовал против этого варианта, поскольку он не следует лучшим практикам объектно-ориентированного программирования.

Второй вариант лучше. Если вы решите использовать метод активной записи, вы действительно должны ввести базу данных в объекты "Пользователь" и "Место". Разумеется, это должно быть сделано каким-то третьим объектом, который обрабатывает все эти взаимодействия. Вероятно, вы захотите взглянуть на инъекция зависимостейи инверсия управления.

В противном случае, если вы выберете метод сопоставления данных, вы должны ввести базу данных в карту данных. Таким образом, по-прежнему можно использовать несколько баз данных, разделяя все ваши проблемы.

Для получения дополнительной информации о шаблоне активной записи и шаблоне карты данных я бы посоветовал вам получить Шаблоны архитектуры корпоративных приложений Мартина Фаулера. Он полон таких моделей и многое, многое другое!

Я надеюсь, что это поможет (и извините, если есть какие-то действительно плохие английские предложения там, я не носитель языка!).

== EDIT ==

Использование шаблона активной записи шаблона карт данных также помогает в тестировании вашего кода (например, 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() {}
}

== EDIT 3: после редактирования bot ==

Обратите внимание, что класс Container будет содержать статический член типа DatabaseHelper. Он будет содержать частную статическую функцию getDatabaseHelper(), которая вернет существующий экземпляр DatabaseHelper или создаст новый экземпляр DatabaseHelper, если он не существует, и в этом случае он заполнит объект соединения в DatabaseHelper. Контейнер также будет содержать статические методы, называемые makeUser и makeLocation, которые будут вводить DatabaseHelper в User и Locations соответственно.

Прочитав несколько ответов, я понимаю, что на начальный вопрос почти был дан ответ. Но все же есть сомнения, что необходимо уточнить, прежде чем я смогу принять окончательный ответ, который заключается в следующем.

Что делать, когда у меня есть несколько баз данных для подключения, а не одна база данных. Как класс DatabaseHelper включает это и как контейнер вставляет соответствующие зависимости базы данных в объекты User и Location?

Я думаю, что нет никакой необходимости в каком-либо статическом свойстве, и контейнеру не нужен этот makeUser методов makeLocation. Предположим, что у вас есть точка входа в приложение, в которой вы создаете класс, который будет контролировать весь поток в вашем приложении. Кажется, вы называете это контейнером, я предпочитаю называть его контроллером. В конце концов, он контролирует, что происходит в вашем приложении.

$controller = new Controller();

Контроллер должен будет знать, какую базу данных он должен загрузить, и если есть одна база данных или несколько. Например, одна база данных содержит пользовательские данные, база данных anonther содержит данные о местоположении. Если активна активная запись пользователя выше и аналогичный класс местоположения, то контроллер может выглядеть следующим образом:

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 или что-то еще. Если у вас несколько классов контроллеров, он может окупиться, чтобы иметь другую точку входа, которая создает все базы данных и толкает их в контроллер. Например, вы можете называть это фронт-контроллером или контроллером ввода.

Отвечает ли вам ответ на вопросы?

Ответ 2

Я бы пошел с инъекцией зависимостей по следующей причине: если в какой-то момент вы захотите написать тесты для своих приложений, это позволит вам заменить экземпляр DatabaseHelper классом-заглушкой, реализуя тот же интерфейс, но это сделать не действительно доступ к базе данных. Это упростит проверку функциональности вашей модели.

Кстати, для того, чтобы это было действительно полезно, ваши другие классы (User, Locations) должны зависеть от DatabaseHelperInterface, а не непосредственно от DatabaseHelper. (Это необходимо для возможности переключения реализаций)

Ответ 3

Вопрос об Injection Dependency vs. Inheritance, по крайней мере в вашем конкретном примере, сводится к следующему: "is a" или "has a".

Является ли класс foo типом класса? Это бар? Если это так, возможно, наследование - это путь.

Использует ли класс 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, чтобы иметь аналогичный интерфейс.)

Ответ 4

То, что вы описываете с помощью классов пользователя и местоположения, называется Table Data Gateway:

Объект, который действует как шлюз к таблице базы данных. Один экземпляр обрабатывает все строки в таблице.

В общем, вы хотите использовать композицию над наследованием и программу в направлении интерфейс. Хотя это может показаться большим количеством усилий для сборки ваших объектов, это поможет улучшить обслуживание и возможность изменения программы в долгосрочной перспективе (и все мы знаем, что изменения всегда постоянны в проекте).

Наиболее очевидное преимущество использования Injection Dependency здесь - это когда вы хотите unit test Шлюзы. Вы не можете легко высмеять соединение с базой данных при использовании наследования. Это означает, что для этих тестов вам всегда нужно подключиться к базе данных. Использование Depedency Injection позволяет вам обманывать это соединение и просто проверять правильность взаимодействия шлюзов с помощником базы данных.

Ответ 5

Несмотря на то, что другие ответы здесь очень хорошие, я хотел отбросить другие мысли из моего опыта, используя CakePHP (a 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 }

Ответ 6

Инъекция зависимостей предпочтительнее, если у вас разные виды услуг, а одна служба хочет использовать другую.

Ваши классы User and 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);
   }

Таким образом, ваш дизайн базы данных и код будут отдельными.