Cuando intentamos mejorar el diseño de nuestras pruebas nos encontramos con un problema común como puede ser el hecho de crear stubs para testing mientras se mantiene la implementación para producción (y para algunas pruebas). Veamos un par de enfoques.

Podemos enfocar el problema mediante en un Abstract Factory. Todos los servicios a stubs los obtenemos desde un único factory. Por ejemplo, una clase de persistencia.

abstract class Persistence...
public:
  static Persistence& instance() {
    return *dynamic_cast<Persistence*>(Factories.get<Persistence>());
  }

  virtual void save() = 0;

Además de tener la capacidad de un Abstract Factory, las pruebas deben tener la idónea capacidad de tener una pila de implementaciones —lo que nos permite configurar los factories de manera simple.

TEST_CASE("FooTest", "[Foo]") {
  TestFactories.push<Persistence>(new MockPersistence());

  SECTION("save") {
    Foo foo = new Foo();
    foo.save();
    ...
  }

  TestFactories.pop<Persistence>();
}

class Foo...
public:
  void save() {
    Persistence.instance.save(this);
  }

Con un enfoque un tanto distinto, en lugar de un único Abstract factory, podríamos usar un prototipo para aquellos servicios que necesitan de set stubs.

class Facade {
public:
  static void facade(Facade *new_prototype) {
    prototype = new_prototype;
  }

  static Facade &instance() {
    return *(prototype ? prototype : new Facade());
  }

private:
  static Facade *prototype = nullptr;
}

En un test, luciría como lo siguiente:

TEST_CASE("ClientTest", "[Facade]") {

  class Client {
  pubilc:
    char[] speak(char[] input) {
      return Facade.instance().echo(input);
    }

    void dance() {
      return Facade.instance().move();
    }
  }

  SECTION("speak") {
    const char expected_input[] = "bar";
    const char expected_output[] = "foo";

    class SpeakFacade : public Facade {
    public:
      char[] echo(char *input) {
        assertEquals(expected_input, input);
        return expected_output;
      }
    }

    Facade.prototype(new SpeakFacade());

    try {
      const char[] actual_output = new Client.speak(expected_input);
      assertEquals(expected_output, actual_output);
    } finally {
      Facade.prototype(nullptr);
    }
  }

  SECTION("dance") {
    const vector<string> proof;

    class DanceFacade : public Facade {
    public:
      void move() {
        proof.push_back("d");
      }
    }

    Facade.prototype(new DanceFacade());

    try {
      (new Client())->move();
      assertTrue(proof.length > 0, "move wasn't invoke");
    } finally {
      Facade.prototype(nullptr);
    }
  }
}

En este caso, se limpian los recursos en el bloque finally como parte de la prueba. Otra alternativa es hacerlo siempre al final de cada prueba.

El último caso sobre dance es similar a lo que hacen aquellos personas de los Mock Objects al fijar expectativas. Podemos ver esto como una manera ligera de hacer mocks.

Anuncios

sHace dos semanas, en un grupo de Slack, preguntaron acerca de los conocimientos que debe posee un desarrollador front-end web. Luego de una acalorada discusión que levantaba pasiones, la mayoría coincidía en algunos puntos que me gustaría compartir en esta publicación. 🙂

Antesala

La crítica más fuerte vino con la pregunta acerca de “qué framework front-end X debería dominar un desarrollador” y la joya de la corona trató de “el valor de usuario que esta elección [del framework] genera”. Ambas tuvieron respuestas directas acerca del error de plantearse abiertamente esas preguntas, sin contextualizar.

Luego

Algunos comentamos en otro canal acerca de lo extraña de la pregunta y sobre nuestras experiencias entrevistando.

Las entrevistas en efecto son complejas. Tenemos que mostrar en unos 30 minutos para mostrar lo que podemos hacer. En ese tiempo, como entrevistador, eso es complicado asegurar que la persona tiene las habilidades que menciona. Principalmente porque no hay una única medida e incluso que no podemos esperar respuestas directas a preguntas abiertas. Si bien preguntamos sobre un tema específico, las preguntas del entrevistado son hechas a su discreción.

