studio programacion composicion codigo agregación agregacion php inheritance dependency-injection

php - programacion - Composición vs herencia. ¿Qué debo usar para mi biblioteca de interacción de base de datos?



composicion php (6)

Considere un módulo de interacción con la base de datos escrito en PHP que contenga clases para interactuar con la base de datos. No he comenzado a codificar la clase, así que no podré dar fragmentos de código.

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 como searchLocation, createLocation, updateLocation, etc.

Además, estoy pensando en crear otra clase de la siguiente manera:

DatabaseHelper : una clase que tendrá un miembro que representa la 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) y así sucesivamente.

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

  1. La clase de usuarios y ubicaciones ampliará la clase DatabaseHelper para que puedan usar los métodos executeQuery y executeUpdate heredados en DatabaseHelper. En este caso, DatabaseHelper se asegurará de que solo haya 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 y Locations a través de una clase Container que creará instancias de User y Location. En este caso, el Contenedor se asegurará de que solo haya una instancia de DatabaseHelper en la aplicación en un momento dado.

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

Editar:

Tenga en cuenta que la clase Container contendrá un miembro estático de tipo DatabaseHelper. Contendrá una función estática privada getDatabaseHelper () que devolverá una instancia existente de DatabaseHelper o creará una nueva instancia de DatabaseHelper si no existe, en ese caso, llenará 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 que pueda aceptar la respuesta final, que es la siguiente.

Qué hacer cuando tengo varias bases de datos para conectarme en lugar de una sola base de datos. ¿Cómo incorpora esto la clase DatabaseHelper y cómo el contenedor inyecta las dependencias de base de datos adecuadas en los objetos Usuario y Ubicación?


A pesar de que las otras respuestas aquí son muy buenas, quise incluir algunos otros pensamientos de mis experiencias utilizando CakePHP (un marco MVC ). Básicamente, solo te mostraré una hoja o dos de su API ; Principalmente porque, para mí, parece 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 }


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 un" o "tiene un".

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

¿La clase foo usa un objeto de la barra de clases? Ahora estás en el 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 base de datos. No utilizaría un UserDAO, por ejemplo, para proporcionar acceso a la base de datos a otra clase DAO. En su lugar, UTILIZA las características de un ayudante de base de datos en sus clases DAO. Ahora, esto no significa que técnicamente no pueda lograr lo que quiere hacer al ampliar las clases de ayudante de 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 evolucione.

Otra forma de pensarlo es si TODOS sus datos provendrán de la base de datos. ¿Qué pasa si, en algún lugar por el camino, desea extraer algunos datos de ubicación de, por ejemplo, un feed RSS. 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 manera de que su LocationDAO use una fuente de datos diferente. Sin embargo, si DatabaseHelper y su RSSHelper tenían una interfaz común, podría conectar el RSSHelper directamente a su DAO y LocationDAO ni siquiera tiene que cambiar en absoluto. *

Si hubiera hecho de LocationDAO un tipo de DatabaseHandler, cambiar la fuente de datos requeriría cambiar el tipo de LocationDAO. Esto significa que no solo tiene que cambiar LocationDAO, sino que todo el código que usa LocationDAO tiene que cambiar. Si ha inyectado una fuente de datos en sus clases DAO desde el principio, entonces la interfaz de LocationDAO seguirá siendo la misma, independientemente de la fuente de datos.

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


Lo que está describiendo con sus clases de Usuario y Ubicación se llama una puerta de enlace de datos de tabla :

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, desea favorecer la Composición sobre la Herencia y el programa hacia una interfaz . Si bien puede parecer un esfuerzo mayor para ensamblar sus objetos, hacerlo beneficiará el mantenimiento y la capacidad de cambiar el programa a largo plazo (y todos sabemos que el cambio es el único constante en un proyecto).

El beneficio más obvio de usar la inyección de dependencia aquí es cuando desea probar en unidad los Gateways. No se puede burlar fácilmente de la conexión a la base de datos cuando se utiliza la herencia. Esto significa que siempre tendrá que tener una conexión de base de datos para estas pruebas. El uso de la inyección de dependencia le permite simular esa conexión y simplemente probar que las puertas de enlace interactúan correctamente con el Asistente de base de datos.


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

Por cierto, para que esto sea realmente útil, sus otras clases (Usuario, Ubicaciones) deben depender de un DatabaseHelperInterface en lugar de directamente en DatabaseHelper. (Esto es requerido para poder cambiar implementaciones)


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

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> como searchLocation, createLocation, updateLocation, etc.

Esencialmente tienes que elegir aquí. El método que describiste se llama el patrón de registro activo . El objeto en sí mismo sabe cómo y dónde se almacena. Para los 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, esta implementación ). Este es un segundo objeto que maneja todas las interacciones de la base de datos, mientras que el objeto en sí (por ejemplo, Usuario o Ubicación) solo maneja operaciones que son específicas de ese objeto (por ejemplo, inicio de sesión o goToLocation). Si alguna vez desea aprovechar el almacenamiento de sus objetos, solo tendrá que crear un nuevo asignador de datos. Su objeto ni siquiera sabrá que algo cambió en la implementación. Esto obliga a encapsulation y separar las preocupaciones .

