Quantcast
Channel: Adictos al trabajo
Viewing all 989 articles
Browse latest View live

Bases de Datos de Firebase con Android

$
0
0

Índice de contenidos

1. Introducción

En este tutorial explicaremos brevemente Firebase y veremos los dos tipos de bases de datos que nos proporciona: Realtime Database y Cloud Firestore. Además, aprenderemos cómo integrar Firebase con un proyecto de Android y un ejemplo básico para cada tipo de base de datos, los cuales estarán escritos en Kotlin.

2. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 17’ (2,66 GHz Intel Core i7, 8GB DDR3)
  • Sistema operativo: macOS Sierra 10.13.6
  • Entorno de desarrollo: Android Studio 3.3
  • Versión SDK mínima: 16

3. ¿Qué es Firebase?

Firebase es una plataforma SaS (Software as Service) proporcionada por Google que ofrece varias funcionalidades:

  • Características para desarrollar aplicaciones como:
    • Almacenamiento en la nube
    • Servicios de autenticación, incluyendo registro con email o utilizando una cuenta de Google, Facebook, Github o Twitter.
    • Bases de Datos que es lo que cubriremos en este tutorial.
    • Reporte de errores.
  • Firebase Analytics: servicios de analítica del comportamiento de los usuarios de la aplicación
  • Recursos para el crecimiento
  • AdMob: herramienta para monetizar la aplicación.

Sus herramientas son fáciles de usar, por lo que simplifica el desarrollo de aplicaciones móviles o web y tiene tanto planes de pago como uno gratuito que sirve para desarrollar proyectos pequeños.

4. Bases de Datos en Firebase

En Firebase tenemos dos tipos de bases de datos: Realtime Database y Cloud Firestore. En la mayoría de los casos se recomienda usar la segunda, que además es más reciente, pero vamos a ver ambos casos ya que es posible que para un proyecto en concreto se prefiera la otra. Además, existe la posibilidad de utilizar ambas en un único proyecto de ser necesario.

Para empezar hay que destacar que las dos son bases de datos no relacionales (y, por tanto, NoSQL), pero su estructura es diferente. También las dos siguen el principio de mobile-first y admiten almacenamiento local de datos para Android e iOS. Además, Cloud Firestore admite también almacenamiento local para aplicaciones web.

4.1. Realtime Database

Vamos a describir las principales características de Realtime Database:

  • Los datos se almacenan en un único árbol de tipo JSON. Esto hace complicado organizar estructuras de datos complejas.
  • Se pueden definir reglas para la base de datos para otorgar permisos de lectura/escritura.
  • Las consultas son profundas, es decir, devuelven todo el sub-árbol de los nodos seleccionados. Esto implica permisos en cascada, por lo que hay que evitar anidar datos todo lo posible.
  • Permite filtrar u ordenar en cada consulta ejecutada (sólo una de las dos opciones).
  • Se permite la definición de reglas de validación para guardar la coherencia de los datos. Estas reglas no se aplican en cascada.
  • Son una solución regional, es decir, los datos se guardan en un único servidor regional. Esto conlleva una menor latencia, pero una disponibilidad más limitada.
  • Si la base de datos requiere muchas conexiones (más de cien mil simultáneas o más de mil lecturas/escrituras por segundas) es necesario particionarla en varias bases de datos.
  • Para opciones de pago, su coste se basa en el almacenamiento y el ancho de banda usado.

 

4.2. Cloud Firestore

Las principales características de Cloud Firestore son las siguientes:

  • Se trata de una base de datos basada en documentos similares a JSON.
    • Permite estructuras más complejas con sub-colecciones.
    • Los documentos se pueden acceder por referencia, por lo que al borrar uno de ellos sus sub-colecciones pueden seguir siendo accesibles.
  • Permite filtrar y ordenar en las consultas, así como devolver una sub-colección en un documento en vez de todo el sub-árbol.
  • El rendimiento se basa en el tamaño del resultado, no del total de datos.
  • Las transacciones se repiten hasta completarse.
  • Es una solución multiregional, es decir, los datos se almacenan en varios centros de datos.
    • La idea es garantizar que, aunque un centro se desconecte, los datos sigan accesibles.
    • Se garantiza la consistencia entre los datos de todos los centros.
  • La validación de datos es automática.
  • Se pueden realizar consultas más complejas sobre muchos datos.
  • Se permite establecer reglas para otorgar permisos sin que se otorguen en cascada por defecto.
  • Escala automáticamente con un límite de un millón de conexiones simultáneas o diez mil lecturas/escrituras, aunque hay planes de ampliar este límite.
  • Para opciones de pago, el coste se basa principalmente en las operaciones realizadas (lectura, escritura y borrado) y en menor medida en el ancho de banda y el almacenamiento usado.
    • Con este sistema, un gran número de pequeñas operaciones puede resultar más caro con Cloud Firebase.
    • Se puede establecer un límite de coste diario.

5. Crear un proyecto de Firebase

Antes de iniciar nuestros ejemplos, vamos a ver cómo crear un proyecto de Firebase y añadirlo a nuestro proyecto de Android. Android Studio permite hacerlo de dos maneras, por lo que vamos a verlas.

5.1. Usando el asistente de Firebase en Android Studio

Para usar el asistente de Firebase en Android Studio seleccionamos Tools > Firebase. Después de eso, nos aparecerá una lista en la cual buscamos el servicio que vayamos a usar (por ejemplo Realtime Database) y hacemos click en el link que aparece. Se nos abrirá una nueva pantalla en la cual presionaremos el botón “Connect to Firebase” que nos redirigirá a la página de Firebase, solicitándonos los permisos necesarios para poder realizar este procedimiento. Asistente de Firebase en Tools

Lista del asistente de Firebase

Una vez tenga permisos, en Android Studio nos debe aparecer una pantalla para crear un nuevo proyecto de Firebase o para escoger uno ya existente. Por último, tan sólo tenemos que añadir las dependencias con el botón del segundo paso, con lo que se modificará nuestra configuración de Gradle y se sincronizará nuestro proyecto.

Realtime Database en el asistente de Firebase

Sin embargo, lo más probable es que nos falte añadir la dependencia básica de Firebase, por lo que, en ese caso, tendremos que añadirla nosotros al fichero de configuración de Gradle (Module app).

dependencies {
    ...
    implementation 'com.google.firebase:firebase-core:16.0.7'
}

Al hacer esto es posible que tengamos problemas con las versiones, ya que Android Studio puede que indique una versión diferente a la del código que tienes justo encima. Si es así, es posible que tengas que cambiar el número de versión en la configuración de Gradle tanto a nivel de módulo como de proyecto. Para evitar complicaciones recomiendo utilizar la última versión disponible de las dependencias de Firebase, a no ser que quieras emplear alguna en concreto.

5.2. Añadir Firebase a Android manualmente

Para añadir Firebase a nuestro proyecto de Android manualmente lo primero que vamos a hacer es conectarnos a la consola de Firebase con una cuenta de Google, la cual puedes encontrar en este enlace. A continuación añade un nuevo proyecto, indicando un nombre y las ubicaciones de las analíticas y del servidor en el que se alojarán nuestros datos de Cloud Firestore si lo fuéramos a usar.

Página de inicio de la consola de Firebase

Nuevo proyecto de FirebaseUna vez ya tengamos nuestro proyecto de Firebase creado, vamos a pulsar el icono de Android para poder añadirlo a nuestra aplicación Android. A continuación nos saldrá un pequeño formulario en el que tenemos que indicar el nombre del paquete de nuestro proyecto Android. Una vez completado, podemos descargar el fichero google-services.json, el cual hay que mover a la carpeta app del proyecto.

Proyecto creado de Firebase

Añadir Firebase a un proyecto de Android

El último paso es añadir las dependencias de Firebase a Gradle. A nivel de proyecto hay que añadir una:

dependencies {
    ...
    classpath 'com.google.gms:google-services:4.2.0'
}

Y a nivel de módulo tenemos que añadir dos líneas: una es una dependencia y otra, que debe ir al final del fichero.

dependencies {
    ...
    implementation 'com.google.firebase:firebase-core:16.0.7'
}
...
apply plugin: 'com.google.gms.google-services'

Para terminar, tan sólo hay que sincronizar los cambios. Al hacer esto es posible que nos dé errores con otras librerías por su versión. Si es así, una forma de solucionarlo es declarar explícitamente la versión de las librerías conflictivas. Por ejemplo, en mi caso he incluido la siguiente dependencia dentro de la configuración a nivel de módulo:

implementation 'com.android.support:support-media-compat:28.0.0'

6. Ejemplo con Realtime Database

Como ejemplo sencillo para ver cómo podemos usar Realtime Database vamos a crear una aplicación que nos permita añadir notas de alumnos y listarlos en tiempo real.

6.1. Preparando las bases

Para comenzar, vamos a crear una base de datos en la consola de Firebase (la página web). Seleccionamos Database en el menú y pulsamos Create Database. Aunque se muestre como si estuviéramos creando Cloud Firestore realmente tendremos disponibles ambas opciones. Respecto a las reglas de seguridad escoge indistintamente el modo locked o test, ya que en este punto no nos interesa cuáles use Cloud Firestore.

Crear las bases de datos en la consola de Firebase

Una vez creada, deberemos seleccionar Realtime Database en el menú desplegable superior y acceder a la pestaña de Rules. Como no vamos a tratar la autenticación, vamos a otorgar permisos a cualquier conexión a la base de datos cambiando las reglas de write y read a true. Ten en cuenta que en un proyecto real esto supone un grave problema de seguridad. Por último, guardamos los cambios pulsando en Publish.

Reglas de Realtime Database

El siguiente paso es añadir la dependencia de Realtime Database a nuestro proyecto Android. Para ello vamos a Android Studio, abrimos el fichero de configuración de Gradle (Module: app) y sincronizamos el proyecto después de añadir la siguiente dependencia:

implementation 'com.google.firebase:firebase-database:16.0.6'

6.2. Usando Realtime Database en nuestro proyecto

Ahora vamos a crear una clase llamada Mark que representará las puntuaciones. Fíjate en que los atributos son públicos y están inicializados. Esto es para permitir la serialización y deserialización al tener acceso a tales atributos y a un constructor por defecto. También sobrescribirmos el método toString para mostrar las puntuaciones.

data class Mark(val name: String = "", val subject: String = "", val mark: Double = 0.0) {

    override fun toString() = name + "\t" + subject + "\t" + mark
}

Ahora vamos a modificar el layout activity_main.xml que puedes encontrar en res>layout. En él vamos a incluir los campos de texto necesarios y un botón para añadir la nota. Debajo de todo esto vamos a contar con un TextView en el que escribir las puntuaciones de la base de datos. En una aplicación real sería recomendable no listarlo con un TextView, pero aquí lo vamos a usar por simplificar el tutorial.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/title_textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/save_button"
        android:textAlignment="center"
        android:textSize="40sp"
        android:text="Notas de Alumnos"
        app:layout_constraintTop_toTopOf="parent"/>

    <EditText
        android:id="@+id/name_editText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textPersonName"
        android:hint="Nombre"
        app:layout_constraintTop_toBottomOf="@id/title_textView"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <EditText
        android:id="@+id/subject_editText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textPersonName"
        android:hint="Asignatura"
        app:layout_constraintTop_toBottomOf="@id/name_editText"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <EditText
        android:id="@+id/mark_editText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="numberDecimal"
        android:hint="Nota"
        app:layout_constraintTop_toBottomOf="@id/subject_editText"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <Button
        android:id="@+id/save_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Guardar"
        app:layout_constraintTop_toBottomOf="@id/mark_editText"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>

    <TextView
        android:id="@+id/list_textView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintTop_toBottomOf="@id/save_button"
        android:textAlignment="center"/>

</android.support.constraint.ConstraintLayout>

Lo último que nos queda por modificar es el MainActivity. Vamos a añadir un listener al botón para recuperar los datos del formulario e insertarlos en la base de datos y otro listener a la base de datos para escuchar cuando se añadan nuevos registros. Como nuestra aplicación no va a permitir borrar o modificar registros con esto sería suficiente. Además, si te das cuenta, con este listener nos es suficiente para listar el contenido existente en la base de datos al iniciar la aplicación, sin necesidad de añadir código adicional.

A parte de lo mencionado, voy a centrar la atención en el método push() que utilizamos al insertar una nueva nota. Este método crea un registro nuevo con una clave única y es recomendable utilizarlo para evitar errores y mejorar el rendimiento.

class MainActivity : AppCompatActivity() {

     private val marksRef = FirebaseDatabase.getInstance().getReference("marks")

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        save_button.setOnClickListener { saveMarkFromForm() }

        marksRef.addChildEventListener(object : ChildEventListener {
            override fun onCancelled(databaseError: DatabaseError) {}
            override fun onChildMoved(dataSnapshot: DataSnapshot, previousName: String?) {}
            override fun onChildChanged(dataSnapshot: DataSnapshot, previousName: String?) {}
            override fun onChildRemoved(dataSnapshot: DataSnapshot) {}

            override fun onChildAdded(dataSnapshot: DataSnapshot, p1: String?) {
                val mark = dataSnapshot.getValue(Mark::class.java)
                if (mark != null) writeMark(mark)
            }
        })
    }

    private fun saveMarkFromForm() {
        val mark = Mark(
            name_editText.text.toString(),
            subject_editText.text.toString(),
            mark_editText.text.toString().toDouble()
        )
        marksRef.push().setValue(mark)
    }

    private fun writeMark(mark: Mark) {
        val text = list_textView.text.toString() + mark.toString() + "\n"
        list_textView.text = text
    }
}

Llegados a este punto, ya puedes iniciar la aplicación, añadir notas a alumnos y verificar en la consola que se añaden los registros.

Aplicación de ejemplo de Realtime DatabaseDatos insertados en la aplicación de ejemplo

7. Ejemplo con Cloud Firestore

Para el ejemplo de Cloud Firestore vamos a usar un ejemplo parecido al utilizado para Realtime Database. Vamos a crear una aplicación que nos permita añadir notas para alumnos, pero en vez de asignatura vamos a guardar el grupo al que están asignados.

Lo primero que vamos a hacer es añadir la dependencia necesaria a la configuración de Gradle a nivel de módulo y a sincronizar el proyecto.

implementation 'com.google.firebase:firebase-firestore:18.0.1'

A continuación, vamos a crear una clase Mark que represente la información a tratar. Nuevamente, los atributos los vamos a establecer como públicos e inicializados para proporcionar acceso a ellos y a un constructor por defecto que permita la serialización y deserialización.

class Mark(val name:String = "", val group:String = "", val mark:Double = 0.0){

    override fun toString()= "$name $group $mark"
}

El siguiente paso es modificar el layout main_activity.xml que se encuentra en res>layout para añadir los campos necesarios del formulario, el botón para guardarlo y un TextView que vamos a utilizar para listar las notas en la base de datos.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <EditText
            android:id="@+id/name_editText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:hint="Nombre"
            android:ems="10"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
    />

    <EditText
            android:id="@+id/group_editText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:hint="Grupo"
            android:ems="10"
            app:layout_constraintTop_toBottomOf="@id/name_editText"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
    />

    <EditText
            android:id="@+id/mark_editText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="numberDecimal"
            android:hint="Nota"
            android:ems="10"
            app:layout_constraintTop_toBottomOf="@id/group_editText"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
    />
    <Button
            android:id="@+id/save_button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/mark_editText"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:text="Guardar"
    />
    <TextView 
            android:id="@+id/markList_textView"
            android:layout_width="match_parent" 
            android:layout_height="wrap_content"
            app:layout_constraintTop_toBottomOf="@id/save_button"
            android:textAlignment="center"
    />
</android.support.constraint.ConstraintLayout>

Nuestro MainActivity debe añadir un listener al botón del layout para que se guarde la nota escrita al pulsarlo y otro a la colección de la base de datos para tratar cuando se añadan nuevos registros a la base de datos. Igual que pasaba con Realtime Database, este listener nos sirve también para añadir las notas ya existentes en la base de datos al iniciar la aplicación.

class MainActivity : AppCompatActivity() {

    private val marksCollection: CollectionReference

    init {
        FirebaseApp.initializeApp(this)
        marksCollection = FirebaseFirestore.getInstance().collection("marks")
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        save_button.setOnClickListener {
            saveMark(
                Mark(
                    name_editText.text.toString(),
                    group_editText.text.toString(),
                    mark_editText.text.toString().toDouble()
                )
            )
        }

        addMarksListener()
    }

    private fun saveMark(mark: Mark) {
        marksCollection.add(mark).addOnSuccessListener {
            Toast.makeText(this, "Regsistro guardado", Toast.LENGTH_SHORT).show()
        }.addOnFailureListener {
            Toast.makeText(this, "Error guardando el registro", Toast.LENGTH_SHORT).show()
        }
    }

    private fun addMarksListener() {
        marksCollection.addSnapshotListener { snapshots, error ->
            if (error == null) {
                val changes = snapshots?.documentChanges
                if (changes != null) {
                    addChanges(changes)
                }
            } else {
                Toast.makeText(this, "Ha ocurrido un error leyendo las notas", Toast.LENGTH_SHORT).show()
            }
        }
    }

    private fun addChanges(changes: List<DocumentChange>) {
        for (change in changes) {
            if (change.type == DocumentChange.Type.ADDED) {
                addToList(change.document.toObject(Mark::class.java))
            }
        }
    }

    private fun addToList(mark: Mark) {
        var text = markList_textView.text.toString()
        text += mark.toString() + "\n"
        markList_textView.text = text
    }
}

Una vez tenemos el MainActivity, ya podemos ejecutar nuestra aplicación y ver cómo se añaden los registros que insertemos.

Si a la hora de ejecutar la aplicación te encuentras problemas y has usado la integración automática de Firebase con Android Studio, es posible que se deba a la posición de la línea apply plugin: ‘com.google.gms.google-services’ en la configuración de Gradle. Por defecto Android Studio la coloca en la parte superior del fichero y ésta debe aparecer al final del mismo.

Aplicación de ejemplo de Cloud Firestore

¡Muchas gracias por haber leído hasta aquí!

8. Referencias

La entrada Bases de Datos de Firebase con Android se publicó primero en Adictos al trabajo.


Haciendo (Vue)n Frontend

$
0
0

¿SOLID, testing, separación por capas y patrones de diseño en el frontend con Vue y TypeScript? ¿Estamos locos? No, se puede hacer y además te lo enseño en este tutorial.


Índice

Vue es un framework progresivo, versatil y que tiende a la optimización. Opta por la simplicidad en algunos aspectos y adopta cosas de React y Angular. En este post veremos consejos para que tu código con Vue sea impoluto y fácil de testear.

El ejemplo en vivo lo tienes aquí: https://codesandbox.io/s/rw7jm9ovzo y el código en Github: https://github.com/cesalberca/gravatar-viewer.


1. Usa TypeScript

TypeScript te va a ayudar mucho a detectar errores antes de que ejecutes el programa, vas a ser más productivo ya que tu IDE te va a ofrecer más ayudas y tendrás documentación acerca de las estructuras y modelos que usas en tu aplicación.

Vue da soporte a TypeScript, e incluso han anunciado con la versión 3 un rewrite de Vue en TypeScript. Hasta entonces el soporte que dan en ficheros .vue deja un poco que desear, ya que dependes de extensiones como Vetur o que tu IDE de un buen soporte.

Por las razones antes mencionadas, y dado que han anunciado que en el nuevo API será orientada a clases y no a objetos yo recomiendo usar vue-class-component y vue-property-decorator. Con estas bibliotecas logramos que nuestro código sea más legible y más seguro en cuanto a tipos. Si usas Vuex te recomiendo que eches un ojo a vuex-class.

Nota: Estas bibliotecas están arropadas por Vue. Su creador es un miembro core del equipo de Vue que lleva toda la parte de TypeScript dentro de Vue.


2. Usa Inject/Provide

Inject / provide es una API de Vue para hacer las veces de un contenedor simple de IoC, donde podremos proveer de dependencias dentro de un árbol de componentes a cualquier nivel, lo que evita en cierta medida el denominado prop drilling.

Un mecanismo análogo en React sería el API de contexto.

Ahora veamos cómo lo usaríamos en una mini aplicación que hace peticiones al API de Libravatar para mostrar el avatar de un usuario buscando por su email.

Comenzamos imaginando que nuestro componente llama a un repositorio:

<template>
  <section class="viewer">
    <header>
      <h1>Gravatar Viewer</h1>
      <label for="email">Email</label>
      <input name="email" type="email" v-model="email" class="email" />
    </header>

    <main v-if="showUser">
      <h3>User</h3>
      <img :src="user.photo" alt="User image" />
    </main>
  </section>
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-property-decorator";
import { GravatarRepository } from "../domains/gravatar/repositories/GravatarRepository";
import { GravatarRepositoryFactory } from "../domains/gravatar/repositories/GravatarRepositoryFactory";
import { User } from "../domains/users/User";
import md5 from "md5";
import { debounce } from "../utils/debounce";

@Component
export default class UserComponent extends Vue {
  email: string = "";
  user: User = User.empty();
  gravatarRepository: GravatarRepository = GravatarRepositoryFactory.photo();
  debouncedQueryEmail!: () => void;

  created() {
    this.debouncedQueryEmail = debounce(this.queryEmail, 1000);
  }

  get showUser() {
    return this.user.exists();
  }

  @Watch("email")
  onEmailChange() {
    this.debouncedQueryEmail();
  }