Desde ambas perspectivas, intentamos en esa charla e intentaremos ver en esta ocasión los temas que podríamos abarcar en el desarrollo front-end web.

Distracciones

No me parece que se deba hacer preguntas triviales como “qué es el box model” o “dime las diferencias entres ‘==’ y ‘===’ en JS”. Esto no nos dice mucho. Más que desear saber que sabemos HTML, CSS y JS, durante una entrevista esperamos demostrar que implementamos UIs, que construimos componentes visuales o que podemos usar utilitarios como underscore.js* y/o Lodash; por ejemplo:

  • Maquetar y manipular las interacciones de aplicaciones web conocidas como los sites de Spotify y Netflix, o las demos de Bootstrap y SemanticUI. (Los ejemplos tienen que ir contextualizadas a las habilidades y conocimientos que esperamos).
  • Implementar componentes visuales como un selector de fechas, un carrusel o un carrito de compras.
  • Codear funciones como clonar un objecto por profundidad o una función de rebote en eventos.

Ahora que comento sobre bibliotecas, algo que nos distrae del objetivo principal es pensar que necesitamos depender (y que las personas dependan) del último grito de la moda en frameworks para resolver una pregunta del entrevistador. Podemos preguntarnos el porqué reinventar la rueda si tenemos a jQuery, a React, a Angular y otros listos para producción. Obviamos lo evidente en nuestra profesión que tiene un cuerpo de conocimiento pequeño y cambiante: las tecnologías, los frameworks, las bibliotecas cambian con el tiempo y usualmente no hay que esperar mucho para ver esos cambios. Es más interesante ver cómo afrontamos los restos sobre los fundamentos sobre el desarrollo front-end que depender de declaraciones de alto nivel. Si no podemos responder esas cuestiones, al menos deberíamos explicar con detalles y razonar sobre lo que el framework o la biblioteca están haciendo internamente.

Debemos considerar que nuestras entrevistas se centren en la práctica y la codificación.

Entonces

Creo que es preferible iniciar con preguntas abiertas y dejar que el entrevistado se presente. Esa información nos ayudará a guiar la entrevista:

Generales

Es bueno saber cómo se desempeña con otras personas y ha visto que se maneja ante las situaciones que ha visto en sus equipos.

  • Qué es lo más interesante que ha visto en la implementación de su último proyecto.
  • Cuántas personas confirmaban su equipo.
  • En qué marcos de trabajo se ha desempeñado: Agile, Scrum, Kanban, etc.
  • Cuáles desarrolladores front-end de las comunidades conoce.
  • Ha oído de la “Gangs of four”.
  • Cuál es la diferencia entre pruebas unitarias y pruebas de integración.
  • Cuáles son los tipos de ataques web más usuales.
  • Que es duck typing.

Javascript

Las expectativas del nivel del desarrollador están respecto al conocimiento del lenguaje. Tendríamos que contemplar

  • Contexto de ejecución, especialmente alcance léxico y clausuras.
  • Hoisting, funciones y alcance de bloqueos, y expresiones de funciones y sus declaraciones.
  • Binding (especialmente call, bind, apply y el this léxico).
  • Prototipos de objetos, constructores y mixins.
  • Composición y funciones de órdenes superiores.
  • Delegación de eventos y bubbling.
  • Coerción de tipos (typeof, instanceof y Object.prototype.toString).
  • Manejar llamadas asíncronas con callbacks, promises, await y async.
  • Cuándo usar declaraciones de funciones y expresiones.

DOM

