Hace una tarde calurosa y bastante veraniega. En una tarde así, no se puede estar en casa escribiendo una entrada como esta… 🙂 El sol cae a plomo a las 6 de la tarde, un día de verano, y parece que todo invita a salir a disfrutar de un rato de deporte. Así que con con vuestro permiso 🙂 inicio la entrada ahora, hasta donde llegue, y la prosigo esta noche si quedara algo por decir.
¿Por donde empiezo? Ummmm…
Lo habíamos dejado en la parte en la que haciamos un pequeño boceto de las dos clases que ibamos a considerar. La clase TGenerador y la clase TAtributo. Eso nos permitía ver como se podían relacionar y a modo de prueba, imaginábamos un método para permitirnos alterar el orden de los mismos, en caso de ser necesario.
Durante esta semana he podido sacer algunos minutos para adelantarme y darle forma a esas dos ideas. Pero no teneis que perder de vista, por favor, que lo importante no es el código en si mismo, que no es mas que un ejemplo cualquiera y puede contener alguna que otra errata al analizarlo con profundidad, sino el hecho mismo de solicitar de cada uno, un ejercicio de análisis previo y reflexión, intentando razonar en términos de clases, en la medida que nos sea posible.
Una imagen vale mas que mil palabras. Este es el ejemplo que preparé para probar la implementación de ambas clases ya con una funcionalidad mayor.
La imagen muestra en la parte superior, 4 atributos que representan los tipos posibles que hemos adminitido. Un atributo combinable (Atributo 2), capaz de seleccionar multiples valores. Un atributo dependiente (Atributo 3), tambien combinable pero que va a permitirnos descartar alguna combinaciones en funcion de una relación imaginaria. Es decir, el caso de que un tipo de atributo solo pueda combinarse con determinados valores de otro atributo. Decirlo es mas complicado que razonarlo. En el ejemplo, el color ’18’ solo puede ser combinado con los tipos ‘AA’ y ‘AB’.
Nos queda el atributo dependiente, representado en el Atributo 1, que toma siempre el mismo valor. Y el Atributo rellenable de, que toma valores respecto a otro atributo cualquiera combinable y no rellenable. Es el Atributo 4.
Así que al pulsar el boton «Solución», nos presenta en los dos componentes TMemo, las combinaciones correctas y las descartadas. En este ejemplo, se aceptan 8 combinaciones y se descartan 16, del total de 24 combinaciones posibles. Las ventanas superiores unicamente existen con el motivo de permitirnos visualizar la información que contiene el generador y los atributos de cara a seguir mejor el ejemplo.
Ahora mismo, el desarrollo del ejemplo se va alejando progresivamente de los bocetos de las dos primeras entradas, representadas en el bucle anidado for, intimamente ligado al interfaz.
Podemos ver la declaración de tipos del módulo que contiene las clases protagonistas:
type TEstructura = Class(TObject) FIndice: Integer; FSiguiente: TObject; private procedure SetIndice(const Value: Integer); procedure SetSiguiente(const Value: TObject); published public constructor Create; destructor Destroy; override; function Situar(AIndice: Integer): TEstructura; property Indice: Integer read FIndice write SetIndice; property Siguiente: TObject read FSiguiente write SetSiguiente; end; TAtributo = class; TGenerador = Class(TObject) private FAtributos: TList; FCombinaciones: TStringList; function GetAtributos(Indice: Integer): TAtributo; function GetDescartado(Indice: Integer): Boolean; procedure SetDescartado(Indice: Integer; const Value: Boolean); function GetCombinaciones(Indice: Integer): String; protected procedure DoCombinar; virtual; procedure DoLimpiarCombinaciones; virtual; procedure AssignCombinaciones; virtual; public constructor Create; destructor Destroy; override; function Count: Integer; procedure SetUpPosition(AAtributo: TAtributo); procedure SetDownPosition(AAtributo: TAtributo); function GetPosicion(AAtributo: TAtributo): Integer; function GetMultiplicador(AAtributo: TAtributo): Integer; function GetTotalCombinaciones: Integer; function AddAtributo: TAtributo; procedure Combinar; virtual; procedure VerificarDependencias; property Atributos[Indice: Integer]: TAtributo read GetAtributos; property Descartado[Indice: Integer]: Boolean read GetDescartado write SetDescartado; property Combinaciones[Indice: Integer]: String read GetCombinaciones; end; TAtributo = class(TObject) private FGenerador: TGenerador; FListaValores: TStrings; FCombinaciones: TStrings; FCombinable: Boolean; FCaption: String; FRellenableDe: TAtributo; FDependienteDe: TAtributo; procedure SetCombinable(const Value: Boolean); procedure SetCaption(const Value: String); function GetValues(Indice: Integer): String; procedure SetValues(Indice: Integer; const Value: String); function GetCombinacion(Indice: Integer): String; procedure SetRellenableDe(const Value: TAtributo); procedure SetDependienteDe(const Value: TAtributo); function GetDependencia(Indice: Integer): TEstructura; function GetPosicion: Integer; { Private declarations } protected function DoGetMultiplicador: Integer; virtual; public { Public declarations } procedure LimpiarCombinaciones; procedure Combinar; function GetMultiplicador: Integer; function ValuesCount: Integer; procedure UpPosicion; procedure DownPosicion; constructor Create(AGenerador: TGenerador); virtual; destructor Destroy; override; function AddValue(const AValue: String): Integer; overload; function AddValue(const AValue, ADependencia: String): Integer; overload; procedure DeleteValue(const AValor: String); procedure VerificarDependencias; property Combinable: Boolean read FCombinable write SetCombinable; property Posicion: Integer read GetPosicion; property Caption: String read FCaption write SetCaption; property Values[Indice: Integer]: String read GetValues write SetValues; property Dependencia[Indice: Integer]: TEstructura read GetDependencia; property Combinacion[Indice: Integer]: String read GetCombinacion; property RellenableDe: TAtributo read FRellenableDe write SetRellenableDe; property DependienteDe: TAtributo read FDependienteDe write SetDependienteDe; end;
No nos es posible en una entrada explicar todas las relaciones, por varios motivos razonables, entre los que considero el tiempo y el espacio. Así que lo mejor es que os descargueis el codigo fuente y le deis un vistazo.
Se me ocurren algunas ideas que pueden ser interesantes:
* A pesar de funcionar correctamente, o al menos eso creo, el hecho mismo de declarar las 3 clases en la parte pública del interfaz, hacen que queden disponibles para el uso, métodos que por ejemplo pertenen a TAtributo y que tan solo deberían estar disponibles para la instancia de TGenerador, puesto que va a ser esta quien quede intimamente ligada a ella, dado que conoce como deben ser ejecutados. De hecho, para ser formalmente correcto, el «usuario», que en este caso es el formulario, debería batallar tanto con TGenerador como con la clase TAtributo, pero parece claro que nunca debería poder ejecutar por ejemplo el metodo VerificarDependencias en el atributo. Y sin embargo, la disposición de las clases y la publicación de los métodos lo facilitan.
* En ocasiones, al pensar en clases aparecen puntos de vista que inicialmente no considerabamos. De hecho, lo que inicialmente era un bucle for anidado, ha sido sustituido por otro razonamiento. ¿Es posible que la clase Generador le pueda decir a cada atributo: Oye majo, que quiero que te combines? Ese es el motivo de que exista un metodo virtual DoCombinar, que se encarga de que cada atributo ejecute su propia expansión de combinaciones…
A eso me refería al comentar que a veces surgen nuevos puntos de vista, mejores o peores, pero distintos. Se me ocurrío razonar al estilo de la cuenta de la vieja. Y generar una lista de las disposiciones individuales de cada uno de los atributos en las combinaciones.
Este es el razonamiento de la vieja.
Supongamos los siguentes pares (Atributos, valores)
Atributo1 > A, B Atributo2 > C, D Atributo3 > E, F
Podemos seguir un patron para combinarlos (el mismo que seguiría el bucle for)
ACE, ACF, ADE, ADF, BCE, BCF, BDE, BDF
Nos permite individualmente obtener una lista de valores
Atributo1 > A,A,A,A,B,B,B,B Atributo2 > C,C,D,D,C,C,D,D Atributo3 > E,F,E,F,E,F,E,F
Por eso aparece por algun lado el multiplicador, que no es mas que las veces que tiene que repetirse el valor en la cadena. En este ejemplo en concreto, el Atributo1 tiene un multiplicador de 4, el Atributo2, un multiplicador de 2, y el atributo3 un multiplicador de 1. Y dichos valores, son factibles de ser implementados mediante una sencilla función.
function TGenerador.GetMultiplicador(AAtributo: TAtributo): Integer; var i,j: Integer; begin if (AAtributo.Combinable) and (not Assigned(AAtributo.RellenableDe)) then begin Result:= 1; i:= FAtributos.IndexOf(AAtributo); if (i < 0) then Result:= -1 else begin for j := i+1 to FAtributos.Count - 1 do if (TAtributo(FAtributos[j]).ValuesCount = 0) or (not TAtributo(FAtributos[j]).Combinable) then Result:= Result * 1 else Result:= Result * TAtributo(FAtributos[j]).ValuesCount; end; end else begin Result:= 1; end; end;
* La tercera reflexión que ya se desprende de todo lo comentado, es el tiempo que dedicamos a implementar lo mismo. Visto de la primera forma, como razonabamos en las dos primeras entradas, la ganancia de tiempo podría ser considerable (menor tiempo). Este módulo, el que vemos en la entrada actual, tiene mayor trabajo para hacer exactamente lo mismo y quizás fuera una buena razón para considerar que perdemos el tiempo. Eso que deciamos de: –acaba como sea pero acaba (El jefe dixit) suele ser pan para hoy y hambre para mañana. Así que de cuando en cuando, deberíais dar un buen puñetazo en la mesa 🙂 y poner una silla trabando la entrada de vuestro despacho de forma que sea imposible que nadie os distraiga. 😉 Por paradojas de la vida, esa misma persona que hoy os pide que acabeis ya el desarrollo como sea, es la misma que mañana os atará de los pulgares en el mástil mayor, si dentro de 6 meses se complica el desarrollo.
Vale… es una exageración bastante malvada pero yo creo que se me entiende. Y el peor enemigo que puede tener vuestro trabajo es que seais vuestro propio jefe… 🙂
* Otra cosa que puede extrañar en el codigo, es que aparezca un tercero en discordia (la clase TEstructura). Es algo bastante habitual y ya lo referia en alguna entrada anterior al hablar de aquel componente Buscador, que dejabamos atras al hablar de los hilos de ejecución. En aquel caso, el tercero en discorida era una implementación del patron iterador, que permitía recorrer la estructura y avanzar. En este caso concreto, me encontré que para poder trabajar con la dependencias, siendo posible que existiera una relación múltiple entre los valores: un color podia pertenecer a varios tipos, pensé que me era necesaria una forma sencilla de recorrerla. En eso me ayuda la clase TEstructura, que apunta a otra Estructura con el valor de relación. De esa forma, puedo buscar si un color pertenece a un tipo y saber si la combinación debe ser descartada o no.
* Y la ultima reflexión que cierra la entrada: la reutilización de nuestro codigo. Ligarnos a nuestros interfaces nos puede permitir reutilizarlos en forma de herencia y en eso ya nos ayuda el IDE de Delphi, al habilitar nuestros formularios en el repositorio de objetos. Sin embargo, al hacerlo así nos obliga a arrastrar no solo las ideas sino las apariencias y eso no es demasiado bueno en todos los casos.
Creo que os lo puedo demostrar… Dadme unos minutos… Pongamos por caso que dos días despues, vamos a formar una peña con el gabinete de programadores para jugar a las quinielas y poder hacernos ricos y dejar de trabajar. 🙂 WOWWWWW… Y como somos previsores recordamos que nuestro programa hacía algo similar… De haberme ligado al interfaz, me hubiera sido imposible en este caso concreto reutilizar esa idea de forma sencilla. Sin embargo, nuestro nuevo planteamiento sí me permite mediante controles distintos y buscando una finalidad que no tiene porque coincidir con la original, reutilizar el código escrito.
Esta es la imagen de nuestro nuevo interfaz de ejemplo:
Dadle un vistazo:
Este es el código que ha permitido ampliar la funcionalidad original (por si teneis curiosidad)
🙂
unit main; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, ButtonGroup, ExtCtrls, StdCtrls, CheckLst, uAtributos; const RestriccionFicticia = 100000; type TForm1 = class(TForm) chbPartidos: TCheckListBox; chb1: TCheckListBox; chbx: TCheckListBox; chb2: TCheckListBox; Label1: TLabel; btnCombinar: TButton; chbCombinables: TCheckListBox; lbxResultados: TListBox; Label2: TLabel; labCombinaciones: TLabel; Label3: TLabel; Label4: TLabel; procedure FormCreate(Sender: TObject); procedure chbPartidosClickCheck(Sender: TObject); procedure btnCombinarClick(Sender: TObject); procedure chb1ClickCheck(Sender: TObject); procedure chbCombinablesClickCheck(Sender: TObject); private { Private declarations } FGenerador: TGenerador; public { Public declarations } end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.btnCombinarClick(Sender: TObject); var i: Integer; begin fGenerador.Combinar; lbxResultados.Clear; for i:= 0 to fGenerador.GetTotalCombinaciones-1 do begin if not fGenerador.Descartado[i] then lbxResultados.Items.Add(fGenerador.Combinaciones[i]); end; labCombinaciones.Caption:= ' Total: ' + IntToStr(lbxResultados.Items.Count); end; procedure TForm1.chb1ClickCheck(Sender: TObject); begin case (Sender as TCheckListBox).Tag of 1: begin if (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex] then FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].AddValue('1') else FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('1'); if not FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].Combinable then begin chbx.Checked[(Sender as TCheckListBox).ItemIndex]:= False; chb2.Checked[(Sender as TCheckListBox).ItemIndex]:= False; end; end; 2: begin if (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex] then FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].AddValue('X') else FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('X'); if not FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].Combinable then begin chb1.Checked[(Sender as TCheckListBox).ItemIndex]:= False; chb2.Checked[(Sender as TCheckListBox).ItemIndex]:= False; end; end; 3: begin if (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex] then FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].AddValue('2') else FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('2'); if not FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].Combinable then begin chb1.Checked[(Sender as TCheckListBox).ItemIndex]:= False; chbX.Checked[(Sender as TCheckListBox).ItemIndex]:= False; end; end; end; if fGenerador.GetTotalCombinaciones > RestriccionFicticia then begin (Sender as TCheckListBox).Checked[(Sender as TCheckListBox).ItemIndex]:= False; case (Sender as TCheckListBox).Tag of 1: FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('1'); 2: FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('X'); 3: FGenerador.Atributos[(Sender as TCheckListBox).ItemIndex].DeleteValue('2'); end; Raise Exception.Create('No se puede exceder de '+ IntToStr(RestriccionFicticia)+' combinaciones...'); end; labCombinaciones.Caption:= 'Total combinaciones ' + IntToStr(fGenerador.GetTotalCombinaciones); end; procedure TForm1.chbCombinablesClickCheck(Sender: TObject); begin if not chbCombinables.Checked[chbCombinables.ItemIndex] then begin if chb1.Checked[chbCombinables.ItemIndex] then begin chb1.Checked[chbCombinables.ItemIndex]:= False; FGenerador.Atributos[chbCombinables.ItemIndex].DeleteValue('1'); end; if chbX.Checked[chbCombinables.ItemIndex] then begin chbX.Checked[chbCombinables.ItemIndex]:= False; FGenerador.Atributos[chbCombinables.ItemIndex].DeleteValue('X'); end; if chb2.Checked[chbCombinables.ItemIndex] then begin chb2.Checked[chbCombinables.ItemIndex]:= False; FGenerador.Atributos[chbCombinables.ItemIndex].DeleteValue('2'); end; end; FGenerador.Atributos[chbCombinables.ItemIndex].Combinable:= chbCombinables.Checked[chbCombinables.ItemIndex]; labCombinaciones.Caption:= 'Total combinaciones ' + IntToStr(fGenerador.GetTotalCombinaciones); end; procedure TForm1.chbPartidosClickCheck(Sender: TObject); begin chbPartidos.Checked[chbPartidos.ItemIndex]:= True; end; procedure TForm1.FormCreate(Sender: TObject); var i: Integer; fAtrib: TAtributo; begin FGenerador:= TGenerador.Create; for i := 0 to chbPartidos.Count - 1 do begin chbPartidos.Checked[i]:= True; fAtrib:= FGenerador.AddAtributo; fAtrib.Caption:= IntToStr(i); fAtrib.Combinable:= True; end; for i := 0 to chbCombinables.Count - 1 do begin chbCombinables.Checked[i]:= True; end; end; end.
Creo que por hoy ya esta bien.
Hace un día estupendo. Y también hay que sacar tiempo para el deporte. No todo puede ser trabajo, ¿no? Además, no se si alguna vez comenté que suelo correr con frecuencia, y a lo largo de la temporada intento participar en alguna que otra carrera popular, casi siempre en la distancia Media Maratón. Creo que es importante el equilibrio personal y que debemos buscarlo en nuestro interior.
Quedan más ideas en el tintero. Las compartiremos en próximas entradas.
Deja una respuesta