  async queryEmail() {
    const hash = md5(this.email);
    const user = await this.gravatarRepository.getUserByEmailHash(hash);
    this.user = user;
  }
}
</script>
<style scoped>
.viewer {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.email {
  margin-left: 8px;
}
</style>

Aquí tendremos un problema muy grande si queremos mañana consumir un repositorio distinto. Tendremos que cambiar todos los componentes donde haya una concreción como la hay cuando se llama a la factoría GravatarRepositoryFactory.photo().

Además los tests de este componente van a ser insufribles, porque tendremos que de alguna forma mockear el import, ya que no queremos que haga peticiones de verdad en el test unitario ya que este pasaría de ser unitario a de integración.

Nota: La función debounce hace que un método no se ejecute hasta que pase un tiempo mínimo de 1000ms, lo que evita que hagamos un montón de peticiones al API.

Con inject podemos mejorar esta situación:

<template>
  <section class="viewer">
    <header>
      <h1>Gravatar Viewer</h1>
      <label for="email">Email</label>
      <input name="email" type="email" v-model="email" class="email" />
    </header>

    <main v-if="showUser">
      <h3>User</h3>
      <img :src="user.photo" alt="User image" />
    </main>
  </section>
</template>
<script lang="ts">
import { Component, Vue, Watch, Inject } from "vue-property-decorator";
import { GravatarRepository } from "../domains/gravatar/repositories/GravatarRepository";
import { User } from "../domains/users/User";
import md5 from "md5";
import { debounce } from "../utils/debounce";

@Component
export default class UserComponent extends Vue {
  email: string = "";
  user: User = User.empty();

  @Inject()
  gravatarRepository!: GravatarRepository;

  debouncedQueryEmail!: () => void;

  created() {
    this.debouncedQueryEmail = debounce(this.queryEmail, 1000);
  }

  get showUser() {
    return this.user.exists();
  }

  @Watch("email")
  onEmailChange() {
    this.debouncedQueryEmail();
  }

  async queryEmail() {
    const hash = md5(this.email);
    const user = await this.gravatarRepository.getUserByEmailHash(hash);
    this.user = user;
  }
}
</script>
<style scoped>
.viewer {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.email {
  margin-left: 8px;
}
</style>

El cambio es muy sutil, pero muy efectivo. Ahora nos falta proveer de la implementación de GravatarRepository (que adelanto que es una interfaz). Para ello nos podemos crear un componente ProviderFactory que sea el que se encarga de gestionar la creación de instancias y el que las inyecta:

<template>
  <div><slot /></div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import { GravatarRepositoryFactory } from "./../domains/gravatar/repositories/GravatarRepositoryFactory";
import { GravatarRepository } from "./../domains/gravatar/repositories/GravatarRepository";
import { debounce } from "./../utils/debounce";
import { hasher } from "./../utils/hasher";

@Component
export default class ProviderFactory extends Vue {
  @Provide()
  gravatarRepository: GravatarRepository = GravatarRepositoryFactory.photo();
}
</script>

Y ahora en el fichero App.vue haremos uso de él:

<template>
  <ProviderFactory> <AvatarViewerContainer /> </ProviderFactory>
</template>

<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import AvatarViewerContainer from "./components/AvatarViewerContainer.vue";
import ProviderFactory from "./components/ProviderFactory.vue";

@Component({
  components: {
    AvatarViewerContainer,
    ProviderFactory
  }
})
export default class App extends Vue {}
</script>

Si mañana tenemos un nuevo repositorio basado en local storage, solamente tendríamos que cambiarlo en el ProviderFactory. Incluso podríamos jugar a cambiar la implementación en caliente basándonos en cierta lógica.

El test además sería mucho más fácil, ya que podríamos decir que para el test se haga un provide de un mock.

¿Identificas además otra dependencia en el componente? Sí, el md5 y el debounce son una concreción, si queremos seguir la D de SOLID no deberíamos depender de concreciones si no de abstracciones.


3. Usa componentes y contenedores

Los componentes y contenedores no son más que el patrón de diseño Mediator. Donde los componentes serán los que se encargar de pintar, podrán tener lógica de pintado, pero muy poca. Reciben datos y los pintas, nada más. Los contenedores son aquellos que orquestan a los componentes y les pasan los datos. Suelen tener más lógica pero no suelen tener ni estilos visuales ni markup.

Los componentes se comunican con los contenedores mediante eventos, y los contenedores se comunican con los componentes mediante props. Esto hace que los tests sean mucho más fáciles, los componentes estén más desacoplados y si falla algo en cuanto a la orquestación, sabremos que tenemos que mirar el contenedor.

Como ejemplo vamos a refactorizar nuestro componente UserComponenteInject. Creamos primero el componente AvatarComponent

<template>
  <div v-if="showUser">
    <h3>User</h3>
    <img :src="user.photo" alt="User image" />
  </div>
</template>
<script lang="ts">
import { Prop, Component, Vue } from "vue-property-decorator";
import { User } from "../domains/users/User";

@Component
export default class AvatarComponent extends Vue {
  @Prop({ type: Object, default: () => User.empty() })
  user!: User;

  get showUser() {
    return this.user.exists();
  }
}
</script>

Ahora podemos crear el componente que se encarga del input del email. Creamos el componente UserForm:

<template>
  <header>
    <h1>Gravatar Viewer</h1>
    <label for="email">Email</label>
    <input name="email" type="email" @input="onEmailChange" class="email" />
  </header>
</template>
<script lang="ts">
import { Component, Vue, Emit } from "vue-property-decorator";

@Component
export default class UserFormComponent extends Vue {
  @Emit()
  onEmailChange(event: Event) {
    return (event.target as HTMLInputElement).value;
  }
}
</script>
<style scoped>
.email {
  margin-left: 8px;
}
</style>

Y por último creamos AvatarViewerContainer:

<template>
  <section class="viewer">
    <UserFormComponent @on-email-change="updateEmail" />
    <AvatarComponent :user="user" />
  </section>
</template>
<script lang="ts">
import { Component, Vue, Watch, Inject } from "vue-property-decorator";
import { GravatarRepository } from "../domains/gravatar/repositories/GravatarRepository";
import UserFormComponent from "./UserFormComponent.vue";
import AvatarComponent from "./AvatarComponent.vue";
import { User } from "../domains/users/User";
import { debounce } from "../utils/debounce";
import { hasher } from "../utils/hasher";

@Component({
  components: {
    AvatarComponent,
    UserFormComponent
  }
})
export default class AvatarViewerContainer extends Vue {
  email: string = "";
  user: User = User.empty();

  @Inject()
  gravatarRepository!: GravatarRepository;

  @Inject()
  debounce!: typeof debounce;

  @Inject()
  hasher!: typeof hasher;

  debouncedQueryEmail!: () => void;

  created() {
    this.debouncedQueryEmail = this.debounce(this.queryEmail, 1000);
  }

  @Watch("email")
  onEmailChange() {
    this.debouncedQueryEmail();
  }

  updateEmail(email: string) {
    this.email = email;
  }

  async queryEmail() {
    const hash = this.hasher(this.email);
    const user = await this.gravatarRepository.getUserByEmailHash(hash);
    this.user = user;
  }
}
</script>
<style scoped>
.viewer {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>

También modificamos el ProviderFactory para inyectar el debounce y el hasher:

<template>
  <div><slot /></div>
</template>
<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import { GravatarRepositoryFactory } from "./../domains/gravatar/repositories/GravatarRepositoryFactory";
import { GravatarRepository } from "./../domains/gravatar/repositories/GravatarRepository";
import { debounce } from "./../utils/debounce";
import { hasher } from "./../utils/hasher";

@Component
export default class ProviderFactory extends Vue {
  @Provide()
  gravatarRepository: GravatarRepository = GravatarRepositoryFactory.photo();

  @Provide()
  debounce = debounce;

  @Provide()
  hasher = hasher;
}
</script>


4. Haz testing

Como puedes ver en el proyecto de ejemplo, todos los componentes y piezas están testadas. Al seguir SOLID y buenas prácticas el añadir test unitarios de todo lo necesario es trivial.

Por ejemplo veamos el AvatarViewerContainer.spec.ts:

import Vue from "vue";
import AvatarViewerContainer from "./../AvatarViewerContainer.vue";
import { Wrapper, shallowMount } from "@vue/test-utils";
import { User } from "../../domains/users/User";
import { flushPromises } from "../../utils/flushPromises";
import { hasher } from "../../utils/hasher";
import { debounce } from "../../utils/debounce";
import { GravatarRepository } from "../../domains/gravatar/repositories/GravatarRepository";

describe("AvatarViewerContainer", () => {
  let wrapper: Wrapper<Vue>;
  let gravatarRepositoryMock: GravatarRepository;
  let debounceMock: typeof debounce;
  let hasherMock: typeof hasher;

  beforeEach(() => {
    hasherMock = jest.fn();
    debounceMock = jest.fn((func: Function) => () => func());
    gravatarRepositoryMock = {
      getUserByEmailHash: jest
        .fn()
        .mockReturnValue(Promise.resolve(new User("foo")))
    };

    wrapper = shallowMount(AvatarViewerContainer, {
      provide: {
        gravatarRepository: gravatarRepositoryMock,
        debounce: debounceMock,
        hasher: hasherMock
      }
    });
  });

  it("should call gravatarRepository when email changes", async () => {
    const userFormComponent = wrapper.find({ name: "UserFormComponent" });

    userFormComponent.vm.$emit("on-email-change", "foo@foo.com");
    await flushPromises();

    expect(gravatarRepositoryMock.getUserByEmailHash).toHaveBeenCalled();
  });

  it("should debounce call when email changes", async () => {
    const userFormComponent = wrapper.find({ name: "UserFormComponent" });

    userFormComponent.vm.$emit("on-email-change", "foo@foo.com");
    await flushPromises();

    expect(debounceMock).toHaveBeenCalled();
  });

  it("should set user to AvatarComponent", async () => {
    const userFormComponent = wrapper.find({ name: "UserFormComponent" });
    const avatarComponent = wrapper.find({ name: "AvatarComponent" });

    userFormComponent.vm.$emit("on-email-change", "foo@foo.com");
    await flushPromises();

    expect(avatarComponent.props("user").photo).toBe("foo");
  });

  it("should use the hasher", async () => {
    expect.assertions(1);

    const userFormComponent = wrapper.find({ name: "UserFormComponent" });

    userFormComponent.vm.$emit("on-email-change", "foo@foo.com");
    await flushPromises();

    expect(hasherMock).toHaveBeenCalledWith("foo@foo.com");
  });
});

O el componente AvatarComponent.spec.ts:

import Vue from "vue";
import AvatarComponent from "../AvatarComponent.vue";
import { Wrapper, shallowMount } from "@vue/test-utils";
import { User } from "../../domains/users/User";

describe("AvatarComponent", () => {
  let wrapper: Wrapper<Vue>;

  beforeEach(() => {
    wrapper = shallowMount(AvatarComponent);
  });

  it("should hide if the user doesn't exist", () => {
    wrapper.setProps({ user: User.empty() });

    expect(wrapper.html()).toBeUndefined();
  });

  it("should show if the user exists", () => {
    wrapper.setProps({ user: new User("foo") });

    expect(wrapper.html()).toBeDefined();
  });

  it("should set the image url with the user's photo", () => {
    wrapper.setProps({ user: new User("foo") });

    const image = wrapper.find("img");
    expect(image.attributes("src")).toEqual("foo");
  });
});

Y el UserFormComponent:

import Vue from "vue";
import UserFormComponent from "./../UserFormComponent.vue";
import { shallowMount, Wrapper } from "@vue/test-utils";

describe("UserFormComponent", () => {
  let wrapper: Wrapper<Vue>;

  beforeEach(() => {
    wrapper = shallowMount(UserFormComponent);
  });

  it("should emit event", () => {
    const input = wrapper.find("input");
    (input.element as HTMLInputElement).value = "foo";
    input.trigger("input");
    expect(wrapper.emitted("on-email-change")[0][0]).toEqual("foo");
  });
});

No hacer testing nunca está justificado.


5. Mueve lógica de negocio fuera de los componentes

Como hemos visto antes en el contenedor AvatarViewerContainer había un GravatarRepository. Básicamente este repositorio luego por debajo se conecta con un API y te devuelve la imagen del usuario. Podríamos hacer la llamada en el contenedor directamente, pero estaríamos acoplando nuestra obtención de datos con un framework (Vue) y romperíamos SOLID, ya que el motivo de cambio del contenedor es que la interacción cambie o que el API cambie.

Veamos más detenidamente GravatarRepository:

import { User } from "../../users/User";

export interface GravatarRepository {
  getUserByEmailHash(hash: string): Promise<User>;
}

Pues resulta que es una interfaz. ¿Por qué? Porque esto nos permite definir varios repositorios para acceder a los datos. Podríamos tener una implementación de esta interfaz en GravatarHttpRepository, GravatarLocalStorageRepository o GravatarBlobRepository como es nuestro caso. Además podríamos cambiar la implementación tanto en “compilación” como en caliente.

Aquí está GravatarBlobRepository:

import { GravatarRepository } from "./GravatarRepository";
import { User } from "../../users/User";
import { Fetcher } from "../../Fetcher";

export class GravatarBlobRepository implements GravatarRepository {
  private url: string;

  constructor(private readonly fetcher: Fetcher) {
    this.url =
      "https://cors-anywhere.herokuapp.com/https://seccdn.libravatar.org/avatar";
  }

  async getUserByEmailHash(hash: string): Promise<User> {
    const response = await this.fetcher(`${this.url}/${hash}`);
    const result = await response.blob();
    const object = URL.createObjectURL(result);
    return new User(object);
  }
}

Por constructor le hemos pasado un fetcher, este es el encargado de recoger los datos de una API y tiene el siguiente tipo:

export type Fetcher = <Response = any>(
  query: string,
  options?: {
    mode: string;
  }
) => Promise<{ json: () => Promise<Response>; blob: () => Promise<Response> }>;

A aquellos que les suene verán que es igualito que el API de fetch, que es el que usaremos luego en verdad.

Y por último, ¿dónde creamos la instancia de GravatarBlobRepository? Pues con una factoría.

GravataRepositoryFactory:

import { GravatarRepository } from "./GravatarRepository";
import { GravatarBlobRepository } from "./GravatarBlobRepository";
import { Fetcher } from "../../Fetcher";

export class GravatarRepositoryFactory {
  static photo(): GravatarRepository {
    const fetcher = window.fetch.bind(window) as Fetcher;
    return new GravatarBlobRepository(fetcher);
  }
}

Aquí vemos que hemos pasado de una abstracción Fetcher a una concreción: window.fetch, siendo en un futuro configurable e intercambiable por otra solución.

Por último hemos modelado nuestro usuario con una clase. Aquí tenemos el modelo del User:

export class User {
  constructor(private readonly photo: string) {}

  static empty() {
    return new User("");
  }

  exists(): boolean {
    return this.photo.length !== 0;
  }
}

Es importante evitar que nuestros modelos sean interfaces sin comportamiento o clases con setters o propiedades públicas, ya que nos pueden llevar a modelos anémicos, es decir modelos que son una bolsa de propiedades, siendo imposible determinar cual es su estado válido, delegando en el consumidor la lógica de validez, lo que haría a su vez que duplicásemos esa lógica.


Conclusión

El front no es fácil. Venimos de un mundo dónde a lo más que podíamos aspirar es a maquetar, aplicar estilos y usar algo de JavaScript para lograr animaciones e interactividad con la página. Todo esto ha cambiado, ahora debemos gestionar un montón de estado, asincronía, optimización de peticiones y caché, diseño responsive, reactividad y un montón más de cosas.

¿Por qué nos privamos de usar las herramientas y mecanismos que se llevan usando en la programación orientada a objetos desde hace más de 20 años que se han visto que funcionan?

Sígueme en Twitter y en Github.

La entrada Haciendo (Vue)n Frontend se publicó primero en Adictos al trabajo.

Resiliency Testing con Toxiproxy

$
0
0

Índice de contenidos

  1. Introducción
  2. Entorno
  3. Resiliencia y patrones de resiliencia
    3.1 ¿Qué es resiliencia?
    3.2 Patrones de resiliencia
  4. Ejemplo
    4.1 Infraestructura
    4.2 Microservicio de transacciones
    4.3 Microservicio de usuarios
    4.4 Implementación de los patrones de resiliciencia
    4.4.1 Circuit Breaker
    4.4.2 Fallback
    4.4.3 Compensating transaction
    4.5 Test de resiliencia
  5. Conclusiones
  6. Referencias

1. Introducción

En este tutorial vamos a ver cómo podemos testear la resiliencia de nuestra aplicación usando Toxiproxy.
En el ejemplo usaremos Micronaut pero se podría haber usado cualquier framework que dé soporte a resiliencia como Spring Cloud o Eclipse Microprofile. Puede verse un ejemplo de qué es o cómo se usa Micronaut aquí.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro 15′ (2,3 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: Mac OS Mojave 10.14.1
  • IntelliJ IDEA 2018.3.4
  • Docker version 18.09.1
  • Docker-compose version 1.23.2, build 1110ad01
  • Java version OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.2+7, mixed mode)
  • Micronaut 1.0.4

3. Resiliencia y patrones de resiliencia

3.1 ¿Qué es resiliencia?

Si buscamos la definición de la RAE para el término resiliencia encontramos lo siguiente:

Capacidad de adaptación de un ser vivo frente a un agente perturbador o un estado o situación adversos.

Extrapolando esta definición al software, la resiliencia es la capacidad que tiene nuestro sistema de recuperarse ante diferentes fallos.

3.2 Patrones de resiliencia

Para poder lidiar con estos fallos (latencia y caídas del sistema, entre otros) existen varios patrones que pueden ayudarnos. Aunque la lista es larga (aquí hay algunos) para el ejemplo nos vamos a centrar en los siguientes:

  • Circuit Breaker: Previene contra continuas llamadas a un servicio que está fallando o que tiene problemas de rendimiento.
  • Fallback: Proporciona un mecanismo a través del cual ofrecer una alternativa ante un servicio que está fallando.
  • Compensating Transaction: Se encarga de deshacer una operación previa para poder dejar el sistema en un estado consistente.

4. Ejemplo

Como ejemplo se expone el caso de una operativa común que podemos encontrar en nuestro día a día:

  • Solicitar información de otro dominio
  • Publicar información hacia otro dominio cambiando su estado
  • Guardar información en el sistema de persistencia local

4.1 Infraestructura

Contaremos con dos microservicios uno que actuará como cliente (el SUT) que será el microservicio de usuarios y otro que actuará como sistema de terceros (el DoC) siendo éste el microservicio de transacciones y estando alojado dentro de un contenedor Docker. Además levantaremos una base de datos Postgres que usaremos para persistir datos, también como un contenedor Docker. El SUT no accederá directamente a ningún recurso sino que lo hará a través de un proxy, concretamente a través de un servidor de Toxiproxy también levantado como un contenedor docker.

El fichero docker-compose.yml define los servicios así:

version: "3"
services:
  toxiproxy:
    image: shopify/toxiproxy
    ports:
      - 8474:8474
      - 9090:9090
      - 5432:5432
  db:
      image: postgres:9.5
      environment:
        - POSTGRES_DB=ms1-db
        - POSTGRES_USER=postgres
        - POSTGRES_PASSWORD=postgres
  ms2:
    build: .

El servicio toxiproxy levantará los siguientes puertos:

  • 8474: Necesario para poder conectar el cliente de Toxiproxy con el servidor
  • 9090: Puerto por el que escucha el microservicio de transacciones
  • 5432: Puerto por el que escucha la base de datos postgres

 

Destacar que el servicio db y ms2 exponen los puertos 5432 y 9090 solo de forma interna, siendo el servicio toxiproxy el que se encarga de exponer estos puertos externamente.

4.2 Microservicio de transacciones

El microservicio de transacciones contiene varios endpoints a través de los cuales poder realizar operaciones:

  • GET /transactions/{userId}
  • POST /transactions/{userId}
  • DELETE /transactions/{userId}/{txId}

4.3 Microservicio de usuarios

Añadir la dependencia del cliente de Toxiproxy en el proyecto:

<dependency>
    <groupId>eu.rekawek.toxiproxy</groupId>
    <artifactId>toxiproxy-java</artifactId>
    <version>2.1.3</version>
</dependency>

El código del microservicio de usuarios contiene las acciones necesarias para interacturar con el microservicio de transacciones. La primera acción será poder comunicarse con él para poder recuperar las transacciones por usuario.

public List<Transaction> getTransactions(UUID someUserId) {
    LOG.info("getting new transactions...");
    // 1º point of failure
    return transactionsClient.getNewTransactions(someUserId); 
}

La segunda acción será poder crear nuevas transacciones para un usuario, comunicándolo al microservicio dependiente y persistiendo los datos en una base de datos. Operativa común.

public void createUserTransaction(UUID someUserId, String concept) {
    // 2º point of failure
    final var newTx = transactionsClient.createTransaction(someUserId, new ConceptRequest(concept));
    LOG.info("transaction created = {}, updating user transactions...", newTx);
    final var user = new User(someUserId, newTx);
    compensatingTransaction(
            // 3º point of failure
            () -> userRepository.save(user), 
            () -> transactionsClient.removeTransaction(someUserId, newTx.getId())
    );
}

4.4 Implementación de los patrones de resiliciencia

 4.4.1 Circuit Breaker

Micronaut se integra con las librerías de Hystrix para implementar este patrón habilitando el uso de la anotación @HystrixCommand.

@HystrixCommand
@Client("http://localhost:9090/transactions")
public interface TransactionClient {

    @Get("/{userId}")
    List getNewTransactions(@PathVariable UUID userId);

    @Post("/{userId}")
    Transaction createTransaction(@PathVariable UUID userId, @Body ConceptRequest conceptRequest);

    @Delete("/{userId}/{txId}")
    void removeTransaction(@PathVariable UUID userId, @PathVariable UUID txId);

}

4.4.2 Fallback

Micronaut nos permite implementar este patrón creando una implementación para la interfaz del cliente y se integra con Hystrix para invocar a estos métodos cuando el circuito se abre.

@Fallback
public class TransactionClientFallback implements TransactionClient {

    private static final Logger LOG = LoggerFactory.getLogger(SomeService.class);

    @Override
    public List getNewTransactions(UUID userId) {
        LOG.warn("executing fallback method when getting new transactions");
        return Collections.emptyList();
    }

    @Override
    public Transaction createTransaction(UUID userId, ConceptRequest concept) {
        LOG.warn("executing fallback method when create transaction");
        return Transaction.errorTransaction();
    }

    @Override
    public void removeTransaction(UUID userId, UUID txId) {
        throw new IllegalStateException("compensation transaction has not been performed. Error occurred.");
    }

}

4.4.3 Compensating transaction

La implementación de este patrón es manual ya que dependerá de cómo queramos compensar cada acción de forma específica:

private static void compensatingTransaction(Runnable action, Runnable compensating) {
     try {
         action.run();
     } catch (Exception e) {
         LOG.warn("Error occurred. Compensating transaction...");
         try { compensating.run(); } catch (Exception ignore) { }
     }
}

4.5 Test de resiliencia

Los test se ejecutan con JUnit y se integra con Micronaut para levantar el contexto y ejecutar los test contra la aplicación real. Se crea el cliente de Toxiproxy y se crean los proxies en runtime ya que cuando levantamos el contenedor el servidor no tiene ningún proxy creado y nuestros servicios estarán totalmente aislados. Los proxies se pueden crear a través del cliente de Java o, manualmente, haciendo uso del api REST de Toxiproxy:

@Before
public void init() {
    // initialize toxiproxy
    this.toxiproxyClient = new ToxiproxyClient("localhost", 8474);
    this.thirdPartyProxy = initProxy(toxiproxyClient, "ms2-proxy", "toxiproxy:9090", "ms2:9090");
    this.postgresProxy = initProxy(toxiproxyClient, "postgres-proxy", "toxiproxy:5432", "db:5432");
}

Lo más importante aquí es tener en cuenta que el servidor de Toxiproxy se está ejecutando dentro de docker por lo que no podemos acceder a los servicios como localhost, sino que tendremos que referirnos a ellos con el nombre de servicio que hayamos indicado en el fichero docker-compose.yml.

Para poder probar cómo se comporta la aplicación ante distintos fallos de red vamos a realizar las siguientes pruebas.

Simular caída de red al intentar comunicarnos con el microservicio dependiente:

@Test
public void shouldOpenCircuitOpenedOnThirdPartyWhenNetworkFailure() throws IOException {
    // GIVEN
    thirdPartyProxy.delete(); // simulate network failure
    // WHEN
    List transactions = someService.getTransactions(userId);
    // THEN
    assertEquals(Collections.emptyList(), transactions);
}

Vemos cómo abre el circuito:

...
08:22:01.090 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:22:02.096 [HystrixTimer-1] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
...

Simular alta latencia de red al intentar comunicarnos con el servicio dependiente:

@Test
public void shouldOpenCircuitOnThirdPartyWhenNetworkHighLatency() throws IOException {
    // GIVEN
    thirdPartyProxy.toxics().latency("high-latency", ToxicDirection.DOWNSTREAM, 10_000);
    // WHEN
    final var transactions = someService.getTransactions(userId);
    // THEN
    assertEquals(Collections.emptyList(), transactions);
}

Vemos cómo abre el circuito también ante latencia:

...
08:22:01.090 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:22:02.096 [HystrixTimer-1] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
...

Simular caída de red contra la base de datos:

@Test
public void shouldCompensateTransactionWhenDatabaseNetworkFail() throws IOException {
    // GIVEN
    postgresProxy.delete();
    // WHEN
    someService.createUserTransaction(userId, "new tx concept");
    // THEN
    final var transactions = transactionsClient.getNewTransactions(userId);
    assertEquals(0, transactions.size());
}

 

08:21:53.589 [main] WARN  com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@32f14274 (This connection has been closed.)
08:21:53.590 [main] WARN  com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@7af56b26 (This connection has been closed.)
08:21:53.591 [main] WARN  com.zaxxer.hikari.pool.PoolBase - HikariPool-1 - Failed to validate connection org.postgresql.jdbc.PgConnection@c86c486 (This connection has been closed.)
...
08:21:58.586 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - HikariPool-1 - Connection is not available, request timed out after 5007ms.
08:21:58.586 [main] ERROR o.h.e.jdbc.spi.SqlExceptionHelper - El intento de conexión falló.
08:21:58.589 [main] WARN  c.s.s.c.s.sample.domain.SomeService - Error occurred. Compensating transaction...

Aquí vemos cómo se compensa la transacción al no haber conexión a base de datos para poder dejar el sistema de terceros en un estado consistente.

Simular latencia añadiendo “nervio”

También existe una opción bastante interesante que nos permite simular “nervio” en la latencia de forma que ésta varía a lo largo del tiempo. Podemos ver cómo el circuito se abre y se cierra en función de la latencia de red en ese momento:

@Test
public void shouldOpenCircuitOnThirdPartyWhenJitter() throws IOException {
    someService.createUserTransaction(userId, "jitter concept");
    // GIVEN
    thirdPartyProxy.toxics().latency("latency-with-jitter", ToxicDirection.DOWNSTREAM, 10_000).setJitter(50_000);
    // WHEN
    for (int i = 0; i < 10; i++) {
        var transactions = someService.getTransactions(userId);
        System.out.println("transactions = " + transactions);
    }
    // some assertions here
}

Aquí la salida por consola de los diez intentos de obtener las transacciones del usuario:

08:45:47.998 [main] INFO  c.s.s.c.s.sample.domain.SomeService - transaction created = {"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}, updating user transactions...
feb. 20, 2019 8:45:48 A. M.
08:45:48.171 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:49.207 [HystrixTimer-1] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:49.208 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:49.231 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:50.238 [HystrixTimer-2] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:50.240 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:50.263 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:51.270 [HystrixTimer-3] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:51.271 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:51.292 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:51.309 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
transactions = [{"Transaction":{"id":ef04cab2-f132-4792-9eee-351594b9c79c, "date":2019-02-20T07:45:47.964227, "concept":"jitter concept", "transactionStatus":"CREATED"}}]
08:45:51.319 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:52.324 [HystrixTimer-6] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []
08:45:52.324 [main] INFO  c.s.s.c.s.sample.domain.SomeService - getting new transactions...
08:45:53.331 [HystrixTimer-4] WARN  c.s.s.c.s.sample.domain.SomeService - executing fallback method when getting new transactions
transactions = []

5. Conclusiones

Aunque este tipo de test son más costosos que los test unitarios a los que estamos acostumbrados, sin duda tienen un gran potencial aportándonos una visión más cercana de cómo se comportará nuestra aplicación ante fallos reales que pueden ocurrir en nuestro sistema y que, como dice la ley de Murphy:

Si algo malo puede pasar, pasará.

6. Referencias

La entrada Resiliency Testing con Toxiproxy se publicó primero en Adictos al trabajo.

Crear una instancia EC2 en AWS

$
0
0

Índice de contenidos

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: Linux Mint 19

2. Introducción

A poco que estés en este mundillo, ya te habrás percatado de que muchas empresas y quizá la tuya ya están o se están pensando en migrar a la nube, donde, hoy por hoy, el rey es AWS.

La nube de AWS nos ofrece un montón de servicios para hacer casi cualquier cosa, y en este tutorial vamos a hablar del servicio E2C que nos permite la creación de máquinas en la nube.

3. Vamos al lío

Nota: para seguir este tutorial necesitarás una cuenta de AWS.

Como ves, si eres nuevo en AWS tienes 12 meses para probar servicios de forma gratuita, pero ojo que las condiciones son severas y solo te dejan “gratis” ciertas configuraciones, que comentaremos en el tutorial.

Una vez tenemos una cuenta de AWS podemos acceder a nuestra consola y seleccionar el servicio EC2 o como ves en la imagen directamente podemos ir la opción “Launch a virtual machine” para iniciar el asistente.

En el primer paso tenemos que seleccionar el AMI (Amazon Machine Image), es decir, la imagen para nuestra máquina. Podemos seleccionarla únicamente con el sistema operativo que queramos instalado, o bien buscar alguna que ya tenga el servicio o la aplicación que queramos desplegar. En este caso vamos a seleccionar una imagen de Ubuntu 18.04.

Nota: ojo al seleccionar porque algunas imágenes llevan costes añadidos. Fíjate que ponga “Free tier eligible” como en la imagen.

Al pulsar en “Select” pasamos al siguiente paso del asistente donde tenemos que seleccionar las características técnicas de la máquina. En función del tipo seleccionado, tendrá más o menos número de procesadores, memoria RAM, etc…

Nota: si no quieres pagar estarás limitado al tipo t2.micro con un solo procesador y solo un 1GB de RAM.

Una vez seleccionado pulsamos en “Next: Configure Instance Details” para ir al siguiente paso donde vamos a configurar detalles de la instancia como: el número de instancias que queremos, en qué VPC y Subnet la vamos a desplegar y algo muy importante poner a Enable la propiedad “Auto-assign Public IP” para permitir el acceso remoto.

Pulsando en “Next: Add Storage” pasamos al siguiente paso del asistente donde debemos darle una capacidad al volumen que se va a asociar con la instancia, es decir, la capacidad del disco duro de nuestra máquina.

Nota: si te fijas en la imagen, te dice que hasta 30Gb de capacidad no tiene coste, por lo que casi tod@s ponemos 29Gb no vaya ser… y ningun@ le damos al “Learn more”, donde te dice que esto es cierto pero solo durante el primer año.

Pulsando en “Next: Add Tags” pasamos al siguiente paso del asistente donde podemos establecer todos los tags en forma de clave-valor que consideremos oportunos. Aquí la recomendación es establecer al menos un Name y un Project de cara a poder identificar los gastos en la factura.

Pulsando en “Next: Configure Security Group” pasamos al siguiente paso del asistente donde podemos establecer los puertos que queremos mantener abiertos en la máquina. Si queremos acceder directamente de forma remota necesitaremos al menos tener abierto el puerto 22, el resto de puertos dependerán de los servicios o aplicaciones que queramos desplegar.

Si pulsamos en “Review and launch” podemos ver una página con el resumen de las características que hemos ido seleccionando para nuestra máquina, y si estamos conformes solo tendremos que pulsar en “Launch” para crear la instancia de nuestra máquina.

Al pulsar en “Launch” se nos abrirá un modal solicitando que seleccionemos o creemos un fichero de seguridad necesario para poder conectar de forma remota con la máquina. Seleccionamos la opción “Create a new key pair” y le damos un nombre, pulsamos en “Download” y, por último, en “Launch Instances”.

Pasados unos segundos el sistema nos informa de que nuestra máquina ya está lista para conectar de forma remota.

Pinchando en el ID podemos acceder directamente a la lista de instancia y ver el estado de la máquina, la IP pública asignada, etc…

Para conectar de forma remota desde nuestra máquina y poder empezar a trabajar con ella, tenemos que pulsar en “Connect”.

Con lo que aparece el siguiente mensaje que nos dice que para conectar tenemos que darle unos permisos restringidos al fichero antes descargado y el comando que tenemos que utilizar para conectar.

Entonces abrimos un terminal de nuestra máquina, nos posicionamos donde tengamos descargado el fichero requerido y nos aseguramos de que tiene los permisos requeridos:

$> chmod 400 tutorial.pem

y ahora, dentro del mismo directorio, copiamos el comando que AWS nos da de ejemplo. Al ejecutarlo tenemos que ver que estamos dentro de nuestra máquina recién creada sin que nos haya pedido usuario y/o contraseña, ya que la seguridad nos la da el fichero .pem.

A partir de aquí como en cualquier máquina con Ubuntu podemos instalar todas las aplicaciones y servicios que nos hagan falta.

4. Conclusiones

Como ves crear una infraestructura en AWS para tus proyectos es bastante sencillo, lo que siempre tienes que tener en cuenta son los costes asociados para no llevarte sustos en la factura al final del mes.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos

La entrada Crear una instancia EC2 en AWS se publicó primero en Adictos al trabajo.

Montar clúster de K8S en AWS con Rancher 2.0

$
0
0

Índice de contenidos

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: Linux Mint 19
  • Rancher 2.0 (2.0.16)

2. Introducción

Que Kubernetes es el estándar de facto para el despliegue de contenedores Docker es algo que a estas alturas no le va a sorprender a casi nadie. Es por ello, que Rancher en su versión 2.0, ha dado una vuelta completa a toda su arquitectura para abrazar por completo Kubernetes y ofrecernos facilidades para la gestión centralizada de varios clústeres, así como su creación en distintos proveedores de cloud y bare metal.

En este tutorial vamos a ver cómo podemos empezar con Rancher 2.0 para crear un clúster de Kubernetes en la nube de Amazon y gestionarlo desde la interfaz de Rancher 2.0.

3. Vamos al lío

Nota: para seguir este tutorial necesitarás una cuenta de AWS y la configuración básica produce costes en la factura. El motivo de por qué se hace este tutorial en AWS es porque necesitamos de un proveedor de cloud para ofrecer el servicio de LoadBalancer y actualmente AWS es el cloud más utilizado a nivel empresarial.

Lo primero que necesitamos es una instancia EC2 donde vamos a desplegar Rancher 2.0. Para ello puedes seguir el tutorial “Crear una instancia EC2 en AWS” teniendo en cuenta la siguiente configuración.

En el primer paso seleccionamos un AMI con Ubuntu 16.04 aunque también nos puede servir Ubuntu 18.04

En el segundo paso seleccionamos un tipo de instancia mínimo de t2.medium con 2 cores y 4 GB de memoria RAM.

En el tercer paso seleccionamos la VPC y la subnet donde vamos a levantar la instancia y habilitamos el uso de auto asignar la IP pública.

En el cuarto paso establecemos el tamaño del volumen para la persistencia de datos. Vamos a establecer el máximo gratuito 29Gb pero hay que tener en cuenta que solo será gratuito durante el primer año.

En el quinto paso podemos establecer todos los tags que queramos, típicamente el “name” y el “project” para poder luego tener un criterio con el que agrupar los costes.

En el sexto paso necesitamos abrir los puertos 22, 80 y 443 para poder acceder a la home de Rancher 2.0 y conectar de forma remota vía SSH.

El resto de pasos están descritos en el tutorial antes mencionado.

Una vez conectados a la máquina recién creada vamos a instalar Rancher 2.0. Para ello antes tenemos que instalar Docker ejecutando los siguientes comandos:

$> sudo apt-get update
$> sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common
$> curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$> sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$> sudo apt-get update
$> sudo apt-get install docker-ce=17.03.2~ce-0~ubuntu-xenial
$> sudo usermod -aG docker $USER

Una instalado Docker podemos ejecutar el siguiente comando para levantar un contenedor con Rancher 2.0.

$> docker run -d --restart=unless-stopped -p 80:80 -p 443:443 rancher/rancher

En caso de querer hacerlo con un certificado de Let’s Encrypt gestionado ejecutamos:

$> docker run -d -p 80:80 -p 443:443 rancher/rancher:latest --acme-domain rancher.yourdomain.com

O si tenemos otro tipo de certificados:

$> docker run -d -p 80:80 -p 443:443 -v /home/rancher/cacerts.pem:/etc/rancher/ssl/cacerts.pem -v /home/rancher/key.pem:/etc/rancher/ssl/key.pem -v /home/rancher/cert.pem:/etc/rancher/ssl/cert.pem rancher/rancher:latest

Pasados unos momentos en la IP o dominio asociado con la instancia veremos la pantalla principal de Rancher pidiendo establecer una contraseña para el administrador.

Una vez establecida entramos en la interfaz de Rancher 2.0 donde podemos añadir un nuevo clúster.

En la siguiente pantalla podemos ver las opciones que tenemos para crear o importar un clúster existente. Para este tutorial vamos a seleccionar la opción de usar la infraestructura de AWS.

Cuando seleccionamos esta opción lo primero que tenemos hacer es darle un nombre al clúster y crear una plantilla que Rancher 2.0 utilizará para la creación de los distintos nodos en nuestra cuenta de AWS.

Obviamente lo primero que nos va a solicitar es que le indiquemos el “Access Key” y el “Secret Key” de nuestra cuenta de AWS así como la región donde queremos desplegar el clúster.

Se abrirá una nueva sección para poder seleccionar la zona y la VPC/Subnet donde queremos crear los nodos del clúster.

A continuación en la sección 3 podemos establecer el grupo de seguridad, dejamos el estándar que nos abrirá los puertos necesarios para la comunicación entre los nodos.

En la siguiente pantalla vamos a poder establecer las características de la máquina: el tipo de instancia (t2.medium), el tamaño de disco que vamos a establecer al límite gratuito establecido por AWS 29 GB (solo el primer año de vida), en el AMI vamos a dejar seleccionado Ubuntu, el ssh user que por defecto en ubuntu.

Añadimos un tag para darle nombre a la instancia:

Le damos nombre al template y pulsamos en “Create”:

Le damos un nombre, seleccionamos el número de instancias que queremos, el template recién creado y el rol del nodo entre etcd, control plane y worker. Para nuestro caso vamos a seleccionar una instancia que tengan todos los roles, es decir, que haga de master y de nodo. Esta no es la configuración ideal para poner en producción 😉

Nota: es importante señalar que la instancia de Rancher 2.0 solo hace de gestor de clústeres y en ningún caso se puede usar ni como master ni como nodo de ninguno de ellos.

Nota importante: en las opciones del clúster hay que asegurarse de seleccionar “Amazon” para que automáticamente se cree el ELB asociado al ingress ya que por defecto se establece a custom y no hará este proceso por nosotros.

Al pulsar en “Create”, comienza el proceso automático de aprovisionamiento del nodo.

Nota:el siguiente apartado hay que ir haciéndolo mientras se aprovisiona el clúster, de lo contrario el proceso de aprovisionamiento informará de un error de permisos.

4. Configuración adicional para tener Load Balancer

El Load Balancer es la pieza de Kubernetes que nos permite poder acceder a los servicios desplegados desde fuera del clúster.

Esta pieza es la que está más pegada a la arquitectura del proveedor de cloud seleccionado y éste nos lo tiene que proporcionar.

Por ejemplo, AWS cuando detecta que queremos tener un Load Balancer lo que hace es proporcionarnos un ELB (Elastic Load Balancer) que permite el acceso al servicio definido de esta forma en el clúster.

Por lo general esta pieza lleva un coste añadido lo que hace que sea imprescindible el uso de Ingress para tener n rutas que pasen por un mismo Load Balancer. Si el Ingress tendríamos n Load Balancers con el coste que ello conlleva.

También ocurre que esta pieza hace que sea mucho más complicado tener un clúster en bare metal dado que tendremos que ser nosotros los que proporcionemos la forma de crear el Load Balancer a través de MetalLB siempre y cuando el proveedor de hosting tenga soporte.

Volviendo a nuestra instalación, para que el aprovisionamiento anterior tenga éxito, antes tenemos que realizar una serie de configuraciones en los elementos de AWS.

Primero, tenemos que ir a la instancia creada en AWS para el tag que se le ha dado al nodo creado, y que va a ayudar a Rancher 2.0 a determinar qué elementos de AWS están bajo su control.

Ese tag (key y value) tenemos que ponerlo manualmente en los siguientes elementos:

  • Todos las instancias creadas para el clúster, incluida la instancia que tiene Rancher 2.0.
  • La subnet donde está desplegado el clúster.
  • El grupo de seguridad cerrado para el clúster, en nuestro caso, rancher-nodes.

Además tenemos que asignarle los permisos a todos los nodos del clúster para poder crear elementos necesarios como los ELBs.

Para crear y asociar estos permisos al nodo tenemos que seleccionar el nodo deseado y seleccionar Actions –> Instance settings –> Attach/Replace IAM Role

En la siguiente pantalla pulsamos sobre “Create Role”:

Y seleccionamos un Role de tipo EC2 y pulsamos en “Next:Permissions”

Pulsamos sobre “Create Policy”:

Y nos vamos a la pestaña “JSON” donde pegamos el siguiente contenido y pulsamos en “Review policy”:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:Describe*",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "ec2:AttachVolume",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "ec2:DetachVolume",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": ["ec2:*"],
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": ["elasticloadbalancing:*"],
      "Resource": ["*"]
    },
    {
      "Effect": "Allow",
      "Action": ["iam:CreateServiceLinkedRole"],
      "Resource": ["*"]
    }
  ]
}

Quedando de esta forma:

Pulsamos sobre “Review policy”

En la siguiente pantalla establecemos el nombre de la policy, damos una descripción y vemos que los permisos quedan asignados. Pulsamos en “Create Policy” para confirmar los cambios.

Ahora en la ventana anterior pulsamos en el botón de refresco y filtramos para seleccionar la “policy” recién creada y pulsamos en “Next: Tags”.

En la siguiente pantalla podemos asignar los tags que consideremos y pulsamos en “Next: Review”.

Por último, le damos un nombre y una descripción al rol y pulsamos en “Create Role” para aplicar los cambios.

Volvemos a la pantalla inicial y pulsamos en refresh, de esta forma debemos ver el nuevo rol que quedará asignado pulsando en “Apply”.

Pasados unos minutos y si todo es correcto, veremos el siguiente estado en el clúster:

Y pulsando sobre el nombre del clúster, podemos acceder a un pequeño dashboard que nos ofrece información como el fichero de configuración necesario para configurar nuestro cliente kubectl y comenzar a utilizar nuestro nuevo clúster de Kubernetes.

5. Conclusiones

Rancher 2.0 es una herramienta muy a tener en cuenta cuando en nuestro stack tenemos uno o varios clústeres de Kubernetes, y en este tutorial hemos visto que también nos ayuda a la creación de clúster desde cero en AWS.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos

La entrada Montar clúster de K8S en AWS con Rancher 2.0 se publicó primero en Adictos al trabajo.

Colaboración en remoto: cómo hacer que funcione

$
0
0

La colaboración remota de equipos es una de esas cosas que parecen fáciles y por lo tanto en muchos casos no se les suele prestar suficiente atención.
Pero nada más lejos de la realidad, es un aspecto del trabajo en equipo que requiere compromiso, honestidad y transparencia.

Índice de contenidos

1. Introducción

Este tutorial se centra en métodos, herramientas y buenas prácticas que a mí me han funcionado para conseguir una buena colaboración remota.
No voy a enumerar los retos y dificultades a los que se enfrenta un equipo deslocalizado ni tampoco sus beneficios, se supone que eso ya lo has descubierto por otros medios.
También hay que tener en cuenta que cada equipo tiene sus peculiaridades y lo que en unos casos funciona puede que en otros no sea lo más adecuado.

Por descontado, nada de lo que proponga aquí servirá si en el equipo no hay confianza, buena voluntad y una genuina intención de colaborar.
Así que si no estás seguro de esto, el primer paso es apuntalar bien estos principios junto con los valores de compromiso, honestidad y transparencia.

1.1. Aspectos de la colaboración

Para este tutorial he considerado tres aspectos fundamentales: la comunicación (en el sentido de intercambio de mensajes), la coordinación (sincronización) y la entrega colectiva (en el sentido de producir un resultado tangible).

2. Comunicación

La comunicación digital remota se puede conseguir por tres tipos de canales diferentes: texto, voz y vídeo.
Sin embargo, en vez de dividir esta sección según esa clasificación voy a utilizar la siguiente:

  1. Mensajería, tanto mensajes directos como chats grupales.
  2. Conversación uno a uno.
  3. Conversación en grupo (reuniones).

Las principales razones para hacerlo así son:

  • No recomiendo la comunicación basada únicamente en la voz, especialmente para reuniones de grupo.
    La parte remota puede sentirse desconectada y adoptar un rol de oyente sin participación activa en la reunión.
  • Un canal de voz no transmite el lenguaje corporal y la comunicación no-verbal en general, empobreciendo la interacción.
  • Las restricciones y propiedades de las conversaciones uno a uno y en grupo son muy diferentes tanto en los canales de voz como en vídeo, así que clasificar por voz vs vídeo no ayuda mucho.

2.1. Mensajería

La mensajería es recomendable en los siguientes casos:

  1. Si no necesitas una respuesta inmediata en tiempo real, es decir, si la comunicación puede ser asíncrona.
  2. La intención es comunicar un mensaje simple y/o corto rápidamente.
  3. Para difundir una comunicación a un grupo de personas.
  4. Si deseas invitar a otras personas a unirse a una conversación o debate pero sin forzar un horario en los participantes.

Esto es aplicable tanto a comunicaciones uno a uno como a reuniones grupales.

2.1.1. Textos largos

Si el mensaje que se quiere enviar es largo y/o complejo, es mejor documentarlo en la herramienta de conocimiento compartido (explicada más abajo) y entonces enviar solamente un enlace al documento.

2.1.2. Identificando a las personas

Para un colaborador remoto no es sencillo poner caras a los nombres de los usuarios con los que se tiene que comunicar.
La manera de facilitar esta labor es seguir estas sencillas normas:

  • Evita nombres de usuario complicados y oscuros.
    Por ejemplo, si mi nombre es “José María Sánchez García”, en vez de llamar a mi usuario “jmsg” o “jomar-saga”, lo podría llamar “jose-maria-sanchez” o algo similar.
  • Evita nicks y apodos de cuando eras un adolescente o de tus otras vidas (privadas).
    Después de todo ya somos profesionales adultos, ¿verdad?
    Ejemplos de nombres muy poco apropiados: “Th3L1z4rd”, “amo-mi-mascota”, “FavDaddy”
  • Utiliza una fotografía real para tu perfil de usuario.
    Ya sabes lo que dicen de que una imagen vale más que mil palabras. Tu interlocutor también agradecerá poder reconocerte cuando os veáis en persona.

2.1.3. Herramientas

Las herramientas que recomiendo aquí son Slack y el viejo e-mail, por supuesto que sí.

Pero, ¿cuándo elegir la una sobre la otra? Aquí van unas reglas efectivas:

  1. Obviamente, si tienes que contactar con personas que están fuera del mundo Slack entonces utiliza el e-mail, al fin y al cabo es el canal de mensajería más universal.
  2. Aunque la mensajería es asíncrona por naturaleza, diferentes mensajes tienen diferentes requisitos temporales:
    • Si el tiempo es importante, usa Slack.
    • Si no, usa el e-mail.
  3. Los e-mails distraen menos que los mensajes directos de Slack.
  4. Los e-mails pueden ser clasificados, archivados y posteriormente buscados por parte de las personas que los reciben.
    Por otro lado, los mensajes de Slack se pierden en el historial del chat e incluso acaban siendo eliminados si usamos el plan gratuito.

2.2. Conversación uno a uno

Este tipo de comunicación es apropiado cuando deseas mantener una convesación informal con un colaborador y necesitas feedback inmediato.

La mayor limitación de este método es que es síncrono por naturaleza: la otra persona debe estar disponible en el momento de la comunicación.

Aunque pueden utilizarse llamadas de voz, la vídeoconferencia es el medio preferido para estas comunicaciones. Ver la cara de la persona con la que estás hablando supone una diferencia importante en empatía y entendimiento.

2.2.1. Herramientas

Estas son las herramientas recomendadas por orden de preferencia:

  1. ZoomUs, que requiere una aplicación cliente o extensión de navegador.
    En Mac se puede instalar con Homebrew Cask: brew cask install zoomus
    La cuenta gratuita no tiene ninguna limitación para comunicaciones uno a uno.
    Esta herramienta tiene todo lo que necesitas y mucho más y además con muy buena calidad.
    Se puede crear una “sala de reuniones” con un enlace permanente en la que cualquiera que tenga el enlace puede contactarte.
  2. Slack dispone de vídeollamada uno a uno en su plan gratuito.
    Esta función está disponible en el icono del teléfono tras seleccionar la persona con la que queremos contactar.
  3. Hangouts tiene el inconveniente de que requiere una cuenta de Google para funcionar.
    Dispone de llamadas de voz, vídeo y compartición de pantalla.
    En mi opinión los codecs no son precisamente eficientes en cuanto a la calidad de la transmisión y el consumo de CPU.

2.3. Conversación en grupo

Una conversación de grupo es una reunión de 3 o más personas que ocurre a una hora determinada y en un lugar concreto.

Las reuniones tienen el inconveniente de que consumen mucho tiempo de muchas personas, lo que puede llegar a ser un verdadero desperdicio de dinero.
Hay muchas otras consideraciones que deben tenerse en cuenta antes de involucrar a varias personas en una reunión, pero eso queda fuera del alcance de este tutorial.

En definitiva, hay que limitar las reuniones a las mínimas imprescindibles, con los participantes justos para aportar el valor necesario y asegurar que tienen un objetivo muy claro y acotado.

En lo referente a la comunicación remota, sigue estos consejos para implicar a los colaboradores remotos:

  1. Evita las llamadas telefónicas siempre que sea posible.
    Entre los muchos inconvenientes destacan: baja calidad del audio, dificultad de entender o incluso escuchar a las personas físicamente presentes en la sala (incluso en manos libres) y dificultad de explicar las ideas propias a todo el mundo sólo con la voz.
  2. Idealmente, utiliza una sala de reuniones virtual en la que todo el mundo participe remotamente con su propio portátil.
    Incluso si 2 o más participantes están físicamente juntos en el mismo lugar, cada uno se conecta a la sala virtual, así que nadie se siente desplazado o fuera de lugar.
  3. Si eso no es posible, intenta utilizar una sala de reuniones con una pantalla grande, cámara y micrófono ambiente.
  4. Si eso tampoco es posible, usa un portátil con un micrófono ambiente y altavoz Bluetooth o USB.
    Posiciona el portátil en un lugar desde el que los colaboradores remotos puedan tener una imagen lo más completa posible de la sala.
    El micrófono y altavoz debería estar en el centro para poder llegar a todo el mundo.
  5. Los participantes deben hablar por turnos.
    Quien quiera hablar debe levantar la mano y el facilitador de la reunión (o el propio grupo) asegurar que se van pasando los turnos de palabra de manera ordenada.
  6. Evita los susurros y las conversaciones paralelas. Lo peor es tener que escuchar varias conversaciones a la vez.

2.3.1. Scrum daily

Las dailies de Scrum tienen una estructura y protocolo que hacen viable el uso de una llamada de voz para integrar a un colaborador remoto.

Cada miembro va cogiendo el teléfono cuando le toca su turno. En cualquier caso, para evitar malentendidos:

  1. Di tu nombre cuando comienza tu turno.
  2. Evita referenciar post-its, historias y tareas con términos genéricos como “esta tarjeta” o “esa tarea”. ¿¿¿Qué es esto y qué es eso???
  3. Respeta los turnos y no interrumpas.

2.3.2. Herramientas

De las tres herramientas de videoconferencia disponibles para comunicaciones uno a uno sólo Hangouts soporta múltiples participantes durante un tiempo ilimitado.

ZoomUs permite salas de reuniones virtuales con múltiples participantes en su versión gratuita, pero la duración de la reunión está limitada a 40 minutos.

3. Coordinación

El objetivo de la coordinación en remoto es fomentar la transparencia, compartir objetivos y situación actual y hacer explícito todo el conocimiento (en contra de tener conocimiento tácito).
La información tácita, ya esté en tu cabeza o en la mente colectiva del equipo físicamente unido, es un gran enemigo de la colaboración remota.

Por ejemplo, no está mal charlar un rato con algunos compañeros junto a la máquina del café o en el ascensor.
Lo que es verdaderamente importante es que las decisiones y resultados de esas charlas queden documentadas e informadas a todos los miembros del equipo.
Obviamente sólo las cosas pertinentes al trabajo, cotilleos no…

3.1. Planificación de tareas

Para coordinar la planificación y ejecución de tareas dentro del equipo, los aspectos que tienen un impacto más grande en la colaboración remota son:

  1. El nivel de detalle y especificación de las tareas e historias de usuario (más adelante doy algunos consejos).
  2. Mantener el estado de las tareas sincronizado con el trabajo real. Es decir, tu tarea actual debería estar “in progress” y debería pasarse a “done” en cuanto la termines.
  3. Mantener los paneles de tareas digitales sincronizados con los paneles físicos.
    No sincronizar paneles digitales y físicos puede llegar a ser incluso peor que no tener paneles: la información falsa o contradictoria es incluso peor que la falta de información.

3.1.1. Mejores prácticas

A la hora de especificar tus tareas, sigue estos consejos:

  1. Utiliza un título corto y autoexplicativo. Evita los detalles y céntrate en el objetivo que pretende conseguirse con esta tarea, no los medios.
  2. Incluye todos los detalles relevantes de la tarea en su descripción:
    • Detalles acerca de lo que se tiene que conseguir (el objetivo).
    • Cualquier información útil sobre cómo debería hacerse.
    • Dependencias y personas implicadas.
    • Si la tarea está relacionada con la resolución de un fallo, explica cómo reproducirlo y los efectos del bug.
  3. Organiza tu backlog en versiones o releases de modo que sea sencillo tener una imagen completa de hacia dónde se dirige el proyecto en el futuro.
    Aunque tu equipo haga entrega continua, es bueno establecer estos hitos como un ejercicio de planificación más a largo plazo.
    Por ejemplo, a cada sprint se le puede poner un nombre que refleje el objetivo de ese sprint.

Pero sobre todo, no utilices títulos de tarea sin sentido que sólo tú puedes entender. Por ejemplo, poner un par de palabras que te recuerden a ti de qué iba esa tarea.

No voy a hablar aquí sobre cómo especificar historias de usuario. Es material suficiente para un curso completo y de una importancia crítica para que el equipo (remoto o no) realice el trabajo correcto.

3.1.2. Herramientas

Normalmente las herramientas de gestión de tareas suelen elegirse a nivel corporativo, así que un desarrollador no tendrá mucho margen para opinar.
Habitualmente, la seleccionada es JIRA a pesar de todos sus grandes defectos.

Si puedes elegir, Pivotal Tracker es una opción excelente que va al grano y elimina toda la paja.
Taiga también puede ser una buena opción con un plan gratuito para proyectos pequeños.

3.2. Compartición de conocimiento

El otro pilar de la coordinación de equipos deslocalizados es la compartición del conocimiento.

Los equipos que están físicamente juntos conviven en un terreno común en el que el conocimiento tácito se asienta y se difunde:

  • Hay conversaciones por todas partes en las que varias personas escuchan y obtienen conocimiento.
  • Pizarras, paneles y posters.
  • Charlas antes o después de las reuniones, en los pasillos, cafeterías, etc…
  • Sentarse junto a un compañero para pedir consejo o preguntar algo.

Hay tantas cosas ocurriendo a tu alrededor que tu cerebro está siempre recogiendo y almacenando trozos de información nuevos.

Los trabajadores remotos se pierden todo eso, por lo tanto es extremadamente importante hacer explícito todo el conocimiento que tiene algún impacto en el proyecto.

La mejor manera de conseguir esto es organizando el conocimiento en un Gestor Documental con control de versiones, colaboración online, autorización, metadatos, comentarios y otras funciones relacionadas.
Y de lejos la herramienta más sencilla y potente de este tipo es ¡un wiki!

3.2.1. Herramientas

Si en tu organización usan JIRA para las tareas, no hay duda de que la herramienta de documentación será Confluence.

Una alternativa para usuarios de Google es Google Docs, que además permite interacción en tiempo real.
Hay cientos de opciones más como Nuclino.

En cualquier caso recomiendo alejarse de los portales y CMS más tradicionales y centrarse en los wikis.
También hay propuestas más innovadoras que giran alrededor de las conversaciones. Según mi experiencia, estas herramientas pueden llevar a una situación de conocimiento desperdigado y desestructurado donde no es fácil encontrar lo que se busca.

4. Entrega colectiva

Cuando hablo de “entrega colectiva” me refiero al esfuerzo conjunto de varias personas para llevar a cabo una tarea cuyo resultado es un tangible.

En este sentido, voy a considerar dos tipos de tareas: diseño/brainstorming y desarrollo de software.

4.1. Diseño y brainstorming

Cuando un grupo de personas se juntan en una sesión creativa de brainstorming es normal producir diagramas, esquemas o dibujos para representar mejor las ideas.
Si todo el mundo está sentado alrededor de la misma mesa esta forma de trabajo es muy sencilla y natural: papel y boli es todo lo que hace falta.
¿Pero qué podemos hacer si uno o varios participantes están en remoto?

Obviamente, para hablar se pueden aplicar los mismos principios de comunicación explicados en la sección sobre conversaciones muchos a muchos: usar videoconferencia.

En lo referente a esquemas y dibujos podríamos simplemente mostrar nuestras obras de arte en la webcam, pero afortunadamente hay otras opciones para la colaboración creativa en remoto: pizarras electrónicas virtuales, post-its online, canvas online y artilugios similares.

Hay otras herramientas más orientadas a modelos (wireframes, diagramas, layouts) dejando fuera la parte de interacción en tiempo real y centrándose en una experiencia más estructurada y a largo plazo a través del concepto de “proyectos”.
Estas herramientas no las voy a tratar en este tutorial.

Por lo tanto, los consejos más importantes en este tipo de colaboración son:

  1. Aprovecha las herramientas online disponibles.
  2. Utiliza la herramienta adecuada según el tipo de colaboración:
    1. Las pizarras electrónicas virtuales son el mejor reemplazo para el papel y el boli.
      Sólo hace falta coger un poco de destreza con el ratón o el trackpad.
    2. Si necesitas organizar conceptos de alguna manera (tiempo, categoría o lo que sea) puedes usar post-its o tarjetas online.
    3. Otras herramientas te permiten escribir un esquema o dibujar un diagrama de manera interactiva en tiempo real.

4.1.1. Herramientas

Para hacer dibujos esquemáticos, RealtimeBoard funciona muy bien y es muy fácil de usar.
ZoomUs también dispone de pizarra electrónica pero recuerda que el plan gratuito tiene un límite de 40 minutos para reuniones multi-usuario.

Trello es la herramienta preferida para organizar ideas mediante tarjetas.

Y la mejor opción para escribir un documento, esquema o presentación interactuando en tiempo real es Google Docs.

4.2. Desarrollo de software

Incluso para equipos que trabajan físicamente juntos, el desarrollo de software hace muchos años que aprovecha a fondo las herramientas de colaboración online.
Además, el desarrollo de software colaborativo es un tema con mucha miga, desde arquitecturas que facilitan la colaboración sin roces hasta las estrategias de ramas pasando por reglas de estilo.

Por lo tanto, este tutorial sólo se va a centrar en una práctica en la que estar físicamente presentes supone una gran diferencia: pair programming.

Por desgracia la mayoría de los desarrolladores no han experimentado los beneficios del pair programming y no son conscientes de la enorme mejora de productividad y aprendizaje que supone.
Si aún no lo has probado, aprende cómo debe ser una buena sesión de pair programming
y búscate un compañero siempre que creas que va a ser beneficioso para el proyecto.

4.2.1. Herramientas

Sin ninguna duda, la mejor herramienta para hacer pair programming remoto es ZoomUs.
Podrás ver y hablar con tu compañero a través de la webcam al mismo tiempo que compartes cualquier ventana o tu escritorio completo.
Incluso con una conexión regulera la pantalla se ve nítida, así que no tendrás dificultades para leer el texto y seguir el cursor y el ratón.

VS Code por su parte dispone del plugin Live Share.

Floobits es otro plugin disponible para varios IDEs como IntelliJ aunque no es gratuito.

5. Conclusiones

Termino más o menos igual que empecé.
Las herramientas y buenas prácticas facilitan el trabajo, pero lo fundamental y verdaderamente importante son los principios y los valores que vuelvo a repetir una vez más: confianza, buena voluntad, genuina intención de colaborar, compromiso, honestidad y transparencia.
Sin ellos no hay mucho que hacer porque nada va a funcionar del todo bien.

6. Referencias

La entrada Colaboración en remoto: cómo hacer que funcione se publicó primero en Adictos al trabajo.

Introducción a Actions on Google

$
0
0

Índice de contenidos

1. Introducción

Con este tutorial aprenderás qué es Actions on Google y los conceptos básicos para desarrollar acciones utilizando su consola y Dialogflow. Además, veremos un ejemplo sencillo para que cuentes con un punto de partida práctico.

2. Actions on Google

2.1. ¿Qué es?

Actions on Google es una plataforma que permite crear acciones para el Asistente de Google. Estas acciones son las interacciones que se entablan con el asistente y que permiten dar respuesta a una necesidad concreta como conocer el clima actual o comprar algo vía internet. Por otro lado, se pueden usar en múltiples dispositivos, como teléfonos móviles, smartwatches o televisiones.

Los usuarios pueden invocar acciones por su nombre, pero también podemos añadir una frase para indicar exactamente qué tienen que hacer. Por ejemplo, supongamos que queremos conectarnos con una acción que se llama “Señor Tormentas”, que nos informa del clima. Podríamos invocarla con “Ok Google, habla con Señor Tormentas” o bien podríamos decir “Ok Google, habla con Señor Tormentas para saber la temperatura mañana”, de forma que se sepa ya qué queremos exactamente.

Para utilizar una acción no es necesario instalarla, pero antes de que los usuarios puedan usarla es necesario enviarla a producción para que pueda ser aprobada. Sin embargo, sí que podremos utilizar nuestras acciones antes de publicarlas si las probamos conectados al dispositivo con la misma cuenta con la que las hemos desarrollado.

Por otro lado, una parte importante del desarrollo de las acciones es planear cómo los usuarios las van a descubrir. Los usuarios pueden hacer esto diciendo la acción que quieren realizar, “quiero saber la temperatura de mañana”, o buscando en el directorio de acciones. También podemos proporcionar links que lleven a nuestra acción o la invoquen desde un navegador.

Por último mencionar que Google también nos proporciona analíticas que analizan el uso, el descubrimiento de las acciones, las conversaciones que se dan y su longitud, entre otras muchas cosas.

2.2. Conceptos básicos

  • Asistente de Google: asistente virtual de Google para permitir conversaciones entre los usuarios y Google, dando respuesta a alguna necesidad.
  • Dialogflow: herramienta de Google para desarrollar experiencias conversacionales que puedan usarse con el Asistente de Google. Utiliza machine learning para analizar el input del usuario e identificar lo que quiere hacer para poder responder de la manera más correcta.
  • Agente: proyecto de DialogFlow para administrar ciertas experiencias en uno o múltiples idiomas.
  • Intent: se trata del objetivo que tiene el usuario cuando habla con el Asistente de Google. Los agentes de Dialogflow se crean con dos intents por defecto: el de bienvenida y el de fallback, que se ejecuta cuando no se identifica qué intent se debe utilizar. Es recomendable no borrarlos, sino modificarlos para que se adapte a nuestras necesidades.
  • Invocación: es cuando un usuario inicia la interacción con una acción. Puede hacerlo explícitamente, indicando su nombre, o implícitamente, indicando qué quiere hacer. Además, existen los eventos, que son una característica que permite que una acción se invoque programáticamente en vez de directamente por el usuario.
  • Parámetro: concepto que proporciona el usuario a través de su interacción con el Asistente de Google y que utilizaremos para dar una respuesta correcta a la necesidad. Por ejemplo, si queremos comprar tres cartones de leche para el viernes podríamos identificar “cartones de leche”, “tres” y “viernes” como parámetros. Estos parámetros pueden ser de un tipo definido como una entidad.
  • Entidad: se trata de representaciones de conceptos reales y que se pueden utilizar en las frases de entrenamiento para que Dialogflow identifique correctamente los parámetros. Por ejemplo, podríamos tener una entidad que represente las opciones de compra con “cartón de leche”, “batido” y “helado” como opciones. Dialogflow proporciona algunas entidades por defecto, pero también podemos añadir las nuestras.
  • Fulfillment: lógica que administra un evento para realizar la acción correspondiente. Un fulfillment puede ser una aplicación externa, un servicio, etc.
  • Frases de entrenamiento: ejemplos de posibles interacciones del usuario para llamar a un intent en concreto. Estas frases son utilizadas por un algoritmo que entrena al agente para que pueda entender lo que el usuario quiere. Es recomendable proporcionar al menos 20 frases.

2.3. ¿Cómo funciona?

A grandes rasgos, cuando un usuario interacciona con el Asistente de Google del dispositivo (teléfono, altavoz inteligente, smartwatch, etc.) se elabora una petición que se enlaza con un agente. Una vez se ha identificado el agente a utilizar, se intentará emparejar lo que el usuario solicita con un intent. Si se consigue, se intentará recuperar los parámetros necesarios. Si no todos los parámetros obligatorios se han proporcionado o si no se han identificado, se solicitarán al usuario.

A continuación se debe devolver una respuesta. En caso de que ésta sea estática o con una lógica mínima se hará directamente. Sin embargo, para una lógica más compleja o para utilizar bases de datos o APIs externas será necesario utilizar un fulfillment, el cual hará el tratamiento necesario.

Al final, se use un fulfillment o no, se mostrará una respuesta al usuario, tras la cual se podrá terminar la conversación o solicitar más información al usuario para continuarla.

3. Ejemplo

Como ejemplo vamos a desarrollar una acción que calcule un número aleatorio dentro de un rango definido por el propio usuario. El objetivo es aprender cómo funciona una acción básica, por lo que no cubriremos el proceso de mandar nuestra aplicación a producción.

3.1. Consola de Actions on Google

El primer paso es conectarse a la consola de Actions on Google, a la cual puedes acceder en este enlace. Para ello nos pedirá identificarnos con una cuenta de Google.

Una vez conectados, podremos crear un nuevo proyecto, proporcionando un nombre para el mismo y seleccionando país y el idioma para las acciones, aunque luego se pueden escoger más. En mi caso será España y castellano (Spanish).

Nuevo proyecto en Actions on Google

 

La siguiente pantalla dispondrá de una serie de opciones para comenzar nuestro proyecto. En nuestro caso escogeremos Conversational.

Conversational experience en el selector

Ya dentro de nuestro proyecto, seleccionaremos Invocation en el menú a la izquierda y escribiremos el nombre que identificará nuestro proyecto. Después, pulsa el botón Save para guardar los cambios. Ten en cuenta que este nombre será el que se use para iniciarla y el que se mostrará en el directorio de acciones. En mi caso he escogido Mr. Random.

p

Panel de Invocation en la consola de Actions on Google

El siguiente paso será crear una acción, por lo que pulsamos en Actions en el menú y añadimos una. A continuación nos dejará escoger entre una lista de intents pre-construidos. Sin embargo, nosotros escogeremos Custom intent y daremos a Build, tras lo cual se nos abrirá una nueva pestaña con Dialogflow.

Crear una acción en la consola de Actions on Google

 

3.2. Dialogflow

Dentro de Dialogflow tendremos que escoger el idioma por defecto de nuestro agente y la zona horaria. Elegimos nuevamente Spanish y la zona horaria que nos corresponda.

Crear proyecto en Dialogflow

3.2.1. Configurar el intent

Ahora crearemos nuestro intent dentro de la sección Intent a la que podemos acceder desde el menú a la izquierda si no estamos ya en ella. Lo primero será escoger un nombre, por ejemplo Random Intent. Después vamos a indicar frases de entrenamiento y los parámetros que vamos a necesitar.

Comenzamos por los parámetros, para lo cual bajamos hasta la sección correspondiente. Vamos a añadir dos denominados number1 y number2, de tipo @sys.number y ambos obligatorios, por lo que marcamos la casilla de required. Además, vamos a modificar sus prompts, que es lo que se le dirá al usuario para solicitar los parámetros que sean obligatorios y no se hayan proporcionado o reconocido.

Parámetros de un intent de DialogflowPrompt para un parámetro de Dialogflow

Ahora vamos con las frases de entrenamiento. Voy a proporcionarte una lista de ejemplo que debería ser suficiente, aunque para un intent que se vaya a utilizar fuera de un tutorial sería conveniente definir más frases. Por otro lado, es posible que a la hora de agregarlas Dialogflow identifique parámetros automáticamente que no sean los que queremos, así que tendremos que indicar los correctos. Para ello elimina los que no estén bien y selecciona las palabras que correspondan a los parámetros adecuados para asignarlos.

  • Random entre dos y nueve
  • Quiero un número cualquiera
  • Dame un número entre siete y veinte
  • Quiero un número
  • Quiero un número aleatorio
  • Dime un número mayor que sesenta
  • Necesito un número aleatorio no mayor que sesenta y dos

 

Frases de entrenamiento de un intento de Dialogflow

Si quisiéramos responder con un mensaje sencillo usaríamos el apartado de Responses. Sin embargo, queremos realizar una operación para calcular un número random, por lo que vamos a habilitar el uso de Fulfillment e, importante, guardamos los cambios en el intent dando al botón Save.

Habilitar fulfillment para un intento de Dialogflow

3.2.2. Configurar el fulfillment

El siguiente paso es ponernos a configurar el fulfillment. Para ello seleccionamos la sección correspondiente en el menú de la izquierda. Una vez en ella, habilitamos el inline editor y substituimos el código que tenemos por el siguiente:

'use strict';
const functions = require('firebase-functions');
const {dialogflow} = require ('actions-on-google');
const RANDOM_INTENT = 'Random Intent';

const app = dialogflow();

app.intent(RANDOM_INTENT, (conv, {number1 ,number2}) => {
  var min, max;
  number1 = parseInt(number1, 10);
  number2 = parseInt(number2, 10);
  
  if(number1 < number2){
  	min = number1;
  	max = number2;
  } else {
  	min = number2;
  	max = number1;
  }
  
  var random = Math.floor(Math.random() * (max-min)) + min;
  conv.close("Tu número es " + random);
});

exports.dialogflowFirebaseFulfillment = functions.https.onRequest(app);

Ten en cuenta que el nombre que asignamos a la constante RANDOM_INTENT debe ser el mismo que hemos puesto como nombre para nuestro intent.

El último paso es dar a Deploy para desplegar nuestro código, lo cual tardará unos segundos o un par de minutos. Tras ello, ya deberíamos poder utilizar nuestra acción.

3.3. Probando nuestra acción

Podemos probar nuestra aplicación tanto en el simulador de la consola de Actions on Google, como utilizando varios dispositivos Android, si nos conectamos con la misma cuenta con la que hemos desarrollado la acción, ya que no la hemos enviado a producción y, por lo tanto, no está disponible para cualquiera.

Si queremos probarlo en el simulador, tan sólo tenemos que volver a la página web de la consola de Actions on Google y seleccionar el simulador en el menú. Una vez iniciado, podemos escribir «Ok Google, habla con Mr. Random» y luego indicarle que queremos un número en un rango determinado.

Ejecucción de Mr. Random en el simulador de la consola de Actions on Google

Respecto a los dispositivos, mencionar que, por ejemplo, si utilizamos un altavoz inteligente conectado con nuestra cuenta, no es necesario hacer nada más. Si, por el contrario queremos utilizarlo con un smartphone, tenemos que instalar el Asistente de Google desde Play Store e iniciarlo.

Ejecucción de Mr. Random en un teléfono con Android

¡Muchas gracias por haber leído este tutorial!

 

4. Referencias

La entrada Introducción a Actions on Google se publicó primero en Adictos al trabajo.

Primeros Pasos con Flutter

$
0
0

Sumario

1. Introducción

He empezado a trastear con Flutter y me gustaría compartir con todos vosotros lo que he aprendido.

En este tutorial explicaremos brevemente qué es Flutter, veremos cómo se instala y realizaremos un ejemplo práctico de una simple aplicación desarrollada utilizando este Framework.

Empezamos ……………

2. Entorno

Este tutorial está escrito utilizando el siguiente entorno:

  • Hardware: Dell Inspiron 15’ (2.50 GHz Intel Core i7, 16 GB DDR3 )
  • Sistema Operativo: Ubuntu 18.04.2 LTS
  • Entorno de desarrollo: Android Studio 3.3

3. ¿Qué es Flutter?

Flutter es un SDK de Google creado para desarrollar aplicaciones nativas multiplataformas, que  permite a los desarrolladores crear aplicaciones  iOS y Android utilizando exactamente el mismo código.

La principales características son:

  • Permite crear aplicaciones nativas multiplataforma (como se indicó anteriormente).
  • Framework Reactivo.
  • El core está escrito en C++, por lo que es muy rápido.
  • Utiliza como lenguaje de programación Dart. Dart es un lenguaje orientado a objetos con varias características útiles: mixint, generics, isolates y tipos opcionales estáticos.
  • Motor propio de renderización basado en Skia (No utiliza el Web View de las aplicaciones híbridas ni los widgets que vienen en los dispositivos).
  • Existen una gran cantidad de widgets propios listos para ser utilizados. Las aplicaciones en Flutter están compuestas por un árbol de widgets, que mantienen entre ellos una relación de padre-hijo.
  • Tiene una funcionalidad llamada Hot Reload que permite realizar los cambios en caliente, sin necesidad de parar y arrancar la aplicación. Esto hace que la programación sea más productiva y con menos esperas.
  • Más rápido y eficaz que React Native, Native Script, Ionic, …

Todo en Flutter es un widget. Existen dos tipos de widget: Stateful (con estado) y Stateless (Sin estado). Los widgets Stateful permiten interacción por parte del usuario, pudiendo cambiar su estado y por lo tanto su apariencia en el UI.

Cuando hablamos del estado de un widget (State), nos estamos refiriendo a valores que pueden cambiar, como por ejemplo un slider, un checkbox, ….

Sin más preámbulos pasemos a la faena, para ver realmente cómo podemos hacer aplicaciones con Flutter.

4. Instalación

Los pasos para la instalación de Flutter, en función del sistema operativo que queramos utilizar,  los podemos encontrar en la siguiente página https://flutter.dev/docs/get-started/install.

En nuestro caso vamos a utilizar Linux.

Instalación del SDK de Flutter

  1. Descargamos la última versión estable de Flutter

  1. Descomprimimos el fichero en la ubicación donde lo queremos instalar

  1. Añadimos Flutter a nuestro PATH

  1. Ejecutamos Flutter doctor para verificar si existe alguna dependencia necesaria que aún no tenemos instalada.

El resultado de esta ejecución mostrará la siguiente información:

Como podemos observar, flutter doctor nos informa sobre las dependencias pendientes de configurar.

Instalamos la librería lib32stdc++6 de 32 bits que nos solicita, ya que estamos en un entorno de 64 bits.

Ejecutamos el siguiente comando para aceptar todas las licencias solicitadas por el SDK

Una vez aceptadas todas las licencias se mostrará el siguiente mensaje en pantalla

Instalamos en Android Studio los Plugins indicados (Flutter y Dart).

Para ello abrimos el Android Studio.

Hacemos click en “Configure” → “Plugins

Haciendo click en “Browse repositories”, buscamos el Plugin para Flutter, lo seleccionamos y lo instalamos.

Cuando instalamos el Plugin de Flutter, nos indicará que necesitamos una dependencia con otro Plugin, en este caso será el de Dart, que tendremos que instalar también.

Si aceptamos, tendremos los dos Plugins instalados.

Volveremos a ejecutar el flutter doctor y deberíamos tener todo correcto

 

5. Creación de un Proyecto con Flutter

Vamos a mostrar cómo crear una primera aplicación en Flutter desde Android Studio. El proceso es muy sencillo y solo tenemos que seguir los siguientes pasos:

Abrimos Android Studio y en la pantalla principal seleccionamos “Start a new Flutter project

 

En la siguiente pantalla seleccionamos que queremos crear una Aplicación Flutter.

Le indicamos el nombre del proyecto. En este caso dejamos el nombre por defecto que nos da Android Studio “flutter_app”.

Seleccionamos el paquete principal de nuestro proyecto. Del mismo modo que en el caso anterior, dejaremos el nombre por defecto que nos da Android Studio.

Pulsamos “Finish” y ya habremos creado nuestra primera aplicación en Flutter.

Si ahora lanzamos la aplicación en nuestro emulador, tendremos ejecutando nuestra primera aplicación Flutter.

6. Ejemplo Práctico

En este apartado partiremos de una aplicación Flutter que he desarrollado como ejemplo y cuyo código lo podéis descargar de mi github https://github.com/vtcmer/flutter-product.git.

La aplicación es muy sencilla, mostrará un listado de productos. Podremos configurar la aplicación para que arranque simulando un entorno productivo o un entorno de desarrollo con un mock.

Estructura del proyecto:

En la carpeta lib del proyecto encontraremos todo el código implementado.

Ahora pasaré a explicar cada una de las parte de este código.

  1. main.dart: Clase principal que se encarga de lanzar la aplicación.

Como comentamos anteriormente, para crear aplicaciones en Flutter tenemos que utilizar Dart. Todos los proyectos Dart comienza con una función main que se encarga de iniciar la aplicación.

Creamos nuestro propio widget de tipo StatelesWidget (MyApp, ocupará el 100% de la pantalla), que será el que pasamos por parámetro al método runApp. En la propiedad home le indicaremos cual será el widget que se corresponderá con el UI principal de nuestra aplicación.

El estado de un widget se guarda en un objeto de tipo State (será nuestra clase_HomePageState  que veremos más adelante); de esta manera se mantiene separado el estado del widget de su apariencia. Cuando un objeto cambia de estado, el objeto que extiende de la clase State llama a setState() y es entonces cuando el widget se actualiza.

  1. View: Vista de la aplicación.

En este caso tendremos:

  • product_list_view.dart: Se trata de una interfaz a modo de Callback. Su objetivo es proporcionar la firma de los métodos que se encargarán de renderizar los datos en el UI y realizar las operaciones correspondientes en caso de un error (por ejemplo mostrar un mensaje identificando dicho error).

  • home_page.dart: UI de la aplicación que renderizará el listado de productos. Será un widget de tipo StatefulWidget,.

 El fichero home_page.dart, internamente contendrá:

  • Una clase HomePage que es un widget de tipo StatefulWidget (con estado), que se actualizará con los productos que recuperemos de nuestro repositorio.
  • Un objeto de tipo State (_HomePageState). Ya que el widget principal es un widget con estado, tenemos que implementar esta clase.

En la clase _HomePageState vamos a implementar los siguiente métodos:

  • initState: Este método se invoca cuando es creado el estado e insertado el objeto en el árbol de widgets. Aprovechamos este punto para realizar una petición al presentador y solicitar el listado de productos.
  • build: Devuelve el widget a renderizar. Este widget es de tipo Scaffold, que tiene una estructura predefinida

  • _productWidget: Devuelve un widget de tipo Contenedor con el listado de productos a mostrar.
  • _getItem: Devuelve un widget especial de tipo ListTile para renderizar cada uno de nuestros productos.
  1. entities: Este paquete contendrá una única entidad (dart) que será la representación de nuestro modelo de datos. En ese caso será un producto, que contendrá un identificador interno y un nombre.

  1. exceptions: Paquete que contendrá las excepciones de la aplicación. En este caso sólo tendremos una excepción para capturar un error en la operación de búsqueda de productos.

  1. presenter: Vamos a implementar un patrón de arquitectura MVP (modelo-vista-presentador) y en este paquete estará la clase que se encarga de solicitar los datos al repositorio y enviarlos a la vista.

  1. repositories: Este paquete contiene las clases encargadas de realizar el acceso a los datos. Ya que se trata de un pequeño proyecto de ejemplo, accederemos directamente desde el presentador al repositorio y nos saltaremos la capa de negocio.

Tendremos tres clases:

  • product_repository.dart (ProductRepository): Interfaz con las firmas de los métodos que tendrán que implementar nuestras concreciones.

  • product_repository_mock.dart (ProductRepositoryMock): Implementación de nuestra interfaz que se utilizará como mock.

  • product_respository_impl.dart (ProductRepositoryImpl): Implementación de nuestra interfaz que se utilizará en el entorno productivo. Debería implementarse un acceso a base de datos o servicio Rest, pero para simplificar el ejemplo, se devolverán los datos desde una lista (Lo podremos mejorar en futuras entradas :)).

  1. di: Este paquete contiene la clase dart, que como su propio nombre indica, se encarga de realizar la inyección de dependencias. En función del entorno establecido cuando se arranca la aplicación (main.dart), esta clase deberá identificar qué instancia de nuestro repositorio utilizar.

 

