Otra cosa que podes hacer es publicar lo que vas diseñando para que otros critiquen. A mi no me molesta, a menos que me pidas revisar un sistema entero
Tengo que admitir que el concepto "constructor injection" me es ajeno... o lo he olvidado o lo estudié y conozco por otro nombre.
Advierto que voy a escribir un "libro" mas que un "post" 
Constructor injection es el patron dominante y preferido dentro de dependecy injection. DI es una serie de tecnicas y patrones que te ayudan con el "programa a una interfaz y no a una implementacion". Te ayuda a construir aplicaciones reduciendo el acoplamiento, facilidad de testeo, y tambien adquieres "late-binding". Late binding vendria a ser que la implementacion que va a resolver las tareas se decide en tiempo de ejecucion, y no en tiempo de compilacion
Una de las primeras cosas que enseñan en cursos orientados a objetos para principiantes son polimorfismo y abstraccion. Muchas veces acompañan con un fragmento de codigo asi:
type
TService = class abstract
public
function Execute: string; virtual; abstract;
end;
// representa el programa principal
TMain = class
private
FService: TService;
protected
property Service: TService read FService;
public
procedure PrintServiceResponse;
end;
procedure TMain.PrintServiceResponse;
begin
Writeln(Service.Execute);
end;
Si alguien preguntara "que codigo se ejecuta cuando se invoca al metodo Execute en el metodo TMain.Print?" la respuesta podria ser "no se", o "necesito mas informacion" o "depende del tipo de la instancia privada" y eso es correcto porque TService es una abstraccion y gracias al polimorfimo y a los metodos virtuales, es en tiempo de ejecucion que se decide que codigo se ejecuta. Esto es casi el "ABC" de OOP. El dispatching de los mensajes es dinamico; el receptor "sabe" que clase es, y sabe que codigo ejecutar; los emisores solo saben que metodos disponibles existen
Pero ahora, si yo te mostrara esta aplicacion:
type
TMain = class
private
FService: TService;
protected
property Service: TService read FService;
public
constructor Create;
destructor Destroy;
procedure PrintServiceResponse;
end;
constructor TMain.Create;
begin
inherited Create;
FService := TServiceImpl.Create;
end;
destructor TMain.Destroy;
begin
FService.Free;
inherited Destroy;
end;
var
Main: TMain;
begin
Main := TMain.Create;
Main.PrintServiceResponse;
end.
Esto es algo que parece natural ya que desde el punto de vista POO las clases son simples, tienen una sola responsabilidad, y ademas, las dependencia de TMain (consumidor o cliente) a TService (servidor) es de bajo acoplamiento porque TService es una clase abstracta
Sin embargo, ahora yo si podria responder cual es el codigo que se ejecuta porque se decide en tiempo de compilacion. Se va a imprimir lo que diga el metodo Execute de la clase TServiceImpl. Este hecho no puede cambiar a menos que cambie la clase TMain, ya que ella es la que esta creando la instancia interna. La unica forma de cambiarlo es modificando y volviendo a compilar.
Esto en realidad muestra que estas violando principio abierto/cerrado y tambien la clase Main esta haciendo trabajo que no deberia hacer (tiene mas de una responsabilidad), esta creando una instancia de su dependencia en el constructor, y tambien se hace cargo del tiempo de vida de la instancia; y ademas implementa el metodo PrintServiceResponse.
Para poder compilar la clase TMain necesitas el paquete o unidad en donde esta definida la clase TServiceImpl. Una clase que crea sus propias dependencias es una instancia de un anti-patron llamado "control freak", porque la clase "controla" o "autogestiona" sus dependencias.
Si quisieras crear un test de unidad para la clase TMain, estas obligado a usar la clase TServiceImpl, y no un objeto falso.
Osea que podria decirse que si ves la foto completa, en realidad perdiste todo polimorfismo y dinamismo, ya que todo se decide estaticamente
Otro problemon:
1. La clase TServiceImpl cambia su constructor por algo como
type
TServiceImpl = class(TService)
public
constructor Create(const ServiceURL: string);
function Execute: override;
end;
Esto provocaria un error de compilacion. La clase TMain debe cambiar porque la clase TServiceImpl cambio. Ironico no? Pero si "mis variables de instancia, mis dependencias son abstracciones!".
2. Aparece una nueva implementacion de TService, con un constructor que tiene parametros
De donde saca la clase TMain los parametros que necesita para enviar a estos constructores? Recibir todo en el constructor no es la solucion, ya que de presentarse 1) o 2) nuevamente, nos trae a la misma situacion y estariamos en un circulo vicioso
Esto ocurre porque estamos cumpliendo con la mitad del camino del "programa a una interfaz y no a una implementacion", porque nuestras dependencias son abstracciones (bien), pero hay un problema con las interfaces o las clases abstractas: no pueden ser instanciadas. Las interfaces no tienen constructores: deben ser implementadas; las clases abstractas podrian tener un constructor, pero si lo ejecutaramos, obtendriamos un error en tiempo de ejecucion. Parece que se llega a una encrucijada. En realidad, se debe aprender que los constructores son una especie de "mal necesario" porque: es la unica manera de tener una referencia valida a una abstraccion, esto nos lleva a que debe ser un metodo publico, estatico (o metodo de clase) pero terminan siendo un detalle de implementacion. A mi me importa un rabano que parametros necesita una abstraccion para implementar el servicio que yo requiero.
Imaginate el ejemplo que yo te mostre, en donde en una aplicacion, toda las clases del modelo hacen lo mismo. Terminas "solucionando" todo con globales, singletons, porque necesitas informacion de "contexto" muy en las entrañas de la aplicacion (referencia a conexion a la bd, referencia a la clase para escribir en en log, referencia a la clase que implementa SSL, etc)
El patron constructor injection te dice que apliques inversion de control y reescribas el codigo de la siguiente manera:
type
TMain = class
private
FService: TService;
protected
property Service: TService read FService;
public
constructor Create(Service: TService);
procedure PrintServiceResponse;
end;
constructor TMain.Create(Service: TService);
begin
if not Assigned(Service) then
raise EArgumentException.Create('TService es nil');
inherited Create;
FService := Service;
end;
Parece una ganzada pero lo que se gana es mucho. Lo primero es que la clase es mas sencilla:
1. El constructor ya no crea una instancia sino que "valida" la entrada y luego la almacena para usarla mas adelante. Esta validacion es importante, ya que de otra manera la clase se pondria en un estado "invalido", algo que debe impedir a toda costa. Como ya hablamos de las pre y post condiciones, la clase verifica que el emisor cumpla las precondiciones y luego te promete cumplir con las postcondiciones
2. No hay necesidad del destructor. La clase no tiene porque hacerse cargo del tiempo de vida de la dependencia. Ella no lo creo. La misma dependencia podria ser usada por otras clases. Destruirla es trabajo de quien la creo
3. Facilidad de testeo, ya que cualquier clase que implemente el contrato que manda TService es valido
4. Ganamos late-binding, polimorfismo y dinamismo nuevamente
Obviamente que el punto 4 en algun momento se "rompe" sino tendriamos el problema del huevo y la gallina. Toda regla tiene su excepcion. Es un caso similar a otro de los conceptos que se enseñan a principiantes en POO: "todo objeto esta ejecutando un metodo porque otro objeto le envio un mensaje".
Existe un "punto" en las aplicaciones, que Seemann define como "composition root". Este punto o lugar varia de lenguaje a lenguaje, y dentro de cada lenguaje, de framework a framework. Este punto deberia estar lo mas cercano posible al inicio de la aplicacion. Los framework por lo general (y sino, hay que rebuscarselas o forzarlo) proveen algun hook para que nosotros podamos escribir codigo de "wiring" o de "cableado". Es el lugar en donde se decide cual es el grafo de objetos que va a intervenir en esta aplicacion, es decir, en donde comienza lo del "huevo y la gallina" en dependency injection. (Por ejemplo en Delphi, en una aplicacion de consola, este lugar es la primer linea de codigo, en una aplicacion VCL podria ser el metodo Create del form principal, etc)
La idea entonces es programar las clases del modelo "teniendo DI en mente", esto es, cada clase pide en su constructor lo que necesite, y que se haga cargo el que me use de darmelo. Si todas las clases "empujan" para atras estos requerimientos, llegas a la composition root. Osea que si queres instanciar una version de tu aplicacion, lo unico que tenes que cambiar, es la composition root. De esta manera eliminas los problemas al cambiar un constructor o cambiar una implementacion por otra. Obviamente que no evitas cambiar codigo. Pero el codigo que cambias esta en la composicion.
El compositor de la aplicacion cumple con el siguiente rol: es el que decide que grafo de objetos debe crearse, como y cuando debe crearse, y tambien muy importante maneja los tiempos de vida. Esto es fundamental. Por ejemplo, si tenes un objeto "pesado", podrias estar pensando en un Singleton del GoF. Yo hoy por hoy hablo del "Singleton" y del "singleton". El que tiene S mayuscula ya lo conocemos todos, es el del libro de patrones. Un "singleton" es una clase de la que solo hay una instancia en la aplicacion. El compositor es el que decide que hay una sola instancia, cuando crearla y cuando liberarla. Los que consumen el singleton no saben ni deberian asumir nada, simplemente consumen una clase.
El tener un lugar centralizado en donde se gestiona todo esto da lugar a realizar "cambios grandes" modificando poco codigo. Un buen uso que se le da a este lugar es para aplicar decoradores, proxies, interceptores y muchas cosas mas. Un ejemplo un poco mas "realista", supongamos una clase que maneja registros de productos de una BD:
type
Producto = record
Codigo: string;
Descripcion: string;
end;
IProductRepository = interface
procedure Save(const Codigo, Descripcion: string);
function SelectAll: TArray<Producto>;
end;
TFakeProductRepository = class(TInterfacedObject, IProductRepository)
private
FProductos: TList<Producto>;
procedure Save(const Codigo, Descripcion: string);
function SelectAll: TArray<Producto>;
end;
TSQLiteProductRepository = class(TInterfacedObject, IProductRepository)
private
FConnection: TSQLiteConnection;
procedure Save(const Codigo, Descripcion: string);
function SelectAll: TArray<Producto>;
public
constructor Create(Connection: TSQLiteConnection);
end;
No muestro las implementaciones porque creo que son obvias: TFakeProductRepository mantiene una variable tipo lista sobre la cual va guardando los productos y a partir de la cual crea el array para responder al metodo SelectAll; TSQLiteProductRepository hara lo propio usando una conexion a una BD SQLite
Si en algun momento decido usar una u otra implementacion, solamente tengo que cambiar la composicion. Una implementacion no tiene dependencias, la otra necesita de una conexion SQLite. No quiero cambiar todas las clases que consumen la interfaz IProductRepository cada vez que cambio de proveedor de BD o me paso a "modo pruebas"
Pero hay mas:
Supongamos que una regla de negocio es que el codigo del producto no este repetido y que para usar la misma logica, metemos una clase abstracta comun, con un metodo CheckCodeIsAvailable que se invoca en Save, y un metodo CodeIsAvailable virtual
type
TAbstractRepository = class abstract(TInterfacedObject)
protected
procedure CheckCodeIsAvailable(const CodigoProducto: string);
function CodeIsAvailable(Code): Boolean; virtual; abstract;
end;
procedure TAbstractRepository.CheckCodeIsAvailable(const CodigoProducto: string);
begin
if not CodeIsAvailable(Code) then
raise EPrimaryKeyViolation.CreateFmt('Primary key violation on %s', [CodigoProducto]);
end;
Esto resuelve el problema, pero ahora supongamos que en la aplicacion de pruebas esto funciona "bien", pero en produccion, no queremos una excepcion sin mas. Queremos "capturarla" y en lugar de abortar, crear un log con la traza y enviarla a nuestro servidor FTP con reportes de bugs, o queremos mostrar un cuadro de dialogo que permite al usuario reintentar, o simplemente mostrar un cuadro de dialogo con un mensaje mas "amigable" y no esa cosa fea en ingles
Supongamos que tenemos en la cabeza: usar ShowMessage y cambiar el mensaje de la excepcion por otro, indicar que el codigo esta en uso, y darle foco a ese TEdit en donde se ingresa el codigo del producto
Como TSQLiteProductRepository es la clase que usamos en produccion, modificamos el codigo para hacer esto, capturando la excepction en algun lugar del metodo Save. Peeero, problemas, y estos son los problemas "clasicos" de la herencia
Que pasa si tengo tambien un TOracleProductRepository? Tengo que duplicar el codigo... O poner una clase comun a ambos.. pero luego TOracleProductRepository la usamos para los clientes premium, a los cuales les brindamos el servicio de "enviar los bugs al servidor FTP".
Este problema lo resuelve bien el patron decorador, y nuevamente, lo unico que tenemos que cambiar es la composicion:
Instanciacion en una aplicacion de consola simple:
program TestApplcation;
// la funcion CreateProductRepository es la composition root;
function CreateProductRepository: IProductRepository;
begin
Result := TFakeProductRepository.Create;
end;
var
ProductRepository: IProductRepository;
begin
ProductRepository := TFakeProductRepository.Create;
// utilizar la interfaz
end;
Instanciacion clientes "normales"
program ClientesNormales;
type
TShowExceptionsOnConsoleRepository = class(TInterfacedObject, IProductRepository)
private
FRepository: IProductRepository;
procedure Save(const Producto: TProduct);
function SelectAll: TArray<TProduct>;
public
constructor Create(const Repository: IProductRepository);
end;
procedure TShowExceptionsOnConsoleRepository.Save(const Producto: TProduct);
begin
try
FRepository.Save(Producto);
except
on E: EPrimaryKeyViolation do
Writeln('El codigo ' + Producto.Codigo + ' ya existe');
else
raise E;
end;
end;
function TShowExceptionsOnConsoleRepository.SelectAll: TArray<TProduct>;
begin
// nada especial aca, delegar
Result := FRepository.SelectAll;
end;
// la funcion CreateProductRepository es la composition root;
function CreateProductRepository: IProductRepository;
var
SQLiteConnection: TSQLiteConnection;
begin
SQLiteConnection := TSQLiteConnection.Create('localhost', 'admin', 'admin');
Result := TShowExceptionsOnConsoleRepository(
TSQliteProductRepository.Create(SQLiteConnection));
end;
var
ProductRepository: IProductRepository;
begin
ProductRepository := TFakeProductRepository.Create;
// utilizar la interfaz
end;
Es importante notar que el compositor debe "conocer el bosque", es decir, conoce todas las implementaciones posibles y selecciona la mas adecuada o las conecta de la manera mas adecuada para que el resto pueda consumir
Para clientes premium:
type
// similar anterior, pero este muestra con ShowMessage
TShowExceptionDialogRepository= class(TInterfacedObject, IProductRepository)
private
FedCodigo: TEdit;
FRepository: IProductRepository;
procedure Save(const Producto: TProduct);
function SelectAll: TArray<Product>; // delegado a instancia FRepository
public
constructor Create(const Repository: IProductRepository; edCodigo: TEdit);
end;
// podria ser una buena idea tambien inyectar en este decorador la implementacion que se encarga
// de enviar el reporte al servidor FTP
TSendBugReportRepository = class(TInterfacedObject, IProductRepository)
private
FIndy: TIndy { no me acuerdo como se llamaba el componente indy para mandar al ftp }
FRepository: IProductRepository;
// capturar excepcion, mandar al FTP y luego propagarla nuevamente
procedure Save(const Producto: TProduct);
function SelectAll: TArray<Product>; // delegado a instancia FRepository
public
// el constructor inicializa el componente Indy
constructor Create(const Repository: IProductRepository; const DireccionFTP, Password: string);
// destruir componente Indy
destructor Destroy; override;
end;
TMainForm = class(TForm)
edFoco: TEdit;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
FOracleConnection: TOracleConnection;
FProductRepository: IProductRepository;
end;
implementation
// en el framework VCL, una opcion es hacer al Main Form la composition root
// el hook mas indicado es el evento OnCreate del form
// tambien es posible crear la composicion en el archivo .dpr y luego pasarsela al form mediante
// algun metodo setter o propiedad
// Esto es asi porque la VCL si o si va a invocar al constructor por defecto en el .dpr
// en su famosa linea Application.CreateForm(TMainForm, MainForm)
procedure TMainForm.FormCreate(Sender: TObject);
begin
// ip base de datos user pass puerto
FOracleConnection:= TOracleConnection.Create('192.168.123.456', 'root', 'root', 1235);
// esto es equivalente al ejemplo anterior, estoy "anidando" decoradores
// pero creo que de esta manera la sintaxis es mas simple
// primero creo el repositorio
FProductRepository := TOracleProductRepository.Create(OracleConnection);
// decorar con envio de reporte de errores
FProductRepository := TSendBugReportRepository.Create(FProductRepository, 'ftp:\\blabla.com', 'password');
// decorar con mostar mensaje en una excepcion
FProductRepository:= TShowExceptionDialogRepository.Create(FProductRepository, edCodigo);
// podria seguir agregando mas decoradores que implementen mas caracteristicas, por ej
// TEncryptDataRepository, TCompressDataRepository, etc
// tambien sacarlos cuando quiera, el resto de la aplicacion ni se entera..
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
FOracleConnection.Free;
end;
procedure TShowExceptionsOnConsoleRepository.Save(const Producto: TProduct);
begin
try
FRepository.Save(Producto);
except
on E: EPrimaryKeyViolation do
begin
ShowMessageFmt('El codigo %s ya existe', [Producto.Codigo]);
FedCodigo.Clear;
FedCodigo.SetFocus;
end
else
raise;
end;
end;
procedure TShowExceptionsOnConsoleRepository.Save(const Producto: TProduct);
begin
try
FRepository.Save(Producto);
except
// registramos todas las excepciones, no solo las de violacion de clave primaria
on E: Exception do
Indy.Send(ArmarTraza(E)); // o como sea que se mande al FTP :)
raise;
end;
end;
Quien se haya quedado con ganas de mas, le advierto que esto es solo la punta del iceberg
Hay muchos mas beneficios a todo esto, entre ellos, el codigo es mas sencillo de mantener, entender, depurar, corregir, extender, testear, (estamos cumpliendo con la maxima "programar a una interfaz, no una abstraccion", para lograr bajo acoplamiento, recuerda
)
A veces existen ciertos objetos que no se puede "empujar tanto" porque se requiere informacion en tiempo de ejecucion para resolver su instanciacion, y aqui es donde las fabricas entran en juego (las factory se usan muchisimo en aplicaciones DI)
Existen frameworks para esto (los famosos DI Container) que agregan mas posibilidades, por ejemplo, leer un archivo de configuracion (tipicamente XML o JSON) e invocar a RTTI para crear las instancias; otros directamente utilizan RTTI, obtienen la informacion de los constructores y usan un algoritmo recursivo, siendo que se pide un objeto de "tipo T":
1. Obtener parametros constructor de T
2. Para cada parametro, ir al punto 1 con el tipo de ese parametro
3. Si es un constructor sin parametros, crear objeto
4. Con todos los parametros recolectados, instanciar el objeto
5. Retornar
Otros tienen una API para configurarlos mediante codigo, otros te permiten interceptar (terminas aplicando programacion orientada a aspectos de manera trivial, el decorador y el proxy tienen algunos problemas, pensar que pasa si quiero utilizar el TShowExceptionDialogRepository sobre una interfaz ICustomerRepository?).
--
Si se aplica esta logica a toda la aplicacion de pronto surgen cosas como:
Esta clase tiene un constructor con muchas dependencias (y aqui es donde aparece lo del articulo de arriba, el codigo revela una clase que hace de mas), o bien el "no viste" un subdominio del problema y crear una fachada o agregacion de un par de clases es trivial, ya que si, adivinaste, lo unico que cambias es la composicion, la fachada implementa la misma interfaz que la clase que lo consume pero te simplifica el diseño
Tambien revela problemas de diseño del estilo "estoy pidiendo en mi constructor dependencias que luego tengo que pasarle a mis dependencias" --> esto es similar a la ley de demeter
Otros problemas que saltan a la vista enseguida es violaciones al principio de substitucion de liskov (tengo que hacer X si me inyectaron la dependencia Y, o tengo que usar QueryInterface o Supports sobre una dependencia para ver si Z)
Te olvidas del singleton y de todos los problemas que acarrea: Como el "contexto" lo vas inyectando desde el principio, hacer disponible informacion "global" es muy facil
Si de pronto cambias la aplicacion a multihilo, todo lo que sea pool de objetos, de hilos, de conexiones a BD, todo es manejado desde la composicion: por ejemplo, se podria tener un diccionario que almacene claves estilo: ThreadID, ObjetoConexionBD e implementar un "singleton-per-thread"
En fin, hay mucho mas pero.. creo por hoy escribi lo suficiente
