2012-06-12 10 views
18

Considere un módulo de interacción de base de datos escrito en PHP que contiene clases para interactuar con la base de datos. No comencé a codificar la clase, así que no voy a poder dar fragmentos de código.Composición vs Herencia. ¿Qué debería usar para mi biblioteca de interacción de base de datos?

Habrá una clase por tabla de base de datos como se explica a continuación.

Usuario - Una clase para interactuar con la tabla del usuario. La clase contiene funciones como createUser, updateUser, etc.

Ubicaciones - Una clase para interactuar con la tabla de ubicaciones. La clase contiene funciones como searchLocation, createLocation, UpdateLocation, etc.

Además, estoy pensando en la creación de otra clase de la siguiente manera: -

DatabaseHelper: Una clase que tendrá un miembro que representa el conexión a la base de datos. Esta clase contendrá los métodos de nivel inferior para ejecutar consultas SQL como executeQuery (consulta, parámetros), executeUpdate (consulta, parámetros), etc.

En este punto, tengo dos opciones para utilizar la clase DatabaseHelper en otras clases: -

  1. La clase de usuario y lugares extenderá la clase DatabaseHelper para que puedan utilizar los métodos executeQuery y executeUpdate heredadas en DatabaseHelper. En este caso, DatabaseHelper se asegurará de que haya solo una instancia de la conexión a la base de datos en un momento dado.
  2. La clase DatabaseHelper se inyectará en la clase User and Locations a través de una clase Container que creará las instancias User y Location. En este caso, el contenedor se asegurará de que haya solo una instancia de DatabaseHelper en la aplicación en cualquier momento dado.

Estos son los dos enfoques que rápidamente vienen a mi mente. Quiero saber qué enfoque seguir. Es posible que estos dos enfoques no sean lo suficientemente buenos, en cuyo caso, deseo conocer cualquier otro enfoque que pueda seguir para implementar el módulo de interacción con la base de datos.

Editar:

Tenga en cuenta que la clase de contenedor contendrá un miembro estático de tipo DatabaseHelper. Contendrá una función privada static getDatabaseHelper() que devolverá una instancia DatabaseHelper existente o creará una nueva instancia DatabaseHelper si no existe, en cuyo caso, completará el objeto de conexión en DatabaseHelper. El Contenedor también contendrá métodos estáticos llamados makeUser y makeLocation que inyectarán el DatabaseHelper en User y Locations respectivamente.

Después de leer algunas respuestas, me doy cuenta de que la pregunta inicial casi ha sido respondida. Pero todavía hay una duda que debe aclararse antes de poder aceptar la respuesta final, que es la siguiente.

Qué hacer cuando tengo varias bases de datos para conectarme en lugar de una única base de datos. ¿Cómo incorpora la clase DatabaseHelper esto y cómo inyecta el contenedor las dependencias apropiadas de la base de datos en los objetos User y Location?

+3

+1 Buena pregunta – Sarfraz

+1

inheritance significa una relación 'is-a' entre las clases. El 'Usuario' no es un' DatabaseHelper'. Por lo tanto, la herencia no debe ser utilizada. Entonces, lo que tiene es la opción de composición: use DatabaseHelper como una variable miembro que cree dentro del constructor o use la inyección del constructor. – jgauffin

+0

¿Puede el infractor por favor dejar un comentario y decirme cuál es el problema con mi pregunta? Tengo un fuerte sentimiento de que esto es un voto de odio. – CKing

Respuesta

18

Respondamos sus preguntas de arriba a abajo, y veamos qué puedo agregar a lo que dices.

Habrá una clase por tabla de base de datos como se explica a continuación.

Usuario: una clase para interactuar con la tabla de usuarios. La clase contiene funciones como createUser, updateUser, etc.

Ubicaciones: una clase para interactuar con la tabla de ubicaciones. La clase contiene funciones> tales como searchLocation, createLocation, updateLocation, etc.

Esencialmente tiene que elegir aquí. El método que describió se llama el patrón active record. El objeto en sí sabe cómo y dónde se almacena. Para objetos simples que interactúan con una base de datos para crear/leer/actualizar/eliminar, este patrón es realmente útil.

