Refactorización y mejora de código (2)

Trabajando con objetos y sus requerimientos.

Felipe Medina
7 min readSep 20, 2017

Articulo editado el 10/11/2017

Temario

  1. Introducción.
  2. El switch del mal (diseño).
  3. Engine.
  4. Cambio de especificaciones.
  5. Requerimientos en la lupa.

Introducción

Esta es la segunda entrega de una serie de artículos sobre mejora de diseño de código durante el desarrollo de un web scraper, el artículo anterior es:

En este capítulo vamos a trabajar con la refactorización de objetos y sus requerimientos, los repasamos:

  • Obtener html de un sitio.
  • Parsear el html en base a extraer los datos solicitados.
  • Guardar los datos en una base no relacional (MongoDB).
  • Devolver los resultados para ser consumidos por otra aplicación.
  • Tener un sistema de caché que evite múltiples llamados a una misma url en un periodo de tiempo.
  • Tener un sistema de cambio de headers, si el request al sitio falla reintentar con otros headers.

Si todavía no les llamó la atención tener seis responsabilidades casi no relacionadas, este es el momento.

Necesitamos separar conceptualmente nuestra aplicación en sus subcomponentes, uno de ellos es el Scraper, que por su definición su responsabilidad es obtener la información que nosotros queremos de un conjunto de datos.

El único requerimiento que tenemos de este modulo entonces es:

  • Parsear el html en base a extraer los datos solicitados.

Teniendo esto en mente, pasemos al siguiente caso, donde la refactorización de métodos nos lleva naturalmente a la creación de nuevos objetos con nuevas responsabilidades o abstracciones de las mismas.

El switch del mal (diseño)

En la entrega anterior vimos que necesitamos poder variar la forma que extraemos la información, ya que a veces nos va a convenir usar regex y tratar el html como un string, otras algo que recorra el html como si fuese un xml o algo que simule un DOM con Cheerio.

En principio lo teníamos abstraído en un switch.

Usar un switch trae un problema, violamos el segundo principio de SOLID:

Principio de abierto/cerrado (Open/closed principle)
Es la noción de que las entidades de software deben estar abiertas para su extensión, pero cerradas para su modificación.

Si bien dejamos la clase abierta para extenderla también quedamos obligados a modificar la funcionalidad de la misma cada vez que agregamos un caso nuevo, así la clase no queda cerrada para su modificación. Una de las soluciones para esto es crear objetos separados que contenga las distintas lógicas que irían dentro de cada caso del switch y que el método que lo contenía consuma dichos objetos.

Engine

Si bien se podría pensar nombres más declarativos, llame al tipo de objeto “Engine”, pensándolo desde la idea de “motor de parseo de un scraper”, “ParserScraperEngine” es un mejor nombre y posiblemente el definitivo, pero por ahora nos mantendremos con el primero ya que quiero introducir este concepto de diseño de software.

Podemos hacer un paralelismo entre el motor de un auto y un motor de proceso de datos, el motor sólo en si mismo no mueve el auto, es el conjunto de componentes el que lo hace, bajo dirección del usuario, con la forma de producir movimiento a partir de un combustible que tiene el motor. En nuestro caso es lo mismo, el engine va a dejar todo listo para reaccionar ante la instrucción de lo que lo consuma y parsear el html para extraer los datos a que este le indique.

Creamos una interfaz, utilizando una clase de ES6.

EngineInterface.js

Extendemos la clase e implementamos el método extractData para utilizar Cheerio como parseador de html, copiando el código que teníamos en nuestra clase Scraper.

CheerioEngine.js

Por último agregamos un sencillo motor que utilice regex. Básicamente devolvemos la función nativa match a procesar el html, por lo que nuestros extractores recibirán dicha función y podrán pasarle la expresión regular específica para el dato a extraer.

RegexEngine.js

Ahora vamos nuestra clase Scraper y modificamos el método extractData quitando el switch del mal, agregando como párametro el Engine y modificando como se consume en htmlRequestSuccessHandler.

Aquí hagamos una pausa y veamos un concepto nuevo antes de continuar.

Cambio de especificaciones

Varias metodologías de desarrollo hacen hincapié en una cosa, los requerimientos vienen primero y deben ser inmutables. Es un objetivo difícil pero alcanzable, requiere de mucho trabajo y tiempo por parte del equipo de desarrollo, del cliente y todas las partes que medien o influyan entre estos. Se logran productos o prototipos funcionales de altísima calidad de esta forma.

En la realidad de las agencias de desarrollo, las subcontrataciones por agencias comerciales o publicitarias que es la realidad que conozco, como freelancer y emprendedor, hay muchos intermediarios entre el producto final, el software, y el equipo de desarrollo.

A diferencia de la fragmentación del proceso de desarrollo que ocurre dentro de una software factory, estos intermediarios no suelen tener conexión entre sí y todos trabajan con una lógica distinta, que, por más flexible que sea, no suele poder implementar todos los pasos requeridos en un proceso de relevamiento para poder cerrar los requerimientos de un proyecto en una primer instancia.

