Buen camino.
Retomo nuevamente la idea expresada al final de la parte cuarta de la serie, en este apéndice. Si te parece, hago memoria sobre el último comentario que servirá de punto de partida:
…
En esta entrada nos hemos valido de métodos y notificaciones (eventos), que nos permiten actualizar esa interfaz en respuesta de las sucesivas peticiones. Sin embargo, existe la posibilidad de enfocar estos cambios a través de la manipulación de interfaces: Imagina que nuestro display, en nuestro ejemplo representado por un string que almacena el valor a visualizar, fuera sustituido por un tipo interfaz, IDisplayView, que suponemos sea capaz de responder a manipular esta información. Cualquier objeto (instancia) que implementara dicha interfaz y que fuera conectado a la interfaz, aportaría al modelo la capacidad de que TCalculadoraBasica actuara sobre él sin conocer el tipo, en base al acuerdo marco de los métodos que el interfaz establece…
Esta inquietud viene a poner sobre la figurada mesa de discusión, la necesidad de que exista no solo una vía de comunicación, que ya establecimos entre la IU (interfaz de usuario) y la clase que representa el controlador (el componente calculadora) sino un canal abierto en dirección contraria, que repercuta los cambios que ocasiona la primera comunicación. En nuestro caso, la invocación de cualquier método de la calculadora, pongamos por caso, el proceso de un digito cualquiera, repercutiría en los adecuados cambios en el display del usuario, fruto precisamente de esa actuación.
Hice trampa… 😉
¿Te diste cuenta…?
Al convertir, pongamos por caso, el método ProcesarDigito( ) en una función que retornaba un string,
function TCalculadoraBasica.ProcesarDigito(const ALexema: Char): String;
que representaba el valor de ese resultado, abría una ventana a que el interfaz pudiera acceder al valor del display (el resultado de esa operación).
Es un pecado venial. 😀
Sin ese retorno, difícilmente hubiera podido actualizar el IU en las primeras partes de la serie. Lo cual no es incorrecto pero nos obliga a reintroducir la invocación cada vez que es aplicada una acción, de forma que se mantenga coherente el interfaz respecto al estado interno del Display en el componente.
Evolucionar o no evolucionar…
Sin duda: ¡evolucionar!.
El siguiente paso, en esa comunicación es entender que podemos crear un modelo de notificaciones basadas en eventos. Recuerda como limpiábamos la ventana ante un error, apoyándonos en dos eventos antagónicos que se disparaban respectivamente para comunicar que había entrado en estado interno de error o que salía de el. La interpretación de esta comunicación es algo así como si la interfaz le dijera a la clase control, «Déjame saber cuando emerge el error y cuando es limpiado y no tendré que comprobar que éste existe cada vez que proceso una acción».
Te muestro, un fragmento del evento de creación del formulario IU, que denota esta alternativa vía de comunicación:
...
FCalculadora:= TCalculadoraBasica.Create(Self, FDisplay);
with FCalculadora do
begin
RegisterEtiqueta(EtiquetaRaizCuadrada);
OnNotifyErrorEvent:= VisualizarEstadoErrorOperacion;
OnCleanNotifyErrorEvent:= VisualizarEstadoErrorOperacion;
end;
...
Así que en ese sentido, si quisiéramos usar esta vía para mantener sincronizado el Display, sería algo factible creando un evento que notificase los cambios y dejar la oportunidad de que el IU se suscriba. Luego tendríamos que buscar cada punto entre bastidores de nuestro componente para canalizar la invocación del evento cada vez que fuera necesario.
Pero… quizás haya otra forma de hacer las cosas.
Hablar el mismo idioma es lo natural
Bueno. Ahora tenemos que introducir el concepto de interfaz, con el que quizás ya te hayas familiarizado. En términos familiares ¿Qué representa una interfaz? La respuesta para mí al menos es: un contrato, un acuerdo. Cuando declaramos una interfaz, expresamos la idea de que cualquier bicho viviente que la posea, se dotará de sus poderes, con independencia de que sea un ser esmirriado y debilucho.
Imagina la interfaz:
ISuperHeroe = Interfaz
Procedure Volar;
end;
Podríamos declarar la clase THeroe con ese nuevo superpoder, estableciendo en su declaración de tipo esta nueva habilidad (e implementando como hace efectivo su super poder de vuelo).
Type
TPersonaSuperSuper = class(TPersona, ISuperHeroe)
...
end;
En nuestra calculadora vamos a aplicar este concepto que nos va a permitir no solo crear un canal de comunicación sino que este se integre de una forma natural en nuestro componente, sin referencias al objeto real que esconde el interfaz declarado.
Te muestro el nuevo esquema que representa las relaciones que van a establecerse:
Receta que vamos a cocinar
El primer ingrediente que necesitamos es definir propiamente la interfaz. Una posible sería:
IDisplay = Interface
procedure SetTextDisplay(const Value: String);
function GetTextDisplay: String;
End;
Ahora tendríamos que hacer los cambios en nuestro componente, de forma que modificamos el tipo string por el nuevo tipo IDisplay, en el registro que representa la pantalla de la calculadora.
TDisplay = record
Display: IDisplay;
function ExistePuntoDecimal(const APuntoDecimal: Char): Boolean;
end;
Hecho esto, nos resta hacer las sustituciones , ante el quejido lastimero del compilador que detecta un tipo no esperado. Los errores son lógicos. Donde ahora hay declarado un tipo interfaz antes existía un string.
Pues, pongamos en tarea y hagamos los cambios.
Donde antes decía por ejemplo
procedure TCalculadoraBasica.DesplazarDigitoALaIzquierda(AOperando: Char);
begin
FDisplay.Display:= FDisplay.Display + AOperando;
end;
ahora podemos poner
procedure TCalculadoraBasica.DesplazarDigitoALaIzquierda(AOperando: Char);
begin
FDisplay.Display.SetTextDisplay(FDisplay.Display.GetTextDisplay + AOperando);
end;
(Nota del 22/02/2015: Las prisas por acabar el apéndice, me llevaron a que el fragmento anterior, se modificara posteriormente y que el codigo se actualizase).
Si te fijas bien, estamos diciendo lo mismo pero el matiz es que delegamos en el objeto que implementa la interfaz la responsabilidad de las acciones. Al componente calculadora le da igual quien se esconda tras la interfaz. Ese es verdaderamente el punto fuerte y lo que marca la diferencia.
¡Importante!
Ya no necesitamos que los métodos públicos de la clase como ProcesaDigito, sean funciones que retornen un string. Porque van a actualizar el valor de la pantalla a través de la referencia a IDisplay. Así que tanto ProcesaDigito( ) como LeeDisplay( ) ahora pueden convertirse en procedimientos, pues carece de sentido que sigan siendo funciones.
Vale la pena que os muestre la nueva unidad UCalculadora, una vez hechos los cambios.
unit UDBCalculadora;
interface
uses System.SysUtils, System.Variants, System.Classes, Generics.Collections,
UDBClasses;
const
MaxDigitos = 12;
type
TDisplayInternalState = (csOk, csError);
IDisplay = Interface
procedure SetTextDisplay(const Value: String);
function GetTextDisplay: String;
End;
TDisplay = record
strDisplay: String;
Display: IDisplay;
function ExistePuntoDecimal(const APuntoDecimal: Char): Boolean;
end;
TCalculadoraBasica = class(TComponent)
private
FDisplay: TDisplay;
FlagNumero: Boolean;
Operando: Double;
Operador: String;
DIS: TDisplayInternalState;
FCatalogo: TObjectDictionary<String, TDigito>;
FOnCleanNotifyErrorEvent: TNotifyEvent;
FOnNotifyErrorEvent: TNotifyEvent;
FErrorMessage: String;
FDecimalSeparator: Char;
FThousandSeparator: Char;
FDecimalCount: Integer;
procedure DesplazarDigitoALaIzquierda(AOperando: Char);
procedure IntroduceDigito(AOperando: Char);
function EsOperadorInmediato(const ALexema: String): Boolean;
function GetOperadorText: String;
procedure SetOnCleanNotifyErrorEvent(const Value: TNotifyEvent);
procedure SetOnNotifyErrorEvent(const Value: TNotifyEvent);
procedure SetErrorMessage(const Value: String);
function GetErrorMessage: String;
function GetDecimalSeparator: Char;
function GetThousandSeparator: Char;
procedure SetDecimalCount(const Value: Integer);
protected
procedure ActualizaDisplay(const AOperando: String); virtual;
procedure DoReset; virtual;
procedure RegisterCatalogo(ACatalogo: Array of TEtiqueta); virtual;
public
constructor Create(AOwner: TComponent; AIDisplay: IDisplay);
destructor Destroy; override;
procedure ExceptionToError(E: Exception);
function ExisteError: Boolean;
procedure LeeDisplay;
procedure ProcesarDigito(const ALexema: Char);
procedure ProcesarOperacion(const ALexema: String);
procedure RegisterEtiqueta(AEtiqueta: TEtiqueta);
function Reset: String;
property DecimalSeparador: Char read GetDecimalSeparator;
property ThousandSeparator: Char read GetThousandSeparator;
property DecimalCount: Integer read FDecimalCount write SetDecimalCount;
property ErrorMessage: String read GetErrorMessage;
property OperadorText: String read GetOperadorText;
property OnNotifyErrorEvent: TNotifyEvent read FOnNotifyErrorEvent write SetOnNotifyErrorEvent;
property OnCleanNotifyErrorEvent: TNotifyEvent read FOnCleanNotifyErrorEvent write SetOnCleanNotifyErrorEvent;
end;
implementation
uses Rtti;
{ TCalculadoraBasica }
procedure TCalculadoraBasica.ActualizaDisplay(const AOperando: String);
begin
FDisplay.strDisplay:= AOperando;
end;
constructor TCalculadoraBasica.Create(AOwner: TComponent; AIDisplay: IDisplay);
var
FS: TFormatSettings;
begin
FDisplay.Display:= AIDisplay;
FCatalogo:= TObjectDictionary<String, TDigito>.Create;
RegisterCatalogo(CatalogoSimbolosBasicos);
Operando:= 0;
Operador:= ' ';
FlagNumero:= False;
DIS:= csOK;
FErrorMessage:= '';
FS:= TFormatSettings.Create;
FDecimalSeparator:= FS.DecimalSeparator;
FThousandSeparator:= FS.ThousandSeparator;
FDecimalCount:= 4;
FDisplay.strDisplay:= '0';
end;
procedure TCalculadoraBasica.DesplazarDigitoALaIzquierda(AOperando: Char);
begin
FDisplay.strDisplay:= FDisplay.strDisplay + AOperando;
end;
destructor TCalculadoraBasica.Destroy;
var
KeyLexema: String;
begin
for KeyLexema in FCatalogo.Keys do FCatalogo.Items[KeyLexema].Free;
FCatalogo.Clear;
FreeAndNil(FCatalogo);
inherited Destroy;
end;
function TCalculadoraBasica.EsOperadorInmediato(const ALexema: String): Boolean;
begin
if (ALexema = ' ') or (ALexema = '=') then
Result:= False
else Result:= TDigitoOperador(FCatalogo.Items[ALexema]).EsOperadorInmediato;
end;
procedure TCalculadoraBasica.ExceptionToError(E: Exception);
begin
if Assigned(E) then
begin
DIS:= csError;
FErrorMessage:= E.Message;
if Assigned(FOnNotifyErrorEvent) then FOnNotifyErrorEvent(Self);
end;
end;
function TCalculadoraBasica.ExisteError: Boolean;
begin
Result:= (DIS = csError);
end;
function TCalculadoraBasica.GetDecimalSeparator: Char;
begin
Result:= FDecimalSeparator;
end;
function TCalculadoraBasica.GetErrorMessage: String;
begin
Result:= FErrorMessage;
end;
function TCalculadoraBasica.GetOperadorText: String;
begin
if (Operador = ' ') or (Operador = '=') then
Result:= Operador
else Result:= TDigitoOperador(FCatalogo.Items[Operador]).GetRepresentacion;
end;
function TCalculadoraBasica.GetThousandSeparator: Char;
begin
Result:= FThousandSeparator;
end;
procedure TCalculadoraBasica.IntroduceDigito(AOperando: Char);
begin
if AOperando = ',' then
FDisplay.strDisplay:= '0' + AOperando
else
FDisplay.strDisplay:= AOperando;
if FDisplay.strDisplay <> '0' then FlagNumero:= True;
end;
procedure TCalculadoraBasica.LeeDisplay;
var
FS: TFormatSettings;
begin
FS:= TFormatSettings.Create;
FDisplay.Display.SetTextDisplay(Format('%'+Format('%d.%d', [MaxDigitos-FDecimalCount-1,FDecimalCount])+'f', [StrToFloat(FDisplay.strDisplay)], FS));
end;
procedure TCalculadoraBasica.ProcesarDigito(const ALexema: Char);
begin
if ExisteError then Exit;
if FlagNumero then
begin
if Length(FDisplay.Display.GetTextDisplay) < MaxDigitos then
begin
if (FDisplay.strDisplay = '0') and (ALexema = '0') then
Exit
else
begin
//evaluamos si ya exite el punto decimal
if (ALexema = DecimalSeparador) and
(FDisplay.ExistePuntoDecimal(DecimalSeparador)) then
Exit;
DesplazarDigitoALaIzquierda(ALexema);
end;
end;
end
else
IntroduceDigito(ALexema);
LeeDisplay;
end;
procedure TCalculadoraBasica.ProcesarOperacion(const ALexema: String);
begin
if ExisteError then Exit;
if EsOperadorInmediato(ALexema) then Operador:= ALexema;
if ((Operador = ' ') or (Operador = '=')) then
Operando:= StrToFloat(FDisplay.strDisplay)
else
try
Operando:= TDigitoOperador(FCatalogo.Items[Operador]).ExecuteAction(Self, Operando, StrToFloat(FDisplay.strDisplay));
except
on E: Exception do if not ExisteError then ExceptionToError(E);
end;
ActualizaDisplay(FloatToStr(Operando));
FlagNumero:= False;
Operador:= ALexema;
LeeDisplay;
end;
procedure TCalculadoraBasica.RegisterCatalogo(ACatalogo: array of TEtiqueta);
var
i: Integer;
begin
for i:= Low(ACatalogo) to High(ACatalogo) do
RegisterEtiqueta(ACatalogo[i]);
end;
procedure TCalculadoraBasica.RegisterEtiqueta(AEtiqueta: TEtiqueta);
var
FClass: TDigitoClass;
begin
FClass:= AEtiqueta.Clase;
FCatalogo.Add(AEtiqueta.Lexema, FClass.Create(AEtiqueta, self));
end;
function TCalculadoraBasica.Reset: String;
begin
DoReset;
LeeDisplay;
end;
procedure TCalculadoraBasica.SetDecimalCount(const Value: Integer);
begin
if (Value <> FDecimalCount) then
begin
if Value > 0 then
FDecimalCount := Value
else FDecimalCount:= 0;
end;
end;
procedure TCalculadoraBasica.SetErrorMessage(const Value: String);
begin
FErrorMessage := Value;
end;
procedure TCalculadoraBasica.SetOnCleanNotifyErrorEvent(
const Value: TNotifyEvent);
begin
FOnCleanNotifyErrorEvent := Value;
end;
procedure TCalculadoraBasica.SetOnNotifyErrorEvent(const Value: TNotifyEvent);
begin
FOnNotifyErrorEvent := Value;
end;
procedure TCalculadoraBasica.DoReset;
begin
if FDisplay.strDisplay = '0' then
begin
Operando:= 0;
Operador:= ' ';
end
else
FDisplay.strDisplay:= '0';
FlagNumero:= False;
DIS:= csOK;
FErrorMessage:= '';
if Assigned(FOnCleanNotifyErrorEvent) then FOnCleanNotifyErrorEvent(Self);
end;
{ TDisplay }
function TDisplay.ExistePuntoDecimal(const APuntoDecimal: Char): Boolean;
begin
Result:= (StrScan(Pchar(StrDisplay), APuntoDecimal) <> nil);
end;
end.
El postre de la receta
Para que todos los cambios efectuados puedan verse en la práctica, debemos ahora actuar sobre la interfaz de usuario. Necesitamos un componente que nos pueda servir de pantalla y para ello, vamos a definir un nuevo TLabel.
type
TNewLabel = class(TLabel, IDisplay)
procedure SetTextDisplay(const Value: String);
function GetTextDisplay: String;
end;
El nuevo componente sabe comportarse de acuerdo a la interfaz IDisplay.
{ TNewLabel }
function TNewLabel.GetTextDisplay: String;
begin
Result:= self.Text;
end;
procedure TNewLabel.SetTextDisplay(const Value: String);
begin
Text:= Value;
end;
¿No te parece algo sencillo?
Como no lo tenemos creado en tiempo de diseño, ya que no lo hemos instalado en el entorno, simplemente lo crearemos en el evento de construcción del formulario, y se lo facilitaremos a nuestra calculadora (yo he utilizado el constructor para ello).
A partir de ese momento, volveremos a disponer de un display funcional con la diferencia de que podríamos haberle entregado cualquier otro componente, que soportara la interfaz y el resultado sería igualmente funcional.
Este es el código modificado:
unit UMain;
interface
uses
System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
FMX.Types, FMX.Controls, FMX.Forms, FMX.Graphics, FMX.Dialogs, FMX.StdCtrls,
UCalculadora;
type
TNewLabel = class(TLabel, IDisplay)
procedure SetTextDisplay(const Value: String);
function GetTextDisplay: String;
end;
TfrmCalculadoraTrad = class(TForm)
bnSiete: TButton;
bnOcho: TButton;
bnNueve: TButton;
bnCuatro: TButton;
bnCinco: TButton;
bnSeis: TButton;
bnUno: TButton;
bnDos: TButton;
bnTres: TButton;
bnCero: TButton;
bnDobleCero: TButton;
bnPuntoDecimal: TButton;
bnResultado: TButton;
bnDivision: TButton;
bnMultiplicacion: TButton;
bnResta: TButton;
bnSuma: TButton;
btnAC: TButton;
bnRC: TButton;
lbError: TLabel;
lbErrorMessage: TLabel;
lbOperador: TLabel;
TrBDecimalCount: TTrackBar;
Label1: TLabel;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
Label5: TLabel;
procedure bnDobleCeroClick(Sender: TObject);
procedure btnACClick(Sender: TObject);
procedure btnClick(Sender: TObject);
procedure bnOperarClick(Sender: TObject);
procedure FormCreate(Sender: TObject);
procedure bnPuntoDecimalClick(Sender: TObject);
procedure TrBDecimalCountChange(Sender: TObject);
private
{ Private declarations }
FCalculadora: TCalculadoraBasica;
FDisplay: TNewLabel;
procedure TeclaRaizCuadradaClick(Sender: TObject);
procedure VisualizarEstadoErrorOperacion(Sender: TObject);
public
{ Public declarations }
end;
var
frmCalculadoraTrad: TfrmCalculadoraTrad;
implementation
{$R *.fmx}
uses UCBExtendTypes;
procedure TfrmCalculadoraTrad.btnClick(Sender: TObject);
begin
with FCalculadora do
begin
ProcesarDigito((Sender as TButton).Text.Chars[0]);
if not ExisteError then lbOperador.Text:= '';
end;
end;
procedure TfrmCalculadoraTrad.FormCreate(Sender: TObject);
begin
FDisplay:= TNewLabel.Create(Self);
FDisplay.Parent:= Self;
FDisplay.Position.X:= 32;
FDisplay.Position.Y:= 0;
FDisplay.Text:= '0';
FDisplay.Width:= 225;
FDisplay.TextSettings.HorzAlign:= TTextAlign(2);
FDisplay.TextSettings.FontColor:= TAlphaColorRec.Blue;
FCalculadora:= TCalculadoraBasica.Create(Self, FDisplay);
with FCalculadora do
begin
RegisterEtiqueta(EtiquetaRaizCuadrada);
OnNotifyErrorEvent:= VisualizarEstadoErrorOperacion;
OnCleanNotifyErrorEvent:= VisualizarEstadoErrorOperacion;
end;
VisualizarEstadoErrorOperacion(FCalculadora);
bnRC.OnClick:= TeclaRaizCuadradaClick;
bnRC.Text:= EtiquetaRaizCuadrada.Representacion;
FCalculadora.LeeDisplay;
end;
procedure TfrmCalculadoraTrad.TeclaRaizCuadradaClick(Sender: TObject);
begin
with FCalculadora do
begin
ProcesarOperacion(EtiquetaRaizCuadrada.Lexema);
lbOperador.Text:= OperadorText;
end;
end;
procedure TfrmCalculadoraTrad.TrBDecimalCountChange(Sender: TObject);
begin
with FCalculadora do
begin
if not ExisteError then
begin
DecimalCount:= Trunc(TrBDecimalCount.Value);
LeeDisplay;
end;
end;
end;
procedure TfrmCalculadoraTrad.VisualizarEstadoErrorOperacion(Sender: TObject);
begin
with FCalculadora do
begin
lbError.Visible:= ExisteError;
lbErrorMessage.Text:= ErrorMessage;
end;
end;
procedure TfrmCalculadoraTrad.bnOperarClick(Sender: TObject);
begin
with FCalculadora do
begin
ProcesarOperacion((Sender as TButton).Text.Chars[0]);
lbOperador.Text:= OperadorText;
end;
end;
procedure TfrmCalculadoraTrad.bnPuntoDecimalClick(Sender: TObject);
begin
with FCalculadora do
begin
ProcesarDigito(DecimalSeparador);
lbOperador.Text:= OperadorText;
end;
end;
procedure TfrmCalculadoraTrad.bnDobleCeroClick(Sender: TObject);
begin
with FCalculadora do
begin
ProcesarDigito('0');
ProcesarDigito('0');
if not ExisteError then lbOperador.Text:= '';
end;
end;
procedure TfrmCalculadoraTrad.btnACClick(Sender: TObject);
begin
FCalculadora.Reset;
lbOperador.Text:= '';
FCalculadora.DecimalCount:= Trunc(TrBDecimalCount.Value);
FCalculadora.LeeDisplay;
end;
{ TNewLabel }
function TNewLabel.GetTextDisplay: String;
begin
Result:= self.Text;
end;
procedure TNewLabel.SetTextDisplay(const Value: String);
begin
Text:= Value;
end;
end.
Reflexión
Me gustaría que volviera ahora al articulo de Daniele Teti que citábamos en la anterior entrada. Quizás ahora veas mayor paralelismo y encuentres sentido, si es que su lectura te pareció difícil y extraña. Espero que te pueda haber ayudado este apéndice.
Descarga el código fuente
Nos leemos en la quinta y última parte de la serie. Espero que la disfruteis.
Hoy he hecho una modificación en el código fuente que se acompaña ya que al incluir las rutinas de formato de decimales generaban un error que se ha corregido en el nuevo código.
Se ha reemplazado el código original del articulo por el nuevo para no inducir a errores.
Basicamente el problema ha surgido ya que no puedes considerar la vista formateada util en el esquema de razonamientos sino que la debes reservarla exclusivamente cuando se ejecuta el procedimiento que leerá el valor del display.
Te pongo un ejemplo de porque no funcionaba bien:
Al intentar escribir por ejemplo ’99’ suponiendo que la calculadora tuviera activos 4 decimales, había propuesto:
procedure TCalculadoraBasica.DesplazarDigitoALaIzquierda(AOperando: Char);
begin
FDisplay.Display.SetTextDisplay(FDisplay.Display.GetTextDisplay + AOperando);
end;
La función hubiera establecido un valor correcto para el primer dígito 9, pero para el segundo hubiera escrito en el display ‘9,0001’. en lugar de ‘99,0000’.
Las prisas nunca son buenas. Internamente había guardado ‘9,0000’ + ‘9’ y por redondeo mostraba ‘9,0001’. 🙂
Así que he vuelto a revisar el código para que opere correctamente, tal y como lo hacia en los ejemplos anteriores.
Corregido.
Me gustaMe gusta