Como manipular el DOM es importante y es donde es más complicado hallar respuestas cuando dependemos de jQuery o sólo hemos desarrollado con React y Angular.

  • Seleccionar y hallar nodos usando document.querySelector y, en caso de necesitar retrocompatibilidad, document.getElementsByTagName.
  • Navegación transversal vertical (Node.parentNodem Node.firstChild y Node.childeNodes).
  • Navegación transversal horizantal (Node.previousSibling y Node.nextSibling).
  • Manipulación (agregar, retirar, copiar y rear nodos en el DOM. Cómo cambiar el text de un nodo y alternar, remover o agregar un nombre de clase CSS).
  • Desempeño (usar el DOM es costoso en procesamiento cuando tenemos muchos nodos, deberíamos conocer sobre fragmentos del documento y cacheado de nodos.

CSS

Tenemos que conocer al menos acerca de la estructura de elementos de una página, cómo los selectores para descendientes directo o los descendientes hijos son usados para ubicar elementos y cuándo usar clases o IDs.

  • Maquetado (ubicación de los elementos junto a otros y su distribución en columnas).
  • Responsive design (cambiar las dimensiones de los elementos según el ancho del navegador).
  • Adaptive design (cambiar las dimensiones según break points).
  • Nivel de especificación (cómo calcular el selector y cómo afecta a los atributos en cascada).
  • Adecuado namespacing y nombrado de clases.

HTML

Saber qué tags de HTML representan mejor el contenido que estamos tratando de mostrar y conocer sus atributos asociados.

  • Marcado semántico.
  • Atributos de tags (como disabled, async, defer, o cuándo usar data-).
  • Cómo declara el doctype y qué meta tags podemos usar.
  • Acceso a elementos visuales, por ejemplo, un input checkbox tiene un área de respuesta (usando un label con “for”). También el uso de role.

Diseño de software

Al parecer se obvia este rubro o se cree que que pertenece sólo a los desarrolladores back-end (o de plano, se obvia en ambos). Usualmente involucra conceptos como MapReduce, diseño distribuido de stores key-value o conocimientos como el princio CAP y los rankings. No debería ser sorpresa preguntar sobre las arquitecturas front-end de las aplicaciones comunes. Aunque estas preguntas suele ser abiertas, como “diseñar un sitio como Path” o “cómo construiríamos un servicio de compras”.

  • Renderizado (en el cliente (CSR), en el servidor (SSR) y el renderizado universal).
  • Maquetado (si estamos construyendo un sistema con múltiples equipos, necesitamos pensar acerca de la construcción de esos componentes y si requerimos que los equipos sigan un marcado consistente para su elaboración).
  • Manejo de estados como la elección de un flujo unidireccional de datos o el binding de datos a dos vías. También tenemos que pensar sobre seguirá una programación pasiva o reactiva, y cómo los componentes se relacionan entre sí.
  • Flujo asíncrono (de los componentes para comunicación en tiempo real con el servidor. Nuestro diseño considerará XHR vs llamadas bidireccionales. Podemos considerar preguntas sobre el soporte a navegadores antiguos teniendo que elegir entre iFrames ocultos, etiquetas script o mensajes XHR; de no ser así, podría proponerse web sockets o eventos hacia el servidor (SSE).
  • Separación de responsabilidades (patrones MCV, MVVM y MVP).
  • Soporte a varios dispositivos (la elección entre si se desea la misma implementación para la web, web mobile y aplicaciones híbridas, o se desea implementaciones separadas. Un sitio como Path podría considerar tres columnas para la web y una columna para dispositivos móviles. ¿Cómo lidiar con esto?
  • Manejo de recursos estáticos (en grandes aplicaciones no se estila tener equipos independientes como dueños de su codebases, las que podrían tener interdependencias y usualmente tienen su propio pipeline para el realese de cambios a producción. Nuestro diseño debe considerar cómo se construyen los activos con sus dependencias (code splitting), cómo se prueban (unit tests and integration tests) y el despliegue, También, cómo manejar los activos de terceros mediante un CDN o en línea para reducir la latencia de la red.

(El diseño de software front-end considera tantos temas que merecen una atención particular.) 😉

Desempeño web

Es importante que podamos reconoces que nuestro diseño y código impactan en el desempeño de la aplicación.

  • Ruta crítica de renderizado.
  • Service workers.
  • Optimización de imágenes.
  • Lazy loading y división de bundles.
  • HTTP/2 y server-push.
  • Cuándo hacer búsquedas previas de datos y cuándo precargarlas.
  • Reducir el flujo del navegador y cuándo promover un elemento a la GPU.
  • Diferencias entre la estructura, la composición y el pintado en el navegador.

Estructuras de datos y algoritmos.

Este tema fue el que levantó más polvo. No obstante, comprender algo del tiempo de complejidad Big-O y la diferencia entre cosas como O(n) y O(nlog\,n) no hace pupa. Los SPAs están de moda y ayuda entender cosas como el manejo de memoria. Conocer las estructuras de datos y algoritmos más comunes harán nuestro software más simple y nuestra vida más sencilla.

Cultura general de la Web

Es de esperar que sepamos lo esencial de las tecnologías y paradigmas que hacen funcionar a la web.

  • HTTP requests (GET y POST con el manejo de cabeceras como Cache-Control, ETag, Status Codes y Transfer-Encoding, por ejemplo.)
  • REST vs RPC.
  • Seguridad (cuándo usar JSONP, CORs o políticas de iFrames).

Conclusiones

Ser un ingeniero o desarrollador web requiere conocer muchas cosas. Pero no es necesario pretender aprender todo es linealmente. No hay un curso o libro que nos convierta en gurús del front-end. Ayuda mucho mantener la mente abierta a aprender sobre las piezas de software más intrincadas del producto final que nuestros usuarios tendrán.

Adicionalmente a las habilidades técnicas, tenemos que poder hablar de nuestros proyectos pasados, describir las situaciones más interesantes y los problemas que hemos enfrentado.

Obviamente, en toda la conversación y en esta publicaciones hay áreas que no se han considerado. No he tenido la intención de hacer un cuestionario ni temario. Hay cuestiones que podemos considerar importantes durante una entrevista que no están listadas aquí.

Yah volveremos con cosas relacionadas a este tema pronto. Suerte a todos. 😉

Hola a todos. 🙂 Continuando con la serie sobre stateless, ahora veremos cómo tratar con datos que compartimos y con en los cuales tenemos que confiar durante su uso. Con esto obtendremos información firmada que nos puede servir para procesos como la autenticación. Empecemos…

Una de las maneras de mantener una sesión es guardar variables en la cookie. Perom, si esto suena a no-stateless y no les gusta, lo mismo es guardarlo en la cookie que enviar el valor de la variable po una cabecera. A fin de cuentas, la cookie es una cabecera y en ambos casos se envía la cabecera y su valor en cada petición.

El riesgo en ambos casos es que alguien vea el valor de la variable y decida cambiarla. Necesitamos de un método que nos indique que el valor ha sido malversado.

Imaginemos que tenemos una tabla de usuarios. Una manera sencilla de mantener la sesión es enviar al software cliente el identificador del usuario (llave primaria, id o cualquier otro término que se desee usar). Es decir, tenemos un usuario cuyo identificador es 3814ebe3 y el cual será reenviado constantemente durante la comunicación.

Obviamente, el problema aquí es que en cualquier momento nos pueden cambiar el valor del identificador logrando suplantar la identidad de otro usuario.

Tenemos que saber que el valor que hemos enviado es uno en el que podemos confiar. Para ello desarrollaremos dos funciones make_secure(value) y check_secure(value).

Necesitamos de un método de hashing para asegurar la integridad de los datos. Usaremos el algoritmo HMAC para obtener el hash de los valores que queremos asegurar.

def make_secure(value, salt):
  return '%s|%s|%s' % (value, salt, hmac.new(SECRET, value+salt).hexdigest())

El valor digest es el hash que representa nuestros datos. En la función anterior vemos una variable SECRET. Esta es la parte que sólo conocemos nosotros. Nos sirve para que el hash generado depende de algo que sólo posee el servidor, por ejemplo. Usualmente, la variable SECRET es una cadena de caracteres. Tambíen, necesitamos un salt que no es otra cosa que una cadena aleatoria de caracteres. Por ejemplo, con

SECRET = 'Js4:su(8"(v/di#f:g)3-g5_=+u3E&"'

obtenemos lo siguiente

>>> make_secure('3814ebe3', '%(aB$N:05')
... '3814ebe3|%(aB$N:05|e3130eb3d9b51cca5a746df2736824d3'

Así, sería necesario conocer el SECRET, lo cual es compplicado pues sólo se necesita en el lado del servidor.

Algo que nos hemos dejado en el camino es cómo obtener el salt.

CHOICES = digits + '!"#$%&\\(;.:)=?+-*/' + letters
def make_salt(length=9):
  return ''.join(random.choice(CHOICES) for _ in range(length))

donde CHOICES es el conjunto de caracteres para formar el salt. (En este caso, números más letras y otros signos.)

Ahora bien, el método make_secure puede refinarse para sólo depender del valor que queremos asegurar:

def make_secure(value, salt=None):
  if not salt:
    salt = make_salt()
  return '%s|%s|%s' % (value, salt, hmac.new(SECRET, value+salt).hexdigest())

Lo que nos falta es el método que nos permite validar el valor:

def check_secure(secure_value):
  value, salt, _ = secure_value.split('|')
  return value if secure_value == make_secure(value, salt) else None

El método de validación regenera la terna a partir del valor y el salt. Si alguna parte del valor ha sido modificado, este método fallará indicando que la información ha sido comprometida.

Este método es la base para firmar payloads y asegurarnos de su autenticidad. No necesita persistencia de caché o disco, pues el SECRET puede estar en la memoria de la aplicación. Cambiarlo implica cerrar las sesiones activas.

Además, es posible añadir conceptos como tiempo de expiración. Sólo sería necesario calcular el hash con el timestamp de expiración. Y así, nuevamente, no es necesario almacenar esa información en caché, por ejemplo. 😉

En la publicación anterior veíamos como intercambiar variables sin usar otra variable intermedia. Ahora explicaremos un poco porqué esto funciona.

Para describir todas las posibles soluciones tenemos que echar mano del concepto matemático de grupo. Un grupo es un conjunto G junto con una operación binaria que satisface

  1. Clausura. Para todo a y b en G, su multiplicación a ⋅ b también está en G.
  2. Asociatividad. Para todo a, b y c en G, tenemos (a ⋅ b)⋅c = a ⋅ (b ⋅ c).
  3. Identidad. Hay un e en G tal que, para todo a en G, se cumple e ⋅ a = a ⋅ e = a.
  4. Inveros. Para cada a en G, hay un inverso a−1 tal que se cumple a−1 ⋅ a = e = a ⋅ a−1.

Así, podemos aplicar el cambio de variables mediante

a ← a ⋅ b
b ← a ⋅ b−1
a ← b−1 ⋅ a

En la siguiente tabla podemos ver paso a paso lo que sucede

a b

c

d

a ← a ⋅ b

c ⋅ d

d

b ← a ⋅ b−1

c ⋅ d

(c ⋅ d)⋅d−1 = c ⋅ (d ⋅ d−1)=c

a ← b−1 ⋅ a

c−1 ⋅ (c ⋅ d)=(c−1 ⋅ c)⋅d = d

c

Notar que tenemos que escribir b−1 ⋅ a y no a ⋅ b−1 ya que en general un grupo no es conmutativo.

Hay varios ejemplos de grupos. Tenemos a los enteros cuya operación sería la adición, la identidad es 0 y el inverso de n es n. No deberíamos preocuparnos por un overflow pues los enteros siguen siendo un grupo bajo la adición módulo N.

También funciona para la representación binaria (con una cantidad de dígitos fijo), ya que las cadenas de bits forman un grupo donde la operación es la disyunción exclusiva, la identidad es la cadena de ceros y toda cadena es su propia inversa.

Los floats distintos de cero forman un grupo bajo la operación de multiplicación, donde la identidad es 1 y el inverso de x es 1/x. Pero hay cuestiones de overflow y underflow, valores NaN y la no asociatividad de la multiplicación de punto flotant

Funciona bien con matrices de dimensiones fijas bajo la operación de adición, donde la identidad es la matriz cero y el inverso está dado por la negación elemento a elemento.

También funciona con las matrices cuadradas, no singulares y de la misma dimensión bajo la operación de la multiplicación matricial, donde la identidad es la matriz identidad y los inversos vienen dados por la matriz inversa (nuevamente, con cuestiones de punto flotante).

Finalmente, las permutaciones también forman un grupo bajo la operación de composición, donde la identidad está dada por la permutación nula y los inverso se obtienen deshaciendo todos los cambios.

Como nota final, quería aclarar que no son necesarias todas las propiedades de grupo. Un cuasigrupo es un conjunto con tres operaciones binarias: multiplicación *, división izquierda \ y división derecha /, que satisfacen

x|(x * y)=y
x * (x|y)=y
(x * y)/y = x
(x * y)*y = x

Todo grupo es automáticamente un cuasigrupo al definir a * b = a ⋅ b, a/b = a ⋅ b−1 y a|b = a−1 ⋅ b. De manera que los cuasigrupos son un generalización de los grupos. Entonces, podemos expresar el cambio de variables como

a ← a * b
b ← a/b
a ← b|a

Es bastante interesante saber que los cuadrados latinos fornan un cuasigrupo, así, por ejemplo, tenemos que las soluciones completas de un Soduku son cuadrados latinos. Luego, cualquier problema de intercambio de variables sin usar una variable temporal pueden ser resueltos con un Soduku suficientemente grande. 😉

Lo bueno de interactuar con otras personas es que uno aprende qué evitar y qué imitar. Al discutir cuestiones relacionadas con el concepto stateless me he topado con visiones acertadas y, por otro lado, descaradas. Así que en las siguientes publicaciones iré exponiendo pequeñas ideas sobre lo que podemos entender por ausencia de estados. Empecemos…

Uno aprende cómo declarar e inicializar variables cuando inicia cualquier curso de programación. Así, por ejemplo, aprendemos a definir las variables a y b como sigue:

a = 12345
b = 67890

Y una acción importante, necesaria en varios algoritmos, es poder intercambiar el valor de ambas variables de manera que se cumpla

a == 67890 and b == 12345

sea verdadero.

Para ello es usual recurrir a una variable auxiliar (también llamada temporal) temp. Mediante ella, guardamos temporalmente el valor de una de las variables para poder recuperar dicho valor durante el intercambio. Así, para cambiar las variables

temp = a
a = b
b = temp

¿Esto es stateless? Cuando formulé la pregunta vi más de una cara de sorpresa. Para el procedimiento de cambio de variable, esto no es stateless; pues, para ejecutarla, hemos necesitado guardar información acerca del estado del problema, donde el estado está definido por el valor de las variables a y b.

Una manera de hacer stateless es recurrir a la disyunción exclusiva.

a ^= b
b ^= a
a ^= b

Con ello logramos los esperado

a == 67890 and b == 12345

sin necesidad de usar una variable intermedia.

Otra forma de lograr lo mismo es la siguiente:

a = a + b
b = a - b
a = a - b

En conclusión, hemos visto dos maneras de realizar un intercambio de variables stateless. 🙂 Las próximas dos publicaciones irán del concepto matemático de grupo, el primero, que el que explica el porqué funciona este intercambio stateless, y el otro, sobre cómo aplicar el mismo principio para lograr una autenticación. Luego, una más sobre JWT. (Con sus respectivos comentarios acerca de lo que me llevó a escribir sobre esto.)

Llevo un tiempo pensando en escribir sobre las arquitecturas frontend. Estaba en aquello hasta que recordé que hace ya algunos años atrás, cuando empezaba esto de desacoplar todo el código del cliente, las herramientas se hacían publicidad como el framework X de MVC en el cliente. Comentando este hecho, he visto que hay poca comprensión acerca de la implementación del patrón. Así que eso va esta publicación.

Inicialmente pensé en algo un tanto más elaborado. Así que gracias al amigo Jefferson, el ejemplo que veremos aquí será usado para un próximo artículo en el que será refactorizado.

El patrón

El patrón Model-View-Controller nació en la época Smalltalk. Buscaba desacoplar la interfaz gráfica de una aplicación existente. Esto fue tan importante que se expandió a otros lenguajes orientados a objectos.

El modelo

En MVC, el modelo es el código que realiza alguna acción. Su construcción no conlleva a pensar en cómo ha de lucir cuando la información sea presentada al usuario. Es una interfaz puramente funcional, lo que quiere decir que tiene métodos públicos que pueden ser usado para realizar acciones u obtener información acerca del estado del modelo, así como métodos que pueden cambiar su estado.

No obstante, el modelo debe permitir registrar las vistas que serán notificadas acerca de la actividad del modelo al cambiar de estado.

Varias vistas

Un modelo puede tener múltiples vistas. Una vista provee una interfaz gráfica de usuario (GUI) que obtiene los valores del modelo para mostrarlos mediante la obtención de los datos del mismo.

Cuando un usuario manipula la vista de un modelo, la vista informa al controlador del cambio deseado.

Varios controladores

Las vistas se asocian con los controladores par actualizar al modelo cuando el usuario interactúa con la vista asociada. El controlador puede invocar los métodos del modelo para cambiar su estado. Además, el modelo notificará a toda las vistas registradas que este ha cambiado y que ellas deben actualizar los valores que se muestran al usuario.

Cómo funciona

Empezamos con un modelo capaz de comunicar sus cambios de estado. Por ejemplo, un termómetro,

class TemperatureModel : public QObject...
public:
  double temperature();
  void temperature(double temperature);
signals:
  void changed(double temperature);
...

(Esto es comúnmente llamado Observer Pattern.) Establecemos métodos para obtener la información del estado actual y actualizarlo. Cada vez que se actualiza el estado, se notifica a cada vista registrada emitiendo el mensaje changed().

Así, podemos crear más de una vista que implementen un método que actualicen apropiadamente en los valores en función de los cambios en el modelo y en la propia vista.

La razón por la que se hereda de QObject es porque este nos provee de la infraestructura de notificación y registro de observadores. De esta manera, podemos concentrarnos en la funcionalidad de nuestra aplicación.

Veamos un ejemplo

Ahora veamos cómo trabajan tres vistas distintas de nuestro modelo de temperatura.

 mvc

Cuando iniciamos el programa MVC, creamos y configuramos a los protagonistas de nuestra historia:

TemperatureModel model;

TemperatureView temperatureView;
SliderView sliderView;
DrawingView drawingView;

TemperatureController temperatureController(model, temperatureView);
SliderController sliderController(model, sliderView);
DrawingController drawingController(model, drawingView);

temperatureView.show();
drawingView.show();
sliderView.show();

model.temperature(0);
...

Hay un sólo modelo que mantiene las funcionalidades de nuestra aplicación. Mediante tres controladores configuramos las GUIs de temperaturas para mostrar un formulario, un control horizontal deslizante y un dibujo de un termómetro. Finalmente, inicializamos el modelo.

Las vistas tienen la responsabilidad de presentar la información del modelo. Por ejemplo, la primera vista construye un formulario para ello.

QLabel *label = new QLabel("Temperature");
layout->addWidget(label);

QLineEdit *lineEdit = new QLineEdit();
layout->addWidget(lineEdit);

QPushButton *lowerButton = new QPushButton("-");
QPushButton *raiseButton = new QPushButton("+");

QHBoxLayout *buttonsLayout = new QHBoxLayout();
buttonsLayout->addWidget(raiseButton);
buttonsLayout->addWidget(lowerButton);

layout->addLayout(buttonsLayout);
setGeometry(90, 140, 188, 95);

La segunda es la configuración de un Slider.

setMaximum(100);
setMinimum(0);
setGeometry(90, 80, 270, 30);

Y la última es un canvas con un gráfico que se asemeja a un termómetro. 🙂

p.setBrush(QBrush(Qt::yellow));
p.drawEllipse(10, 100, 50, 50);
p.drawLine(20, 105, 20, 0);
p.drawLine(50, 105, 50, 0);
p.setPen(Qt::PenStyle(Qt::NoPen));
p.drawRect(21, 100 - _temperature, 29, 6 + _temperature);

Por otro lado, los controladores sólo tienen la función de conectar los eventos entre las vistas y el modelo, así como manejar la interacción en el intercambio de datos.

connect(&view, &View::changed, this, &Controller::update);
connect(&view, &View::lower, this, &Controller::down);
connect(&view, &View::raise, this, &Controller::up);

connect(&model, &Model::changed, this, &Controller::change);
connect(this, &Controller::changed, &view, &View::change);

Resumen

Hemos visto cómo interactúan las vistas y sus controladores con el modelo. Este mismo principio ha sido fuente de mucho debate en las arquitecturas del lado cliente. Por eso consideré importante reforzarlo antes de iniciar con otras dos publicaciones: uno sobre cómo refactorizar este ejemplo y la otra sobre las arquitecturas front-end. Dejo el código completo para ser compilado en un repositorio. Saludos a todos. 😉

Una de las ideas que la orientación a objetos desarrolla es la exponer el comportamiento, mas no la estructura interna del objeto. Esto es conocido como encapsulamiento. Tell-Don’t-Ask es un principio que nos ayuda a recordar este hecho; esto es, en lugar de preguntar a un objeto —por los datos que agrupa y manipularlos, le diremos al objeto qué hacer. Esto encamina a implementar el comportamiento de ese objeto junto con los datos.

Comportamiento y datos

20170902-tell_don_t_ask

Ejemplo

Imaginemos que deseamos lanzar hechizos usando alguna tipo de varita con cierto nivel de mana. Si escribimos nuestra clase con el estilo “ask”, obtendremos una estructura…

class AskSorcerer...
public:
  AskMonitor(Wand wand, int limit, string name):
    wand(wand), limit(limit), name(name) { }
private:
  Wand wand;
  bool isTooLow;
  int limit;
  string name;
  int value;

…a la cual añadirle algunos métodos de acceso sus datos

class AskSorcerer...
pubilc:
  int getValue() { return value; }
  void setValue(int arg) { value = arg; }
  int getLimi() { return limit; }
  string getName() { return name; }
  Wand getWand() { return wand; }

Entonces podríamos usar esa estructura como lo siguiente:

AskSorcerer as = new AskSorcerer(wand, 11, "Willow");
as.setValue(7);
if (as.getValue() < as.getLimit())
  as.getWand().bewitch(as.getName() + " too low");

En lugar de esto, tendríamos que haber puesto el comportamiento dentro del objeto hechicero mismo (usando los mismo campos).

class TellSorcerer...
public:
  void setValue(int arg) {
    value = arg;
    if (value < limit) wand.bewitch(name + " too low");
  }

El cual puede ser usado así

TellSorcerer ts = new TellSorcerer(wand, 11, "Willow");
tm.setValue(7);

Es usual que los datos el comportamiento estén acoplados y por ello pertenezcan al mismo componente. Esto ayuda a que veamos como pueden cohabitar.

Como mencionaba en un artículo anterior: depende. Aunque este principio tiene fuertes defensores; personalmente, no suelo usarlo tan a menudo porque se vuelve un argumento a favor de quienes pretenden eliminar todos los getters, incluso cuando hay situaciones en los que los objetos colaboran precisamente compartiendo información. Un ejemplo son los objetos que reciben datos y los transforman para sus clientes, como puede ser un JSONEncoder extensible como. Este principio es bueno para relacionar y hacer cohabitar el comportamiento y los datos —no necesariamente en la misma clase pues un buen diseño va de responsabilidades y este principio es sólo un factor a considerar— pero es el punto más importante.

Categorías