Arrancamos la aplicación para ver el resultado con cada uno de los entorno que tenemos configurados:


7. Conclusiones

 

Hemos visto los primero pasos con Flutter, cómo instalarlo y hacer una aplicación básica para ir tomando contacto con este nuevo Framework de desarrollo. Cada vez van surgiendo más y más widget que facilitan los desarrollos de nuevas funcionalidades.

Flutter (Flutter + Dart) será el Framework oficial de desarrollo para el nuevo sistema operativo que está desarrollando Google (Fuchsia), con lo que parece que tiene un futuro prometedor.

En próximas entradas trataré de mostrar nuevas funcionalidades, para ver las muchas posibilidades que nos puede ofrecer este Framework de desarrollo.

A continuación os dejo algunos enlaces de interés relacionados:

https://flutter-es.io/docs

https://www.dartlang.org/guides/language/language-tour

https://material.io/develop/flutter/

https://en.wikipedia.org/wiki/Google_Fuchsia

 

La entrada Primeros Pasos con Flutter se publicó primero en Adictos al trabajo.


JNI: Ejecutar algoritmos en C desde Java

$
0
0

Ejecutar algoritmos en C tiene sus ventajas y muchas veces puede que queramos hacerlo desde Java. Con JNI, esta tarea es muy sencilla. Ya existe algún tutorial hablando de JNI, pero de vez en cuando viene bien actualizar los contenidos para mantener el ritmo de evolución de la tecnología.

