Pero estos presentan dos grandes desventajas.
Desventaja #1: Mienten.
Se disparan siempre que hay asignación de valor al campo, no solamente cuando éste cambia de valor. Para entender la diferencia, considere que tenemos un campo de tipo entero con un valor de 5 en el registro actual y ahora le asignamos el resultado de la suma 3 + 2, o bien el usuario captura nuevamente 5 en un cuadro de texto que está asociado a ese campo. En cualquiera de estos casos, ¿cambió el campo de valor? La respuesta es no, sin embargo se ejecuta el código de esos mentirijillos eventos “change”. Esto resulta, bajo la mayoría de los casos, en un procesamiento innecesario que puede consumir pocos o muchos recursos (procesador, memoria, tiempo), dependiendo de lo que haga el manejador de evento que hayamos programado.
Desventaja #2: Ocultan el pasado.
Una vez que el campo cambia, ¿cómo podemos saber qué valor tenía previamente? Puede ser que, tras ser modificado el valor del campo, necesitemos realizar algo en especial con el valor que tenía anteriormente.
Ni siquiera el evento OnValidate de TField se salva de esas dos desventajas. Alguna vez me vi utilizando una variable auxiliar para guardar el último valor asignado de un campo, con el fin de verificar si éste realmente había cambiado de valor o de ejecutar una tarea que requería el valor previo como parámetro.
Abriendo un pequeño paréntesis, estoy seguro que en algún momento Delphi contará con un manejo nativo, rico y muy eficiente de entidades de datos, a través de clases Row o Record cuyas instancias representen las filas de un cursor, como ha sucedido ya en otras tecnologías de programación y como de alguna manera se intentó con ECO, y donde cuestiones como las planteadas aquí y otras menos ordinarias (como saber si el valor de un campo está siendo asignado o no) estén felizmente resueltas. Cuando el fabricante se vaya a poner a trabajar en ello, me gustaría muchísimo tener algún tipo de participación, pues bajo el desarrollo de mi marco personal he venido asentando una serie de mecanismos y conceptos que siento deben ser considerados.
Por el momento y regresando al tema, podría ser buena idea que la clase TDataSet tuviera un par de eventos adicionales llamados BeforeFieldChange y AfterFieldChange, ambos disparados cuando un campo realmente cambie de valor. El primero justo antes de que ocurra el cambio, e informando mediante un parámetro Variant cuál es el valor que está por ser asignado. El segundo justo después de ocurrir el cambio, e informando mediante un parámetro Variant cuál es el valor que tenía previamente.
De esta forma todas las clases descendientes de TDataSet heredarían esos nuevos eventos, permitiéndonos usarlos con cualquiera de los componentes de acceso a datos nativos o de terceros.
procedure TDataModule1.DataSet1BeforeFieldChange(
Field: TField; NewValue: Variant);
begin
{ Verificamos qué campo está por cambiar (Field)
y cuál será su nuevo valor (NewValue),
elevando una excepción si queremos impedir el
cambio o realizando alguna tarea específica }
end;
procedure TDataModule1.DataSet1AfterFieldChange(
Field: TField; PrevValue: Variant);
begin
{ Verificamos qué campo acaba de cambiar (Field)
y cuál era su valor anterior (PrevValue),
realizando alguna tarea específica }
end;En Delphi, como en muchos otros lenguajes, podemos redefinir métodos que previamente han sido declarados como virtuales. Esto nos permite extender las capacidades de una clase, creando una derivada con dichos métodos reimplementados por nosotros mismos. Eso es algo de lo más común. No obstante, pocos son los que se han atrevido a tocar el tema de la redefinición de clases, clases virtuales o herencia insertada, como si se tratara de un tabú en la POO. Con esos términos no acuñados me refiero a la capacidad de ampliar una clase sin tocar su código fuente, consiguiendo que sus actuales descendientes adquieran en automático la nueva herencia al ser compilada la aplicación que los utilice. O, dicho de una manera burda, insertar herencia adicional en medio de una jerarquía de clases.
Si las clases Delphi fueran redefinibles en ese sentido, podríamos agregar los eventos BeforeFieldChange y AfterFieldChange a la nativa TDataSet, sin esperar a que Embarcadero lo haga y sobre todo respetando el código fuente de la VCL. Logrando así que todas las clases de conjuntos de datos nativas y de terceros (TClientDataSet, TADODataSet, TIBTable, TMemoryTable, TMDOQuery, etc.) que utilicemos en nuestros proyectos cuenten con dichos eventos, sin necesidad de crear una clase derivada para cada una escribiendo código repetido. Lo más cercano que Borland estuvo de ello fue la inclusión de los ayudantes de clases, pero éstos dejan mucho qué desear.
Imaginemos que ya estamos en el mundo justo y feliz que a Object Pascal le corresponde tener, y que podemos escribir algo como esto:
Uses
DB;
Type
TFieldAfterChangeEvent = Procedure (
Field :TField; PrevValue :Variant) Of Object;
TFieldBeforeChangeEvent = Procedure (
Field :TField; NewValue :Variant) Of Object;
// Ampliaremos a TDataSet
TDataSet = Class Override
Private
FAfterFieldChange :TFieldAfterChangeEvent;
FBeforeFieldChange :TFieldBeforeChangeEvent;
Protected
{ Versión "superior" del método virtual
SetFieldData, usada para implementar el
disparo de los eventos BeforeFieldChange y
AfterFieldChange. Top significa que, para
todos los descendientes de TDataSet, se
ejecutará esta versión del método como si
fuera una redefinición por encima de las
ya existentes. }
Procedure SetFieldData (Field :TField;
Buffer :Pointer); Top;
Published
Property AfterFieldChange
:TFieldAfterChangeEvent
Read FAfterFieldChange
Write FAfterFieldChange;
Property BeforeFieldChange
:TFieldBeforeChangeEvent
Read FBeforeFieldChange
Write FBeforeFieldChange;
End;Estoy consciente de que una característica como ésta de la redefinición de clases presentaría varias repercusiones a considerar, pero es posible que en muchos casos se trate de situaciones salvables en las que los beneficios de programación tengan mayor peso que las medidas de control subyacentes que haya que tomar. En todo caso, si nuestra falta de experiencia en una tecnología prácticamente inexistente nos impide ver con claridad las vicisitudes que vendrían si intentáramos crear ésta, al menos deberíamos empezar por debatir el tema y sacar en claro lo que sí podríamos hacer y lo que no deberíamos hacer. ¿No lo creen?
En la segunda parte de este artículo entraremos de lleno a la práctica, implementando estos dos nuevos eventos en una clase derivada de TClientDataSet. Estén pendientes de ello.
Mientras tanto arrojen sus comentarios.
Un abrazo natural y eficiente.
Al González.
Es algo simple, en teoría, pero la de veces que hace falta algo así y te encuentras con ese problema, al final tienes que tirar por añadir un par de propiedades del estilo 'ValorAlEntrar' y 'ValorAlSalir' para salir del paso.
ResponderSuprimirMuy interesante, Al, ya estoy esperando tu siguiente post para ver cómo lo has implementado :)
Saluditos.
En las ocaciones que he intentado utilizar esos eventos, me lanza alguna excepcion o en los mejores de los casos no consigo que funcione adecuadamente.
ResponderSuprimirEspero que se logre encontrar alguna alternativa.
Saludos.
CASIMIRO dice:
ResponderSuprimiral final tienes que tirar por añadir un par de propiedades del estilo 'ValorAlEntrar' y 'ValorAlSalir' para salir del paso
Hola Antonio. Viéndolo con mayor generalidad, ese es precisamente uno de los problemas más comunes de la programación. La escasez de información de contexto. Lo cual instiga al programador final a trabajar más, o bien a solicitar / sugerir dicha información al programador bibliotecario.
Pero, cuando el bibliotecario se encuentra con que la clase a ampliar ya tiene muchas hijas, tener que tocar el código fuente ajeno en aras de no duplicar el propio o viceversa (duplicar el código propio para respetar el ajeno) se vuelve bastante incómodo. Y al decir ajeno no me refiero a si es de tipo propietario o libre, sino al respeto que por cualquier razón queramos guardarle a una clase que no hicimos nosotros, pero de la cual sí queremos sacar ventaja derivando de ella, sin preocuparnos demasiado por lo que vaya a suceder cuando reemplacemos esa biblioteca por una nueva versión (como cuando actualizamos una versión de Delphi). ¿Realmente es tan difícil de abordar el tema de la redefinición de clases para los fabricantes de compiladores? Dejo la pregunta en el aire. ;)
CASIMIRO dice:
ya estoy esperando tu siguiente post para ver cómo lo has implementado
Es bueno saber que alguien lo espera. Uno intuye una cifra aproximada de seguidores de los temas que publico aquí, pero el sólo hecho que te digan "ya estoy esperando" te motiva como no tienes idea, amigo mío. :)
Recientemente agregué esos eventos a mi derivado TMagiaClientDataSet y me están facilitando mucho las cosas.
EDWARD dice:
En las ocaciones que he intentado utilizar esos eventos, me lanza alguna excepcion o en los mejores de los casos no consigo que funcione adecuadamente
Te invito a ahondar respecto a eso, Edward. ¿Qué problemas has tenido con los eventos XXXChange de Delphi?
Hola Al:
ResponderSuprimirResulta curioso observar que una buena parte del código de la VCL se emplea en métodos "Set" de escritura de propiedades, donde su principal misión y lo primero que se hace es comprobar si el valor existente es distinto del nuevo antes de emprender cualquier acción innecesaria, y en cambio, como tú bien dices, esto no está contemplado a la hora de asignar valores a campos de un TDataset, algo tan frecuente y que muchas veces requiere una alta personalización y detalle.
La mentirijilla de estos eventos la llevan grabada en el nombre, debieran llamarse algo así como OnDataAssigned, el cual no indica que el dato haya cambiado realmente.
En cuanto al acceso al valor anterior, para sortearlo siempre he acudido a los eventos BeforeEdit / BeforeInsert, pero esto es francamente rudimentario y pesado, pudiendo venir de fábrica con propiedades OldValue y NewValue, como de hecho sucede con los ClientDataSets o al trabajar con CachedUpdates.
Fijándome en el código de la unit Db.pas, y concretamente en el método SetFieldData de la clase TDataSet que has apuntado, veo que éste es virtual y no hace nada, lo cual significa que es responsabilidad del autor de cada derivado de TDataSet el acometer dicha característica (aunque como dices el fabricante de Delphi debiera proveer de los eventos Before y After en la clase base para facilitar las cosas). Me he sonreido al leer "estoy seguro que en algún momento Delphi contará con un manejo nativo, rico y muy eficiente de entidades de datos"; después de 14 Delphis, y siendo precisamente el tratamiento de datos uno de sus tradicionales puntos fuertes, me pregunto si esa característica no la tienen ya desechada por algún motivo que ignoramos.
Hablas también de "redefinición de clases, clases virtuales o herencia insertada", suena interesante a la vez que peligroso, ya que puede suceder que se desvirtúe el comportamiento de una clase "ajena", desarrollada por terceros, con las implicaciones que esto puede acarrear. Otra solución elegante a este tipo de problemas consiste en enviarle un email al creador de las susodichas clases y pedirle que las reprograme, ¿no te parece? No sé si alguien de Embarcadero leerá este blog pero estaría bien hacerle estas sugerencias y recabar su opinión directamente.
Curiosamente, siempre he creído que la clase TDataSet era de las más "abiertas de miras" dentro de la VCL, tanto que echa para atrás programar un descendiente por la gran cantidad de métodos virtuales que "hay que" heredar y reprogramar, pero he aquí una carencia que valdría la pena que incorporaran, en la clase base y en los Datasets descendientes que ya vienen con Delphi.
Saludos,
Andrés
ANDRÉS dijo:
ResponderSuprimirEn cuanto al acceso al valor anterior, para sortearlo siempre he acudido a los eventos BeforeEdit / BeforeInsert, pero esto es francamente rudimentario y pesado, pudiendo venir de fábrica con propiedades OldValue y NewValue, como de hecho sucede con los ClientDataSets o al trabajar con CachedUpdates.
Ojo con eso, Andrés. La propiedad OldValue, además de funcionar solamente en los casos que señalas, te dice cuál es el valor que tenía el campo cuando fue leído de la base de datos, no necesariamente el valor previo al actual. El campo puede cambiar varias veces durante el estado de edición, o incluso guardarse el registro en memoria con el método Post y volverse a editar, y OldValue regresará la misma cosa.
De la ayuda de Delphi:
Read the OldValue property to examine or retrieve the original value of the field that was obtained from the dataset before any edits were posted.
Respecto a la propiedad NewValue, solamente es útil ante un evento de error como los señalados por la misma ayuda:
NewValue is the same as Value, except when errors are encountered while posting records. Setting NewValue in an OnUpdateError event handler, an OnUpdateRecord event handler, or an OnReconcileError event handler causes NewValue to differ from Value until the records have finished being applied to the underlying database table.
ANDRÉS dijo:
Fijándome en el código de la unit Db.pas, y concretamente en el método SetFieldData de la clase TDataSet que has apuntado, veo que éste es virtual y no hace nada, lo cual significa que es responsabilidad del autor de cada derivado de TDataSet el acometer dicha característica
De ahí que, en el ejemplo de la teórica TDataSet redefinida, haya puesto “Top” en lugar de “Override”. ;)
ANDRÉS dijo:
Me he sonreido al leer "estoy seguro que en algún momento Delphi contará con un manejo nativo, rico y muy eficiente de entidades de datos"; después de 14 Delphis, y siendo precisamente el tratamiento de datos uno de sus tradicionales puntos fuertes, me pregunto si esa característica no la tienen ya desechada por algún motivo que ignoramos.
Estoy seguro que lo han considerado, pero da la impresión que las prioridades actuales son otras y de índole estabilizadora. Quizá tendremos que esperar a que el propietario de Delphi termine de sentirse cómodo con el negocio.
ANDRÉS dijo:
ResponderSuprimirHablas también de "redefinición de clases, clases virtuales o herencia insertada", suena interesante a la vez que peligroso, ya que puede suceder que se desvirtúe el comportamiento de una clase "ajena", desarrollada por terceros, con las implicaciones que esto puede acarrear.
Reitero:
Estoy consciente de que una característica como ésta de la redefinición de clases presentaría varias repercusiones a considerar, pero es posible que en muchos casos se trate de situaciones salvables en las que los beneficios de programación tengan mayor peso que las medidas de control subyacentes que haya que tomar.
Pero acepto de buena gana el comentario, pues yo mismo invité a debatir el tema. :)
ANDRÉS dijo:
Otra solución elegante a este tipo de problemas consiste en enviarle un email al creador de las susodichas clases y pedirle que las reprograme, ¿no te parece?
Definitivamente. Esa sería OTRA solución elegante, y lo sería más si el correo va notariado y con el aval de los ministros de comercio y cultura xD. Pero si hablamos de lo que se conoce por elegante en términos de programación, es decir, la acepción de esa palabra que reza «dotado de gracia, nobleza y sencillez», me parece que algo fácil de escribir, poco invasivo y de resultado inmediato como redefinir parte de una clase en otro archivo de código lo sería todavía más. :)
A la hora de actualizar la biblioteca original con una nueva versión, no sería tanto lío meterse al extenso código la misma (imaginemos la VCL) para rescatar o revisar la vigencia de las partes que hemos añadido —en caso de que hubiéramos optado por modificar las clases ajenas en lugar de escribir código repetido en clases “puntas” derivadas—. Desde luego que tendríamos que revisar la vigencia de nuestras clases redefinidas, pero, vaya, es lo mismo que ya hacemos (y debemos hacer) con los tradicionales métodos virtuales que solemos redefinir en clases derivadas propias (en archivos de código propios). Y al respetar el código fuente ajeno (que es lo mejor cuando el fabricante planea emitir nuevas versiones), no nos veríamos orillados a implementar las nuevas características en más de una derivación (duplicidad de código) mientras esperamos a que el autor original libere la siguiente versión con nuestras sugerencias (si es que las pondera).
La redefinición de clases, en concreto, propone extender las características de toda una rama de clases mediante la conexión de nuevo código lateral con la raíz de esa rama. Pero, a diferencia de los limitados ayudantes de clases, gozando de todas las ventajas de una clase normal y quizá una que otra cosilla más. Evitando con ello dos despropósitos que actualmente son infranqueables al mismo tiempo: invasión de código ajeno y duplicidad de código propio.
ANDRÉS dijo:
No sé si alguien de Embarcadero leerá este blog pero estaría bien hacerle estas sugerencias y recabar su opinión directamente
No sé cuántas personas de Embarcadero leen en castellano, y además con la voluntad de apoyar mis ideas, pero siempre está la opción de hacer sugerencias en QualityCentral (http://qc.embarcadero.com/wc/qcmain.aspx).
Antes que nada, decirte que es un placer leer tus razonamientos y la meticulosidad con que analizas todos estos asuntos.
ResponderSuprimirEn realidad la necesidad de esa "redefinición de clases" viene dada, no porque el fabricante no dispusiera de un método virtual por donde atacarlo, que lo hay, sino en que queremos "insertar" nuestro comportamiento entre dicha clase y las ya existentes derivadas, osea las ADODataSets, IBDataSets ... etc para aprovecharnos de lo ya implementado en estas últimas.
Pero otra alternativa es heredar de cada una de estas clases e incluir dicho comportamiento, el de los eventos BeforeFieldChange y AfterFieldChange, una por una (aunque sea redundante y no polimórfico) teniendo en cuenta además que "quizás" cada una de esas clases disponga una manera diferente de tratar el Buffer que se le pasa en el método SetFieldData (con esto último insinúo la razón por la cual quizás el fabricante de Delphi no se decidió a hacer las comprobaciones del Field a nivel de la clase TDataset).
Ahora bien, sobre esto último que he escrito, como ya has publicado una nueva entrada y en ella dejas caer que has encontrado la manera de obtener el Variant adecuado según el Buffer, deduzco que hay una forma común a todos los descendientes de TDataSet.
Me voy a la nueva entrada ...
Saludos
ANDRÉS dijo:
ResponderSuprimirsino en que queremos "insertar" nuestro comportamiento entre dicha clase y las ya existentes derivadas
¡Exacto! De ahí que en mis primeras inquietudes sobre el tema, plasmadas en algunos hilos de Club Delphi, le llamase herencia insertada.
Recuerdo haber leído decir a Román Sánchez que JavaScript tenía algo de esto (lo cual no he revisado aún, lo confieso). Y hace unos días, buscando "redefinición de clases" para averiguar si éste último término ya había sido acuñado, encontré un admirable currículum vítae de un paisano que, según parece, diseñó un lenguaje con esa capacidad para la NASA.
ANDRÉS dijo:
"quizás" cada una de esas clases disponga una manera diferente de tratar el Buffer que se le pasa en el método SetFieldData
En efecto. Hay diferencias interesantes entre la manera en que lo hace cada grupo de componentes. DataSnap usa un formato de bajo nivel, mientras que ADO, curiosamente, convierte el Buffer a Variant (empleando un nutrido Case privado en TCustomADODataSet.SetFieldData). En cierta forma, se entiende que ADO guarde OLEVariants en sus registros por usar COM. Así pues, los client data sets, por lo visto, son más ligeros.
ANDRÉS dijo:
como ya has publicado una nueva entrada y en ella dejas caer que has encontrado la manera de obtener el Variant adecuado según el Buffer, deduzco que hay una forma común a todos los descendientes de TDataSet
Así es, Andrés. Una forma común, polimórfica y virtual. ;)
Muy interesante, mi buen amigo Alberto, pero me pregunto si las "clases ayudantes" (Helpers)no te pueden ayudar a conseguirlo sin complicarse demasiado.???
ResponderSuprimirPues segun se ve que a partir de la version Delphi 2007 o superior se puede ampliar la funcionalidad una clase atraves de las llamadas "Clases ayudantes", para mas informacion ver la suiguiente Página: http://delphiallimite.blogspot.com/2008/08/pasando-de-delphi-7-rad-studio-2007-6.html
ROBERTO (RGSTUAMIGO) dijo:
ResponderSuprimir[...] me pregunto si las "clases ayudantes" (Helpers)no te pueden ayudar a conseguirlo sin complicarse demasiado.???
Pues segun se ve que a partir de la version Delphi 2007 o superior se puede ampliar la funcionalidad una clase atraves de las llamadas "Clases ayudantes" [...]
Hola Roberto, es un gusto verte participar aquí. :)
AL GONZÁLEZ dijo:
Lo más cercano que Borland estuvo de ello fue la inclusión de los ayudantes de clases, pero éstos dejan mucho qué desear.
Los ayudantes de clases (class helpers), no "clases ayudantes" (porque no son clases que ayudan, sino elementos que ayudan a las clases), estuvieron a punto de ser incluidos en la versión 7, siendo la versión 8 de Delphi la primera en admitirlos. Una de sus principales limitaciones es que no pueden definirse en ellos nuevos campos.
De todas formas, todo esto de la herencia insertada o redefinición de clases es un paréntesis en el artículo, cuyo tema principal no es como introducir esa capacidad en un lenguaje, sino cómo implementar eventos "change" REALES y un poco más informativos en los conjuntos de datos de Delphi.
Y la respuesta está en la segunda y tercera parte. ;)
Un abrazo en la mira.
Al González.