Buen camino amigo mío.
😀
Tercer día. Finalizando los últimos retoques de la entrada, pero ya me pesa la falta de tiempo para prepararla como a mi me gusta. Estamos celebrando nuestro #DelphiWeek, un poco ajenos a lo que pasa fuera de esta diminuta ventana.
Capturar las imágenes. Pensar en el hilo argumental de la entrada. Tenerlo todo a punto y que no falle nada. Es complicado. Pero hoy nos toca dar un paso mas. No renunciamos a ese compromiso. Seguimos buscando la forma de mejorar nuestro código inicial y buscamos una vía para construir nuevas operaciones, afectando de una forma mínima al cuerpo central de código que las discriminaba.
Hagamos memoria nuevamente. Este era nuestro punto de partida.
case Operador of
' ' : Operando:= StrToFloat(lbDisplay.Caption);
'+' : Operando:= Operando + StrToFloat(lbDisplay.Caption);
'-' : Operando:= Operando - StrToFloat(lbDisplay.Caption);
'x' : Operando:= Operando * StrToFloat(lbDisplay.Caption);
'/' : Operando:= Operando / StrToFloat(lbDisplay.Caption);
end;
Es nuestro caballo de batalla hoy, el corazón del método ProcesarOperacion()
Lo que a mi se me ocurrió, en la sana idea de mejorar o salvar ese tropiezo, fue la idea de que siendo nuestro operador una clase, en lugar de una mísera y vulgar variable (*), quizás podría conocer como debía interpretar el carácter recibido. Es decir, me hice la composición de lugar de llegar tener un catalogo de operaciones, que nos permitiera relacionar cada carácter o caracteres con las distintas clases ligadas a las mismas.
(*) Dicho esto en tono cariñoso, con empatía. 😉
Para mi esa estructura podría formarse por:
- Un lexema (un identificador único para cada operación)
- La clase asociada al lexema
- La representación textual del lexema (podría dar contenido al texto de los botones)
- Un valor booleano sobre si es un operador inmediato u opera con dos operandos de la forma que ya implementada en la función original.
Los tres primeros son mas evidentes pero el cuarto se añadió a posteriori.
Verás:
Si prestas atención a la implementación completa del método ProcesarOperacion() que habíamos compartido en la parte II:
function TCalculadoraBasica.ProcesarOperacion(const ALexema: Char): String;
begin
case Operador of
' ','=' : Operando:= StrToFloat(FDisplay);
'+' : Operando:= Operando + StrToFloat(FDisplay);
'-' : Operando:= Operando - StrToFloat(FDisplay);
'x' : Operando:= Operando * StrToFloat(FDisplay);
'/' : Operando:= Operando / StrToFloat(FDisplay);
end;
ActualizaDisplay(FloatToStr(Operando));
FlagNumero:= False;
Operador:= ALexema;
...
Descubriremos un pequeño problema en el caso de operaciones como la raíz cuadrada, que aunque tiene dos operandos (como las anteriores), el índice de la raíz y el radicando, a diferencia de las operaciones habituales, en las que existe un orden lógico respecto al modo en el que se introducen y solo devolvemos el resultado al conocer el segundo operando, no entraría en ese grupo, con un comportamiento particular. La operación raíz no va a esperar ese segundo operando sino que retornará inmediatamente el resultado (pues se supone que el valor intrínseco que tiene el indice, que vale 2 en cualquier caso). Elegimos una vía sencilla para interpretar esta «anomalía» dejando que conozca en función de esa operación si debe retornar o no inmediatamente.
En la practica, ha supuesto crear un array de registros, para las operaciones básicas que ya conocíamos. Un array del tipo TEtiqueta que será un registro con los campos ya comentados y una funciones de apoyo a la manipulación.
TEtiqueta = record
Lexema : String;
Clase: TDigitoClass;
Representacion: String;
EsOperadorInmediato: Boolean;
function Init: TEtiqueta;
function Assign(ASource: TEtiqueta): TEtiqueta;
end;
El catalogo se inicializará con el total de operaciones básicas que asumimos:
const
CatalogoSimbolosBasicos: Array[0..3] of TEtiqueta = (
(Lexema:'x'; Clase: TDigitoOperadorMulti;Representacion: '*'; EsOperadorInmediato: False),
(Lexema:'+'; Clase: TDigitoOperadorSuma; Representacion: '+'; EsOperadorInmediato: False),
(Lexema:'-'; Clase: TDigitoOperadorResta;Representacion: '-'; EsOperadorInmediato: False ),
(Lexema:'/'; Clase: TDigitoOperadorDivision; Representacion: #$2797; EsOperadorInmediato: False)
);
Y una etiqueta adicional, que crearemos en tiempo de ejecución, demostrando así que somos capaces de añadir operaciones y determinar qué queremos que hagan. Ahora, esa etiqueta forma parte del mismo proyecto, pero quizás en un futuro decidamos que sea aislada en una librería adicional, algo razonable y factible.
const
EtiquetaRaizCuadrada: TEtiqueta = (Lexema: 'RC';
Clase: TDigitoOperadorRaizCuadrada;
Representacion: #$221A;
EsOperadorInmediato: True); // digito raiz
Uno de los pequeños cambios que he tenido que valorar, es permitir que nuestro lexema tenga mas de un carácter de forma que, ante la incertidumbre de que número máximo admitir, te permitiría identificar operaciones mediante una cadena textual indeterminada. Podría haber elegido por ejemplo SEN para calcular el seno(), o como este que nos ocupa, RC, para la Raíz cuadrada.
Por otro lado, si nos fijamos en un determinado caso de la estructura
'+' : Operando:= Operando + StrToFloat(FDisplay);
necesitaría probablemente una función que devolviera una valor decimal, recibiendo como parámetro de entrada cada uno de los operandos.
A ver que te parece ésta:
function DoOperar(AOperador1, AOperador2: Extended): Double;
Resumiendo todos estos conceptos, puedo presentarte ya la unidad UCBExtendTypes.pas que va a alojar nuestra clase TDigitoRaizCuadrada, que hereda de un ascendente comun a todas las operaciones TDigitoOperador. El motivo de hacerlo así no es otro que el de poder trabajar a nivel interno con la clase ascendente (en el interior del corazón de nuestro proceso de calculo) y que sea cada descendiente el que decida como debe operar.
Sencillo, ¿no?
😀
Apunta el siguiente código:
unit UCBExtendTypes;
interface
uses System.Classes, UCBClasses;
type
TDigitoOperadorRaizCuadrada = class(TDigitoOperador)
public
function DoOperar(AOperador1, AOperador2: Extended): Double; override;
end;
const
EtiquetaRaizCuadrada: TEtiqueta = (Lexema: 'RC';
Clase: TDigitoOperadorRaizCuadrada;
Representacion: #$221A;
EsOperadorInmediato: True); // digito raiz
implementation
uses Math;
{ TDigitoOperadorRaizCuadrada }
function TDigitoOperadorRaizCuadrada.DoOperar(AOperador1,
AOperador2: Extended): Double;
begin
Result:= Sqrt(AOperador2);
end;
end.
Volviendo la vista al código principal…
¿Recuerdas la interfaz de nuestro usuario?
Podemos añadir el nuevo botón que representa la operación raíz cuadrada, acomodando el resto de botones al espacio disponible. No hay mas cambios en nuestro formulario.
Eso sí, el código ahora debe considerar la nueva operación, por lo que hemos necesitado establecer una vía para registrar esa etiqueta, y seguidamente, asignamos el evento de respuesta de una forma similar a como lo hicimos en tiempo de diseño: al evento onClick le vinculamos un nuevo manejador para evaluar la respuesta de ProcesarOperacion( ) para el nuevo lexema. No olvides añadir en el uses de la implementación de UMain la unidad UCBExtendTypes.
procedure TfrmCalculadoraTrad.FormCreate(Sender: TObject);
begin
FCalculadora:= TCalculadoraBasica.Create(Self);
FCalculadora.RegisterEtiqueta(EtiquetaRaizCuadrada);
bnRC.OnClick:= TeclaRaizCuadradaClick;
bnRC.Text:= EtiquetaRaizCuadrada.Representacion;
end;
procedure TfrmCalculadoraTrad.TeclaRaizCuadradaClick(Sender: TObject);
begin
lbDisplay.Text:= FCalculadora.ProcesarOperacion(EtiquetaRaizCuadrada.Lexema) +
FCalculadora.OperadorText;
end;
Y también debemos hacer unos cambios en el módulo donde residía la especificación de la clase TCalculadoraBasica, que justamente habíamos añadido al proyecto en la parte anterior de la serie.
Añadimos una clase capaz de guardar nuestro catalogo
TCalculadoraBasica = class(TComponent)
private
FDisplay: String;
FlagNumero: Boolean;
Operando: Double;
Operador: String;
FCatalogo: TObjectDictionary<String, TDigitoOperador>;
...
Y añadimos las tres piezas claves y una opcional (marcadas en color rojo en el texto). La tres primeras son respectivamente: Una función (EsOperadorInmediato( )) que evaluará si debe retornar de forma inmediata, y dos procedimientos que te permitirán añadir el array de registros con las operaciones básicas (RegisterCatalogo( )), de uso interno de la clase (o de sus descendientes) y una función publica para registrar la etiqueta individual (RegisterEtiqueta( )).
function EsOperadorInmediato(const ALexema: String): Boolean;
function GetOperadorText: String;
protected
procedure ActualizaDisplay(const AOperando: String); virtual;
procedure DoReset; virtual;
procedure RegisterCatalogo(ACatalogo: Array of TEtiqueta); virtual;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
function ExisteError: Boolean;
function LeeDisplay: String;
function ProcesarDigito(const ALexema: Char): String;
function ProcesarOperacion(const ALexema: String): String;
procedure RegisterEtiqueta(AEtiqueta: TEtiqueta);
function Reset: String;
property OperadorText: String read GetOperadorText;
end;
Nos queda ver que cambios ha supuesto, a nivel de la implementación del módulo.
No te preocupes ya que son bastante sencillos:
Añadimos nuestro catalogo, representado en una instancia de la clase TObjectDictionary, que se adapta bastante bien a lo que buscamos. Esta clase, es una de las clases genéricas pertenecientes a la categoría de colecciones que ya tenemos a nuestra disposición en el entorno, lista para que la uses. Imagínala de un modo similar a como hubieras usado un TStringList. La diferencia y la ventaja es la parametrización. Nuestro diccionario va a relacionar una cadena textual con una instancia de la clase TDigitoOperador (la clase base sobre la que trabajamos al nivel de TCalculadoraBasica, que nunca conocerá a sus descendientes ni parentesco).
constructor TCalculadoraBasica.Create(AOwner: TComponent);
begin
inherited;
FCatalogo:= TObjectDictionary<String, TDigitoOperador>.Create;
RegisterCatalogo(CatalogoSimbolosBasicos);
Operando:= 0;
Operador:= ' ';
FlagNumero:= False;
end;
Igualmente debemos liberar la memoria asociada al contenido del catalogo antes de que se destruya la clase. Esa parte pertenecería al ámbito del evento Destroy de la calculadora.(*)
Un matiz: En muchos casos podremos tomar ventaja del recuento de referencias automático, dado que la instancia creada de la clase hereda de TInterfaceObject, y no sería necesario liberar expresamente.
destructor TCalculadoraBasica.Destroy;
var KeyLexema: String;
begin
for KeyLexema in FCatalogo.Keys do FCatalogo.Items[KeyLexema].Free;
FCatalogo.Clear;
FreeAndNil(FCatalogo);
inherited Destroy;
end;
(*) Ahondaremos en ese punto más adelante y nos serviremos de una herramienta adicional integrada en el IDE de Delphi, como puede ser CodeSite, de la que ya hemos hablado días atrás.
Añadimos igualmente una función que nos permita evaluar si la operación debería retornar de forma inmediata y para ello, consideramos todos los casos posibles, respecto al parámetro de entrada:
function TCalculadoraBasica.EsOperadorInmediato(const ALexema: String): Boolean;
begin
if (ALexema = ' ') or (ALexema = '=') then
Result:= False
else Result:= FCatalogo.Items[ALexema].EsOperadorInmediato;
end;
Y de una forma similar, también crearía un método que nos retorne la representación textual.
function TCalculadoraBasica.GetOperadorText: String;
begin
if (Operador = ' ') or (Operador = '=') then
Result:= Operador
else Result:= FCatalogo.Items[Operador].GetRepresentacion;
end;
Y el punto fuerte, sería la forma en la que jugando con la herencia y el polimorfismo respondemos a la invocación de ExecuteAction( ), sustituyendo así nuestra estructura case
function TCalculadoraBasica.ProcesarOperacion(const ALexema: String): String;
begin
if EsOperadorInmediato(ALexema) then Operador:= ALexema;
if ((Operador = ' ') or (Operador = '=')) then
Operando:= StrToFloat(FDisplay)
else
Operando:= FCatalogo.Items[Operador].ExecuteAction(Self, Operando, StrToFloat(FDisplay));
ActualizaDisplay(FloatToStr(Operando));
FlagNumero:= False;
Operador:= ALexema;
Result:= LeeDisplay;
end;
Piensa como se va a ejecutar en la clase base:
function TDigitoOperador.ExecuteAction(ACalculadora: TComponent;
AOperando1, AOperando2: Double): Double;
begin
Result:= DoOperar(AOperando1, AOperando2);
end;
Y DoOperar() virtual por naturaleza en la clase base, no hará nada y será sobrescrito por cada operación añadida. Toma como ejemplo la operación Resta:
TDigitoOperadorResta = class(TDigitoOperador)
protected
public
function DoOperar(AOperador1, AOperador2: Extended): Double; override;
end;
Nos falta algo…
Veo que estás bastante atento.
😀
Olvidaba mostarte los métodos que vamos a añadir para registrar las etiquetas. El primero, RegisterCatalogo( ), lo hemos definido como protegido y lo usaremos para añadir el catalogo inicial de operaciones en el momento en el que se crea la clase. Por ello hemos considerado una rutina que nos permita recorre cada registro de ese catálogo. Mientras que en el segundo caso, RegisterEtiqueta(), actuamos a nivel individual y con la mirada puesta a que pueda ser utilizado de forma pública, a nivel de interfaz, en base al diseño de la aplicación.
procedure TCalculadoraBasica.RegisterCatalogo(ACatalogo: array of TEtiqueta);
var
i: Integer;
FClass: TDigitoClass;
begin
for i:= Low(ACatalogo) to High(ACatalogo) do
begin
FClass:= ACatalogo[i].Clase;
FCatalogo.Add(ACatalogo[i].Lexema, FClass.Create(ACatalogo[i], self));
end;
end;
procedure TCalculadoraBasica.RegisterEtiqueta(AEtiqueta: TEtiqueta);
var
FClass: TDigitoClass;
begin
FClass:= AEtiqueta.Clase;
FCatalogo.Add(AEtiqueta.Lexema, FClass.Create(AEtiqueta, self));
end;
¿Y el resto de código?
Con lo comentado en las lineas anteriores, no deberías tener problema para entender la última de las unidades que vamos a añadir al proyecto. En esta unidad, profundizamos en la interpretación de esa clase base que representa la abstracción del concepto de «operación» en el marco de la calculadora.
Tu parte esta ahora en leerla y devolverme cualquier comentario que creas conveniente.
Estamos en #DelphiWeek, nuestro tercer día compartiendo como Construir una calculadora con Delphi.
unit UCBClasses;
interface
uses System.SysUtils, System.Variants, System.Classes;
type
TDigitoOperador = class;
TDigitoClass = class of TDigitoOperador;
TEtiqueta = record
Lexema : String;
Clase: TDigitoClass;
Representacion: String;
EsOperadorInmediato: Boolean;
function Init: TEtiqueta;
function Assign(ASource: TEtiqueta): TEtiqueta;
end;
TDigitoOperador = class(TInterfacedObject)
private
FEtiqueta: TEtiqueta;
FCalculadora: TComponent;
function GetClass: TDigitoClass;
function GetEtiqueta: TEtiqueta;
protected
public
constructor Create(AEtiqueta: TEtiqueta; ACalculadora: TComponent); virtual;
destructor Destroy; override;
function DoOperar(AOperador1, AOperador2: Extended): Double; virtual;
function EsOperadorInmediato: Boolean;
function ToString: String; override;
procedure Init;
function GetRepresentacion: String;
function ExecuteAction(ACalculadora: TComponent; AOperando1, AOperando2: Double): Double;
property ClassOfDigito: TDigitoClass read GetClass;
property Etiqueta: TEtiqueta read GetEtiqueta;
end;
TDigitoOperadorSuma = class(TDigitoOperador)
protected
public
function DoOperar(AOperador1, AOperador2: Extended): Double; override;
end;
TDigitoOperadorResta = class(TDigitoOperador)
protected
public
function DoOperar(AOperador1, AOperador2: Extended): Double; override;
end;
TDigitoOperadorMulti = class(TDigitoOperador)
protected
public
function DoOperar(AOperador1, AOperador2: Extended): Double; override;
end;
TDigitoOperadorDivision = class(TDigitoOperador)
protected
public
function DoOperar(AOperador1, AOperador2: Extended): Double; override;
end;
const
CatalogoSimbolosBasicos: Array[0..3] of TEtiqueta = (
(Lexema:'x'; Clase: TDigitoOperadorMulti;Representacion: '*'; EsOperadorInmediato: False),
(Lexema:'+'; Clase: TDigitoOperadorSuma; Representacion: '+'; EsOperadorInmediato: False),
(Lexema:'-'; Clase: TDigitoOperadorResta;Representacion: '-'; EsOperadorInmediato: False ),
(Lexema:'/'; Clase: TDigitoOperadorDivision; Representacion: #$2797; EsOperadorInmediato: False)
);
implementation
{ TDigitoOperador }
uses UCalculadora;
constructor TDigitoOperador.Create(AEtiqueta: TEtiqueta;
ACalculadora: TComponent);
begin
Assert(Assigned(ACalculadora),'Error Instancia Clase TClassCalculadora no definida');
FCalculadora:= ACalculadora;
FEtiqueta.Assign(AEtiqueta);
end;
destructor TDigitoOperador.Destroy;
begin
FCalculadora:= Nil;
inherited Destroy;
end;
function TDigitoOperador.DoOperar(AOperador1, AOperador2: Extended): Double;
begin
end;
function TDigitoOperador.EsOperadorInmediato: Boolean;
begin
Result:= FEtiqueta.EsOperadorInmediato;
end;
function TDigitoOperador.ExecuteAction(ACalculadora: TComponent;
AOperando1, AOperando2: Double): Double;
begin
Result:= DoOperar(AOperando1, AOperando2);
end;
function TDigitoOperador.GetClass: TDigitoClass;
begin
Result:= FEtiqueta.Clase;
end;
function TDigitoOperador.GetEtiqueta: TEtiqueta;
begin
Result:= FEtiqueta;
end;
function TDigitoOperador.GetRepresentacion: String;
begin
Result:= FEtiqueta.Representacion;
end;
procedure TDigitoOperador.Init;
begin
FEtiqueta.Init;
end;
function TDigitoOperador.ToString: String;
begin
Result:= GetRepresentacion;
end;
{ TDigitoOperadorSuma }
function TDigitoOperadorSuma.DoOperar(AOperador1, AOperador2: Extended): Double;
begin
Result:= AOperador1 + AOperador2;
end;
{ TDigitoOperadorResta }
function TDigitoOperadorResta.DoOperar(AOperador1,
AOperador2: Extended): Double;
begin
Result:= AOperador1 - AOperador2;
end;
{ TDigitoOperadorMulti }
function TDigitoOperadorMulti.DoOperar(AOperador1,
AOperador2: Extended): Double;
begin
Result:= AOperador1 * AOperador2;
end;
{ TDigitoOperadorDivision }
function TDigitoOperadorDivision.DoOperar(AOperador1,
AOperador2: Extended): Double;
begin
Result:= AOperador1 / AOperador2;
end;
{ TEtiqueta }
function TEtiqueta.Assign(ASource: TEtiqueta): TEtiqueta;
begin
with Self do
begin
Lexema := ASource.Lexema;
Clase := ASource.Clase;
Representacion := ASource.Representacion;
EsOperadorInmediato:= ASource.EsOperadorInmediato;
end;
Result:= Self;
end;
function TEtiqueta.Init: TEtiqueta;
begin
with Self do
begin
Lexema := '';
Clase := Nil;
Representacion := '';
EsOperadorInmediato:= False;
end;
Result:= Self;
end;
end.
Este es el código fuente: calculadorafmx_3
Finalizando la entrada: ahora te toca a tí…
Si estás siguiendo la serie, te propongo una pausa de dos días en los que te invitaría a intentar poner en práctica lo que hemos comentado. Monta un pequeño proyecto que resuma estas tres entradas, y me lo remites por correo añadiendo alguna operación adicional o mejora. Tienes jueves y viernes para disfrutarlo.
La dirección de correo es la asociada a este blog:
salvador[at]delphibasico[dot]com (salvador[arroba]delphibasico[punto]com)
Viviendo #DelphiWeek!!!!!
PD: Si te decides remitirme el código no incluyas ejecutables. Solo las unidades pas y el dpr del proyecto.
Deja una respuesta