Como siempre, antes de comenzar, aquí os dejo un enlace al proyecto de ejemplo en GitHub.

JNI: qué es y cómo funciona

Para facilitarnos la tarea, Java ofrece su propio mecanismo: JNI (Java Native Interface). Con JNI podemos wrappear nuestro código C y traducir automáticamente objetos primitivos entre C y Java.

El archivo C que queramos ejecutar debe incluir la cabecera jni.h. De esta forma, ya podrá trabajar con los diferentes objetos que ofrece JNI:

JNI object types
Captura de la documentación oficial

Ahora bien, los métodos en C que queramos llamar desde el mundo Java deben seguir una sintaxis especial (no basta solo con importar JNI). Como esta sintaxis es un poco fuera de lo común, Java pone a nuestra disposición el comando Javah. Con este comando podemos, a partir de una clase Java, generar el archivo de encabezado (.h) de nuestro wrapper en C, y después simplemente debemos implementarlos.

Ejemplo: calculadora de sumas y restas

En cualquier aplicación Java podríamos crear una clase como la siguiente:

public class JavaCalculator {

    public int sum(int a, int b){
        return a + b;
    }

    public int multiply(int a, int b){
        return a * b;
    }

}

Ahora bien, supongamos que queremos que estos algoritmos se ejecuten en C. Ya sea para mejorar su eficiencia si son complejos o porque es un software completo y grande que queremos ejecutar desde Java llamando a alguna de sus funciones sin tener que traducir todo ese tedioso código a Java.

Supongamos, entonces, que tenemos el mismo código, pero escrito en C o C++:

#include "calculator.h"

int sum(int x, int y){
    return x + y;
}

int multiply(int x, int y){
    return x * y;
}

¿Cómo llamar a esas dos funciones? Para ello necesitamos nuestro wrapper, que traduzca dos enteros desde el mundo Java de forma que puedan ser entendidos en el mundo C.

Para hacerlo automáticamente, vamos a utilizar los comandos que Java nos ofrece. Debemos crear nuestra clase Java tal y como os he mostrado antes pero, en lugar de implementar cada método, los marcamos como nativos y sólo los declaramos, a modo de interfaz:

public class JavaCalculator {

    public native int sum(int a, int b);

    public native int multiply(int a, int b);

}

Esto, evidentemente, no va a funcionar. Ahora abrimos una terminal y viajamos hasta donde tenemos nuestro archivo JavaCalculator.java con el código. Debemos compilar con Javac el archivo indicándole que genere nuestro archivo .h para implementarlo en C:

javac JavaCalculator.java -h .

Esto nos creará un archivo cuyo nombre estará basado en la estructura de paquetes de nuestra aplicación. En este caso, el nombre exacto es:

com_urbanojvr_jniexample_JavaCalculator.h

Y veamos qué contiene este archivo escrito en C:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_urbanojvr_jniexample_JavaCalculator */

#ifndef _Included_com_urbanojvr_jniexample_JavaCalculator
#define _Included_com_urbanojvr_jniexample_JavaCalculator
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_urbanojvr_jniexample_JavaCalculator
 * Method:    sum
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_urbanojvr_jniexample_JavaCalculator_sum
  (JNIEnv *, jobject, jint, jint);

