You are currently browsing the category archive for the ‘sw’ category.

 

El equipo acaba la primera parte de sprint planning desgranando os PBIs en tareas, estimándolas y cada miembro del equipo empieza a trabajar en un ítem distinto. Esto lleva a problemas de integración y silos de implementaciones en los cuales no tenemos ni de lo que está sucediendo. Las tareas quedan a mitad de desarrollo al final del sprint y no da tiempo a integrar (ofreciendo algo de calidad). Sin embargo, el product owner aprieta los tiempos y el esfuerzo estimado para el sprint review. “Cuanto más, mejor”. Los desarrolladores (si se lo permiten) añaden la “deuda técnica” al backlog para seguramente no volver a verla.

Ward Cunningham presentó el concepto de “Technical Debt” en 1992. Es una metáfora para explicar cómo la ausencia de calidad en nuestro código terminará por generar sobrecostos en el matenimiento de la aplicación en dinero como esfuerzo del equipo (usualmente este último, lo que termina causando más deuda).

En mi opinión, el costo que nos genera la deuda técnica aumenta de manera geométrica respecto al tiempo. Así como no tenemos una manera de calcular la calidad del código, tampoco tenemos una para averiguar las consecuencias de la mala calidad. El problema radica en la definición de calidad. La ignorancia respecto a la necesidad de negocio que tratamos cubrir nos lleva a adquirir la deuda. Una manera de tener una idea acerca de la deuda técnica es clasificarla en dos factores, la prudencia y la conciencia de las decisiones de diseño que tomamos.

cuadrantes

La deuda prudente y deliberada es a la que nos vemos obligados a incurrir por factores externos siendo conscientes de ello. La prudente e inadvertida es la que tenemos que explicar a los stakeholders porque aparece en todo proyecto mientras vamos conociendo más el problema que tratamos de resolver e incluso analizamos si vale la pena pagar este tipo de deuda. La imprudente y deliberada es la peor de todas pues damos por sentado que estamos desarrollando algo que acabará con mala calidad y puede ser producida por una mala gestión, despropósitos técnicos y falta de compromiso. La imprudente e inadvertida se debe a una mala formación profesional que nos hace tomar una serie de decisiones equivocadas.

Es importante que los stakeholdders comprendan que siempre habrá deuda técnica en un proyecto. Por otro lado, los desarrolladores cuentan con principios básicos de programación como SOLID, YAGNI, KISS y DRY.

tiempo

También, tenemos algunos indicadores que debemos procurar detectar pronto:

  • Cuando es complejo adaptar el código es porque este es rígido resultando difícil de mantener y extender.

  • El código empieza a fallar en lugares donde no hemos tocado nada al cambiar un pequeño detalle del mantenimiento en el que estemos trabajando. Luego, solemos tratar con miedo el código, procuramos modificarlo lo menos posible generando más código repetitivo y hard-codeando haciéndolo incluso más frágil.

  • Aunque la aplicación tiene componentes útiles en otras, hay un gran riesgo de desacoplarlas del resto del código y el esfuerzo es tanto que preferimos estas soluciones inamovibles volviendo a escribir las implementaciones.

  • El proceso de desarrollo se vuelve lento e ineficiente, el código es difícil de utilizar y hay una ausencia de diseño. Ambos son viscosos porque tenemos que recurrir a trucos, genialidades, chiches para programar como el “diseño” fue pensando.

  • Los componentes son innecesariamente complejos con centenares de líneas de código, difíciles de identificar y entender su utilidad o manera de usar, con funciones y clases no usadas, y, en absoluto, código inútil.

  • Recurrimos al truco más recurrido: copiar y pegar. Las repeticiones de código nos llevan a un diseño pobre donde no hemos podido identificar los contextos comunes.

  • El código se vuelve opaco, complicado de leer, haciendo que queramos leerlos menos inclusive.

