Buen camino amigo mío.

Cerramos la semana #DelphiWeek con esta cuarta entrega de la serie Construye tu propia calculadora con Delphi, que nos sirve de pretexto para compartir algunos temas básicos y avanzados de Delphi, desde el prisma de un taller eminentemente práctico. Y quisiera aprovechar estas primeras lineas para dar las gracias a todos los compañeros que han ayudado a que se difunda entre nuestras redes sociales, con el único propósito de que pueda ser de ayuda a muchos mas personas. ¡Al menos esa ha sido la intención!. Ha sido muy buena la acogida, aunque ninguno de quienes pueden seguirla, al menos de momento, se ha animado a remitir comentarios, o el código modificado con alguna adición, de acuerdo a mi propuesta de la ultima parte que compartimos. Tampoco importa demasiado. Lo realmente importante es tender esa mano, o como me gusta decir, ese gesto solidario.

Así que si no tenéis mas comentarios, seguimos avanzando.

🙂

Modificaciones a la vista…

Si el diseño original que se ha establecido anteriormente, partía de una clase base TDigitoOperador, de la cual se extendían el conjunto del catálogo de operaciones, ahora modificaremos este diseño de forma que exista una clase base raíz, TDigito, que no solo pueda ser representativa en su descendiente de las operaciones, sino ella misma de cualquier lexema que sea reconocible en el ámbito y contexto de la calculadora. Eso nos obligara a mover cuantos campos, métodos y propiedades sean aplicables a esa clase raíz y dejar exclusivamente en TDigitoOperador, lo que sea del dominio de la clase ligada a los operadores.

Para que sea mas fácil visualizar este cambio en la composición de ese diagrama de clases, nos apoyamos en el mismo IDE de Delphi para generar mediante Model View una representación de las relaciones existentes.

El siguiente diagrama te muestra las relaciones de herencia y composición de clases:

diagrama_clases

A muchos no les habrá pasado desapercibido que nuestro diseño tiene alguna semejanza a los patrones MVC (Model View Controller) o MVP (Model View Presenter), en cuanto que se persigue como objetivo la separación de las capas de presentación respecto a las capas de negocio. En ese diagrama, diríamos que conviven el controlador y el modelo de datos, con una distancia “clara” de la interfaz de usuario, que se limita a conversar con ese controlador (representado en la clase TCalculadoraBasica) para dar respuesta a las peticiones del usuario. En el diagrama de clases de la imagen superior, no aparece ninguna referencia al interfaz. Aquí podrías hacer un punto de parada y releer un antiguo artículo de Daniele Teti, que resalta los beneficios del patrón MVP:  A Simple start with MVP in Delphi for Win32, Part 1 . Léelo con detenimiento y descarga el código fuente que adjunta. y puedes, posteriormente, hacer un paralelismo de este enfoque, respecto a los avances que hemos ido haciendo a través de cada artículo. Pero quisiera dejar esta reflexión para un poco mas adelante, mientas valoramos los cambios a nivel de interfaz. En ese momento, sí podremos cuestionarnos de que forma podamos establecer vías válidas de comunicación entre los distintos agentes, enlazando de alguna forma con el contenido del articulo de Daniele Teti.

Podéis visualizar como se han movido gran parte de los métodos hacia la clase ascendente y TDigitoOperador, ahora publica exclusivamente DoOperar( ), EsOperador( ) y ExecuteAction( ) que están relacionados directamente con las operaciones y las acciones asociadas. El resto, son propias de la clase TDigito, que asumiría mediar respecto a la información del registro de etiqueta, que entiendo que a su vez representaría el area del modelo de datos.


unit UCBClasses;