Si las operaciones de la base de datos se vuelven más extensas y menos fáciles de entender, a menudo es una buena opción ir con un asignador de datos (por ejemplo, this implementation). Este es un segundo objeto que maneja todas las interacciones de la base de datos, mientras que el objeto mismo (por ejemplo, Usuario o Ubicación) solo maneja operaciones que son específicas de ese objeto (por ejemplo, login o goToLocation). Si alguna vez quieres tener la oportunidad de almacenar tus objetos, solo tendrás que crear un nuevo mapeador de datos. Su objeto ni siquiera sabrá que algo cambió en la implementación. Esto aplica encapsulation y seperation of concerns.

Existen otras opciones, pero estas dos son las formas más utilizadas para implementar las interacciones de la base de datos.

Además, estoy pensando en la creación de otra clase de la siguiente manera: -

DatabaseHelper: Una clase que tendrá un miembro estático que representa la conexión con la base de datos. Esta clase contendrá los métodos de nivel inferior para ejecutar consultas SQL como executeQuery (consulta, parámetros), executeUpdate (consulta, parámetros), etc.

Lo que está describiendo aquí suena como singleton. Normalmente, esta no es realmente una buena opción de diseño. ¿De verdad estás realmente seguro de que nunca habrá una segunda base de datos? Probablemente no, por lo que no debe limitarse a una implementación que solo permita una conexión de base de datos. En lugar de crear un DatabaseHelper con miembros estáticos, puede crear mejor un objeto de base de datos con algunos métodos que le permitan conectarse, desconectarse, ejecutar una consulta, etc. De esta forma, puede reutilizarlo si alguna vez necesita una segunda conexión.

En este punto, tengo dos opciones para utilizar la clase DatabaseHelper en otras clases: -

  1. La clase de usuario y lugares se extenderá la clase DatabaseHelper para que puedan utilizar el executeQuery heredada y executeUpdate métodos en DatabaseHelper. En este caso, DatabaseHelper se asegurará de que haya solo una instancia de la conexión a la base de datos en un momento dado.
  2. La clase DatabaseHelper se inyectará en la clase User and Locations a través de una clase Container que creará las instancias User y Location. En este caso, el contenedor se asegurará de que haya solo una instancia de DatabaseHelper en la aplicación en cualquier momento dado.

Estos son los dos enfoques que rápidamente vienen a mi mente. Quiero saber qué enfoque seguir. Es posible que estos dos enfoques no sean lo suficientemente buenos, en cuyo caso, deseo conocer cualquier otro enfoque que pueda seguir para implementar el módulo de interacción con la base de datos.

La primera opción no es realmente viable. Si lees el description of inheritance, verás que la herencia se usa normalmente para crear un subtipo de un objeto existente. Un usuario no es un subtipo de un DatabaseHelper, ni es una ubicación. Una MysqlDatabase sería un subtipo de una base de datos, o un administrador sería un subtipo de un usuario. Recomendaría esta opción, ya que no sigue las mejores prácticas de programación orientada a objetos.

La segunda opción es mejor. Si elige usar el método de registro activo, debe inyectar la Base de datos en los objetos Usuario y Ubicación. Por supuesto, esto debería hacerse con un tercer objeto que maneje todo este tipo de interacciones. Es probable que desee echar un vistazo a dependency injection y inversion of control.

De lo contrario, si elige el método del correlacionador de datos, debe inyectar la base de datos en el correlacionador de datos. De esta manera, aún es posible usar varias bases de datos, mientras se separan todas sus preocupaciones.

Para obtener más información sobre el patrón de registro activo y el patrón del correlacionador de datos, le aconsejo que obtenga el libro Patterns of Enterprise Application Architecture de Martin Fowler. ¡Está lleno de este tipo de patrones y mucho, mucho más!

Espero que esto ayude (y siento si hay algunas oraciones en inglés realmente malas allí, ¡no soy un hablante nativo!).

== == EDITAR

Utilizando el patrón de registro activo del patrón de asignador de datos también ayuda a probar su código (como dijo Aurel). Si separa todas las peaces de código para hacer una sola cosa, será más fácil comprobar que realmente está haciendo esto. Al usar PHPUnit (o algún otro marco de prueba) para verificar que su código funcione correctamente, puede estar bastante seguro de que no habrá errores en cada una de sus unidades de código. Si mezclas las preocupaciones (como cuando eliges la opción 1 de tus elecciones), esto será mucho más difícil. Las cosas se mezclan bastante, y pronto obtendrás un gran grupo de spaghetti code.

== == Edit2