/*
 * Class:     com_urbanojvr_jniexample_JavaCalculator
 * Method:    multiply
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_urbanojvr_jniexample_JavaCalculator_multiply
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif

Al principio, en el primer comentario, nos indica claramente que no modifiquemos este archivo. Es importante mantener la sintaxis que nos ha puesto en los nombres de las funciones. JNIEXPORT y jnicall son instrucciones necesarias para poder gestionar el entorno de JNI desde C. El nombre de la función Java_com_urbanojvr_jniexample_JavaCalculator_sum no debemos cambiarlo. Si lo cambiamos nos dará error ya que será imposible para el entorno de JNI poder encontrar el punto de entrada al mundo C.

Además, podemos observar que el tipo de función es jint. Esto significa que la función devuelve un entero del contexto de Java. Vemos también que la función tiene dos parámetros que nosotros no hemos añadido: JNIEnv* y jobject. Estos dos primeros parámetros son obligatorios. Más adelante veremos la importancia de poder apuntar al entorno de Java para, por ejemplo, traducir cadenas de texto de Java (jstring) a cadenas de caracteres de C. El resto de parámetros de los métodos son los que nosotros hemos añadido. Si modificamos nuestra lógica y añadimos o quitamos parámetros, podemos modificarlos sin problema; siempre y cuando los dos primeros sean los mencionados anteriormente.

Implementación de las funciones nativas

Antes vimos qué lógica tenía nuestra calculadora básica de ejemplo. Ahora, gracias a javac -h, ya tenemos nuestro archivo de encabezado con la declaración de las funciones. Lo único que tenemos que hacer es implementarlas. Creamos un archivo .c que incluya el archivo .h mostrado antes con la lógica de las funciones. Le he llamado igual que su clase cabecera, para que quede claro que es la implementación de nuestro wrapper: com_urbanojvr_jniexample_JavaCalculator.c.

#include "com_urbanojvr_jniexample_JavaCalculator.h"

JNIEXPORT jint JNICALL Java_com_urbanojvr_jniexample_JavaCalculator_sum
  (JNIEnv *env, jobject jobj, jint jinteger1, jint jinteger2){

    int x = jinteger1;
    int y = jinteger2
    int sum = x + y;

    return sum;

  }

JNIEXPORT jint JNICALL Java_com_urbanojvr_jniexample_JavaCalculator_multiply
  (JNIEnv *env, jobject jobj, jint jinteger1, jint jinteger2){

    int x = jinteger1;
    int y = jinteger2;
    int mult = x * y;

    return sum;

  }

Como podemos ver en este código, es tan simple com una función normal de C o C++. La implementación se llama igual que la declaración pero contiene la lógica, no es ningún concepto nuevo. Lo único que puede resultar lioso es la sintaxis algo fuera de lo común. Pero nada más.

Compilando el código C

Esto dependerá de nuestro sistema. Es posible ejecutar el código C o C++ incluso en sistemas Android (si te interesa, busca NDK). Pero, evidentemente, debemos tener previamente nuestra librería compilada para el sistema que se va a utilizar. Para los sistemas basados en Unix (MacOS en mi caso) podemos utilizar el GCC para copilarla. Primero debemos compilarla como un objeto c común (.o) y, después, crear el shared object (.so) para poder ser importado.

Para compilar a .o:

gcc -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_urbanojvr_jniexample_JavaCalculator.c -o com_urbanojvr_jniexample_JavaCalculator.o

Es importante incluir nuestro directorio Java con las carpetas include y include/darwin porque contienen los archivos jni.h y jni_md.h, necesarios para poder utilizar las herramientas proporcionadas por JNI. Generalmente, en sistemas MacOS, las direcciones a incluir son:

  • -I /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/include/
  • -I /Library/Java/JavaVirtualMachines/jdk1.8.0_201.jdk/Contents/Home/include/darwin/

A partir de com_urbanojvr_jniexample_JavaCalculator.o generamos el .so:

gcc com_urbanojvr_jniexample_JavaCalculator.o -shared -o libcalculator.so

Importar librería nativa en Java

Ahora bien, esto todavía no funciona. Tenemos en nuestra calculadora de Java las funciones nativas, pero no hemos importado la librería. Debemos añadir dicha importación, y para ello Java nos proporciona algunos métodos como este:

static {
        System.loadLibrary("calculator");
    }

loadLibrary vs load

Es importante que, si queremos que nuestra librería pueda ser utilizada automáticamente, la creemos con ese nombre: libMILIBRERIA.so, ya que al importarla en el código visto en Java (utilizando loadLibrary), con poner el nombre MILIBRERIA el sistema, automáticamente, añadirá «lib» delante y «.so» detrás. También debo avisar de que, si queremos que esto funcione, debemos tener nuestro .so guardado en el directorio donde se encuentran las librerías de Java (java.library.path), que será diferente en cada sistema. Si no hacemos esto, Java nos lanzará un error:

no calculator in java.library.path (donde «calculator» será, evidentemente, el nombre que le hayamos dado al importar la librería).

Para ahorrarnos esto, podemos importar directamente la librería dándole la ruta absoluta con el nombre completo de la misma y así no busca en los directorios automáticos. Pero para esto, en lugar de usar loadLibrary debemos utilizar el método load:

public static final String ABSOLUTE_LIB_DIR = "Put HERE absolute path to your library.so";

    static {
        System.load(ABSOLUTE_LIB_DIR + "libcalculator.so");
    }

Solo debemos rellenar nuestra ruta absoluta y ya tenemos linkeado nuestro proyecto Java con el código C.

Main de ejemplo

Para completar la explicación, en el proyecto os incluyo un main con el que probar las operaciones:

public class Main {

    public static void main (String[] args){
        int num = 20;
        int num2 = 10;

        JavaCalculator javaCalc = new JavaCalculator();

        int sum = javaCalc.sum(num, num2);
        int mult = javaCalc.multiply(num, num2);

        System.out.println(num + " + " + num2 + " = " + sum);
        System.out.println(num + " x " + num2 + " = " + mult);
    }

}

Y ya está. Ahora podrás ejecutar código C sin tener que traducirlo, pudiendo importar algoritmos ya escritos y crear nuevas funciones que se ejecuten directamente en el procesador de tu dispositivo, mejorando su eficiencia.

Si quieres profundizar más en esto, te recomiendo leer sobre el NDK. ¡Así podrás hacer lo mismo en Android!

La entrada JNI: Ejecutar algoritmos en C desde Java se publicó primero en Adictos al trabajo.

Crear una instancia EC2 a partir de una máquina virtual local

$
0
0

Índice de contenidos

1. Entorno

Este tutorial está escrito usando el siguiente entorno:

  • Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
  • Sistema Operativo: Linux Mint 19

2. Introducción

Resulta que tienes en local una máquina virtual con un montón de cosas instaladas súper curradas y ahora quieres que otras personas puedan trabajar contigo para ampliarla, ¿qué haces?

La solución puede ser crear una instancia EC2 en AWS a partir de esa máquina virtual y en este tutorial vamos a ver cómo hacerlo.

3. Vamos al lío

Nota: para seguir este tutorial necesitarás una cuenta de AWS y conlleva costes en la factura.

Partimos de que tenemos exportada nuestra máquina virtual a un fichero export.ova

Lo primero que vamos a hacer es instalar el CLI de AWS que nos evitará tener que usar la consola vía web. Para hacer la instalación en Ubuntu de la última versión es tan sencillo como ejecutar:

$> curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"
$> unzip awscli-bundle.zip
$> ./awscli-bundle/install -b ~/bin/aws

Y dentro del fichero .bashrc establecemos el ejecutable dentro del PATH:

export PATH=~/bin:$PATH

Hecho esto, lo que nos resta es configurar las herramientas con nuestras credenciales de AWS ejecutando el comando:

$> aws configure

Este comando nos va a solicitar introducir nuestro «AWS Access Key ID», nuestro «AWS Secret Access Key», un «Default Region Name» (es importante que lo establezcas correctamente, si por ejemplo, estás trabajando en eu-west-1b, la región que tienes que poner es eu-west-1) y por último el formato preferido de salida, que podemos establecer como yaml o json.

Ahora vamos a crear un bucket para subir nuestro fichero export.ova, para ello vamos a usar el CLI y vamos a ejecutar:

$>  aws s3 mb s3://

Nota: cambia por el nombre que le quieras dar al bucket.

Ahora para subir el fichero al bucket recién creado tenemos que ejecutar el comando:

$> aws s3 cp export.ova s3:///

Nota: este proceso será más o menos lento dependiendo del tamaño en GB de tu máquina virtual y el ancho de subida que tengas disponible. Haciéndolo desde la línea de comandos te aseguras de que la subida por mucho que tarde no se va a detener por pérdida de sesión como si puede ocurrirte si lo hacer a través de la consola web. Te lo digo por experiencia 😉

Mientras se sube el .ova podemos ir preparando los ficheros necesarios para la conversión de OVA a AMI.

El primer fichero a preparar es el que permite la importación de la máquina virtual, y tiene el siguiente contenido:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Service": "vmie.amazonaws.com" },
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals":{
         "sts:Externalid": "vmimport"
      }
    }
  }]
}

Para habilitarlo en la cuenta, en el mismo directorio donde hayamos almacenado el fichero, tenemos que ejecutar:

$> aws iam create-role --role-name vmimport --assume-role-policy-document file://trust-policy.json

Ahora creamos otro fichero role-policy.json con el rol y la política de permisos que tenemos que aplicar, tendrá el siguiente contenido:

{
 "Version": "2012-10-17",
 "Statement": [{
   "Effect": "Allow",
   "Action": [
     "s3:ListBucket",
     "s3:GetBucketLocation",
     "s3:FullAccess"
   ],
   "Resource": [
     "arn:aws:s3:::"
   ]},
   {
     "Effect": "Allow",
     "Action": [
       "s3:GetObject"
     ],
     "Resource": [
       "arn:aws:s3:::/*"
     ]
   },{
     "Effect": "Allow",
     "Action":[
       "ec2:ModifySnapshotAttribute",
       "ec2:CopySnapshot",
       "ec2:RegisterImage",
       "ec2:Describe*",
       "ec2:FullAccess"
     ],
     "Resource": "*"
   }
 ]
}

Y lo aplicamos ejecutando:

$> aws iam put-role-policy --role-name vmimport --policy-name vmimport --policy-document file://role-policy.json

Ahora vamos a crear el fichero que le va a indicar a AWS dónde está nuestro OVA para que inicie la conversión a AMI. Tendrá el siguiente contenido y lo guardamos con el nombre container.json:

[{
  "Description": "My OVA",
  "Format": "ova",
  "UserBucket": {
    "S3Bucket": "",
    "S3Key": "export.ova"
  }
}]

El siguiente paso solo se puede aplicar cuando se haya finalizado la subida del fichero .ova al bucket, solo entonces podemos ejecutar:

$> aws ec2 import-image --description "My OVA" --license-type BYOL --disk-containers file://containers.json

Este proceso es asíncrono así que una vez está lanzado podemos consultar el estado del proceso con el siguiente comando:

$> aws ec2 describe-import-image-tasks --import-task-ids import-ami-xxxx

Nota: las xxxx se corresponde con el identificador de la respuesta que nos dio en el anterior comando a este último; y los estados que nos podemos encontrar son:

  • active –> el proceso de importación está en progreso.
  • deleting –> el proceso de importación está siendo cancelado.
  • deleted –> el proceso de importación está cancelado.
  • updating –> el proceso de importación se está actualizando.
  • validating –> el proceso de importación se esta validando.
  • converting –> el proceso de importación está convirtiéndose en AMI.
  • completed –> el proceso de importación está completado y el AMI se puede utilizar ya.

Cuando nos devuelva completed solo nos resta ir al listado de instancias y como hicimos en el anterior tutorial creamos una instancia EC2 con el detalle de que en el primer paso seleccionamos la opción «My AMIS» encontrando el AMI que acabamos de crear a partir del fichero .ova.

El resto de pasos son iguales, con el detalle de que en el paso de crear volúmenes, éste ya viene creado dado que tiene todo el contenido de nuestra máquina virtual.

4. Conclusiones

Como ves, el proceso para publicar una máquina virtual como AMI en AWS no es complicado y aporta la ventaja fundamental de que varias personas puedan interactuar en la misma máquina, sin tener que hacer una instalación desde cero.

Cualquier duda o sugerencia en la zona de comentarios.

Saludos

La entrada Crear una instancia EC2 a partir de una máquina virtual local se publicó primero en Adictos al trabajo.

Implementando un crawler sencillo con Jsoup

$
0
0

Con un crawler podemos examinar un sitio buscando todos sus enlaces para, después, poder buscar lo que deseemos en cada página. Es el primer paso para poder acceder a la información. El objetivo de este tutorial es indexar todas las URLs de un sitio web y poder acceder fácilmente a toda su información de una manera más ordenada.

El web scraping es el arte de extraer datos de sitios web. Normalmente, en la web los documentos vienen en formatos estándar HTML y de forma desestructurada. Los datos están ahí, pero para darles uso es necesaro interpretarlos y estructurarlos de forma accesible y útil. Una persona lee una web y entiende lo que pone pero ¿y si queremos automatizarlo? ¿Y si queremos analizar todos los productos de una categoría de Amazon, por ejemplo, y que nos avise cuando alguno baja de precio? ¿y si queremos tomar estadísticas de documentos no tabulados? Es entonces cuando un trabajo manual se vuelve demasiado largo y tedioso, y para eso están los crawlers o spiders, que nos ayudan automatizando esta labor.

Qué es

Jsoup es una libreria Java que proporciona operaciones para trabajar con HTML. Permite extraer y manipular datos, que podrán ser utilizados convenientemente para nuestras necesidades.

Con Jsoup podemos construir desde parseadores básicos de HTML para analizar y procesar páginas estáticas hasta herramientas de análisis recursivo de sitios completos (crawlers o spiders). No obstante, Jsoup está más pensado para análisis de páginas estáticas que para un crawler complejo. Si lo que queremos es recopilar diferentes tipos de datos de un sitio completo independientemente de sus URLs, puede ser más adecuado utilizar Crawler4j.

Cómo usar Jsoup

Para poder utilizar Jsoup basta con descargarse su jar correspondiente desde la web oficial. No obstante, se encuentra en el repositorio oficial de Maven. Por tanto, usaremos mejor la dependencia Maven para este caso. Podeis acceder al código fuente de este proyecto maven de ejemplo en este repositorio de GitHub.

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.11.3</version>
</dependency>

Con la librería ya importada, creamos un main vacío para ejecutar ejemplos. Además, para tratar de hacer el ejemplo con mejor código, en mi caso he creado una clase aparte en la que iré metiendo las operaciones que luego llamaremos desde el main. Esta clase la he llamado “ParserEngine”. Pero esto queda a vuestra libertad.

Para los ejemplos que vamos a ejecutar partiremos de un ejemplo base de la documentación oficial, que iremos modificando para indexar todas las URLs de la web que deseemos.

Ejemplo básico: lista de las URLs de una página determinada.

El primer método, basado (como he indicado) en un ejemplo de la documentación, recopilará e imprimirá en pantalla todas las URLs encontradas a lo largo de todo el documento HTML obtenido de una URL concreta.

En la clase ParserEngine insertamos este nuevo método:

public void listAllLinks(String url) throws IOException {
   System.out.println("Parsing page " + url + "...");

   Document doc = Jsoup.connect(url).get();
   Elements links = doc.select("a[href]");
   Elements media = doc.select("[src]");
   Elements imports = doc.select("link[href]");

   print("\nMedia: (%d)", media.size());
   for (Element src : media) {
       if (src.tagName().equals("img"))
           print(" * %s: <%s> %sx%s (%s)",
                   src.tagName(), src.attr("abs:src"), src.attr("width"), src.attr("height"),
                   trim(src.attr("alt"), 20));
       else
           print(" * %s: <%s>", src.tagName(), src.attr("abs:src"));
   }

   print("\nImports: (%d)", imports.size());
   for (Element link : imports) {
       print(" * %s <%s> (%s)", link.tagName(),link.attr("abs:href"), link.attr("rel"));
   }

   print("\nLinks: (%d)", links.size());
   for (Element link : links) {
       print(" * a: <%s>  (%s)", link.attr("abs:href"), trim(link.text(), 35));
   }

}

Elementos clave de la librería

En este extracto de código vemos algunos elementos clave de Jsoup:

  • Document: es el objeto base de la librería. Contiene el HTML parseado de la dirección que estamos inspeccionando. Podemos obtener todo el código del contenido con el método document.outerHtml().
  • Element: es el componente mínimo de document. En plural, Elements, hace referencia a una lista de los mismos (extiende de ArrayList<Element>).
  • Método select(): este método es el que hace la magia de Jsoup. Es implementado tanto por Document como por Element y Elements. Admite buscar elementos CSS e incluso jquery. De esta forma, conociendo un poco cómo está construido el sitio web que queramos analizar, podemos buscar elementos concretos. En la documentación oficial viene bastante bien definido. Podemos buscar por nombre de elemento (como <id id = «myDiv»> o incluso clases). Es bastante potente este método y admite incluso expresiones regulares. Para hacer un buen scraper es vital saber qué queremos buscar y saber qué expresion utilizar con este método.

Par poder ejecutar exitosamente el ejemplo anterior, como habrás observado, es necesario tener implementados los métodos print y trim, que generarán las cadenas de texto propias para formatear el texto e imprimirlo. Os los dejo aquí:

private static void print(String msg, Object... args) {
   System.out.println(String.format(msg, args));
}

private static String trim(String s, int width) {
   if (s.length() > width)
       return s.substring(0, width-1) + ".";
   else
       return s;
}

Hasta ahora, nada nuevo. Lo único que hacemos es hacer una petición de una página y buscar ciertos elementos en el HTML devuelto.

Probando el ejemplo: imprimiendo los links de una página

Editamos nuestro main para crear un ejemplo de ejecución a cualquier web. En el caso de ejemplo usaré mi blog personal (así aprovecho y hago un poco de spam).

public static void main(String[] args) throws IOException {
   ParserEngine parser = new ParserEngine();
   String url = "http://elfreneticoinformatico.com";
   parser.listAllLinks(url);
}

Obtenemos un resultado como este:

Result of Jsoup default doc exampleProbablemente, sobretodo si lees esto desde un dispositivo móvil, la imagen se vea demasiado pequeña. Te recomiendo encarecidamente que la amplíes. Obtenemos, como vimos en el código, 3 listas de enlaces.

Tipos de datos obtenidos

Con este ejemplo podemos ver un ejemplo de 3 tipos de búsquedas que podemos hacer:

  • Media: enlaces a contenido insertado en el documento HTML. En este caso, scripts e imágenes. Con estos datos podemos obtener mucha información de la web que estamos analizando. Sin ir más lejos, con el penúltimo enlace de tipo <script> ya sabemos que mi blog utiliza el tema hemingway en su versión 1.74, que Google es el proveedor de anuncios e incluso, si quisiéramos, podríamos automáticamente descargar las fotos de esta web. ¿Conocéis el caso de Facemash, aquella “travesura” del creador de Facebook? Pues acabamos de ver que obtener las imágenes de un sitio web no es, para nada, algo difícil.
  • Imports: todas las importaciones (generalmente en el head). Vemos, por ejemplo, el nombre de la hoja de estilos del plugin simple code highlihter, el plugin que utilizo para insertar código fuente en las entradas.
  • Links: estos son los enlaces. Hacia fuera o dentro del sitio. Cuando añades un hipervínculo como este, estás insertando un <a href=”misitio.com”> que, por supuesto, jsoup sabe interpretar y así, podemos ver todas las URLs de una web. Los títulos de las entradas tienen un link al artículo completo, Amazon tiene, en cada artículo, un link a la página propia del artículo. Además, en cada artículo, puedes acceder a más artículos por la sección de recomendados que aparece debajo. Si accedemos de forma recursiva a estos enlaces, ¿te das cuenta de todo lo que podemos hacer? Bienvenido al web scraping.

Estos tipos obtenidos no son, ni mucho menos, los únicos que podemos obtener. Ahora nos estamos centrando solo en los links de un documento, que los necesitaremos para nuestro crawler, pero podemos obtener absolutamente cualquier tipo de elemento, buscar texto, etc.

¿Qué hace un crawler?

Un crawler analiza la información de un sitio web. También se conocen como arañas (spyder) o bots. Accede a una URL y hace una petición, como la que hemos hecho nosotros. Esta dirección puede venir dada de base como directorio raíz o puede haber llegado desde otra. Tras analizar el resultado, busca nuevas URLs en el documento obtenido y accede, de forma recursiva, a cada una de ellas. De esta forma, podemos explorar el contenido completo de un sitio web obteniendo todas sus direcciones. Es decir, todas las URLs que enlazan a absolutamente todo su contenido. Es muy potente y, a la vez, muy simple (si se hace bien). Evidentemente, según el sitio que queramos analizar y el tipo de información que queramos recuperar, habrá que aplicar diferentes directrices al algoritmo.

Siempre que hablo del web scraping, lo describo como un arte. No se trata solamente de un simple algoritmo que extrae datos. Es muy importante saber qué información queremos obtener, cómo está formateada en el sitio, cómo se organiza y cómo poder recorrerlo entero evitando el contenido que no nos interesa.

Es por esto que existen muchísimas plataformas de web scraping ofreciendo estos servicios. Existen, y cada vez más comunes, los sitios DAAS (Data As A Service). Muchos de ellos llaman a estos servicios como Datafiniti. Se trata de ahorrarte precisamente lo tedioso del scraping. Ellos tienen sus bots analizando infinidad de sitios web y tú, mediante unas simples llamadas a APIs, puedes obtener los datos que quieres estructurados adecuadamente bajo estándares como JSON. Algunos ejemplos de estos servicios son ScraperAPI, ParseHub o Mozenda. Y existen muchísimos más.

Diseñando nuestro crawler

En nuestro sencillo ejemplo lo único que queremos es indexar de forma recursiva todas las URLs posibles de un sitio web determinado con nuestro propio crawler. Para ello, partiremos de una URL raíz. La filtraremos con Jsoup y obtendremos los enlaces que nos interesen. En nuestro caso, de los 3 tipos vistos antes, solo nos interesan los enlaces de tipo <a href….>, que son los que llevan a otras direcciones.

Eso sí, debemos tener en cuenta que un hipervínculo puede llevar a sitios externos. Por ejemplo, un artículo que recomienda un producto seguramente tenga un enlace al producto en Amazon. Y si no controlamos esto, nuestro crawler se irá hasta Amazon y empezará a analizar recursivamente todo Amazon. Y claro, Amazon es una web muy pequeña, ¿verdad?. Si llegamos a sitios como Amazon por error, podemos dar por sentado que comenzar a analizar recursivamente todos los enlaces del sitio es lo mismo que un bucle infinito. Así que, antes de acceder a cada enlace, debemos verificar que es un enlace perteneciente al sitio que deseamos analizar.

Filtrando solo los enlaces interesantes

Lo primero que vamos a hacer va a ser centrarnos en el tercer tipo de enlaces del ejemplo base. Los links. No nos interesan ni las importaciones ni los objetos media embebidos. Nuestro método listAllLinks queda ahora reducido a esto:

public void listAllLinks(String url) throws IOException {
        System.out.println("Parsing page " + url + "...");

        Document doc = Jsoup.connect(url).get();
        Elements links = doc.select("a[href]");

        print("\nLinks: (%d)", links.size());
        for (Element link : links) {
            print(" * a: <%s>  (%s)", link.attr("abs:href"), trim(link.text(), 35));
        }

    }

Implementación recursiva

Ahora le cambiamos el nombre (para mejorar la legibilidad, ya que ahora será recursivo). Además, nuestra clase ParserEngine ahora tendrá dos atributos:

private String baseUrl;
private ArrayList<String> urlList;

El primero es la raíz de nuestro scraping. Si analizamos el sitio miweb.com, la raíz es miweb.com y no debemos perderla de vista. Así podemos evitar que se vaya a otras webs e indexar URLs no deseadas. De esta forma evitamos el error anterior de irnos a sitios indeseados, ya que siempre verificaremos que nuestro sitio contentga baseUrl.

Ya que tenemos dos atributos, vamos a meterle también un constructor:

public ParserEngine(String baseUrl){
        this.baseUrl = baseUrl;
        this.urlList = new ArrayList<String>();
    }

Nuestro método crawler quedaría algo así:

public void crawl(String url) throws IOException {
        Document doc = Jsoup.connect(url).get();
        Elements links = doc.select("a[href]");

        for (Element link : links) {
            String actualUrl = link.attr("abs:href");

            if (!urlList.contains(actualUrl) &
                actualUrl.startsWith(baseUrl)){

                print(" * a: <%s>  (%s)", actualUrl, trim(link.text(), 35));
                urlList.add(actualUrl);
                crawl(actualUrl);

            }
        }
    }

En esa sentencia if filtramos 2 cosas importantísimas:

  1. La URL que vamos a insertar no debe existir en la lista de URLs ya añadidas por nuestro crawler. En todas las webs es común que se repitan enlaces. Sobretodo al directorio raíz. Si estamos en un blog, todos los artículos en la cabecera tienen, normalmente, el título del sitio y, si haces click en él, te lleva a la página de inicio miblog.com. Por tanto, si no verificamos esto, accederemos infinitamente a la página de raíz, ya que la página raíz también tiene, en la primera posición a la que accedemos, la misma cabecera con el enlace a la página de inicio. Si no incluímos esto sólo veremos la web raíz infinitamente.
  2. Debe comenzar con la dirección raíz. Lo explicado antes, para evitar irnos, por ejemplo, a Amazon.

Main para ejecución

Tras tener claro todo esto, podemos escribir nuestro main creando una instancia de ParserEngine y ejecutando nuestro crawler:

public static void main(String[] args) throws IOException {
        String url = "http://elfreneticoinformatico.com";
        ParserEngine parser = new ParserEngine(url);
        parser.crawl(url);
        System.out.println("Crawler finished. Total URLs: " + parser.getUrlList().size());
    }

Ejecutamos y vemos como va avanzando en la recolección de enlaces.

Problemas con el tipo de contenido

Es común en los blogs hechos en WordPress que las imágenes tengan un link a sí mismas. Es decir, que al hacer click sobre ellas accedas a una URLs paginaweb.com/imagen.jpg.

A la hora de indexar todas las URLs de una web con nuestro propio crawler, esto puede presentar un problema porque, visto desde el documento, se trata de un <a href=»enlace a la imagen»> pero luego intenta leer el contenido de esa dirección y se encuentra que no es un HTML, sino una imagen. Nos dará un error como este:

«Unhandled content type. Must be text/*, application/xml, or application/xhtml+xml»

Estos enlaces deben recibir un tratamiento especial pero, para no alargarnos más (que ya es suficiente) simplemente le indicaremos a la conexión que ignore el tipo de documento que está parseando. De esta forma no dará error. Cuando lea el contenido de ese enlace, no añadirá nada a la lista de elementos y el bucle accederá a una lista vacía. Como es recursivo, pasará a la siguiente lista que no esté vacía de la iteración anterior.

Hacer esto es muy sencillo. Sólo debemos cambiar la línea donde instanciamos el objeto Document añadiendo el método para ignorar el tipo de contenido:

Document doc = Jsoup.connect(url).ignoreContentType(true).get();

Ejecución exitosa

Tras un tiempo, habremos recuperado todas las direcciones de un sitio web. En este caso mi blog personal es un sitio pequeño, idóneo para este tipo de ejemplos. Cuando ya ha recorrido todas las páginas de forma recursiva nos mostrará el siguiente resultado:

Felicidades: si obtienes un resultado similar, habrás conseguido indexar todas las URLs del sitio web deseado, !y con tu propio crawler!

Conclusiones

Ahora ya tenemos en una lista TODAS las URLs de un sitio. ¿Quieres saber qué paginas contienen una palabra? Puedes hacerlo. ¿Quieres analizar el número de palabras de cada página? También puedes. ¿Quieres saber cuántos enlaces a sitios externos hay en total, o por cada página? Efectivamente, también puedes. Ahora, con el crawler, tenemos acceso controlado a TODAS las páginas del sitio. Hemos conseguido indexar todas las URLs de una web con nuestro propio crawler. Lo siguiente es centrarte en buscar lo que quieres, utilizando esta lista de páginas como fuente.

Con el tiempo se irá incrementando el número de enlaces, así que es conveniente cada cierto tiempo reactualizar el listado volviendo a ejecutar el crawler.

Otras consideraciones

Otra razón que me he callado por la que he decidido utilizar mi blog personal es porque no tengo ninguna clase de firewall anti-bots. En muchos sitios (amazon, por ejemplo) si empiezas a analizar recursivamente todas las URLs el servidor te echará para evitar sobrecargar sus servidores a base de bots. En ese caso lo normal es pausar la ejecución un tiempo aleatorio. Por ejemplo, entre 5 y 20 segundos. Lo malo de eso es que va a retrasar mucho el proceso, así que ten mucha paciencia.

La entrada Implementando un crawler sencillo con Jsoup se publicó primero en Adictos al trabajo.

Provide/Inject en Vue.js

$
0
0

Índice de contenidos

1. Introducción

En este tutorial aprenderemos a usar el sistema de Provide/Inject de Vue.js con Typescript.

Responderemos a algunas preguntas como:

  • ¿Para qué sirve?
  • ¿Para qué no sirve?
  • ¿Cuándo usarlo?

Este tutorial asume un conocimiento intermedio de Vue.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3)
  • Sistema operativo: macOS Mojave 10.14.4
  • Entorno de desarrollo: Visual Studio Code
  • Versión de Vue: 2.6.10
  • Versión de Typescript: 3.4.3

3. El problema

Si has usado Vue (o cualquier otro framework de front basado en componentes), probablemente hayas sufrido este problema:

  • Tengo un componente padre con propiedad nombre.
  • Tengo un componente hijo, que no quiere saber el nombre de su padre.
  • Tengo un componente nieto, que necesita saber el nombre de su abuelo.

 

¿Cómo le pasamos al nieto, el nombre del padre, sin pasar por el hijo?

4. Provide/Inject

Vue nos proporciona de manera nativa la inyección de dependencias en componentes, llamada

Provide/Inject
 .
  • Un componente puede proveer de algo (un valor, un objeto o una función).
  • Cualquier componente hijo, nieto, etc puede inyectar (leer) ese algo.

 

Ventajas:

  • Centralización de las instancias a usar por los componentes.
  • Inversión de control.
  • Facilita el testing de componentes.

 

Desventajas:

  • No es reactivo*.
  • Puede ocultar el origen de los elementos inyectados.
  • No sigue el estándar prop/event de Vue.

 

Podrías pensar que, todo eso está muy bien, pero si no es reactivo, ¿para qué me sirve? Te recomiendo seguir leyendo hasta la sección de Reactividad.

5. Uso real

En este ejemplo veremos un uso real y útil de Provide/Inject.

Tenemos un par de servicios,

LanguageService
  y
DateService
 , que uno de nuestros componentes necesita utilizar.

src/services/language

export interface LanguageService {
  getLanguage(): string;
}

export class NavigatorLanguageService implements LanguageService {
  getLanguage(): string {
    return navigator.language;
  }
}

src/services/date

export interface DateService {
  getDate(): Date;
}

export class BrowserDateService implements DateService {
  getDate(): Date {
    return new Date();
  }
}

Podríamos crear la instancia en el mismo componente que la va a utilizar, pero no estaríamos siguiendo el concepto de inversión de control. Y nuestro testing sería menos agradable de hacer.

Crear un provider es tan sencillo como:

src/providers/ServiceProvider.vue

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import { LanguageService } from "../services/language/LanguageService";
import { NavigatorLanguageService } from "../services/language/NavigatorLanguageService";
import { DateService } from "../services/date/DateService";
import { BrowserDateService } from "../services/date/BrowserDateService";
import { Person } from "../models/Person";

@Component({ name: "ServiceProvider" })
export default class ServiceProvider extends Vue {
  @Provide()
  languageService: LanguageService = new NavigatorLanguageService();

  @Provide()
  dateService: DateService = new BrowserDateService();
}
</script>

La creación de las instancias de nuestros servicios va a estar centralizada en este proveedor. Si algún día estos cambian, solo tendremos que cambiar este archivo.

Todos los componentes que estén dentro del context del

<slot>
  tendrán acceso a los servicios.

Para dar acceso al componente a los servicios provistos, sólo tenemos que:

src/App.vue

<template>
  <div id="app">
    <ServiceProvider>
      <Child />
    </ServiceProvider>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import ServiceProvider from "./providers/ServiceProvider.vue";
import PersonProvider from "./providers/PersonProvider.vue";
import Child from "./components/Child.vue";
import PersonInfo from "./components/PersonInfo.vue";

@Component({
  name: "App",
  components: { ServiceProvider, Child, PersonProvider, PersonInfo }
})
export default class App extends Vue {}
</script>

Ahora, todos los componentes dentro de

<ServiceProvider>
  tendrán acceso a los servicios.

Si vemos el código de

<Child>
 , veremos que es muy sencillo, a propósito:

src/components/Child.vue

<template>
  <div>
    <GrandChild />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import GrandChild from "./GrandChild.vue";

@Component({ name: "Child", components: { GrandChild } })
export default class Child extends Vue {}
</script>

Child
 , el hijo, no utiliza los servicios que su padre ha proveído. Inyectar es totalmente opcional.

Si vemos

GrandChild
 , el nieto, por otro lado:

src/components/GrandChild.vue

<template>
  <section>
    <div>
      Your language is
      <span>{{ languageService.getLanguage() }}</span>
    </div>
    <div>
      Today is
      <span>{{ dateService.getDate() }}</span>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue, Inject } from "vue-property-decorator";
import { LanguageService } from "../services/language/LanguageService";
import { DateService } from "../services/date/DateService";
import { Person } from "../models/Person";

@Component({ name: "GrandChild" })
export default class GrandChild extends Vue {
  @Inject()
  languageService!: LanguageService;

  @Inject()
  dateService!: DateService;
}
</script>

Podemos ver que inyectar unas instancias de un proveedor es muy sencillo.

Al poner

!
  al final una variable le estamos diciendo a Typescript que ésta siempre va a estar inicializada.

Y si vemos los tests de

GrandChild
 , veremos cómo proveer al componente de unas instancia de servicios creadas específicamente para facilitar el test.

src/components/tests/GrandChild.spec.ts

import { shallowMount } from '@vue/test-utils';
import GrandChild from '../GrandChild.vue';
import { LanguageService } from '@/services/language/LanguageService';
import { DateService } from '@/services/date/DateService';

class LanguageServiceStub implements LanguageService {
  constructor(private readonly expected: string) {}
  getLanguage(): string {
    return this.expected;
  }
}

class DateServiceStub implements DateService {
  constructor(private readonly expected: Date) {}
  getDate(): Date {
    return this.expected;
  }
}

describe('GrandChild', () => {
  it('should render the language', () => {
    const given = 'EXPECTED LANGUAGE';
    const expected = 'EXPECTED LANGUAGE';

    const wrapper = shallowMount(GrandChild, {
      provide: {
        languageService: new LanguageServiceStub(given),
        dateService: new DateServiceStub(new Date())
      }
    });

    expect(wrapper.html().includes(expected)).toBe(true);
  });

  it('should render the date', () => {
    const given = new Date('10-10-2010');
    const expected = 'Sun Oct 10 2010';

    const wrapper = shallowMount(GrandChild, {
      provide: {
        languageService: new LanguageServiceStub(''),
        dateService: new DateServiceStub(given)
      }
    });

    expect(wrapper.html().includes(expected)).toBe(true);
  });
});


6. Reactividad

Si intentamos hacer

Provide()
  de un
string
  que se puede modificar por un
<input>
:
<template>
  <div>
    <label for>
      Person Name
      <input type="text" v-model="name">
    </label>
    <slot></slot>
  </div>
</template>
<script>
@Component({ name: 'PersonProvider' })
export default class PersonProvider extends Vue {
  @Provide()
  name: string = "Thor"
}
</script>

Y lo inyectamos de esta manera:

<template>
  <div>
    Your name is
    <span>{{ name }}</span>
  </div>
</template>
<script>
@Component({ name: "PersonInfo" })
export default class PersonInfo extends Vue {
  @Inject()
  name!: string;
}
</script>

La impresión inicial será que el nombre se está renderizando. Pero al cambiar el valor del input, veremos que

PersonInfo
  no se actualiza.

Esto es porque

Provide/Inject
no es reactivo con tipos primitivos (number, string, boolean, etc). Si queremos proveer de manera reactiva, debemos hacerlo con una propiedad observable, como una propiedad de un objeto. Por ejemplo:

Teniendo la clase

Person
:

src/models/Person.ts

export class Person {
  constructor(public name: string, public age: number) {}
}

Y el proveedor

PersonProvider
:

src/providers/PersonProvider.vue

<template>
  <div>
    <label for>
      Person Name
      <input type="text" v-model="person.name" />
    </label>
    <label for>
      Person Age
      <input type="number" v-model="person.age" />
    </label>
    <hr />
    <slot></slot>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import { Person } from "../models/Person";

@Component({ name: "PersonProvider" })
export default class PersonProvider extends Vue {
  @Provide()
  person: Person = new Person("Thor", 18);
}
</script>

Podemos inyectar el objeto en

PersonInfo.vue
:

src/components/PersonInfo.vue

<template>
  <section>
    <div>
      Your name is
      <span>{{ person.name }}</span>
      and your age is
      <span>{{ person.age }}</span>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue, Inject } from "vue-property-decorator";
import { Person } from "../models/Person";

@Component({ name: "PersonInfo" })
export default class GrandChild extends Vue {
  @Inject()
  person!: Person;
}
</script>

E incluso podemos encapsular un Provider dentro de otro Provider en

App.vue

src/App.vue

<template>
  <div id="app">
    <ServiceProvider>
      <PersonProvider>
        <Child />
        <PersonInfo />
      </PersonProvider>
    </ServiceProvider>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import ServiceProvider from "./providers/ServiceProvider.vue";
import PersonProvider from "./providers/PersonProvider.vue";
import Child from "./components/Child.vue";
import PersonInfo from "./components/PersonInfo.vue";

@Component({
  name: "App",
  components: { ServiceProvider, Child, PersonProvider, PersonInfo }
})
export default class App extends Vue {}
</script>

Si ahora modificamos el valor del

<input>
 , veremos cómo el componente
PersonInfo
se actualiza.

Si necesitas más reactividad, échale un vistazo a Vue Reactive Provide.

7. Conclusiones

Provide/Inject
en Vue.js nos permite pasar información entre componentes de una manera más limpia que con
props
 , pero tiene unas limitaciones claras.

Su uso está recomendado para la creación de librerías de componentes o plugins. Si no te convence para tu proyecto, échale un vistazo a Vuex.

Todo el código del tutorial está disponible en Github.

8. Referencias

La entrada Provide/Inject en Vue.js se publicó primero en Adictos al trabajo.

Migración de Java 8 a Java 11

$
0
0

Hace tiempo que salió la versión 11 de Java (de hecho ya tenemos la 12 e incluso empezamos a ver la 13), al igual que vemos que el soporte de Oracle para Java 8 llega a su fin. Con esta situación y teniendo en cuenta lo altamente extendido que está Java 8 a muchos les surge la duda de migrar o no, cuándo es el mejor momento y a dónde?!?! ya que el panorama de JVMs ha cambiado totalmente desde los tiempos donde Oracle e IBM eran prácticamente las únicas opciones.

En este tutorial veremos cuándo y por qué migrar, y cuáles pueden ser las mejores opciones de JVM.

1. Java 8

1.1. Futuro de Java 8

La respuesta corta sería: No tiene futuro

En general, y pasa no sólo con Java y Oracle, todas las versiones de todos los productos nacen para morir. Es decir, tienen un ciclo de vida que acaba en algún momento donde se le deja de dar soporte. Normalmente, en este ciclo de vida, la mejor opción es saltar a la siguiente versión antes de que la actual se quede sin soporte oficial. Así siempre conseguiremos:

  • estar bajo una versión con soporte,
  • adaptarnos poco a poco a las características de las nuevas versiones.

En el caso concreto de Java 8 según Oracle (https://www.oracle.com/technetwork/java/java-se-support-roadmap.html) las actualizaciones gratuitas para uso personal estarán hasta final de diciembre de 2020, pero para uso comercial terminaron en enero de 2019 (así que a día de hoy ya no tiene soporte).

Para las opciones de pago por soporte los plazos de Oracle son:

Esto quiere decir que con las opciones de pago conseguimos extender un poco más el soporte pero no mucho más (realmente máximo 5-6 años).

Más información sobre versiones Java de Oracle:

new-JDK-release-model-Dalibor-Topic
Extraído de la presentación mencionada justo antes de Dalibor Topic

​1.2. ¿Merece la pena pagar soporte Java para seguir teniendo actualizaciones de Java 8?

No, no merece la pena puesto que es retrasar lo inevitable, no durante demasiado tiempo (como hemos visto en el punto anterior) y encima a un coste que según nuestra implantación puede ser para nada despreciable.

Costes máximos (https://www.oracle.com/technetwork/java/javaseproducts/overview/javasesubscriptionfaq-4891443.html):

Además, mientras más paguemos, más distancia habrá entre la versión obsoleta y la que esté en ese momento en vigor, y por tanto cuando finalmente nos quedemos sin soporte y no nos quede más remedio que actualizar de versión, aumentará la dificultad de migración y errores asociados (es más fácil cambiar poco pronto que mucho tarde).

Para mi pagar el soporte para mantener una versión ya obsoleta es “cavar nuestra propia fosa a golpe de talonario”.

El único caso que podría tener sentido es si estamos hablando de un sistema que vamos a dejar morir y que no vamos a evolucionar. En tal caso sí puede merecer la pena continuar con Java 8 y no hacer el esfuerzo de migrar, ya que estamos hablando de algo que se va a descontinuar.

1.3. ¿Qué caminos está tomando la empresa privada?

Todo el mundo está migrando a las nuevas versiones y de hecho en mi opinión parece que el nuevo ciclo de versiones cada 6 meses y la apertura con el OpenJDK ha sido una bocanada de aire fresco que ha revitalizado todo el ecosistema Java, que últimamente algunos querían dar por muerto y nada más lejos de la realidad.

Lo que sí veo es que no se está adoptando el ciclo de releases cada 6 meses para entornos de producción (sobre todo de grandes empresas) y se están eligiendo los ciclos de las LTS (Long Term Support), lo que tiene bastante sentido.

2. Migración de Java 8 a Java 11

2.1. Necesidades de cambiar a Java 11, pros y contras

Más que una necesidad es una obligación para no quedarnos anclados en una versión obsoleta y sin soporte.

Como ventaja a nuestro favor tenemos el excelente trabajo que ha hecho siempre Java manteniendo compatibilidad hacía atrás, por lo que las migraciones no suelen ser complicadas (más adelante hablaremos de qué puntos tener en cuenta a la hora de migrar de Java 8 a 11).

Otro punto que juega a nuestro favor si actualizamos la versión de Java son las mejoras de la JVM. Constantemente en cada versión de la JVM se hacen mejoras de rendimiento, gestión de memoria con el Garbage Collector, y en general de uso de recursos, incluso de tiempo de arranque. Todas estas mejoras las disfrutaremos sin necesidad de cambiar nuestra base de código.

2.2. Recomendación: Oracle JDK vs. Open JDK

Para mi la opción recomendable es el OpenJDK dentro de alguno de sus sabores. Y esto no es por el coste, sino por la capacidad de mantenimiento y otras cualidades que nos proporciona el código abierto.

De hecho, siempre tenemos la opción de pagar por un soporte específico ya que hay distintas empresas que se dedican a ello, como por ejemplo:

Lo que está claro es que debido a las restricciones que ha puesto Oracle para el uso de su JDK con propósitos comerciales, podríamos decir que ahora se ha abierto la veda y han surgido numerosas alternativas.

Si por ejemplo nos fijamos en SDKMAN (https://sdkman.io/), conocida herramienta para instalar de forma simultánea distintos Software Development Kits

$ sdk list java

nos encontramos con las siguientes opciones de instalación para Java 11:

Tenemos que tener en cuenta que todas estas alternativas no son más que distintas builds del mismo código fuente, el del OpenJDK (http://openjdk.java.net/projects/jdk/), así que salvo esa pequeña personalización que haga cada proveedor, el código origen es el mismo.

De hecho todas pasan un proceso de certificación por la Java Community Process (https://www.jcp.org), que es la que otorga el Technology Compatibility Kit (TCK, en algunos casos también referido como JCK).

openJDK-many-builds
Imagen sacada del artículo original https://blog.joda.org/2018/09/time-to-look-beyond-oracles-jdk.html de Stephen Colebourne.

Diferencias entre Oracle JDK y OpenJDK (https://www.baeldung.com/oracle-jdk-vs-openjdk)

  • Lo más importante del artículo es que NO hay una diferencia técnica real entre los dos, ya que el proceso de compilación para Oracle JDK se basa en el de OpenJDK. Quizás, la única diferencia real puede ser la estabilidad que Oracle garantiza en la versión con soporte oficial, mientras que en el caso de OpenJDK las actualizaciones más frecuente pueden generar ciertos casos puntuales de inestabilidad.

De todas las opciones posibles, la «mejor» seguramente va a depender del uso que le queramos dar, así que sería bueno hacer algunas pruebas de carga y ver resultados. Por ejemplo:

  • si estamos dispuestos a cambiar de versión cada 6 meses podemos seguir ligados a Oracle (11.0.2-open),
  • pero si no queremos soportar este ritmo de versionado y sí contar con un ritmo constante de actualizaciones de mantenimiento/seguridad seguramente la opción más recomendable sea AdoptOpenJDK (11.0.2.hs-adpt en su sabor con HotSpot por ser más estándar).
  • Una muy buena opción puede ser Azul Systems (11.0.2-zulu) con buena cadencia de mantenimientos y opciones de soporte comercial.
  • Si ejecutamos en entornos concretos puede que las OpenJDKs propias del fabricante sean buenas opciones. Por ejemplo, si ejecutamos en AWS usar la de Amazon (11.0.2-amzn).

Si sólo pudiera elegir una, a día de hoy seguramente me quedaría con AdoptOpenJDK (11.0.2.hs-adpt), por lo menos para empezar y luego ver si damos el salto a otra más específica por razones concretas. Además tiene el respaldo de grandes compañías como IBM, Microsoft Azure, Azul Systems, jClarity…

Documento muy recomendable sobre las distintas alternativas de JDK, elaborado por los Java Champions comunidad de líderes y expertos independientes de Java:

2.3. Implicaciones del cambio a Open JDK

En principio ninguna ya que:

  • la comunidad de OpenJDK crea y mantiene la Implementación de Referencia (RI) open-source (GPLv2+CE)
  • de la especificación de Java SE,
  • gobernada por la Java Community Process (JCP),
  • y definida bajo el paraguas de la Java Specification Request (JSR) para cada una de sus features.

Es decir estamos tratando con una especificación que todas las implementaciones deben cumplir por igual.

Y digo ‘en principio’ porque no dejamos de hablar de software y por lo tanto, cada implementación puede tener sus propias pequeñas diferencias o bugs.

Pero esas diferencias deberían ser menores de lo que ya existía hasta ahora con las diferentes implementaciones que teníamos como la de Oracle o la de IBM, ya que ahora todas parten del mismo fuente: OpenJDK.

Esas diferencias deberían ser sólo en funcionalidades no-core, como monitorización extendida, diagnóstico, herramientas, etc.

2.4. Modelo de migración: ¿Se puede pasar directamente de Java 8 a 11 o es necesario hacer una migración escalonada?

Sí, se puede migrar directamente, y de hecho sería lo recomendable ya que Java 11 es una Long-Term-Support (LTS) release (https://www.oracle.com/technetwork/java/java-se-support-roadmap.html). Las LTS son versiones cuyo compromiso de mantenimiento es de 3 años. Entre medias saldrán varias versiones non‑LTS (como la 9 la 10, 12, …) donde cada una de estas versiones añade funcionalidad pero que, a ritmo de 6 meses, será reemplazada por otra versión. Si no queremos enfrentarnos a este ritmo de actualizaciones lo mejor es mantenernos en las LTS.

Para realizar la migración podemos encontrar por Internet mucha literatura, por ejemplo si buscamos en Google «java 11 java 8 compatibility» obtenemos referencias como:

Un resumen de los puntos a tener en cuenta para la migración podría ser:

2.5. Evolución de los principales framework, componentes de desarrollo y servidores web

La tendencia de las herramientas, frameworks, librerías y en general de todo el ecosistema es ir actualizándose y soportar el ciclo de releases de 6 meses.

Así encontramos múltiples ejemplos de como las más típicas ya han hecho su migración a Java 11 hace tiempo:

2.6. Alternativas a JNLP

Java Network Launch Protocol (JNLP) (https://docs.oracle.com/javase/tutorial/deployment/deploymentInDepth/jnlp.html) es el protocolo que permite a un aplicación ejecutarse en un escritorio cliente usando recursos que se encuentran en un servidor web remoto.

Java Plug-in y Java Web Start se consideran clientes de JNLP.

Todas esta tecnologías (JNLP, Web Starts, Applet, …) desaparecen en Java 11, por lo que NO hay una alternativa directa (https://www.oracle.com/technetwork/java/javase/javaclientroadmapupdate2018mar-4414431.pdf). Así que si estamos usando alguna de estas tecnologías es posiblemente el peor punto de la migración a Java 11 y habrá que ver cada uno de los casos.

Para aplicaciones de escritorio “stand-alone” que distribuimos mediante Java Web Start podemos usar alguna de estas alternativas:

Para aplicaciones Web donde el Applet es simplemente el frontal, habría que reescribir este front en otra tecnología HTML + JavaScript, usando algún framework como Angular, React, o similares (siendo esta la tendencia de mercado para aplicaciones web ricas).

3. Conclusiones

Si pretendes que tu sistema dure todavía unos cuantos añitos, migra lo antes que puedas.

Si lo vas a descontinuar mantente en Java 8.

Y en cualquier caso salte de la JDK de Oracle y vete a otra (por ejemplo la de AdoptOpenJDK), y si más adelante ves que necesitas soporte específico, contrátalo a alguna de las empresas que lo dan de forma profesional.

4. Sobre el autor

Alejandro Pérez García (@alejandropgarci)

Ingeniero en Informática (especialidad de Ingeniería del Software) y Certified ScrumMaster

Socio fundador de Autentia Real Business Solutions S.L. – «Soporte a Desarrollo»

Socio fundador de ThE Audience Megaphone System, S.L. – TEAMS – «Todo el potencial de tus grupos de influencia a tu alcance»

La entrada Migración de Java 8 a Java 11 se publicó primero en Adictos al trabajo.

Los mejores programas para hacer Diseño de Páginas Web Profesionales Sin ser diseñador

$
0
0

Tener un sitio web profesional puede marcar la diferencia entre tú y tu competencia.

Es por esta razón que existen programas, que permiten crear fácilmente páginas web sencillas, pero que permitirán a tu empresa o negocio proyectar profesionalismo y atraer a los clientes que necesitas.

Con base en mi experiencia de más de 10 años creando sitios web para negocios, hice una selección de los mejores 7 programas para crear páginas web profesionales y cautivantes, aunque no seas diseñador web.

¡No te pierdas ningún detalle!

1.- WordPress.com. El CMS más popular del mundo

WordPress es el software más utilizado para el diseño de páginas web profesionales.

Es un gestor de contenidos en donde puedes crear de manera muy sencilla desde un blog, una página profesional o incluso una tienda en línea.

Este sistema es el más usado en el mundo y la mayoría de los proveedores de Hosting lo ofrecen gratuitamente.

Usabilidad de WordPress

Se trata de una de las mejores plataformas para la creación de páginas web, ya que es muy fácil aprender a usarlo y sus menús son muy prácticos.

Su variedad de temas y plugins permite que el usuario se sienta libre de elegir el que más le guste, sin tener que pagar costos muy altos.

Cuenta con plantillas gratuitas que, si sabes elegir, pueden ser muy útiles y prácticas. Si deseas algo más sofisticado puedes comprar plantillas que te permitirán crear sitios más atractivos y completos.

Cómo funciona  WordPress

Hay dos formas de usar WordPress para crear tu sitio web.

La primera y más sencilla es contratando un hosting.

Por ejemplo en mi hosting favorito, HostGator, entro a mi cPanel y solo le doy donde dice instalar WordPress, después de ello te brindará tus accesos a la instalación ¡y listo! puedes comenzar a diseñar tu página web profesional.

La otra manera es accediendo a WordPress.com, una vez ahí, deberás crear una cuenta y automáticamente te brindarán las opciones de servicio gratuito o planes de pago.

No recomiendo el plan Gratuito, porque es muy limitante y no te permitirá explotar al máximo las bondades de WordPress.

Al momento de preguntarte ¿cómo crear un sitio web?

WordPress te brinda todas las respuestas, pues con su plataforma podrás acceder a las diversas áreas, te descargará una amplia variedad de plantillas, fuentes, colores y opciones para personalizar tu sitio web.

Lo mejor de todo es que cuentas con asesoría constante, si necesitas ayuda o te surge alguna duda durante la configuración, podrás acceder a videos tutoriales que te facilitan el aprendizaje.

Ventajas y Desventajas WordPress

Calificación

Al ser una de las plataformas más utilizadas, la calificación de WordPress es de 5 estrellas, pues tiene una amplia variedad de herramientas y plantillas para diseñar una página web profesional y atractiva.

5 ESTRELLAS

2.- Websitebuilder.com  El constructor de sitios para principiantes

Esta es una de las mejores plataformas para aquellos que tienen pocos conocimientos de diseño web.

Este creador de sitios fue creado para diseñar sitios profesionales de forma rápida y sencilla. Tiene algunas limitaciones, pero si estás empezando en este mundo de los medios digitales, Site Builder es una excelente opción.

Usabilidad de Websitebuilder.com

La forma de utilizarlo es muy sencilla, consiste en seleccionar, arrastrar y soltar los elementos que desees poner en tu sitio web.

Cuenta con una amplia variedad de plantillas (más de 10.000 diseños), puedes personalizarla a tu antojo y además incluir elementos multimedia como imágenes o videos.

Cómo funciona Websitebuilder.com

Al igual de WordPress este sistema es muy utilizado por algunos proveedores de hosting.

Ellos te ofrecen la alternativa de instalación y así lo puedes utilizar directamente es tu dominio web.

Si deseas también puedes entrar directamente al sitio de la plataforma y utilizarlo desde ahí, pero tienes que crear una cuenta.

Una vez creada tu cuenta puedes elegir un diseño, editar tu sitio web según tus preferencias y publicarlo en la web.

Es una interfaz sencilla, no maneja aspectos técnicos y cuentan con buena atención al cliente.

Ventajas y Desventajas Websitebuilder.com

Calificación

Es una muy buena opción para todos los emprendedores, sobre todo los pequeños empresarios, la calificación de esta plataforma es de:

4.5 ESTRELLAS  

3.- Wix. El Constructor de sitios más versátil

Wix es uno de los  creadores de páginas web más populares del mundo, es muy sencillo de usar, sus herramientas son claras y basta con un sencillo registró a través de tu email, Facebook o cuenta de Google para comenzar.

Lo malo de este sistema es el precio, es más caro que otras plataformas y puede generar problemas para vincularlo con complementos más avanzados.

Usabilidad de Wix

Se trata de una aplicación muy fácil de usar, que te permite crear una página web, acorde a las necesidades de tu empresa. Te permite crear tu página web en HTML 5 y también tiene un sistema que consiste en «arrastrar y soltar elementos».

Cómo funciona Wix

Debes crear una cuenta, elegir el plan de desees (nunca utilices un plan gratuito, esto le resta profesionalismo a un sitio web).

Una vez que ya hayas hecho tu cuenta debes seleccionar uno de sus múltiples temas y diseños, los cuales son muy sencillos de usar, basta con elegir y arrastrar los elementos desde el panel de herramientas para colocarlos en el lugar que tu elijas, Wix hace la creación de páginas web algo muy sencillo.

Ventajas y desventajas Wix

Calificación

Wix es recomendable para todos aquellos que no tienen ningún tipo de conocimientos acerca de la elaboración de páginas web. Es una buena oportunidad para los emprendedores.

4 ESTRELLAS

4.- SITE123.  El constructor de Sitios más sencillo de usar

Se trata de una plataforma donde aprenderás cómo hacer una página de internet de forma sencilla, pero diferente a las demás plataformas.

Usabilidad de SITE123

Es un sitio muy eficiente que no abruma a los usuarios con múltiples opciones. Su servicio de atención al cliente es de alta calidad.

Cómo funciona Site123

Tiene una barra lateral en donde la edición se vuelve mucho más fluida, permitiendo personalizar tu sitio web en tan solo minutos.

Además de una gran variedad de tutoriales, artículos y reseñas, en donde te enseñan a usar la plataforma.

Ventajas y desventajas SITE123

Calificación

Definitivamente Site123 brinda una de las mejores opciones para la creación de páginas web. Todos los beneficios que otorga la posicionan con la siguiente calificación:

4 ESTRELLAS

5.- Weebly. Muy útil para crear sitios web profesionales

Weebly es una plataforma online que te enseña cómo diseñar una página web, aún sin conocer mucho sobre el tema de diseño. Su interfaz es muy sencilla y fácil de comprender.

Usabilidad de Weebly

Cuenta con diversos tipos de plantillas de diseño y es fácil de usar.

No es una plataforma muy conocida, por lo tanto, puede existir desconfianza de los clientes al entrar a tu sitio web.

Cómo funciona Weebly

Su registro es muy sencillo, cuenta con planes gratuitos y de paga, su configuración suele ser muy rápida.

Eliges fondos, fuentes, colores y herramientas desde su panel de control, aprendes cómo crear un sitio web, de una manera muy sencilla.

Ventajas y desventajas Weebly

Calificación

Weebly te enseña cómo hacer una página de internet, pero no tiene una amplia variedad de plantillas, como algunas otras plataformas. Sin embargo, es muy útil para crear sitios web profesionales.

Su calificación es:

3.5 ESTRELLAS

6.- Jimdo.com. Requiere un poco más de conocimientos

Jimdo, es una plataforma para la creación de páginas web, que utiliza su propio sistema de gestión de contenido.

Usabilidad Jimdo.com

Es fácil de usar pues no maneja herramientas complejas, cuenta con plantillas pre-programadas que puedes modificar a tu antojo.

Cómo funciona Jimdo.com

Debes crear tu propia cuenta en la página web de Jimdo, posteriormente podrás acceder al panel de administración para crear tu propio sitio web.

Existen versiones gratuitas y de pago.

A diferencia de otras plataformas, debes hacer un poco más que seleccionar y colocar los elementos, requiere de un poco más de trabajo, pero aun así vale la pena.

Ventajas y desventajas

Calificación Jimdo.com

Jimdo, definitivamente no es la opción más recomendable para novatos, aunque sea fácil de usar, los beneficios para tu sitio web serán muy pocos si no sabes cómo configurar correctamente el sitio web con esta plataforma.

Su calificación es:

3 ESTRELLAS

7.- Drupal. Excelente calidad, pero no es apta para novatos

Drupal es un Sistema de Gestión de Contenidos más especializado, por lo tanto, requiere de conocimientos técnicos para su entendimiento y función, algo que lo convierte en menos accesible para todos los que desean aprender cómo crear un sitio web profesional, pero básico.

Usabilidad Drupal

Cuenta con una gran variedad de herramientas, que te pueden permitir realizar el diseño de páginas web profesionales y de muy alta calidad.

Un detalle importante es que no cuenta con plantillas básicas, por lo tanto, puede complicarse su utilización.

La efectividad de Drupal, depende de las habilidades y la creatividad de quien va a crear el sitio web.

Cómo funciona Drupal

Drupal es un programa que debe instalarse para llevar a cabo la elaboración de páginas web, se maneja con ficheros y mediante la creación de módulos.

Estos módulos son los que te permiten realizar las modificaciones pertinentes, adherir secciones, fuentes, entre otros elementos.

Es necesario tener conocimientos especializados para comprender el funcionamiento de este programa, pero si lo dominas el resultado puede ser muy atractivo.

Drupal cuenta con soluciones para cada tipo de sitio web que desees crear.

Ventajas y desventajas Drupal

Calificación

Drupal es una excelente herramienta para todos los diseñadores de páginas web, pero si no tienes conocimientos técnicos, puede convertirse en un dolor de cabeza al no comprender todas las funciones y términos.

3.5 Estrellas

Conclusión

Si has llegado hasta aquí es porque te has preguntado ¿cómo hacer mi página de internet?, por esta razón, te he mostrado las 7 opciones más utilizadas actualmente. Cada una cuenta con sus pros y sus contras.

Antes de decidir contratar una de estas plataformas, da un vistazo por cada una de ellas y decídete por la opción que más te haya facilitado y se adapte al tipo de sitio web que pretendes crear.

Si tienes alguna recomendación o consejo por favor déjala en los comentarios.

¡Nos leemos la próxima!

La entrada Los mejores programas para hacer Diseño de Páginas Web Profesionales Sin ser diseñador se publicó primero en Adictos al trabajo.

Y los demás ¿cómo lo hacen? Resultado del 13 reporte anual del estado Agile

$
0
0

Recientemente Collab Net Version One publicó su decimotercer estudio anual sobre la situación del agile a nivel mundial y no quería dejar pasar la oportunidad de dar mi opinión respecto de las estadísticas que más me llamaron la atención.

Antes de comenzar con el análisis de los datos, conviene que entendamos el ámbito en el que se realizó el estudio: qué tipo de organizaciones suelen participar, qué tamaño tienen y cómo de “ágiles” son.

  • 64% de las empresas participantes tienen más de 1000 empleados.
  • 40% de los participantes tienen más de 1000 desarrolladores de software.
  • 47% de las empresas son Norteamericanas, 30% Europeas y 8% Suramericanas.
  • 34% de los encuestados son Scrum Masters o Coaches internos.
  • 97% de los encuestados dicen practicar el agilismo.

Teniendo en cuenta el origen de la muestra, a continuación describiré algunas de las estadísticas que a mi parecer son más relevantes.

Tendencia que continua

  • Este último dato no me sorprende al ser el 64% grandes empresas que suelen sentirse “cómodas” con SAFe, dado lo fácil que es acomodar sus estructuras jerárquicas sin hacer grandes cambios.

 

  • Las debilidades en la cultura organizacional continúan siendo el principal impedimento para la adopción de las metodologías ágiles. Los principales problemas reportados son:
    • La resistencia al cambio.
    • Carencias en el apoyo de la directiva.
    • Cultura organizacional opuesta a los principios ágiles.

 

  • El cambio propuesto por la cultura DevOps continúa siendo muy importante. Prácticas como la integración continua, despliegue continuo y la automatización de pruebas son reportadas como claves para cambiar la relación entre desarrollo y operaciones.

 

Los cambios respecto al reporte anterior

Aunque las estadísticas mencionadas son las resaltadas como las más relevantes por el propio estudio, también se mencionan los siguientes puntos como cambios importantes según los reportes previos:

  • La reducción de costes aumentó su importancia como razón para emprender el cambio hacia modelos ágiles de desarrollo. En tal sentido, el 71% de las organizaciones declararon haber asumido el cambio por este motivo pero sólo el 21% reportó una reducción real en los costes.
  • Otro punto importante que reportan los encuestados es la importancia de la inversión para el éxito de un proceso de transformación al agilismo. De los puntos resaltados como claves en la inversión, vale la pena mencionar la incorporación de agile coaches y programas internos de formación.

 

Razones para ser ágiles

De las estadísticas presentadas en el informe, esta en particular coincide con el patrón que suelen alegar nuestros clientes como motivo para que les ayudemos en sus procesos de transformación.

  • 74% quiere acelerar la entrega de software.
  • 62% quiere mejorar la habilidad para gestionar los cambios de prioridad.
  • 51% quiere aumentar la productividad de sus equipos.
  • 50% quiere mejorar el alineamiento entre negocio e IT.
  • 43% quiere mejorar la calidad del software.

Principales beneficios reportados

Aunque el punto anterior destaca los motivos más relevantes que alegan las empresas encuestadas  para ser “ágiles”, me parece mucho más importante lo que realmente se reporta como beneficio tras un proceso de transformación ágil.

En tal sentido, según el estudio se podría destacar que:

  • 69% mejoró su habilidad para gestionar los cambios de prioridad.
  • 65% mejoró la visibilidad de sus proyectos.
  • 64% mejoró el alineamiento entre negocio e IT.
  • 64% mejoró la moral de sus equipos.
  • 63% mejoró la velocidad de entrega o TTM (time to market).

Estos resultados en la práctica puede que resulten demasiado subjetivos para justificar una inversión. Con esto, lo que quiero decir es que aunque se hace un esfuerzo por reflejar los beneficios reportados, no nos deja estadísticas reales de los beneficios obtenidos y surgen preguntas reales cómo: ¿Cuánto va a mejorar mi velocidad de entrega? O si voy más rápido, ¿cuánto nos ahorraremos?

Obtener estos números/métricas creo que es una gran materia pendiente que echamos de menos y que este estudio debería reflejar, al menos los rangos reales de mejora que suelen reportar las empresas encuestadas, por muy  complicado que sea.

Métricas y éxito

Partiendo de lo expuesto en el punto anterior, realmente la siguiente estadística es la clave para determinar cúal es el valor real que pueden incorporar las metodologías ágiles. Según el estudio, las empresas participantes reportaron los siguientes resultados como factor para evaluar el éxito de sus equipos:

  • 52% evalúa la satisfacción de sus usuarios.
  • 48% evalúa el valor aportado a negocio.
  • 41% incorpora entre sus métricas la entrega a tiempo de los objetivos.
  • 38% la calidad del producto.

Agile + DevOps

Ya para terminar, quería comentar la que para mí es la métrica más importante puesto  que refleja la estrecha relación que existe entre las prácticas metodológicas y el conjunto de prácticas técnicas promovidas por la cultura DevOps.

Según los encuestados, los siguientes son los beneficios obtenidos al incorporar este tipo de prácticas:

  • 66% reporta una aceleración en la velocidad de entrega.
  • 61% indica que mejoró la calidad de su software.
  • 48% reporta una reducción de riesgos.
  • 46% alega que aumentó la satisfacción de sus usuarios.
  • 34% indica haber obtenido mayor visibilidad de sus flujos de entrega de valor.
  • 34% reportó que disminuyeron sus costes de IT.

Conclusiones

Como datos adicionales en el reporte se mencionan algunas otras estadísticas como:

  • JIRA está en la cúspide de las herramientas de gestión de proyectos ágiles con un 65%, pero contradictoriamente Excel es la segunda con un 48%.
  • 46% de las empresas están dispuestas a usar metodologías ágiles para gestionar a sus proveedores externos.
  • Los encuestados posicionaron las siguientes sesiones como el top 5 de las prácticas metodológicas por excelencia:
    • Daily standup (86%).
    • Sprint/iteration planning (80%).
    • Retrospectivas (80%).
    • Sprint/iteration review (80%).
    • Iteraciones cortas (86%).
  • Aunque se le da importancia a la cultura DevOps, sólo el 1% de las empresas dice utilizar prácticas XP (eXtreme Programming).

Finalmente, quería destacar la relevancia que van ganando las prácticas de calidad y DevOps como sustento del agilismo práctico. En Autentia creemos en la excelencia técnica y las prácticas metodológicas bien entendidas como medios infalibles para lograr la satisfacción de los usuarios finales. Proporcionamos soporte a la implantación de metodologías ágiles ayudando a la transformación digital de grandes organizaciones. Te invito a que te informes sobre los servicios profesionales de Autentia y el soporte que podemos proporcionar a tu empresa.

La entrada Y los demás ¿cómo lo hacen? Resultado del 13 reporte anual del estado Agile se publicó primero en Adictos al trabajo.


Cómo implementar infinite scroll en Angular

$
0
0

El llamado infinite scroll en Angular (y en cualquier otro ámbito/lenguaje) hace referencia a poder deslizarnos por una ventana, generalmente hacia abajo, sin necesidad de interactuar con elementos de paginación y navegación. Es muy utilizado, por ejemplo, en redes sociales como Twitter o Instagram, donde se observa claramente que el contenido se va cargando mientras deslizamos hacia abajo de forma infinita (de ahí su nombre).

Para este tutorial se necesitan ciertos conceptos sobre Angular de base. Si no sabes, te recomiendo estos artículos con los que puedes aprender mucho sobre este framework.

Ventajas y desventajas

Este sistema es ampliamente utilizado porque su mayor ventaja es, desde luego, que mejora mucho la facilidad de navegación y la experiencia de usuario en cualquier aplicación. Sin este método, sería mandatorio utilizar siempre botones de navegación. Para lo que habría que bajar al fondo de la página, hacer click en siguiente o en la página que queramos, esperar a que cargue la nueva página y repetir lo mismo (al más puro estilo forocoches).

No obstante, es un método que hay que saber utilizar. En un sistema de navegación por páginas convencional nos aseguramos tener en memoria solo el número de objetos seleccionados. Con este ejemplo, si tenemos cien mil objetos que representar, se irán acumulando sumando peso en la memoria y lastrando el rendimiento, por lo que es conveniente saber qué tenemos entre manos.

Además, cabe destacar que, como casi siempre en este mundo, el hecho de que facilite la vida al usuario significa que el desarrollador lo tiene más difícil. El hecho de que el usuario no deba interactuar con elementos de paginación no significa que dejen de estar ahí. Es el desarrollador el que debe gestionarlos de forma automática, lo que supone un extra en la complejidad del producto. Todo sea por enamorar a los usuarios, ¿no?

Contexto

Como todo lo bueno en esta vida, ya ha habido alguien el planeta que nos ha resuelto el problema en forma de librería, así que nosotros solo tenemos que integrarla en nuestro proyecto con el comando:

npm install --save ngx-infinite-scroll

Para el desarrollo de este tutorial vamos a implementar un ejemplo básico, que será cargar en bucle elementos en una tabla y, por supuesto, mostrarlos utilizando infinite scroll.

Podéis encontrar éste y más ejemplos en la rama infinite_scroll de este repositorio en GitHub.

Cómo funciona

Para que funcione adecuadamente, debemos controlar dos procesos:

  • Carga de datos cuando se haga scroll en la página hacia abajo a una altura determinada.
  • Botón para volver arriba, que debe aparecer a partir de cierta posición del scroll y desaparecer cuando volvamos arriba

Preparación del entorno

Lo primero que necesitamos es importar el componente InfiniteScrollModule en el app.module.ts, para poder utilizar las directivas que nos proporciona ngx en este paquete:

import {InfiniteScrollModule} from 'ngx-infinite-scroll';

@NgModule({
  declarations: [
  ],
  imports: [
    InfiniteScrollModule
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule { }

Y también, aunque no es necesario, os recomiendo importar una librería de estilos para poder utilizar clases predefinidas y centrarnos en la práctica de forma más directa. En mi caso utilizaré Bootstrap CDN, que solo hay que importar su librería en el index.html para tener acceso a sus clases:

<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossorigin="anonymous">

Ejemplo básico: carga en bucle de datos

Partimos de un proyecto ya creado. Generamos un componente nuevo al que vamos a llamar InfiniteScrollExample para contener toda nuestra lógica.

ng generate component components/InfiniteScrollExample

Para visualizar este componente, debemos anclarlo en el app.component.html con su selector correspondiente:

<app-infinite-scroll-example></app-infinite-scroll-example>

Carga de datos según scroll

En este componente de ejemplo vamos a implementar, a continuación, la lógica y los elementos de la vista necesarios para ir cargando más datos según el usuario va scrolleando en nuestra página.

Lógica del componente

Lo primero que debemos implementar es un método que nos añada elementos a una lista que podamos recorrer en bucle y mostrar, por ejemplo, en una tabla.

export class InfiniteScrollExampleComponent implements OnInit {

  private linesToWrite: Array<string>;

  constructor() { }

  ngOnInit() {
    this.linesToWrite = new Array<string>();
    this.add40lines();
  }

  add40lines() {
    const line = 'Another new line -- ';
    let lineCounter = this.linesToWrite.length;
    for (let i = 0; i < 40; i ++) {
      this.linesToWrite.push(line + lineCounter);
      lineCounter ++;
    }
  }
}

Nuestro componente tiene una lista de cadenas de texto llamada linesToWrite. Aquí guardaremos el contenido que queramos mostrar en nuestra vista.

El método add40lines() añade 40 líneas de texto a nuestro array. Vemos que se ejecuta en el ngOnInit(), de forma que tenemos 40 líneas de datos disponibles nada más arrancar. Lo que haremos será que, cada vez que hagamos scroll hacia abajo, ejecutaremos de nuevo este método para cargar otras 40 líneas de código, y ver cómo se ejecuta hasta que queramos. Pero a esto todavía no hemos llegado.

Evitar infinitos reales

Para controlar el comportamiento y que no se ejecute de forma ilimitada, crearemos dos variables que nos permitirán controlar:

  • El máximo de iteraciones a realizar (número de páginas máximo)
  • Iteración (o página) en la que nos encontramos en cada instante

private finishPage = 5;
private actualPage: number;

Implementamos un método llamado onScroll() de la siguiente forma:

onScroll() {
    if (this.actualPage < this.finishPage) {
      this.add40lines();
      this.actualPage ++;
    } else {
      console.log('No more lines. Finish page!');
    }
  }

Este método será el que llamaremos cuando se produzca el evento deseado, que veremos más adelante. Es la clave para que funcione bien nuestro infinite scroll en Angular. Como puede verse, llamará al método de añadir líneas a nuestro array solo si estamos dentro del intervalo establecido. La variable finishPage (que funciona a modo de constante predefinida) se define al declararla. No obstante, la variable actualPage debemos inicializarla en el constructor con valor 1.

constructor() {
    this.actualPage = 1;
  }

Así, cada vez que recargemos la página, nos encontraremos siempre en la página 1 y podrán cargarse elementos al array hasta llegar al tope definido gracias al infinite scroll que estamos implementando con Angular.

La vista HTML

Para poder llamar al método onScroll() cuando el evento deseado se produzca, vamos a utilizar unas directivas que nos proporciona el componente InfiniteScrollModule que importamos al principio del tutorial. Fijaos en esto:

<div class="container" infiniteScroll [infiniteScrollDistance]="2"  (scrolled)="onScroll()">

Inicializamos el componente infiniteScroll y hacemos uso del parámetro [infiniteScrollDistance]. Dicho parámetro marca la distancia, en porcentaje, a la que se ejecuta el evento (scrolled). Es la distancia al fondo. Es decir, que en nuestro caso, se trata de un 20% de página restante bajo el scroll o, lo que es lo mismo, un 80% de página scrolleada. Eso es lo que definen en la documentación oficial. Por la experiencia que he tenido, no se comporta realmente así a veces, y depende mucho del navegador que utilicéis. Pero, aunque ese porcentaje no sea muy exacto, lo importante es que según vayamos scrolleando en la página se irán dando eventos scrolled y, como vemos, cada vez que se de dicho evento, se ejecuta el evento onScroll() que creamos.

Para visualizar los datos crearemos una tabla, de forma que nuestra vista HTML quedaría algo así:

<div class="container" infiniteScroll [infiniteScrollDistance]="2"  (scrolled)="onScroll()">
  <h1 style="text-align: center">INFINITE SCROLL EXAMPLE</h1>
  <table class="table">
    <thead>
      <tr>
        <th scope="col">#</th>
        <th scope="col"><strong>TEXT</strong></th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let line of linesToWrite; index as i">
        <th scope="row">{{i}}</th>
        <th scope="row">{{line}}</th>
      </tr>
    </tbody>
  </table>
</div>

Con esto, nuestro infinite scroll en Angular ya es totalmente funcional. Pero claro, no tenemos un botón que nos devuelva a la parte más alta de nuestra web.

El botón para volver arriba

Al igual que el infinite scroll, en Angular necesitamos también la parte visual del botón y la parte lógica.

Lógica

Creamos un nuevo método en typescript:

scrollTop() {
    document.body.scrollTop = 0; // Safari
    document.documentElement.scrollTop = 0; // Other
  }

Este método restablecerá el valor del scrollTop a 0, volviendo así al tope de la página. Se comporta diferente en Safari que en los demás navegadores, por eso debemos especificarlo de las dos formas.

Pero con esto no basta. Par ser capaces de gestionar cuándo se ve y cuándo no, debemos crear una variable para este propósito:

private showGoUpButton: boolean;

Y debemos inicializarla en el constructor como falsa, junto a la otra variable inicializada antes:

constructor() {
    this.actualPage = 1;
    this.showGoUpButton = false;
  }

De esta forma, al construir nuestro componente el botón no se mostrará por defecto hasta que avancemos hacia abajo el scroll una distancia que determinaremos a continuación. Y debemos determinar también la distancia a la que el botón vuelve a desaparecer:

showScrollHeight = 400;
hideScrollHeight = 200;

Cuando bajemos el scroll más de 400 píxeles, el botón debe mostrarse. Y cuando volvamos a subir a menos de 200, se ocultará de nuevo. Pero, ¿cómo gestionamos a qué altura estamos en el scroll? Existe una anotación precisa para este propósito. Veamos:

@HostListener('window:scroll', [])
  onWindowScroll() {
    if (( window.pageYOffset ||
      document.documentElement.scrollTop ||
      document.body.scrollTop) > this.showScrollHeight) {
      this.showGoUpButton = true;
    } else if ( this.showGoUpButton &&
      (window.pageYOffset ||
        document.documentElement.scrollTop ||
        document.body.scrollTop)
      < this.hideScrollHeight) {
      this.showGoUpButton = false;
    }
  }

De esta forma, cuando los elementos mencionados en el código superen el valor mencionado antes para mostrar el botón, la variable de control será posicionada a verdadera. Mientras que, al revés, cuando se supere (hacia arriba) el mínimo de la distancia para ocultarlo, volverá a posicionarse la variable de control como falsa.

Vista y estilos

Con esta lógica implementada, creamos un botón en nuestra página donde queramos que esté:

<button [ngClass]="'no-hidden'"
*ngIf="showGoUpButton" class="btn btn-dark"
(click)="scrollTop()">GO UP</button>

Cuando este botón sea pulsado, nos llevará al tope de la página. Pero ojo, el hecho de volver al tope no significa que los elementos cargados desaparezcan. Al volver arriba, no se eliminarán los elementos ya cargados.

Con las directivas [ngClass]*ngIf, podemos aplicar un estilo o no aplicarlo según el estado de la variable creada para ello. Y, para completarlo, debemos asignar los estilos que queramos a nuestra clase no-hidden en el css de nuestro componente.

.no-hidden{
  position: fixed;
  bottom: 10px;
  right: 10px;
  visibility: visible;
}

Conclusión

Para configurar bien una navegación de tipo scroll infinito, debemos tener en cuenta dos eventos:

  • OnScroll: cuando el usuario se deslice hacia abajo, debemos gestionar este evento para que cargue más datos. En nuestro ejemplo se trata de un simple generador de líneas de texto (y todas iguales, nada raro). Pero, en la realidad, puedes llamar a la API que estés consumiendo para que te devuelva la siguiente página de elementos. Y bueno, todo lo que se te ocurra. Para esto, la librería que estamos utilizando pone a nuestra disposición el módulo InfiniteScrollModule con directivas que nos ayudan a automatizar y entender mejor este proceso de una forma rápida, limpia y sencilla
  • Mostrar y ocultar el botón para volver arriba del todo, de forma que el usuario no se pierda en tanto scroll a lo largo de su estancia en nuestra aplicación. Para ello, nos apoyamos en unas de las directivas más básicas de Angular: ngClass y ngIf.

La entrada Cómo implementar infinite scroll en Angular se publicó primero en Adictos al trabajo.

Google Colab: Python y Machine Learning en la nube

$
0
0

Índice de contenidos

1. Introducción

En este tutorial veremos qué es y cómo utilizar Google Colab, la herramienta de Google en la nube para ejecutar código Python y crear modelos de Machine Learning a través de la nube de Google y con la posibilidad de hacer uso de sus GPU . Sí, has leído bien: con sus GPU y en la nube.

La principal ventaja que ofrece esta herramienta es que libera a nuestra máquina de tener que llevar a cabo un trabajo demasiado costoso tanto en tiempo como en potencia o incluso nos permite realizar ese trabajo si nuestra máquina no cuenta con recursos suficientemente potentes. Y todo de forma gratuita.

Otro de los beneficios que tiene lo indica el propio nombre, «Colaboratory», es decir, colaborativo, nos permite realizar tareas en la nube y compartir nuestros cuadernos si necesitamos trabajar en equipo.

2. Entorno

Este tutorial se ha escrito usando el siguiente entorno:

  • Hardware: Portátil MacBook Pro de 15′ (2,2 GHz Intel Core i7, 16GB DDR3).
  • Sistema Operativo: macOS Mojave 10.14.2
  • Software: Google Colab

3. ¿Qué es Google Colab?

«Google Colaboratory es un entorno gratuito de Jupyter Notebook que no requiere configuración y que se ejecuta completamente en la nube».

Esa es la definición «oficial», pero me gustaría ahondar un poco más para todo el que no esté familizarizado con Jupyter o Python.

Python permite su utilización para diversos paradigmas de programación, como programación orientada a objetos o programación funcional, pero quizá las características más importantes en este contexto sean que también se usa como lenguaje de scripting y que es un lenguaje interpretado.

Python trae consigo un «modo interactivo» que con un intérprete de línea de comandos permite lanzar sentencias y obtener los resultados, pero sus funcionalidades se quedaron cortas para los desarrolladores, por lo que surgió IPython, que más tarde evolucionaría en Jupyter.

Jupyter (sobre el que Gwydion hizo un tutorial de introducción) es un entorno interactivo que permite desarrollar código Python de manera dinámica. Jupyter se ejecuta en local como una aplicación cliente-servidor y posibilita tanto la ejecución de código como la escritura de texto, favoreciendo así la «interactividad» del entorno y que se pueda entender el código como la lectura de un documento.

En el siguiente apartado explicaré los componentes básicos, que son los mismos tanto para Jupyter como para Colab.

4. Funcionamiento y componentes básicos

Como ya he comentado, Colab es gratuito y forma parte de la suite de aplicaciones de Google en la nube. Por ello, para utilizarlo basta con acceder a nuestra cuenta de Google y, o bien entrar directamente al enlace de Google Colab o ir a nuestro Google Drive, pulsar el botón de «Nuevo» y desplegar el menú de «Más» para seleccionar «Colaboratory», lo que creará un nuevo cuaderno (notebook).

Ahora bien, ¿qué es un cuaderno? Un cuaderno es un documento que contiene código ejecutable (por ejemplo, Python) y también elementos de texto enriquecido (links, figuras, etc.). Es nuestro «entorno de trabajo» en Colab y tiene la siguiente pinta nada más crearlo:

En la parte superior se encuentra el nombre del cuaderno (pudiendo cambiarlo) y su formato .ipynb, que viene de IPython Notebook y es un formato que nos permite ejecutar cuadernos tanto en IPython, como en Jupyter y Colab. El fichero .ipynb contiene en formato JSON cada celda y su contenido.

Un cuaderno está compuesto por celdas. Una celda es la unidad mínima de ejecución dentro de un cuaderno, es decir, es donde incluimos nuestro código y lo ejecutamos. Para ejecutar una celda podemos pulsar el botón con el icono de Play que se encuentra a la izquierda o pulsando Ctrl+Enter (ejecutar celda) o Shift+Enter (ejecutar celda y saltar a la siguiente). Tras la ejecución, debajo de la celda encontramos el resultado (si lo tiene).

En la parte izquierda de una celda ejecutada se puede observar un número entre corchetes. Este número indica el orden en el que se ha ejecutado cada celda. En la imagen, por ejemplo, se puede ver un 11 porque he ejecutado 11 veces la misma celda. Si tuviésemos dos celdas y ejecutásemos la primera y después la segunda, en la primera aparecería un 1 y en la segunda un 2. Si pasamos el cursor por esta parte izquierda de la celda también podemos ver información sobre quién ejecutó la celda, en qué momento y cuánto tardó la ejecución.

Podemos cambiar el orden de las celdas con las flechas de la parte superior, hacia arriba (también con el atajo Ctrl+M K) o hacia abajo (Ctrl+M J), o modificarlas con el menú desplegable de la parte derecha de cada celda, borrándolas (Ctrl+M D) o añadiendo comentarios y enlaces.

Desde los botones de la parte superior o en el menú «Insertar» podemos añadir nuevas celdas, tanto específicas para código como para texto. La distinción que hace Colab sobre ellas es que las celdas de código son ejecutables, mientras que las de texto muestran directamente el texto que incluyamos y además incluyen un pequeño editor de texto.

4.1. El entorno de ejecución

Cada celda es independiente, pero todas las celdas en un cuaderno utilizan el mismo kernel. El kernel es el motor de computación que está por debajo y que se encarga de ejecutar nuestro código y devolver el resultado para mostrarlo en la celda. El estado del kernel persiste durante el tiempo, con lo que, aunque cada celda sea independiente, las variables declaradas en una celda se pueden utilizar en las demás celdas.

Normalmente, el flujo de ejecución de las celdas será de abajo hacia arriba, el orden natural en el que están creadas, pero es posible que en algún momento queramos ejecutar o cambiar algo en una celda anterior. Por ello es útil el número que aparece a la izquierda que, como hemos comentado antes, indica el orden de ejecución.

En este cuaderno, primero importo la librería numpy y luego declaro una función para elevar números al cuadrado. En celdas siguientes, utilizo esa función y la librería para elevar al cuadrado números aleatorios.

Ahora se puede ver como, en la siguiente celda cambio el valor de «y» y vuelvo a ejecutar la celda que imprime, por lo que la frase que se imprime no tiene sentido pero gracias a los números de orden puedo ver qué ha ocurrido.

Todo esto facilita mucho el separar nuestro código en bloques lógicos sin la necesidad de volver a importar librerías, o recrear variables o funciones.

Desde el menú de «Entorno de ejecución» se puede reiniciar el estado del entorno de ejecución (Ctrl+M .) para liberar toda la memoria que hayamos utilizado (ojo, perdiendo variables y demás), interrumpir cualquier ejecución lanzada (Ctrl+M I), y lanzar ejecuciones sobre las celdas: Ctrl+F9 para ejecutar todo el cuaderno, Ctrl+F8 para ejecutar las celdas anteriores, Ctrl+Shift+Enter para ejecutar las celdas seleccionadas o Ctrl+F10 para ejecutar la siguiente celda.

Cuando creamos un nuevo cuaderno, este es «estático», es decir, vemos su contenido, pero no estamos conectados a ningún entorno de ejecución. Nuestro cuaderno se conecta a una VM de Google Compute Engine (la infraestructura de máquinas virtuales de Google en la nube) cuando ejecutamos una celda o pulsamos sobre el botón de «Conectar». Al hacerlo, el cuaderno toma un momento en conectarse y después muestra, de ahí en adelante, el espacio de RAM y disco que estamos consumiendo. La máquina en un inicio cuenta con 12 GB de RAM y 50 GB de almancenamiento en disco disponibles para el uso.

La duración de la máquina virtual a la que nos conectamos, es decir, el tiempo máximo que podemos estar conectados a una misma máquina desde un cuaderno es de 12 horas. Aquí hay que tener cuidado, sobre todo si estamos llevando a cabo ejecuciones que toman mucho tiempo: si pasamos más de 90 minutos sin utilizar un cuaderno, el entorno se desconecta. Para que no se desconecte basta con dejar la ventana del navegador abierta o celdas ejecutándose. La ventaja que tiene para estos casos es que, si dejamos una celda ejecutándose y cerramos el navegador, la ejecución continuará y si posteriormente abrimos el navegador tendremos nuestro resultado.

4.2. Cómo utilizar un entorno con GPU o cambiar la versión de Python

Por defecto, el entorno al que se conecta Colab utiliza un kernel con Python 3 y no permite la ejecución con GPU. En un inicio, Colab utilizaba Python 2, pero ahora permite crear nuevos cuadernos tanto en Python 3 como en Python 2, desde el menú de «Archivo».

De esta forma, crearíamos un cuaderno utilizando la versión de Python que deseemos. No obstante, podría ocurrir que querramos cambiar la versión de Python una vez creado el cuaderno o utilizar la GPU en lugar de la CPU (que es la que se usa por defecto) para realizar ejecuciones más ponentes, como la creación de modelos de aprendizaje profundo. Colab nos permite cambiar los ajustes del entorno para utilizar una GPU de forma gratuita. Para ello, vamos al desplegable de «Entorno de ejecución» y seleccionamos «Cambiar tipo de entorno de ejecución».

Aquí podemos cambiar la versión entre Python 3 o Python 2 y, lo más importante, cambiar el «Acelerador por hardware» de «None» a «GPU». La otra opción es «TPU», que son unidades de procesamiento de tensores, específicas de Google para ejecutar modelos de machine learning.

Cuando pulsamos «Guardar», Colab guarda nuestros ajustes y cambia de entorno de ejecución para darnos una máquina con esos ajustes. Es posible que por temas de limitación de recursos no nos podamos conectar a una máquina con GPU. Esto puede ocurrir si hay demasiados usuarios al mismo tiempo y, además, Google limita el uso que podemos hacer de estas GPU para evitar que, por ejemplo, se minen criptomonedas. Por esto, puede pasar que si estamos realizando una ejecución muy prolongada y muy potente, el entorno se desconecte.

Si, por alguno de estos u otros motivos, no podemos conectarnos a un entorno, Colab nos dará un aviso diciendo que no hay entornos con GPU disponibles.

Para comprobar si estamos conectados al entorno de GPU, podemos chequear que el botón de «Conectar» en la parte superior tenga un tick verde y podemos ejecutar el siguiente código:

import tensorflow as tf
device_name = tf.test.gpu_device_name()
if device_name != '/device:GPU:0':
  raise SystemError('GPU no encontrada')
print('Encontrada GPU: {}'.format(device_name))

Si el resultado es un error, no estamos conectados a una GPU. Si estamos conectados, la respuesta debería ser «Encontrada GPU: /device:GPU:0».

Puede darse la situación que se ha comentado de que no podamos conectarnos a un entorno con GPU, que la ejecución que queramos hacer sea más pesada de lo que permite Google y nos esté tirando la conexión al entorno, o que queramos utilizar una versión distinta a las que vienen de Python 3 o Python 2.

Colab nos permite conectarnos a un servidor de Jupyter que tengamos configurado en local si vamos al botón de «Conectar» y «Conectar a un entorno de ejecución local». Esto permite acceder a nuestro sistema de archivos local, con las versiones que queramos y tengamos instaladas en local. La desventaja es que el hardware que estaríamos utilizando es el de nuestra máquina local y perderíamos la capacidad de ejecutar el código sobre una GPU de Google y perderiámos también el uso compartido del cuaderno, pues si un usuario se conecta a local con un cuaderno, el resto de usuarios se conectarían a un entorno en la nube.

5. Funciones útiles

5.1. Las celdas de texto

Cuando entramos en la página de inicio de Google Colab, , lo que nos encontramos es un cuaderno a modo de guía introductoria. En este cuaderno se pueden encontrar algunas directrices y enlaces de interés, pero ahora nos va a servir para ilustrar un poco el tema de las celdas de texto enriquecido.

Si echamos un vistazo a las dos primeras celdas, nos encontramos lo siguiente:

Lo único que se ve en estas celdas es puro texto y un vídeo incrustado. Fijándonos, la primera celda es una celda de texto, mientras que la segunda es una celda de código (ejecutable). Hacemos doble clic en ambas para ver su contenido:

Viendo la primera, el contenido está escrito en HTML, el icono de Colab está incrustado mediante la etiqueta img y el encabezado con la etiqueta h1. Esto no es porque en Colab el texto se escriba en HTML para que lo interprete y lo pinte correctamente en la celda. Colab utiliza Markdown como lenguaje de marcado para las celdas de texto, solo que este lenguaje es un superset de HTML, así que es capaz de interpretar sus etiquetas. Aquí podeís encontrar un cuaderno de Colab con una guía de referencia sobre Markdown.

La celda siguiente es más curiosa, porque incluye texto pero es una celda ejecutable. Esto lo hace gracias a las etiquetas @title y @markdown, que, respectivamente, le da un título a la celda en formato Markdown y permite incluir texto en Markdown. Además, vemos que utiliza YoutubeVideo de la librería de IPython display para incrustar un vídeo de Youtube. Las celdas también admiten formato LaTeX.

Hemos visto así como se puede hacer de los cuadernos no sólo un script ejecutable sino también una conjunción de texto y código que permita acompañar a los bloques de código con fórmulas, guías, tutoriales o comentarios.

5.2. El menú lateral

En la parte lateral izquierda encontramos un menú bastante útil que tiene tres pestañas: Índice, Fragmentos de código y Archivos.

La primera pestaña, de Índice, sirve como tal para los cuadernos que tengan capítulos y secciones, dado que, como hemos comentado antes, las celdas de texto permiten convertir nuestro cuaderno en una especie de documento legible.

La pestaña de Fragmentos de código es muy útil, porque contiene muchos ejemplos de código que nos pueden servir para trabajar con Colab (por ejemplo, cómo capturar contenido de una webcam y tratarlo en tiempo de ejecución). Además, permite tanto ver el código en un cuaderno separado como insertar los fragmentos en nuestro propio cuaderno.

Y por último, la pestaña de Archivos, que tiene la función de permitirnos acceder al sistema de archivos de la máquina que estamos usando.

5.3. Ejecutando comandos bash

¿Y si vamos a utilizar nuestro entorno de ejecución en la nube, con nuestra GPU gratuita, y resulta que nos falta alguna librería? ¿Y si el proyecto que vamos a tocar está alojado en un remoto de un sistema de control de versiones? Ningún problema, porque Colab nos permite ejecutar comandos bash en nuestro entorno de ejecución como si estuviésemos lanzando comandos desde la consola de la máquina.

Para ello, basta con añadir el signo de exclamación «!» delante del comando que queramos ejecutar y Colab entenderá que queremos ejecutar un comando. Así, si queremos traernos nuestro proyecto al entorno podríamos clonarlo haciendo

!git clone

Si necesitasemos alguna librería (lo cual es raro que ocurra porque Colab viene con las librerías comunes ya instaladas), podemos ejecutar el mítico gestor de paquetes de Python con

!pip install

Esta función es muy interesante porque nos permite manipular hasta cierto punto el entorno en la nube para que se ajuste a nuestras necesidades. Siempre hay que tener en cuenta que todo lo que instalemos en nuestra máquina es volátil y que si nos conectamos a otro entorno posteriormente tenemos que instalarlo de nuevo.

5.4. Importación y exportación

Otra función bastante obvia pero útil es la importación y exportación de cuadernos.

Al abrir Colab aparece un diálogo con múltiples opciones para abrir un cuaderno. Este mismo diálogo aparece si vamos al menú «Archivo» y pulsamos «Abrir cuaderno…» (Ctrl+O).

En este diálogo tenemos la opción de abrir un cuaderno de entre los ejemplos que pone Google a nuestra disposición, abrir un cuaderno reciente, abrir un cuaderno que tengamos almacenado en Drive, y las dos más útiles: importar un cuaderno desde un repositorio de GitHub o subir un cuaderno que tenemos en local (de IPython o Jupyter).

Por el otro lado, nos da bastantes opciones para exportar/guardar nuestros cuadernos.

Así, desde el menú «Archivo» tenemos estas opciones, que nos permiten guardar el cuaderno en Drive, GitHub, como gist de GitHub e incluso descargar el .ipynb o el .py si queremos tener directamente un fichero ejecutable de Python.

6. Cómo subir y utilizar archivos locales o externos

Una de las cosas que más me chocó cuando me puse a usar Colab es que no encontraba una manera directa o «a simple vista» para subir archivos que necesitemos para nuestros cuadernos. Por suerte, existen varias maneras de hacerlo así que quiero exponerlas aquí.

6.1. Desde local

Para subir un archivo desde local basta con ejecutar el siguiente fragmento de código:

from google.colab import files

files.upload()

Al ejecutarlo, aparecerá el típico botón de subida de archivos que abrirá una ventana para seleccionar los archivos que queramos subir de nuestro sistema. También tenemos la opción de ir al menú lateral y en la pestaña de «Archivos» pulsar el botón de «Subir».

El directorio raíz de Colab es /content. Por tanto, es el directorio que aparece abierto cuando abrimos la pestaña de «Archivos» y es el directorio donde se guardan todos los ficheros que subamos. Por ejemplo, si subiéramos un archivo llamado ejemplo.csv, el path para acceder a este sería /content/ejemplo.csv.

De la misma forma que subimos archivos, podemos descargarlos de la máquina remota con

files.download()

, por lo que podríamos descargar el fichero que hemos subido ejecutando

files.download("ejemplo.csv")

6.2. Desde Google Drive

Esta opción nos permite utilizar archivos que tengamos almacenados en Google Drive. Existen varias opciones, cómo utilizar la API REST de Drive o la librería de Python PyDrive, pero sin duda la que más nos facilita la vida es montar nuestro Google Drive localmente en la máquina.

La ventaja de usar esta opción no es únicamente que podemos tener nuestros archivos alojados en Google Drive y acceder fácilmente a ellos. Cuando trabajamos con machine learning o ciencia de datos, en la mayoría de ocasiones contamos con archivos enormes de datos. Si cada vez que nos conectamos a un entorno distinto tenemos que subir estos archivos, perdemos demasiado tiempo. Por ello, si tenemos estos archivos alojados en Drive y montamos Drive en la máquina, podemos acceder a ellos como si estuvieran en local.

Para montar nuestro Drive en la máquina lanzamos el siguiente código:

from google.colab import drive

drive.mount('/content/drive')

Esto nos abre una URL que nos pide un código para autorizar a Colab a utilizar nuestro Drive y lo tenemos que pegar en el input que aparece.

Una vez el proceso termina, nuestro Drive se ha montado dónde le hemos indicado (/content/drive) y podemos acceder tranquilamente a nuestros archivos.

7. Conclusiones

Hemos visto qué es Colab, cómo funciona y algunas de sus funciones más útiles, lo que nos da la oportunidad de poder trabajar con Python en la nube y llevar a cabo tareas de machine learning, análisis de datos y demás de forma colaborativa, pudiendo compartir nuestro trabajo y aprovechando la potencia que nos da Google.

Ahora ya podéis lanzaros a probar con vuestros notebooks en la nube.

Cualquier pregunta o sugerencia, no dudes en dejar un comentario.

La entrada Google Colab: Python y Machine Learning en la nube se publicó primero en Adictos al trabajo.

Arquitectura Software y Metodologías Ágiles. ¿Realmente son compatibles?

$
0
0

En este artículo discutiremos sobre cómo Arquitectura Software y Metodologías Ágiles pueden trabajar en conjunto para sacar lo mejor de ambos.

¿Qué es Arquitectura Software?

Existen bastantes definiciones sobre qué es Arquitectura Software. A mí, particularmente, me gusta esta:

«Grupo de principios y restricciones sobre cómo las soluciones software deben ser construidas dentro de un ámbito dado (normalmente una empresa o subconjunto de ella)».

Algunos ejemplos de principios y restricciones podrían ser:

  • Solo se pueden utilizar lenguajes de programación basados en JVM
  • La comunicación síncrona entre componentes se realizará mediante sus correspondientes APIs REST. Cada API estará documentada en Swagger.
  • La comunicación asíncrona se realizará utilizando el bróker de mensajería corporativo (ej: RabbitMQ).
  • Las trazas (logs, auditoría) que generen los módulos serán centralizados en ELK. Cada mensaje debe tener un formato específico.

Además de este grupo de principios y restricciones, todo software contiene de manera inherente unos atributos de calidad (requisitos no funcionales) que deben ser tenidos en cuenta. Por ejemplo:

  • Alta disponibilidad
  • Seguridad
  • Rendimiento
  • Modificabilidad
  • Testeabilidad
  • Etc…

El grado de aplicación de cada uno de estos atributos debe ser proporcional a la solución que queremos construir. No será lo mismo construir una pequeña aplicación web que usarán dos usuarios para consulta de información no sensible dentro de una LAN que crear un sistema que gestione historia clínica de pacientes (SEGURIDAD), o que un software para el control del tráfico ferroviario (RENDIMIENTO, DISPONIBILIDAD, ROBUSTEZ…). Evidentemente el grado de aplicación de los atributos de calidad diferirá sustancialmente entre cada solución.

Por tanto, “arquitecturar” una solución será aplicar dichos principios, restricciones, requisitos funcionales y no funcionales para construir una solución software: sus componentes, interfaces, relaciones, estructuras, etc…

Martin Fowler describe Arquitectura Software como las decisiones que son difíciles de cambiar. Una descripción tan simple como brillante.

¿Por qué Arquitectura Software?

Conforme el número de soluciones software va creciendo, se empieza a requerir un cierto grado de homogeneización. En la mayoría de los casos, esto tiene implicaciones económicas (ahorro de costes de manera directa o indirecta).

Veamos esto mejor con un ejemplo volviendo al punto anterior donde decíamos que en una compañía (o parte de ella) la Arquitectura de referencia requería que solo se pudiesen utilizar lenguajes de programación basados en JVM. Visto de este modo, cualquiera podría pensar:

  • ¿Qué lleva a un grupo de arquitectos (o quien sea) a tomar esta decisión?
  • ¿Por qué restringir el desarrollo de software a un único lenguaje (o grupo de ellos)?

Más aún, si además dijimos que la comunicación entre componentes sería a través de APIs REST, el lenguaje en sí no debería ser una barrera en este sentido ya que el propio API nos abstraerá del lenguaje en el que esté escrito el servicio: principio de alta cohesión y bajo acoplamiento.

Las razones, como comentamos anteriormente, suelen tener connotaciones económicas.

  • ¿Qué sucederá si toda nuestra plataforma está escrita utilizando siete lenguajes diferentes?
  • ¿Qué habilidades deberán tener los desarrolladores que incorporemos para trabajar en ella?
  • ¿Qué sucederá si queremos contratar un soporte empresarial para cada lenguaje?
  • ¿Cómo mantenemos todo esto en producción: monitorización, actualizaciones, infraestructura, etc…?
  • ¿Qué sucederá si queremos proporcionar un framework corporativo para desarrollar los diferentes componentes? ¿Tendremos que escribirlo y mantenerlo en siete lenguajes diferentes?

Estos son solo algunos de los problemas que podríamos tener sin dicha restricción. Por tanto, lo que en un principio podría considerarse como una limitación (usar un solo lenguaje de programación), puede convertirse en un modo de ahorro de costes en un futuro. Por supuesto, este tipo de restricciones también tienen sus inconvenientes que deben ser analizados para ver cómo encajan con la cultura de la compañía.

Por tanto, podremos decir que nuestra Arquitectura Software es útil si ayuda a mantener los principios y restricciones en los que se basa al mismo tiempo que sienta las bases para desarrollar las soluciones que demanda negocio.

¿Qué es Agile?

La esencia de Agile gira en torno a la entrega rápida y constante de valor y la aceptación del cambio. Es mejora continua. Es entregar una solución con un alto grado de calidad en un periodo corto de tiempo. Es eliminar el desperdicio.

En Agile existen diferentes metodologías como Scrum, que se basa en equipos autónomos y auto-organizados. Scrum se apoya en un proceso iterativo (repetido en pequeños ciclos) e incremental (entrega nueva funcionalidad).

Ahora bien, ¿cómo encajan estos equipos autónomos y que entregan valor de manera rápida con una serie de principios y restricciones a la hora de construir software? ¿No retrasará el proceso de “arquitecturar” la solución esta entrega continua de valor? ¿Cuánto tiempo debemos invertir en “arquitecturar” en cada iteración?

Sin duda no existe una respuesta sencilla a esta última pregunta. Probablemente lo más sensato sea decir: lo justo. La mínima cantidad de arquitectura para alcanzar estos principios, visión y restricciones para construir la solución. Ahora bien, ¿cuánto es “lo justo”?

En este punto deberíamos valorar la propia naturaleza del producto/proyecto en el que nos encontramos inmersos. Si es un proyecto muy caótico con múltiples cambios drásticos, probablemente la cantidad de “Arquitectura” que debemos aplicar en el diseño por adelantado deberá ser menor que en otro tipo de proyectos más estables.

Como regla general, podría ser interesante hacer algo de diseño por adelantado antes del inicio del proyecto (¿Sprint 0?) y otra pequeña cantidad antes de cada iteración. Además, podríamos incluir una revisión de la Arquitectura como parte del Definition of Done (DoD) de cada historia de usuario. Esto último debe ser tenido en cuenta a la hora de planificar el Sprint. Podría ser también muy interesante que, al menos, un miembro del equipo fuese responsable de asegurar que la Arquitectura y el desarrollo del producto se encuentran alineados.

Si simplemente esperamos que la Arquitectura Software emerja, podemos acabar en un ciclo de “Sprints de refactorización” debido a la falta de diseño por adelantado, especialmente en proyectos grandes y complejos. Como decía Martin Fowler, la Arquitectura Software es un grupo de decisiones relevantes que son difíciles de cambiar. No tener en cuenta estas decisiones durante el desarrollo del producto puede llevar a una enorme deuda técnica y no es esto, precisamente, lo que promueven las metodologías ágiles.

¿Qué cantidad de Arquitectura Software necesitamos?

Idealmente, la mínima cantidad para cumplir con los principios y restricciones, requisitos funcionales y no funcionales además de proporcionar las bases para construir la solución.

Veamos diferentes escenarios. Si vamos a construir una solución para la recolección y consulta de datos de historia clínica de pacientes (información muy sensible), probablemente la Arquitectura Software estará muy enfocada a diferentes políticas de acceso a datos, ofuscación, certificados, trazabilidad, protocolos, etc…

Por otro lado, si vamos a rehacer un sistema debido a que se ha vuelto tecnológicamente obsoleto además de inmantenible, seguramente aparecerán muchos principios relativos a la modularidad, testeabilidad, nuevo stack tecnológico, etc…

Por último, si vamos a trabajar en un producto nuevo que, de manera experimental, trata de enfocarse en un nuevo nicho de mercado, probablemente con una arquitectura muy ligera bastaría debido a la gran incertidumbre que rodea al producto.

Muchas empresas tienen un framework corporativo que implementa o ayuda a implementar muchos de los principios de la Arquitectura de referencia. Usar dicho framework de manera apropiada puede ser muy valioso para el equipo ágil. Para ello, debemos asegurarnos de que, al menos, uno de los miembros del equipo lo conozca profundamente para asegurar un uso óptimo y ayudar a mejorarlo.

Arquitectura Software y Metodologías Ágiles en empresas grandes

Alguien podría preguntarse, ¿cómo pueden los equipos autónomos y auto-organizados dar lo mejor de sí al mismo modo que se benefician de la Arquitectura Software? ¿Cómo podemos hacer que la Arquitectura continúe evolucionando sin ralentizar el ritmo de los equipos?

Vayamos por partes. Por un lado, cada uno de estos equipos suele estar enfocado en un área de negocio específica dentro de una organización, lo que es genial. Sin embargo, es muy difícil que un equipo que trabaja de esta forma se pueda anticipar a problemas que puedan aparecer fuera del contexto (dominio de negocio) en el que se mueve.

Probablemente, el equipo no tenga un conocimiento y visión global de todo el sistema, lo que puede derivar en la construcción de soluciones redundantes o heterogéneas dentro del contexto global de la compañía: probablemente otros equipos ya se habrán enfrentado a un problema parecido. Esto hace que sea muy deseable que algún componente del equipo tenga un conocimiento de la Arquitectura de referencia a alto nivel.

Por otro lado, la mejora y optimización de la Arquitectura de referencia, al igual que el propio proceso de innovación, no debería ser propiedad de un grupo cerrado de individuos, sino que debería poder emerger de manera natural desde cualquiera de los equipos de la organización. Lógicamente, cada nueva iniciativa en este sentido tendría que ser gestionada por un equipo destinado a ello.

En este sentido, existe un concepto muy interesante dentro del framework SAFe que se llama Agile Architecture y que trata de sacar lo mejor de los dos enfoques. Parte de una Arquitectura diseñada por adelantado y que se va alimentando de los diseños que emergen de los equipos autónomos.

De este modo obtenemos dos grandes beneficios:

  • Contar con una Arquitectura de referencia que nos ayude a construir nuestras soluciones.
  • Permitir a los equipos contar con un cierto grado de innovación al mismo tiempo que alimentan la Arquitectura, permitiendo a otros equipos beneficiarse de ello.

¿Cómo podemos alinear Arquitectura Software y Equipos Ágiles?

Cuando nos referimos a equipos ágiles y autónomos, también nos estamos refiriendo a equipos multidisciplinares. Dichos equipos pueden estar compuestos por roles tales como: devops, scrum master, desarrollador back, desarrollador front, product owner, líder técnico, etc… Y es justo aquí donde el líder técnico jugará un papel fundamental.

El líder técnico (en algunos casos esta labor la desempeña el Arquitecto Software) será el responsable de hacer entender la Arquitectura de referencia (visión y roadmap) con el resto del equipo. Esta persona estará a cargo de la resolución de cuestiones técnicas, gestionar los atributos de calidad, manejar los requisitos que requieran un cierto grado de innovación y sincronizar los que apliquen en la Arquitectura de referencia.

Por supuesto, el Arquitecto no es una autoridad dentro del equipo sino un mentor que ayuda al equipo ágil a mejorar su efectividad trabajando codo a codo con ellos. Por otro lado, este rol debe tener un profundo conocimiento de la Arquitectura de referencia al mismo modo que se mantiene en permanente contacto con el resto de Arquitectos de otros equipos (¿Chapter de Arquitectura Software?).

Si queremos contar con equipos autónomos y que esto no acabe desencadenando en el caos, la comunicación y el alineamiento de principios deberán ser piezas clave. Es imposible conseguir coherencia y consistencia sin un cierto grado de control.

Enlaces relacionados

Conclusiones

Las Métodologías Ágiles y la Arquitectura Software no son conceptos incompatibles sino totalmente complementarios. La Arquitectura no debería ser considerada como un factor limitante sobre los equipos sino como un impulsor dentro de la propia cadena de entrega de valor.

La entrada Arquitectura Software y Metodologías Ágiles. ¿Realmente son compatibles? se publicó primero en Adictos al trabajo.

Implementando un registro remoto de logs con Angular

$
0
0

Si estás desarrollando una apliación con Angular y quieres hacer un registro remoto de los logs para que sean explotados por un sistema BELK, este es tu tutorial.

Índice de contenidos

1. Introducción

Como desarrollador que responde por cada línea de código que escribe, has decidido implementar un sistema de registro en su proyecto Angular.¡Bien hecho! Con esta biblioteca, puede implementar fácilmente un sistema de registro confiable con una configuración mínima que le sirva bien. NGX-logger actualmente admite las versiones Angular 6.x y 7.x

El sistema NGX-logger ofrece actualmente las siguientes características:

  1. Indicación de ubicación de registro y número de línea
  2. Envío de mensajes de registro a su punto final de backend como solicitudes POST
  3. Manera de controlar su nivel de registro basado en el entorno actual
  4. Habilitar / deshabilitar el registro de la consola del lado del cliente
  5. Pretty print en consola.

2. Para empezar

Teniendo en cuenta que está implementando este registrador por primera vez, le sugiero que comience un nuevo proyecto para que puede jugar un poco con él en un campo limpio 🙂

$> ng new logger-test-project

El primer paso para comenzar es instalar el paquete usando el siguiente comando

$> npm install --save ngx-logger

El segundo paso es importar la biblioteca en el módulo raíz (app.module.ts) de su aplicación

@NgModule({
  imports: [
      BrowserModule,
      FormsModule,
      LoggerModule.forRoot({
        serverLoggingUrl: '/api/logs',
        level: NgxLoggerLevel.TRACE,
        serverLogLevel: NgxLoggerLevel.ERROR,
        disableConsoleLogging: false
   })],
   declarations: [AppComponent],
   bootstrap: [AppComponent]
})
export class AppModule { }

Veamos qué hace esta configuración línea por línea.

  • serverLoggingUrl – es la ruta completa de su punto final de back-end donde ngx-logger intentará publicar los registros. Entonces, si no necesita mantener los registros en su servidor para algunos propósitos, no dude en eliminar esta línea.
  • level – Define el nivel de registro mínimo en la consola del navegador. Los niveles disponibles son: TRACE, DEBUG, INFO, LOG, WARN, ERROR, FATAL y OFF. La mayoría de estos valores provienen de NgxLoggerLevelclass. Por ejemplo, si configuramos el nivel en ERROR, eso significa que solo se registrará el nivel de error o superior.
  • serverLogLevel – define el nivel de registro mínimo para el registro del lado del servidor. Por ejemplo, si ajustamos el nivel a ERROR, eso significa que solo el nivel de error y superior se enviarán al servidor.
  • disableConsoleLogging – Indicador realmente útil que activa / desactiva el registro de la consola global cuando es necesario.

Después de que tengamos la configuración necesaria, podemos implementar algunos registros para realizar pruebas. Debemos agregar el siguiente código en el módulo principal de nuestro proyecto para que podamos generar los registros en el inicio de la aplicación.

constructor(private logger: NGXLogger) {
     this.logger.debug("Debug message");
     this.logger.info("Info message");
     this.logger.log("Default log message");
     this.logger.warn("Warning message");
     this.logger.error("Error message");
}

Después de ejecutar la aplicación podemos ver lo siguiente en la consola del navegador:

Podemos ver los mensajes de registro que hemos declarado en el constructor y también podemos ver algunos otros errores relacionados con el registro del lado del servidor. Esto se debe a que hemos declarado que los registros del lado del servidor deben enviarse a /api/logs endpoint, que no tenemos desarrollado.

3. Configuración por entorno

A veces, debemos considerar registrar solo errores en el entorno de producción y eliminar otros niveles y al mismo tiempo mantener todos los niveles de registro habilitados.

Si tenemos que cambiar manualmente la configuración cada vez que cambiemos de entorno, es fácil que cometamos errores y perdamos mucho tiempo.

Para solventar esto, la librería tiene la posibilidad de cambiar la configuración por entorno.

Para ello tenemos que agregar una dependencia de entorno en nuestro app.component.ts . Esta dependencia nos permitirá leer la configuración especificada en los «environments» de Angular. Luego reemplazamos la configuración de registro para usar variables de entorno.

import { environment } from 'src/environments/environment';
 
@NgModule({
   imports: [ 
     BrowserModule,
     FormsModule,
     LoggerModule.forRoot({
        serverLoggingUrl: `${environment.apiUrl}api/logs`,
        level:environment.logLevel,
        serverLogLevel: environment.serverLogLevel,
        disableConsoleLogging: false
      })
      ],declarations: [AppComponent],
        bootstrap: [AppComponent]
   })
   export class AppModule { }

Por defecto, hay dos entornos en una aplicación Angular: desarrollo y producción. Normalmente, el entorno de desarrollo utiliza environment.ts y el entorno de producción utiliza environment.prod.ts estos archivos están bajo la carpeta src/environment. Lo importante, para evitar errores de ejecución, es que los dos archivos definan las mismas propiedades de entorno, proporcionando el valor adecuado al entorno. De forma que ahora la configuración tendrá en cuenta el «environment» de Angular.

import { NgxLoggerLevel } from 'ngx-logger';

export const environment = {
    production: false,
    apiUrl: 'http://localhost:8080/', // Reemplazar con API local
    logLevel: NgxLoggerLevel.WARN,
    serverLogLevel: NgxLoggerLevel.OFF
};

Cuando ejecute la aplicación, se utilizará la configuración del entorno de desarrollo: solo se mostrarán los registros de advertencia y el registro del lado del servidor está desactivado ya que solo queremos ver los errores en la consola del navegador.

El fichero para el entorno de producción quedaría del siguiente modo:

import { NgxLoggerLevel } from 'ngx-logger';

export const environment = {
     production: true,
     apiUrl: 'http://api.myservice.com/api/', // Reemplazar con API remoto
     logLevel: NgxLoggerLevel.OFF,
     serverLogLevel: NgxLoggerLevel.ERROR
};

Nuestra configuración de producción indica que no habrá registro del navegador y solo se registrarán los errores en el servidor en la URL de API especificada. El comando utilizado para ejecutar en modo de producción es ng serve –configuration=production o ng serve –prod

4. Cosas a considerar

1. Si considera implementar un punto final para almacenar los registros, tenga en cuenta que las solicitudes POST están llegando en esta forma:

{
   "message":"Now we got a problem",
   "additional":[],
   "level":5,
   "timestamp":"2018-10-11T08:43:30.543Z",
   "fileName":"home-home-module.js",
   "lineNumber":"121"
}

2. Cuando esté implementando NGX-logger en su aplicación, las pruebas de los componentes que usan la biblioteca se verán afectadas y no se compilarán. Si inyecta cualquiera de los servicios del registrador NGX en su aplicación, deberá proporcionarlos en su módulo de pruebas. La librería presenta módulos para testing:

TestBed.configureTestingModule({
   imports: [
      LoggerTestingModule
   ],
   ...
});

5. Conclusiones

Al final podríamos decir que NGX-Logger hizo que el registro con Angular fuera tan simple como eso. Biblioteca clara, fácil de configurar que nos proporciona características impresionantes. Tenemos la posibilidad de agregar registros en todo el sistema y activarlos /
desactivarlos cuando lo deseemos. Registrando toda la información que necesitamos rastrear y localizar problemas en diferentes entornos.

Have fun logging!

La entrada Implementando un registro remoto de logs con Angular se publicó primero en Adictos al trabajo.

The Pixar Pitch

$
0
0

Introducción

A todos nos encantan las películas de Pixar, cada una de ellas nos ha hecho sentir diferentes emociones que nos han terminado atrapando. Quién no recuerda el viaje emocionante de Marlin buscando a su hijo Nemo, o la famosa frase de Buzz Lightyear: “Hasta el Infinito y más allá…” y quién no se ha emocionado con el anciano de UP. Todas ellas comparten un mismo patrón, una fórmula que les ha reportado su éxito actual y que hace un tiempo Emma Coats, guionista de Pixar, desveló para todo el mundo a través de su cuenta de twitter: https://twitter.com/lawnrocket.

Desarrollo

¿Y por qué hablamos aquí de una fórmula de éxito en películas de animación? Pues porque como ya se encargó de popularizar Daniel Pink, en su libro “To Sell is Human”, la fórmula del éxito de Pixar también la puedes aplicar a tu negocio. Y nosotros desde Autentia te vamos a indicar cómo utilizamos esta técnica para mejorar con nuestros clientes.

El origen del Pixar Pitch lo encontramos en el Story Telling. En la actualidad las famosas charlas TED, las keynotes de Apple o cualquier persona de renombre en los Open Space utilizan el Story Telling para contar su idea, para contar su historia.

Esto es así por el valor que aportan las historias, desde tiempos inmemoriales los hechos y conocimientos se transmitían generación tras generación mediante ellas. ¿Y por qué nos gustan tanto las historias? Porque crean una emoción positiva que inspiran a las personas a tomar acciones, enseña valores, virtudes, humanidad y una moraleja. Desde Autentia damos soporte para crear ese Story Telling que ayude a crear emociones que inspiren a los clientes y a ganar su confianza.

El Pixar Pitch está formado por la siguiente estructura de 6 frases secuenciales:

1.- “Había una vez…” …te permite abrir con una descripción general de la situación.

2.- “Cada día…” …representa la situación cotidiana que te ayuda a restringir el problema.

3.- “Un día…” …ese momento culmen y decisivo.

4.- “Debido a ello…” …1er casual; ¿qué cambió debido a la acción?

5.- “Debido a ello…” …2º casual; ¿qué cambió debido a la acción del 1er casual?

6.- “Hasta que finalmente…” …esto concluye y resalta la esencia del viaje o mensaje.

Esto es así ya que nuestra mente procesa mejor la información si viene de forma estructurada.

Ejemplo en las Películas Pixar

Veamos cómo lo utiliza Pixar en sus películas con el ejemplo de “Buscando a Nemo”:

1.- “Había una vezun pez viudo, llamado Marlin, quien era extremadamente protector de su único hijo, Nemo.”

2.- “Cada díaMarlin advertía a Nemo de los peligros del océano y le imploraba que no nadara lejos.”

3.- “Un díaen un acto de desafío, Nemo ignora las advertencias de su padre y nada en aguas abiertas.”

4.- “Debido a esto…es capturado por un buceador y acaba en la pecera de un dentista en Sidney.”

5.- “A causa de esto…Marlin pone en marcha un plan para recuperar a Nemo, consiguiendo la ayuda de otras criaturas marinas a lo largo del viaje.”

6.- “Hasta que finalmenteMarlin y Nemo se encuentran el uno al otro, se reúnen y aprenden que el amor depende en la confianza”

Ahora vamos a mostrar un ejemplo, donde se busca una mejora, como en Autentia empleamos la técnica del Pixar Pitch para la definición de HdU y como herramienta para las sesiones de Retrospectiva.

Ejemplo de Uso en la definición de HdU’s

Usando el Pixar Pitch para mostrar la funcionalidad a desarrollar y el entendimiento de las Historias de Usuario, dando pie a la conversación que nos dará el detalle de la Historia  de Usuario:

 



Ejemplo en la ceremonia de Retrospectiva

Además, la técnica puede ser usada para obtener un resumen del Sprint en la Retrospectiva, veamos el ejemplo:

1.- “Había una vezuna empresa tecnológica con una crisis que acechaba a cada uno de los departamentos y áreas que la integraban.”

2.- “Cada díalos diferentes equipos de trabajo se enfrentaban a la problemática de escasez de salas de reuniones y ante la falta de ellas no podían mantener las ceremonias Scrum con normalidad.”

3.- “Un díaun equipo desarrolló una definición simple y fácil de lo que deberían ser nuestras salas de reuniones y áreas de descanso y la expuso al resto.”

4.- “Debido a estolos diferentes departamentos empezaron a reubicar los puestos de trabajo creando nuevos espacios y la organización comenzó a ceder más áreas para reuniones.”

5.- “A causa de estoel personal se sintió libre para adaptar los nuevos espacios disponibles y reutilizarlos como áreas de reuniones y zonas de descanso.”

6.- “Hasta que finalmentecada miembro de la empresa, independientemente del área o nivel jerárquico, llegó a ser más productivo e incrementó su bienestar en la empresa.”

Imagen de Stephen Keeling en Pixabay Zona de Descanso Lean Coffee

Conclusión

Existen numerosos estudios sobre la memoria humana, te dejo el siguiente de Soledad Ballesteros: http://www.psicothema.com/psicothema.asp?id=323, cuyos resultados arrojan que nuestra mente procesa y reproduce la información mucho mejor cuando se presenta de forma estructurada. Seis valores es el máximo que estamos dispuestos a escuchar y leer (por ello los números de teléfono se suelen componer de un prefijo de provincia más los 6 números que identifican el teléfono), casualmente el Pixar Pitch presenta esta característica, a lo largo de 6 frases representa una lógica que lo hace más fácil para convencer.

¿Te atreves a crear tu propio Pixar Pitch y compartirlo con nosotros?

La entrada The Pixar Pitch se publicó primero en Adictos al trabajo.

Viewing all 989 articles
Browse latest View live