Supongamos que tenemos un programa que necesita leer cierta información (por ejemplo un listado de clientes con sus datos personales y la lista de facturas asociadas a cada cliente (esto parece un problema de la uni eh!!)). El cliente no nos ha pedido ningún tipo de configuración especial para obtener los datos de forma que estamos barajando el obtener la información de un fichero o de una base de datos (aunque el cliente es un poco 'cliente' de forma que nunca se puede estar segudo de que no vaya a cambiar de opinión).
Una de las opciones que tenemos (para mi la peor) es meter el código que obtiene la información "a pelo" en nuestro código, por ejemplo, si suponemos que cuando se pulsa un botón se un form con un lista de clientes podríamos tener algo así (por simplicidad no voy a tener en cuenta el manejado de excepciones):
De esta forma cada vez que se cree el form (también podría ir en el OnShow) se obtiene la información de la base de datos... sencillo, rápido ... y bastante chapucero. Mediante esta forma de hacer las cosas hay poca reutilización de código (siempre ponemos la misma query cuando queremos obtener los clientes) y un cambio en los nombres de las tablas o de los campos nos obliga a reescribir el código de todas las funciones en las que se haga referencia.
La solución más obvia y elegante a este problema es encapsular todas las funciones de acceso a base de datos en una clase estática o singleton y proporcionar las estructuras de datos necesarias para acceder de forma correcta a la información (no está todo el código, omito algunas partes que son más o menos obvias).
TCliente = class
private
FNombre : string;
FApellidos : string;
FFacturas : TListaFacturas;
function GetFacturas : TListaFacturas;
public
constructor Create(AId : integer);
property Nombre : String read FNombre write FNombre;
property Apellidos : String read FApellidos write FApellidos;
property Facturas : TListaFacturas read GetFacturas;
end;
TListaClientes = class
private
FList : TList;
function GetItem(Index : Integer) : TCliente;
public
constructor Create;
property Items[Index : integer] : TCliente read GetItem; default;
function AddCliente(cliente : TCliente) : TCliente;
procedure DelCliente(index : integer)
end;
TAccesoBD = class
public
function ObtenerClientes : TListaClientes;
function ObtenerFacturasCliente(AIdCliente : Integer) : TListaFacturas;
end;
implementation
function TAccesoBD.ObtenerClientes : TListaClientes;
var
Qry : TQuery;
Cliente : TCliente;
begin
result := TListaClientes.Create;
Qry := TQuery.Create;
// Configurar la query para acceder a la base de datos
ConfiguraBDQuery(Query);
Qry.SQL.ADD('SElECT * FROM Clientes');
Qry.Open;
while not Qry.Eof do
begin
// Rellenar la lista
Cliente := TCliente.Create(Qry.FieldByName('id').AsInteger);
Cliente.Nombre := Qry.FieldByName('nombre').AsString;
Cliente.Apellidos := Qry.FieldByName('apellidos').AsString;
// ETC para el resto de propiedades ...
result.AddCliente(Cliente);
Query.Next;
end;
end;
De esta forma (no he puesto el código correspondiente a que la clase sea un singleton o una clase estática de CSharp puesto que ya está explicado en este artículo como ya he mencionado) tenemos todo el acceso a la base de datos centralizado en un solo lugar de forma que, si algo cambia, nos será muchisimo más facil actualizarlo y, si el cambio solo afecta a como se rellenan las estructuras, el resto del código no habrá que tocarlo lo más minimo, además de hacer el añadido de campos mucho más sencillo.
Aún así todavía podemos darle una vuelta de tuerca más, abstraer los orígenes de datos. ¿Que significa esto?. Puesto que ya hemos definidos unas estructuras en código que van a representar los datos que nuestra aplicacion necesita (en este caso TCliente, TFactura, TListaClientes, etc...) a nosotros (como usuarios de esos datos) no nos importa quien nos los proporcione, mientras tengamos los datos quien nos los de nos da igual. Así que vamos a refinar un poquito, lo primero es abstraer la figura del origen de datos para que refine un modelo de comportamiento:
type TAccesoBD = class(TOrigenDatos)
public
function ObtenerClientes : TListaClientes; override;
function ObtenerFacturas(AIdCliente) : TListaFacturas; override;
function ObtenerListaDeCobros(AIdCliente : Integer) : TListaCobros; override;
end;
type TAccesoFichero = class(TOrigenDatos)
public
function ObtenerClientes : TListaClientes; override;
function ObtenerFacturas(AIdCliente) : TListaFacturas; override;
function ObtenerListaDeCobros(AIdCliente : Integer) : TListaCobros; override;
end;
type TAccesoNetWork = class(TOrigenDatos)
public
function ObtenerClientes : TListaClientes; override;
function ObtenerFacturas(AIdCliente) : TListaFacturas; override;
function ObtenerListaDeCobros(AIdCliente : Integer) : TListaCobros; override;
end;
type TStub = class(TOrigenDatos)
public
function ObtenerClientes : TListaClientes; override;
function ObtenerFacturas(AIdCliente) : TListaFacturas; override;
function ObtenerListaDeCobros(AIdCliente : Integer) : TListaCobros; override;
end;
De esta forma podemos definir distintos tipos de origenes de datos sin tocar absolutamente nada de nuestra aplicación. Definimos un comportamiento, lo que entendemos por origen de datos, y definimos distintos tipos. Además, podemos definir e implementar primero un Stub que proporcione unos datos pequeños introducidos a mano (2 o 3 clientes con 2 o 3 facturas) para probar nuestra aplicación y, una vez que todo funcione, implementar origenes de datos más complejos (como un orígen de datos de red o un origen de fichero).
Por ejemplo, podemos tener los siguientes fragmentos de código (en la inicialización del programa y en el mostrado del form citado al principio)
{ En otra unidad, en que hay un form que necesita los datos }
procedure TForm1.OnShow(Sender : TObject);
var
clientes : TListaClientes;
i : integer;
begin
clientes := OrigenDatos.ObtenerListaClientes;
for i := 0 to clientes.Count - 1 do
begin
ListView1.Add(clientes[i].Nombre);
end;
end;
De esta forma, el origen de los datos es independiente del código de la aplicación y sea cual sea, el form1 obtendrá dichos datos y los mostrará en su TreeView, ya vengan de la red, de un archivo o de donde sea, el nunca lo sabrá.
Por último, es interesante mencionar otra forma de programar los accesos a bases de datos. Es una técnica conocida como metaprogramación que consiste en "escribir código que escribe código". Gran parte de los accesos a base de datos que se realiza (y la mayor parte de las aplicaciones que se realizan hoy en dia se apoyan sobre una base de datos en algún momento) son básicamente del mismo tipo, obtención de listados con algun tipo de filtro y poco más. Por otro lado, las consultas SQL son relativamente sencillas y podrían generarse de forma automatizada de forma sencilla. Existen herramientas que permiten realizar esta generación de código de forma que, analizando la estructura de la base de datos, se genere automáticamente el código necesario para obtener los listados más comunes. AF-Gen es una de esas herramientas, forma parte de la Arquitectura AF y permite la generación de dicho código de forma automática.
Disclaimer: La gente de ASPL son colegas del nene pero eso no tiene nada que ver,(bueno, quizá solo un poco) la herramienta es muy buena y es GPL así que echarle un buen ojo.