type
...
 TDigito = class(TInterfacedObject)
  private
    FEtiqueta: TEtiqueta;
    FCalculadora: TComponent;
    function GetClass: TDigitoClass;
    function GetEtiqueta: TEtiqueta;
  protected
    procedure DoInit; virtual;
    procedure MessageException(const AMessage:String);
  public
    constructor Create(AEtiqueta: TEtiqueta; ACalculadora: TComponent);
    destructor Destroy; override;
    function ToString: String; override;
    procedure Init;
    function GetRepresentacion: String;
    property ClassOfDigito: TDigitoClass read GetClass;
    property Etiqueta: TEtiqueta read GetEtiqueta;
  end;

  TDigitoOperador = class(TDigito)
  private
  protected
  public
    function DoOperar(AOperador1, AOperador2: Extended): Double; virtual;
    function EsOperadorInmediato: Boolean;
    function ExecuteAction(ACalculadora: TComponent; AOperando1, AOperando2: Double): Double;
  end;
...

En lo que respecta a la implementación, no existen apenas cambios a este nivel. Existe un nuevo método protegido para la clase TDigito:

procedure MessageException(const AMessage:String);

que me posibilita centralizar en la calculadora, las excepciones generadas a nivel de TDigitoOperador. Cualquier operador puede llamar a este método que internamente delegará en la propia calculadora el tratamiento, de forma que se facilite la gestión centralizada de las excepciones.

La implementación del operador División, ahora puede contemplar la División por Cero.


function TDigitoOperadorDivision.DoOperar(AOperador1,
 AOperador2: Extended): Double;
begin
 Result:= 0;
 if AOperador2 = 0 then
    MessageException('Error division por cero')
else
 Result:= AOperador1 / AOperador2;
end;

Y MessageException, acaba invocando un método a través de la instancia interna que referencia a la calculadora.

procedure TDigito.MessageException(const AMessage: String);
begin
 try
   Raise Exception.Create(AMessage)
 except
   on E: Exception do TCalculadoraBasica(FCalculadora).ExceptionToError(E);
 end;
end;

Te muestro una imagen de dos capturas distintas, durante la ejecución en tiempo de depuración, respectivamente para IOS y para Windows. No nos preocupa de momento la apariencia (todavía no hemos aplicado estilos ni hemos sobrescrito el interfaz maestro, que nos permite trabajar con todas las plataformas).

nan_error

zerodivzero_win_error

 

 

 

 

 

 

 

 

 

 

 

La calculadora en tiempo de ejecución no mostrará ningún mensaje textual, de forma similar a como actuaría en la vida real. He añadido dicho texto a nivel de interfaz para evaluar que funciona de forma adecuada pero en teoría, en la versión final, limitaría esa gestión a mostrar la etiqueta ERROR, sin información del contenido de dicho error y bloquear cualquier acción hasta que el usuario reinicia  mediante la acción Reset. 

Salvo sí…

Preguntas en el aire…

type
  TDisplayInternalState = (csOk, csError);

Este tipo enumerado se define a nivel del componente TCalculadoraBasica con la sana idea de mantener dos posibles valores (quizás mas en el futuro), uno de ellos indicativo de que se ha producido un error. El valor del campo DIS (abreviatura de DisplayInternalState)(*), consideraría csError  como el asociado a la gestión de errores en nuestra calculadora.

(*) Nombre inventado con la única intención de que parezca mas interesante…  🙂

Es fijado al invocar el método ExceptionToError( ).

procedure TCalculadoraBasica.ExceptionToError(E: Exception);
begin
  if Assigned(E) then
  begin
   DIS:= csError;
   FErrorMessage:= E.Message;
   if Assigned(FOnNotifyErrorEvent) then FOnNotifyErrorEvent(Self);
  end;
end;

El tratamiento de la excepciones he intentado que fuera sencillo: fijamos el estado interno a csError, guardamos el mensaje de error (que no será visible pues no deseamos mostrarlo) y lanzamos un evento que puede ser suscrito por el interfaz de usuario, de forma que actualice el interfaz de la forma deseada.