Un equipo maduro no empieza algo nuevo hasta no haber acabado el actual —manteniendo una o dos tareas simultáneamente. Mantener un ritmo de desarrollo ayuda a tratar la deuda técnica. También, podemos recurrir a algunas prácticas de XP como son TDD, y pair programming. Reescribir parte de la aplicación, o peor, toda la aplicación, no es una solución y es poco profesional así como permitir que la deuda técnica se acumule. Desde el punto de vista del desarrollador, estimar considerando el impacto, componentes invlucrados, refactoring, unit tests, entre otros, es lo deseable; no se necesita consultarlo con nadie de la misma manera que un médico no consulta con nosotros si debe o prescribir un medicamento. Simplemente lo hace y queda a responsabilidad del paciente tomarlo. Por otro lado, están los gestores que “tienen” que entregar más. Pero esto se maneja más con habilidades comunicativas y cuya responsabilidad escapa del equipo como un todo.

Anuncios

Validar

Implementar una función valid para que lance un error si se llama sin argumentos o un argumento no está definido. De lo contrario debe devolver el valor dado.

Remover una propiedad

Escribir una función removeProperty que recibe un objeto y el nombre de una propiedad realizando lo siguiente: si el objeto obj tiene una propiedad prop, la función elimina la propiedad del objeto y devuelve true; en los demás casos, devuelve false.

function removeProperty(obj, prop) {
  return null;
}

Fecha

Codear una función que convierta la fecha formateada como “M/D/AAAA” en un formato requerido “AAAAMMDD”. El parámetro “date” y el valor de retorno son string’s.

Por ejemplo, “31/12/2014” a “20141231”.

function format(date) {
  // M/D/YYYY => YYYYMMDD
}

console.log(format('12/31/2014'));

Galería de imágenes

El siguiente ejemplo es una galería de dos imágenes con botones de eliminación correspondientes:

class="image"> src="https://goo.gl/2oZU2S" alt="First"> class="remove">X
class="image"> src="https://goo.gl/tniGAc" alt="Second"> class="remove">X

Implementar una función registerClickHandler: cuando se hace clic en el botón de la clase remove, su elemento *

  • principal debe ser eliminado de la galería.

Por ejemplo, después de que la primera imagen haya sido eliminada de la galería anterior, su código HTML debería verse así:

class="image"> src="https://goo.gl/tniGAc" alt="Second"> class="remove">X
function registerClickHandler () {
  // implementar el clic para el botón 'remove'
}

Cerraduras

Corregir los errores en la función registerHandlers. Un alert debe mostrar el índice del en lugar de seguir el enlace.

Por ejemplo, en el documento de abajo, la alerta debe mostrar “2” cuando se hace clic en Google.


  My web search engines:
<a href="//www.yahoo.com">Yahoo!
<a href="//www.altavista.com">AltaVista
<a href="//www.google.com">Google
function registerHandlers() {
  var as = document.getElementsByTagName('a');
  for (var i = 0; i < as.length; i++) {
    as[i].onclick = function() {
      alert(i);
      return false;
    }
  }
}

Bucle

La función appendChildren debería agregar un nuevo div a cada div existente.

Por ejemplo, después de ejecutar appendChildren en

id="a">
id="b">

debería tomar la siguiente forma:

id="a">
id="b">

Por alguna razón el siguiente código tiene un bucle infinito:

function appendChildren() {
  var allDivs = document.getElementsByTagName('div');
  for (var i = 0; i < allDivs.length; i++) {
    var newDiv = document.createElement('div');
    allDivs[i].appendChild(newDiv);
  }
}

Usar el objeto $rootScope es como usar variables globales en Javascript, excepto que este vive en el sistema de inyección de dependencias de AngularJS. Aunque esto sólo lo hace un tanto menos problemático: hace que el estado de un programa sea impredecible.

Imaginemos que tener un par de objetos que utilizan la misma variable global. Suponiendo incluso que no se usa ninguna función aleatoria en los módulos, entonces, el resultado de cualquier método puede predecirse ,y por lo tanto probarse, cuando se conoce el estado desde el cual se ejecuta.

Sin embargo, si un método de alguno de los objetos desencadena un efecto secundario que cambia el valor del estado global, no se sabrá cuál es el estado inicial cuando se ejecuta un método en el otro objeto. Ahora ya no se puede predecir el resultado del método y, por lo tanto, no puede probarse.