Hay 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 crear otra clase de la siguiente manera:

DatabaseHelper: una clase que tendrá un miembro estático que representa la 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) y así sucesivamente.

Lo que estás describiendo aquí suena como un singleton . Normalmente esto no es realmente una buena opción de diseño. ¿Está 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 hacer 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 manera puede reutilizarlo si alguna vez necesita una segunda conexión.

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

  1. La clase de Usuario y Ubicaciones ampliará la clase DatabaseHelper para que puedan usar los métodos inherentes executeQuery y executeUpdate en DatabaseHelper. En este caso, DatabaseHelper se asegurará de que solo haya una instancia de la conexión a la base de datos en un momento dado.
  2. La clase DatabaseHelper se inyectará en la clase de Usuario y Ubicaciones a través de una clase de Contenedor que creará instancias de Usuario y Ubicación. En este caso, el Contenedor se asegurará de que solo haya una instancia de DatabaseHelper en la aplicación en un momento dado.

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

La primera opción no es realmente viable. Si lees la descripción de la herencia , 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 base de datos Mysql sería un subtipo de una base de datos, o un administrador sería un subtipo de un usuario. No 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 utilizar el método de registro activo, debe inyectar la Base de datos en los objetos Usuario y Ubicación. Por supuesto, esto debería hacerlo un tercer objeto que maneje todo este tipo de interacciones. Probablemente querrá echar un vistazo a la inyección de dependencia y la inversión de control .

De lo contrario, si elige el método del asignador de datos, debe inyectar la Base de datos en el asignador de datos. De esta manera, todavía es posible utilizar varias bases de datos, mientras separa todas sus inquietudes.

Para obtener más información sobre el patrón de registro activo y el patrón del asignador de datos, le recomendaría que obtenga el libro de Patrones de arquitectura de aplicaciones empresariales de Martin Fowler. Está lleno de este tipo de patrones y mucho, mucho más!

Espero que esto ayude (y perdón si hay oraciones en inglés realmente malas, ¡no soy un hablante nativo!).

== EDIT ==

El uso del patrón de registro activo del patrón del mapeador de datos también ayuda a probar su código (como dijo Aurel). Si separa todos los números de código para hacer una sola cosa, será más fácil comprobar que realmente está haciendo esto. Al utilizar PHPUnit (o algún otro marco de prueba) para verificar que su código funciona correctamente, puede estar bastante seguro de que no habrá errores en cada una de sus unidades de código. Si confunde las preocupaciones (como cuando elige la opción 1 de sus opciones), esto será mucho más difícil. Las cosas se confunden y pronto obtendrás un gran número de códigos de espagueti .

== EDIT2 ==

Un ejemplo del patrón de registro activo (que es bastante perezoso y no está realmente 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 del mapeador 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() {} }

== EDIT 3: después de editar por bot ==

Tenga en cuenta que la clase Container contendrá un miembro estático de tipo DatabaseHelper. Contendrá una función estática privada getDatabaseHelper () que devolverá una instancia existente de DatabaseHelper o creará una nueva instancia de DatabaseHelper si no existe, en ese caso, llenará 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 que pueda aceptar la respuesta final, que es la siguiente.

Qué hacer cuando tengo varias bases de datos para conectarme en lugar de una sola base de datos. ¿Cómo incorpora esto la clase DatabaseHelper y cómo el contenedor inyecta las dependencias de base de datos adecuadas en los objetos Usuario y Ubicación?

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

$controller = new Controller();

El controlador tendrá que saber qué base de datos tiene que cargar, y si hay una sola base de datos o varias. Por ejemplo, una base de datos contiene los datos del usuario, y otra base de datos contiene los datos de ubicación. Si se proporciona el usuario activo de registro anterior y una clase de ubicación similar, entonces el controlador podría tener el siguiente aspecto:

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 mover todos los ecos a una clase de Vista o algo así. Si tiene varias clases de controlador, podría valer la pena tener un punto de entrada diferente que cree todas las bases de datos y las introduzca en el controlador. Podría, por ejemplo, llamar a esto un controlador frontal o un controlador de entrada.

¿Esta respuesta te abre preguntas?


Se prefiere la inyección de dependencia si tiene diferentes tipos de servicios, y un servicio desea utilizar otro.

Sus clases Usuario y Ubicaciones suenan más como la capa DAO (DataAccessObject), que interactúa con la base de datos. Por lo tanto, para su caso dado, debería estar usando Herencia. La herencia se puede hacer extendiendo la clase o implementando 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 y el código de su base de datos estarán separados.