Y por supuesto, una vez fijado, puede existir cualquier función como esta para evaluar este estado interno, del que solo se sale al ejecutar el procedimiento Reset

function TCalculadoraBasica.ExisteError: Boolean;
begin
  Result:= (DIS = csError);
end;

 Por cierto, de igual forma existe un evento para notificar que se ha reiniciado y el estado vuelve a ser csOK, restableciendo los valores de inicio.

procedure TCalculadoraBasica.DoReset;
begin
...
   DIS:= csOK;
   FErrorMessage:= '';
   if Assigned(FOnCleanNotifyErrorEvent) then FOnCleanNotifyErrorEvent(Self);
end;

Hasta esta entrada no disponíamos un tratamiento para determinar los estados de error propios del dominio de la aplicación. Pero inicialmente, y desde la primera parte, presumía que pudieran existir mas estados internos, fuera de los propios de la calculadora. La naturaleza de ese error sería un tanto distinta en el caso de que nuestro componente, fruto de nuevos requerimientos, evolucionase hacia un “evaluador” de expresiones, donde sí se haría necesario conocer que errores (uno o varios) concurren y en ese caso, si sería necesario disponer de una gestión de excepciones mas personalizada. Pensaba para mi mismo, que esta calculadora, dado que puede tener un catálogo que ahora cubriría el total de lexemas, podría evaluar expresiones complejas recibidas como parámetro de entrada de tipo string, similares a “(5 + Raiz(9) * 9) + Sen(45)=”. En ese caso, nuestro usuario necesitaría conocer información adicional de la posición del error o errores.  O bien mostrar en el display el resultado de la operación si tuvo éxito. En ese caso, debería implementar algún proceso de análisis léxico y sintáctico adicional, que permitiera descomponer la cadena en tokens (lexemas) y el orden en el que se procesarían.

Un valor añadido a mi calculadora: decimales a la carta.

La unidad UCalculadora también ha sufrido algunos cambios en esta entrada. Si omitimos los que hacen referencia al apartado anterior, respecto a la gestión del estado interno de error, nos quedaría:

Ahora el Display se representa en base a un registro, aunque sigue siendo un string.

TDisplay = record
    Display: String;
    function ExistePuntoDecimal(const APuntoDecimal: Char): Boolean;
  end

 También hemos añadido algunos campos que hacen referencia al formato visual, como FDecimalSeparator, FThousandSeparator o FDecimalCount.

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;
...

La idea es que durante la creación del componente, recopilemos información adicional de la plataforma, que pueda ser utilizada para formatear correctamente los valores decimales, evitando posibles errores

Estas acciones tienen lugar ahora durante la creación de la clase y nos apoyamos en la estructura TFormatSettings para obtener cual es el valor separador decimal en lugar de presuponer como hacíamos en entradas anteriores que sea la ‘ , ‘.

constructor TCalculadoraBasica.Create(AOwner: TComponent);
var
  FS: TFormatSettings;
begin
  inherited;
  FCatalogo:= TObjectDictionary<String, TDigito>.Create;
  RegisterCatalogo(CatalogoSimbolosBasicos);
  Operando:= 0;
  Operador:= ' ';
  FlagNumero:= False;
  DIS:= csOK;
  FErrorMessage:= '';
  FS:= TFormatSettings.Create;
  FDecimalSeparator:= FS.DecimalSeparator;
  FThousandSeparator:= FS.ThousandSeparator;
  FDisplay.Display:= '0';
  FDecimalCount:= 2;
end;

Pero también nos permite, mostrar la cantidad de decimales deseada, y responder correctamente a la interfaz de usuario, que ahora cuenta con esta funcionalidad adicional.

function TCalculadoraBasica.LeeDisplay: String;
var
  FS: TFormatSettings;
begin
  FS:= TFormatSettings.Create;
  Result:= Format('%'+Format('%d.%d', [MaxDigitos-FDecimalCount-1,FDecimalCount])+'f', [StrToFloat(FDisplay.Display)], FS);