Un ejemplo del patrón de registro activo (que es bastante lento y no muy activo):

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() {} 
} 

Y un ejemplo del patrón de asignador de datos:

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() {} 
} 

== Datos 3: después de edición == bot

Tenga en cuenta que la clase Container contendrá un miembro estático del tipo DatabaseHelper. Contendrá una función privada static getDatabaseHelper() que devolverá una instancia DatabaseHelper existente o creará una nueva instancia DatabaseHelper si no existe, en cuyo caso, completará el objeto de conexión en DatabaseHelper. El Contenedor también contendrá métodos estáticos llamados makeUser y makeLocation que inyectarán el DatabaseHelper en User y Locations respectivamente.

Después de leer algunas respuestas, me doy cuenta de que la pregunta inicial casi ha sido respondida. Pero todavía hay una duda que debe aclararse antes de poder aceptar la respuesta final, que es la siguiente.

Qué hacer cuando tengo varias bases de datos para conectarme en lugar de una única base de datos. ¿Cómo incorpora la clase DatabaseHelper esto y cómo inyecta el contenedor las dependencias apropiadas de la base de datos en los objetos User y Location?

Creo que no hay necesidad de ninguna propiedad estática, ni el Contenedor necesita esos métodos makeUser de makeLocation. Supongamos que tiene un punto de entrada de su aplicación, en el cual crea una clase que controlará todo el flujo en su aplicación. Parece que lo llamas contenedor, prefiero llamarlo controlador. Después de todo, controla lo que sucede en su aplicación.

$controller = new Controller(); 

El controlador tendrá que saber qué base de datos tiene que cargar, y si hay una única base de datos o varias. Por ejemplo, una base de datos contiene los datos de usuario, otra base de datos contiene los datos de ubicación. Si el usuario activa registro desde arriba y una clase ubicación similar se da, entonces el controlador puede tener un aspecto como el siguiente:

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

Probablemente sería bueno para mover todos los de eco a una clase View o algo así. Si tiene varias clases de controlador, puede ser útil tener un punto de entrada diferente que cree todas las bases de datos y las introduzca en el controlador. Por ejemplo, podría llamarlo controlador frontal o controlador de entrada.

¿Responde a esta pregunta usted abre preguntas?

+2

+1 gran explicación – Sarfraz

+0

Aunque tengo una buena respuesta, no estoy de acuerdo con que el OP describa un ActiveRecord. Active Record representa una sola fila y agrega lógica comercial. Las clases que interactúan con una tabla en su conjunto son Table Data Gateways. – Gordon

+0

@Gordon Por favor, eche un vistazo al libro PEAA o en la página wiki de [active record] (http://en.wikipedia.org/wiki/Active_record_pattern). Un objeto que tiene operaciones CRUD en él, más o menos describe el patrón de registro activo. – Thanaton

8

Me gustaría ir con la inyección de dependencia, por el siguiente motivo: si en algún momento quieres escribir pruebas para tus aplicaciones, te permitirá reemplazar la instancia de DatabaseHelper por una clase de stub, implementando la misma interfaz pero realmente no accedas a una base de datos. Esto hará que sea más fácil probar las funcionalidades de su modelo.

Por cierto, para que esto sea realmente útil, sus otras clases (Usuario, Ubicaciones) deberían depender de una DatabaseHelperInterface en lugar de directamente en DatabaseHelper. (Esto es necesario para poder cambiar implementaciones)

+0

+1 Te pido por la segunda opción, por ejemplo, inyección de dependencia para obtener beneficios adicionales, mientras mantengo otras clases solas e intactas y fácilmente extensibles. – Sarfraz

+0

Gracias por la referencia de la interfaz. Solo para confirmar, crearé una interfaz llamada DatabaseHelperInterface que contendrá las firmas para los métodos executeQuery, executeUpdate. Inyectaré DatabaseHlperInterface en User and Locations en lugar de inyectar DatabaseHelper directamente ¿no? Una pregunta más, ¿es una buena idea crear DatabaseHelper en primer lugar? – CKing

+1

Sí, eso es :) Y sí, necesita una forma de abstraer las consultas de DB actuales y evitar escribirlas en varios lugares. Su DatabaseHelper es una buena solución para esto. – Aurel

3

Lo que usted está describiendo con sus clases de usuario y ubicación se denomina Table Data Gateway:

un objeto que actúa como una puerta de enlace a una tabla de base de datos. Una instancia maneja todas las filas en la tabla.