Esto no puede parecer tan serio, pero ser incapaz de hace una prueba unitaria dificulta probar la corrección de los objetos. Supongamos que tenemos una clase que asigna valores a una estructura de datos global y otra clase que consume los datos de esa estructura cambiando su estado o destruyéndola en el proceso. Si la clase que procesa ejecuta un método previo a que se complete la asignación, el resultado es que este procesador probablemente tendrá datos incompletos que procesar y la estructura de datos en la que estaba trabajando el asignador podría estar dañada o destruida. El comportamiento es impredecible y dará lugar a pérdida de información.

Además, los estados globales dificultan la legibilidad del código. Hay una dependencia externa que no se introduce explícitamente en el código. Así, cualquier responsable de mantener el código tendrá que averiguar de dónde vino determinado valor.

En cuanto a alternativas, es imposible no tener ningún estado global en absoluto, pero es posible restringirlos a un sólo objeto que engloba a todos los demás y que nunca debe referenciarse en base a las reglas de alcance del lenguaje. Si un objeto en particular necesita un estado particular, debería pedirlo explícitamente haciéndolo para como un argumento a su constructor o por un método setter. Esto se conoce como inyección de dependencia.

Puede parecer tonto pasar un estado al que ya se puede acceder mediante as reglas de alcance del lenguaje, pero tiene sus ventajas. Si alguien mira el código de forma aislada, queda claro qué necesita y de dónde viene. También tiene beneficios respecto a la flexibilidad del código mejorando las oportunidades de reuso. Si el estado se transmite y los cambios en el estado son locales al bloque de código, podemos pasar a cualquier estado que deseemos (con el tipo de dato correcto) y hacer que nuestro código lo procese. Con este estilo se tiende a tener la apariencia de una colección de componentes ligeramente asociados que pueden intercambiarse fácilmente. Al código de un módulo no debería importarle de dónde proviene el estado, sino cómo procesarlo. Si pasamos el estado a un bloque de código, este bloque puede existir de forma aislada, lo que no sucede si confiamos en la existencia de un estado global.

Resumen

  • Los errores del estado global mutable. Los errores puede ser causados por el cambio en cualquier lugar del programa que son difíciles de rastrear.

  • Pobreza de las pruebas. Cualquier prueba necesitará configurar el estado global. Esto complica escribir las pruebas, por ejemplo, las credenciales para acceder a una base de datos de toda la aplicación.

  • Poco flexible. Cuando una parte del código requiere determinado valor en el estado global, pero otra parte requiere otro valor. Se ve una desagradable refactorización.

  • Funciones impuras. Las funciones puras son aquellas cuyo resultado depende únicamente de sus argumentos, las mismas que son más sencillas de analizar y componer para crear grandes aplicaciones. Un estado global hace que sea impura.

  • Legibilidad. Un código con una cantidad considerable de variables globales se vuelve difícil de comprender haciendo incluso intratable su mantenimiento.

  • Problemas de concurrencia. El estado global requiere algún tipo de bloqueo, lo que es muy complicado y costoso de mantener.

  • Rendimiento. Si el mismo estado es usado globalmente por varios subprocesos, provoca la contención de caché y lentitud del proceso.

Alternativas

  • Parámetro de función. Tener una estructura de datos como contexto de la función. Nos ayuda a mantenerla pura y puede ser usado para pasar ese contexto a otras funciones que envuelvan toda la información relevante.

  • Inyección de dependencias. Como en el caso de los parámetros. Debemos tener cuidado si las dependencias son mutables (que pueden causar el mismo problema que las variables globales).

  • Estado global inmutable. Una constante que no debe ser mutable en ningún momento posterior.

  • Singletons inmutables. Similar al anterior, pero se les tiene que definir un momento de instanciación. Son útiles cuando grandes estructuras requieres un precálculo costoso. En su versión mutable son tan nocivos con el estado global mutable.

  • Enlazado dinámico. No se aplica a todos los lenguajes. Aisla las variables de determinado proceso haciendo que se actúe sólo sobre el hilo asignado. Es útil cuando se manejan múltiples subprocesos de transacciones independientes.

Leyendo un rato la lista de cambios de Angular me entero que hay un cambio en los proveedores de la plataforma y el compilador y que se ha reemplazado el inyector reflexivo por uno estático. No todos usamos el ReflectiveInjector directamente en nuestro código así que no parece claro en qué nos afectaría.