end;

Nuestro esfuerzo debería seguir siendo revisar continuamente el código para que sea lo mas limpio posible. No es algo gratuito pero ninguna tarea, en ese sentido, cae en balde o es inútil.  Hoy por ejemplo detecté que había escrito:

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;

cuando podia haber simplificado:

procedure TCalculadoraBasica.RegisterCatalogo(ACatalogo: array of TEtiqueta);
var
  i: Integer;
begin
  for i:= Low(ACatalogo) to High(ACatalogo) do
    RegisterEtiqueta(ACatalogo[i]);
end;

El interfaz de usuario: pequeños cambios…

La captura os muestra los cambios introducidos. La etiqueta de error y la barra de formato decimal, expresada en una barra TTrackBar que admite valores desde 0 hasta 4 decimales.

interfaz_user

Los cambios tampoco son excesivos. Principalmente se concentran en el evento de creación del formulario, momento en el que ademas nos suscribimos a los eventos de notificación del error.

procedure TfrmCalculadoraTrad.FormCreate(Sender: TObject);
begin
   FCalculadora:= TCalculadoraBasica.Create(Self);
   with FCalculadora do
   begin
     RegisterEtiqueta(EtiquetaRaizCuadrada);
     OnNotifyErrorEvent:= VisualizarEstadoErrorOperacion;
     OnCleanNotifyErrorEvent:= VisualizarEstadoErrorOperacion;
   end;
   VisualizarEstadoErrorOperacion(FCalculadora);
   bnRC.OnClick:= TeclaRaizCuadradaClick;
   bnRC.Text:= EtiquetaRaizCuadrada.Representacion;
   lbDisplay.Text:= FCalculadora.LeeDisplay;
end;

Y el cambio en la barra TrackBar, en respuesta a una selección del valor de dígitos decimales a visualizar.

procedure TfrmCalculadoraTrad.TrBDecimalCountChange(Sender: TObject);
begin
   with FCalculadora do
   begin
     if not ExisteError then
     begin
       DecimalCount:= Trunc(TrBDecimalCount.Value);
       lbDisplay.Text:= LeeDisplay;
     end;
   end;
end;

Ahora, por ejemplo, podemos utilizar información adicional para conocer el valor del separador decimal, a la hora de invocar el proceso del dígito asociado, en lugar de presuponer que sea ‘ , ‘.

procedure TfrmCalculadoraTrad.bnPuntoDecimalClick(Sender: TObject);
begin
  with FCalculadora do
  begin
    lbDisplay.Text:= ProcesarDigito(DecimalSeparador);
    lbOperador.Text:= OperadorText;
  end;
end;

Lo mejor es que os descargues el código fuente de esta parte cuarta, para verlo con mas detenimiento. Me gustaría que lo vieras simplemente como una idea a desplegar o experimentar y no como algo final, dado que no es esa mi intención.

calculadoraFMX_4

Concluyendo la entrada

Nos queda una próxima entrada (la ultima de la serie) en la que afrontaremos ya la parte visual del interfaz, de forma que se pueda ver el resultado al aplicar un estilo. También comentaremos los cambios  en XE7 en el modo en el que ahora podemos aplicar vistas y sobrescribir el formulario Master inicial, que fue una de las novedades que aportaba la última versión.

Comparto una captura en video del resultado hasta el momento:

Saludos

Nota a pie de entrada

Por falta de espacio y tiempo, ha quedado en el tintero compartir unas reflexiones sobre una vía adicional de comunicación entre el interfaz de usuario y el componente, que precisamente enlazaba con los comentarios iniciales sobre los patrones Modelo-Vista-Controlador. 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, de esa forma, desligando cualquier ligadura entre el controlador y la capa IU.  Quizás fuera motivo de crear un pequeño apéndice donde se pueda ver esta idea.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s