Como respuesta a esta problemática la industria generó varias formas distintas de responder a esto, dos muy utilizadas, la primera, una adaptación de la metodología de desarrollo en cascada, muy popular en 1980’s, que incluye la interacción del cliente final acortando y repitiendo las etapas en pequeños ciclos idénticos a los utilizados en cascada, llamada desarrollo en espiral.

La segunda, es la metodología ágil de desarrollo, tremendamente popular actualmente, principalmente en startups. En ambos casos se agendan encuentros con el cliente una vez finalizado un ciclo de desarrollo para rever los requerimientos. También se da cuando hay un input nuevo de un equipo de experiencia de usuario, o luego de pruebas a/b, entre otros.

Dicho todo esto, es lógico que necesitemos estar preparados para la modificación del origen de nuestro código.

Requerimientos en la lupa

Si repasamos el estado en que quedó htmlRequestSuccessHandler en la muestra de código anterior, vemos que nos falta incluir la clase CheerioEngine, cosa que podríamos hacer, pero inyectar una nueva dependencia no tiene que ser un tema trivial. Algunas de las preguntas que nos podemos hacer para guiarnos cuando nos encontramos con este momento son:

  1. ¿Necesitamos inyectar la funcionalidad dinámicamente?
  2. ¿El usuario final de la aplicación necesita control sobre el importado?
  3. ¿El siguiente desarrollador que toque el código necesita control sobre esto?
  4. ¿Existe algún caso de extensión de la funcionalidad que se vea limitada por incluir la funcionalidad donde se incluye?

Esto nos va ayudar a saber si es el lugar para realizar la composición de objetos, si no lo es, tiemblan los requerimientos, como es nuestro caso:

  1. Actualmente no, posiblemente en un futuro, pero no será responsabilidad del scraper.
  2. Si, justamente poder manejar cómo extraer la información en los casos que elija el usuario es lo que necesitamos.
  3. Si, la idea es que el el scraper sea extensible sin tener que modificar el código existente.
  4. Si, cuando necesitemos extender la cantidad de engines disponibles tendremos que modificar la clase Scraper.

Al comienzo del artículo comentaba que la única responsabilidad de nuestra aplicación debería ser parsear el html dado para extraer los datos deseados. Por lo que la duda de dónde colocar la funcionalidad principal, la de extracción de datos, nos lleva a otra pregunta que nos deberíamos haber hecho desde el comienzo: ¿cómo va a ser consumida nuestra aplicación?

Veamos el código de index.js

Nuestro método scrap recibe una url y los extractores, ahora que tenemos dos tipos de engines distintos que usan diferentes tipos de extractores, deberíamos agregar acá una forma de cargarlos en nuestro scraper. Por otra parte, nuestro scraper debe recibir un html, no una url, hacer el llamado a la url no es parte de las responsabilidades de nuestro objeto.

De esta manera separamos responsabilidades del llamado al servidor en otro objeto, abrimos nuestra clase Scraper al cambio ya que se pueden cargar nuevos engines y sus respectivos extractors, pero sin necesidad de modificar su código, por lo tanto cerrada a modificaciones.

También podemos utilizar varias veces un mismo engine y/o los mismos conjuntos de extractors para distintos html, o guardar un html en el estado y recorrerlo con distintos engines y/o extactors.

Por lo que, siendo más detallados, ese único requerimiento que tiene nuestro scraper se convierte en los siguientes subrequerimientos:

  • Recibir un conjunto de datos en formato html
  • Poder guardar el html en el estado interno del scraper
  • Utilizar un tipo de objeto que parsee la información, los engines.
  • Utilizar un tipo de objeto que extraiga un dato concreto, los extractors.
  • Parsear dinstintos htmls con una misma configuración.
  • Variar la configuración para parsear un mismo html.

Para cumplir con estos nuevos requerimientos removemos un montón de métodos, que luego utilizaremos en otras clases, y refactorizamos de la siguiente manera:

No volveremos a utilizar el kernel que utilizaba express de fondo. Los métodos que hacían la llamada al servidor de la url los vamos a colocar en otra clase que va a ser consumida por nuestro Crawler.

Pero antes de continuar con esta separación de funcionalidades en nuevos objetos, en la próxima entrega vamos a comenzar a utilizar unidades de testeo para facilitarnos los siguientes desarrollos.

Pueden leer la siguiente entrega:

Mi nombre es Felipe Medina, soy un arquitecto de software argentino especializado interfaces gráficas web, trabajo desde DoSocials! con la convicción de formar redes interprofesionales y multidisciplinarias. Creo en el conocimiento libre e intento aportar al mismo, por lo cual, cualquier pregunta es bienvenida. Asimismo agradezco cualquier crítica o comentario, también pueden contactarme vía:

--

--