En general, quiere favor Composition over Inheritance y programm towards an interface. Si bien puede parecer más esfuerzo armar sus objetos, hacerlo beneficiará el mantenimiento y la capacidad de cambiar el programa a largo plazo (y todos sabemos que el cambio es la única constante en un proyecto).

El beneficio más obvio de usar Dependency Injection aquí es cuando desea probar las pasarelas. No se puede burlar fácilmente la conexión a la base de datos cuando se usa la herencia. Esto significa que siempre tendrá que tener una conexión de base de datos para estas pruebas. El uso de Depedency Injection le permite simular esa conexión y simplemente probar que los Gateways interactúen correctamente con Database Helper.

+0

Esto tiene sentido – CKing

+0

Una buena conclusión de esta respuesta es "composición sobre herencia". La Inyección de Dependencia no es más que una forma de lograr la composición, por lo que compararla con el concepto de herencia es comparar algo bastante concreto con algo conceptual. – Marvo

+0

Eso es correcto. Todo se reduce a la composición frente a la herencia. Voy a reformular la pregunta! – CKing

2

Dependency Injection es preferible si tiene diferentes tipos de servicios y un servicio desea usar otro.

Tus clases Usuario y Ubicaciones suena más como capa DAO (DataAccessObject), que interactúa con la base de datos, por lo que para el caso dado deberías estar usando In Integrity. La herencia puede hacerse mediante la extensión de la clase o la implementación de interfaces

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

De esta manera el diseño de su base de datos y el código será separada.

3

Aunque las otras respuestas aquí son muy buenas, quería agregar algunas otras ideas de mis experiencias usando CakePHP (un marco MVC). Básicamente, les mostraré una o dos hojas de su API; principalmente porque, para mí, parece estar bien definido y pensado (probablemente porque lo uso a diario).

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 } 
+0

+1 para la explicación de la jerarquía! – CKing

5

La cuestión de la inyección de dependencia frente a la herencia, al menos en su ejemplo específico se reduce a lo siguiente: "es una" o "tiene una".

¿Es clase foo un tipo de barra de clase? ¿Es un bar? Si es así, tal vez la herencia sea el camino a seguir.

¿La clase foo usa un objeto de clase de barra? Ahora estás en territorio de inyección de dependencia.

En su caso, sus objetos de acceso a datos (en mi enfoque de código son UserDAO y LocationDAO) NO son tipos de ayudantes de bases de datos. No utilizaría un UserDAO, por ejemplo, para proporcionar acceso a la base de datos a otra clase DAO. En su lugar, USTED UTILIZA las características de un ayudante de base de datos en sus clases DAO. Ahora bien, esto no significa que técnicamente no podría lograr lo que desea hacer extendiendo las clases de ayuda de la base de datos. Pero creo que sería un mal diseño y causaría problemas en el futuro a medida que su diseño evoluciona.

Otra forma de pensar es, ¿TODOS sus datos procederán de la base de datos? ¿Qué pasa si, en algún momento del camino, desea extraer algunos datos de ubicación de, por ejemplo, un canal RSS? Usted tiene su LocationDAO esencialmente definiendo su interfaz - su "contrato", por así decirlo - en cuanto a cómo el resto de su aplicación obtiene datos de ubicación. Pero si hubiera extendido DatabaseHelper para implementar su LocationDAO, ahora estaría atascado. No habría forma de que su LocationDAO use una fuente de datos diferente. Sin embargo, si DatabaseHelper y su RSSHelper tienen una interfaz común, puede conectar el RSSHelper directamente en su DAO y LocationDAO ni siquiera tiene que cambiar en absoluto. *

Si ha hecho de LocationDAO un tipo de DatabaseHandler, cambiar el origen de datos requeriría cambiar el tipo de LocationDAO. Esto significa que no solo tiene que cambiar LocationDAO, sino que todo su código que usa LocationDAO tiene que cambiar. Si había inyectado una fuente de datos en sus clases DAO desde el principio, la interfaz de LocationDAO se mantendría igual, independientemente de la fuente de datos.

(* Solo un ejemplo teórico. Habría mucho más trabajo para obtener un DatabaseHelper y un RSSHelper para tener una interfaz similar.)

+0

+1 por sugerir el uso de una interfaz común para las fuentes de datos . – CKing

Cuestiones relacionadas