Las preguntas, después del salto desde AngularJS y Angular 2, son: ¿por qué es mejor? ¿Cómo nos afecta? ¿Necesitaremos migrar?

¿Por qué reflexivo?

Veamos el siguiente ejemplo

class Resource { }

class Reader {
  constructor(@Inject(Resource) resource) { }
}

const injector = ReflectiveInjector.resolveAndCreate([Reader, Reource]);
const reader = injector.get(Reader);

Tenemos dos servicios y el servicio Reader depende del servicio Resource. Cuando los proveedores van hacia resolveAndCreate no especificamos qué depende de qué. El decorador Inject usa la biblioteca Reflect para asociar metadata a la Reader. (Me gustaría que veamos reflexión y metaprogramación en JS.) 😉

Esencialmente, el decorador Inject hace lo siguiente

function Inject(cls: any, unusedKey: any, index: number) {
  ...  // `parameters` contiene a `{token: Resource}`
  Reflect.defineMetadata('parameters', parameters, cls);
  return cls;
}

Con esto mantenemos la información de las dependencias asociadas a una clase.

Cuando pasamos los proveedores a resolveAndCreate, este recorre cada uno de ellos y recolecta todas sus dependencias usando el mismo objeto Reflect.

function resolveAndCreate(providers) {
  ...
  providers.forEach(provider => resolverReflectiveFactory(provider));
  ...
}

function resolverReflectiveFactory(provider) {
  ...
  if (provider.useClass) {
    const cls = resolve(provider.useClass);
    ...
    resolveDepdenencies = dependenciesFor(cls);
  }
}

Con esto vemos que el inyector se basa en la capacidad de reflexión provista por Reflect (y el lenguaje) para extraer las dependencias y hacernos la programación de ello implícita.

¿Cómo difiere del inyector estático?

Este nuevo inyector es más rápido ya que de momento los navegadores y javascript no soporta reflexión de manera “nativa”. Por ejemplo, este almacena en un mapa todas nuestras clases.

const getOrCreateMetadataMap = (target, targetKey, create) {
  const targetMetadata = store.get(target);
  if (!targetMetadata) {
    if (!create) return;
    store.set(target, targetMetadata = new Map);
  }
}

Personalmente, supongo que con la llegada de los decoradores en ES7, Reflect ya no sería necesario, por lo tanto, sería removido en el futuro.

¿Cómo nos afecta?

El inyector reflexivo no se hace explícito cuando creamos módulos. No obstante, Angular crea otros tres inyectores para Platform, Compiler y NgZone, opr lo que el uso ReflectiveInjector los afectará.

Notemos que nosotros usamos una línea como la siguiente:

platformBrowserDynamic().bootstrapModule(AppModule);

La primer llamada toma los proveedores de la plataforma. La segunda inicia el compilador JIT y toma sus proveedores. Con esto quiero decir que podríamos pasar los proveedores al inyector de la plataforma mediante

class B { }
class A { constructor(@Inject(B) b) { } }

platformBrowserDynamic([A, B])

y, al compilador,

class D { }
class C { constructor(@Inject(D) d) { } }

bootstrapModule(AppModule, {providers: [C, D]});

Ya que la plataforma migrará al inyector estático, no podremos resolver de manera implícita las dependencias mediante la metadata. De manera que tendremos que adaptar nuestro código.

class B { }
class A { constructor(@Inject(B) b) { } }

platformBrowserDynamic([{provide: A, useClass: A, deps: [B]}, B]);

class D { }
class C { constructor(@Inject(D) d) { } }

bootstrapModule(AppModule, {
  providers: [
    { provide: A, useClass: A, deps: [B] },
    B
  ]
});

Para quienes se hayan enfrentado al problema de inyectar en inyectables, esto no resultará extraño. Pero no nos preocupes. Como dije, esto sólo afecta a inyector de la plataforma y del compilador. Como el inyector reflexivo está marcado como “deprecated”, es buena idea que vayamos migrando hacia el nuevo inyector estático, en tanto lo hayamos usado en nuestro código.

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.

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

Categorías