Esta publicación será corta. Va de nombrar adecuadamente nuestras variables. La idea es siempre pensar en el mantenimiento futuro y de tener por dogma de fe que nuestro código será menos comprensible para los demás y para nosotros mismos cuando pasen unos días, las semanas, etc.

Veamos el siguiente ejemplo:

Tiene claros inconvenientes:

  • No sabemos de qué va los cuatro elementos del arreglo de enteros.
  • El valor cuatro del cuarto elemento tiene un significado especial.
  • Hay una estructura de datos, pero desconocemos los detalles.
  • El código no es auto-explicativo y no nos aclara la estructura detrás del código.

No obstante, esto podría mejorar un poco.

Ahora tenemos claro algunos aspectos.

  • La función filtra piezas.
  • El filtro se hace sobre un tablero de juego.
  • El cuarto elemento del arreglo representa el progreso de la pieza.
  • Este progreso puede estar en “coronado”.

No obstante, la idea siempre es enriquecer nuestro dominio con nuevas clases y mejorar la semántica del código.

Desde hace unos meses he estado lidiando con el tema de la complejidad del código. Ya sea por no percibir los antipatrones o lidiar con las fechas de entrega, terminamos añadiendo un cúmulo de líneas de código que pueden ser manipulados para no añadir más complejidad a la que ya se tiene y generar un modelo de dominio durante ese proceso. Este es el primer artículo en el que trataremos algunas ideas que pueden ayudar a hacer más mantenible el código.

Como observación, antes de iniciar un proceso de refactoring es necesario contar con una buena batería de pruebas que aseguren el comportamiento de la aplicación. En el siguiente repo dejo las versiones del proceso.

El tanque

Nuestro ejemplo iniciará con un tanque que se mueve en una sola dirección avanzando a pasos.

class Tank:
  def __init__(self):
    self.position = [0, 0]

  def execute(self, orders):
    ...

  def __backward(self):
    ...

  def __forward(self):
    ...

Este tanque ejecuta órdenes para avanzar o retroceder.

def execute(self, orders):
  for order in orders:
    if order is 'b':
      self.__backward()
    elif order is 'f':
      self.__forward()

def __backward(self):
  self.position[1] -= 1

def __forward(self):
  self.position[1] += 1

Command pattern

Cuando creamos un modelo de dominio siempre terminaremos con más clases. Y eso es bueno. Enriquece nuestras estructuras y nos permite razonar sobre ellas.

Es importante notar que cada patrón tiene un intención. Es importante que detectemos eso. Acá podríamos usar un Strategy o usar un Command en lugar del primero (como veremos más adelante), pero es esa intención la que nos ayudará a hallar el patrón que mejor se adapta. Así, la idea del patrón Command es la de ejecutar acciones dada una condición directa.

Iniciemos creando nuestros comandos.

class Backward:
  def move(self, position):
    new_position = copy(position)
    new_positionp[1] -= 1
    return new_position

class Forward:
  def move(self, position):
    new_position = copy(position)
    new_positionp[1] += 1
    return new_position

Como tenemos las mismas acciones que antes eran realizadas por los métodos __backward y __forward, estos no son necesarios y podemos dejar la ejecución de las órdenes como sigue

class Tank:
  def __init__(self):
    ...
    self.actions = {
      'b': Backward(),
      'f': Forward()
    }

  def execute(self, orders):
    for order in order:
      self.position = self.actions[order].move(self.position)

Así, actualizamos la posición a cada nueva orden.

Nueva funcionalidad: dirección

Ahora hagamos que nuestro tanque pueda girar. Una manera sencilla en que el command pattern nos ayuda con esto es la idea que a cada orden debemos actualizar el estado del tanque. Este estado está representado por la posición y la dirección.

class Tank:
  ...
  def execute(self, orders):
    for order in order:
      self.position = self.actions[order].move(self.position)
      self.direction = self.actions[order].turn(self.direction)

La idea es la misma, actualizamos la dirección en función de la anterior a cada nueva orden.

Esto lo logramos modificando los comandos de la siguiente manera:

class Backward:
  def move(self, position):
    ...

  def turn(self, direction):
    return direction


class Left:
  def move(self, position):
    return position

  def turn(self, direction):
    if direction is 'N':
      return 'W'
    elif direction is 'W':
      return 'S'
    elif direction is 'S':
      return 'E'
    elif direction is 'E':
      return 'N'

Con las clases Left y Right realizamos los giros del tanque.

Podríamos dudar un poco sobre la implementación de la actualización de los valores de la posición y la dirección. Pero esto no contradice la idea que tenemos que actualizar el estado del tanque después de cada comando. No obstante, es posible optimizarla encapsulando ese estado del tanque en una estructura adicional.

Sin embargo, vemos nuevamente el antipatrón en la implementación del giro.

State object pattern

Una manera de eliminar esos molestos elif’es es representando la dirección actual por un estado que sepa cómo ir a la siguiente dirección.

class North:
  def left(self):
    return West()

  def right(self):
    return East()

Así, habiendo enriquecido nuestro dominio, podemos actualizar los métodos de giro para manejar los nuevos objetos de dirección.

class Left:
  ...
  def turn(self, direction):
    return direction.left()

class Right:
  ...
  def turn(self, direction):
    return direction.right()

De manera que no importa en que dirección nos encontremos, siempre sabremos como ir a la siguiente.

Con turbo

Imaginemos que nuestro tanque puede habilitar un modo turbo para poder ir el doble de rápido. Para ello, el método de movimiento tiene que saber si aplica el avance rápido o el normal.

class Tank:
  ...
  def execute(self, orders):
    for order in order:
      self.position = self.actions[order].move(self.position, self.mode)
      self.direction = self.actions[order].turn(self.direction)

Con este criterio, el método move(position, mode) recibe un argumento adicional.

class Forward:
  def move(self, position, mode):
    new_position = ...
    new_position[1] += self.__step(mode)
    return new_position

  def __step(self, mode):
    return 2 if mode is 'turbo' else 1

Vemos que el nuevo método __step(mode) calcula la cantidad de movimiento en función del modo en el que se encuentra el tanque.

Nuevamente, esos if’es nos dejan una mala espina.

Strategy pattern

La idea para eliminar esos if’es radica en que cada modo debe saber calcular la cantidad según sí mismo. Así, podemos tener dos modos que saben cuanto movimiento tienen que aplicar.

class Normal:
  def step(self):
    return 1

class Turbo:
  def step(self):
    return 2

Lo que deja a nuestro comando simple en su implementación evitando la evaluación.

class Forward:
  def move(self, position, mode):
    new_position = ...
    new_position[1] += mode.step()
    return new_position

Conclusiones

Hemos visto algunas técnicas para reducir la complejidad de nuestro código y generar un modelo de dominio a la vez. Esto no es exclusivo de un lenguaje (por eso he dejado en el repo el mismo ejemplo en un par de lenguajes adicionales). Los patrones no son algo que podamos encapsular fácilmente. Son técnicas de desarrollo para atacar problemas recurrentes. En este ejemplo hemos visto tres escenarios con dos muy similares en el abuso del if. Recordemos que esto afecta a la complejidad del código, aunque los desaparecimos con un beneficio adicional que es creando un modelo de dominio. Enriquecer las manera en la que el código representa la realidad permite explotar el código en varios aspectos de entre los cuales resalto: la capacidad de pensar sobre el código y hallar incoherencias sin tener que programar para probarlo, y hacerlo escalable en el sentido de mantenibilidad.

Llevo unos días dando mantenimiento a un código que sigue creciendo en su proceso de desarrollo. Esto me da algunas perspectivas sobre las maneras de desarrollar que reduzcan el tamaño de los métodos y añadan semántica a las estructuras de datos así como algunas bases de diseño. Precisamente, estando en esas labores recordé un princpio que podría servir. Ahora veremos algunos aspectos de la Ley de Démeter aplicado a los métodos de una clase.

La Ley de Démeter es un principio que podemos aplicar durante el desarrollo de software. La idea central es la búsqueda de objetos pequeños que nos faciliten su manejo y su uso de recursos. También nos sirve para reducir la labor de darle semántica y aplicar principios como el de única responsabilidad, entre otros.

Podríamos resumir este principio de la siguiente manera:

class Class {                   // los métodos de un objeto
public:                         // sólo deberían llamar a
  ...                           // los métodos pertenecientes a:
  void doSomething(Argument &argument) {
    Direct direct;
    int value = method();       // sí mismo

    argument.do();              // cualquier argumento

    created = new Created();
    created.execute();          // cualquier objeto
                                // que él construya

    direct.show();              // cualquier objeto
                                // creado directamente
  }
private:
  Created *created;
  int method();
}

Básicamente estamos limitando el número de funciones invocadas dentro de cada método así como sus responsables. Lo que directamene ayuda mantener clases menos infladas y reducir su número de colaboradores los necesarios para ejecutar determinada tarea.

En las siguientes líneas dejo unos ejemplos en los que podríamos cuestionarnos cuáles están menos acopladas o siguen el principio y cuáles no:

  1. Veamos cuál mantiene el principio
#include <date>              class Date;
class Race1 {                class Race2 {
  Date start;                  Date start;
public:                      public:
  Race1(Date &start);          Race2(Date &start);
}                            }
  1. Un ejemplo en Java
public void showAddress(Person person) {
  Address address = person.getAddress();
  sendToScreen(address.screenFormat());
}
  1. Otro de Java
public class Recipe {
  private Casserole casserole;
  private Vector ingredients;

  public Recipe() {
    casserole = new Casserole();
    ingredients = new Vector();
  }

  private void make() {
    casserole.add(ingredients.elements());
  }
}
  1. Este se parece al ejemplo
void makeTransaction(Account account, int) {
  Customer *customer;
  Money amount;

  amount.setValue(123.456789);
  account.setBalance(amount);
  customer = account.getOwner();
  logWorkflow(customer->name(), MAKE_TRASACTION);
}

Desde hace poco más de un mes lidio el testing de componentes con AngularJS. Ahora veremos algunos detalles sobre este proceso.

Un componente encapsula un elemento UI y sus interacciones con el usuario. Tiene que ser claro, reusable y probado como una unidad sin recurrir a ninguna implementación de Selenium como puede ser Protractor.

Ahora bien, usualmente no encontraremos con dos maneras de escribir pruebas unitarias. La primera puede ser como en el ejemplo de AngularJS probando el controlador sin considerar la vista. El otro usa el servicio $compile para renderizar el componente accediendo con selectores del DOM y lanzando eventos.

Podríamos tener un componente como el siguiente:

<div>
  <item-input on-added="$ctrl.onAdded(item)"></item-input>
  <items titles="$ctrl.titles"></items>
</div>

Vemos en dos subcomponentes más.

angular.module(...)
  .component('item', {
    bindings: {
      title: '=',
    },
    template: 'Title: {{ $ctrl.title }}',
  })
  .component('items', {
    bindings: {
      titles: '=',
    },
    template: `
    <ul>
      <li>
        <item title="title"></item>
      </li>
    </ul>
    `,
  })
;

La prueba unitaria del controlador

La prueba unitaria elemental (e incompleta) es la que se hace mediante el controlador asociado al componente. El problema de esto es que se necesita exponer los métodos que actúan sobre el formato de los atributos que son usados en el template.

class ItemController {
  displayTitle() {
    return `Title: ${this.title}`;
  }
}

angular.module(...)
  .component('item', {
    bindings: {
      title: '=',
    },
    controller: ItemController,
    template: '{{ $ctrl.displayTitle() }}',
  })
;

Luego, podemos escribir la prueba unitaria con los displayers en el controlador:

describe('ItemController', () => {
  beforeEach(angular.mock.module(...));

  it('should display labeled title', inject($componentController => {
    const bindings = {
      title: 'A Title',
    };
    const controller = $componentController('item', null, bindings);

    expect(controller.displayTitle()).toBe('Title: A Title');
  }));
});

No se necesita más para identificar algunos problemas:

  1. Tenemos que adaptar el componente incluso creando código particular para lograr la prueba unitaria, el mismo que irá a parar a producción; incluso usando el servicio $componentController para evitar el registro del controlador.
  2. Exponemos el comportamiento interno del componente ya que si miramos el método displayTitle, este no es parte de la API pública; además, la prueba fallaría con sólo renombrarlo.
  3. La prueba podría ser inútil, pues nada nos asegura que el método es usado por la vista. Si no llamados apropiadamente, simplemente no lo llamamos o lo ponemos en la vista, la prueba seguirá pasando como correcta.

La prueba unitaria de la vista

Podemos orientar la prueba desde la vista de la siguiente manera:

describe('ItemComponent', () => {
  let $compile, $rootScope;

  beforeEach(angular.mock.module(...));

  beforeEach(inject((_$compile_, _$rootScope_) => {
    $compile = _$compile_;
    $rootScope = _$rootScope_;
  }));

  it('should display labeled title', () => {
    const $scope = $rootScope.$new();
    $scope['title'] = 'A Title';

    const element = $compile('<item title="title"></item>')($scope);

    $scope.$digest();

    expect(element.text()).toBe('Title: A Title');
  });
});

Estas pruebas no son exhaustivas.

describe('ItemsComponent', () => {
  let $compile, $rootScope;

  beforeEach(angular.mock.module(...));

  beforeEach(inject((_$compile_, _$rootScope_) => {
    $compile = _$compile_;
    $rootScope = _$rootScope_;
  }));

  it('should list titles', () = {
    const $scope = $rootScope.$new();
    $scope['titles'] = ['A Title', 'Other Title'];

    const element = $compile('<items titles="titles"></items>'){$scope);

    $scope.$digest();

    expect(element.find('item').length).toBe(2);
  });

  it('should display titles', () => {
    const $scope = $rootScope.$new();
    $scope['titles'] = ['A Title', 'Other Title'];

    const element = $compile('<items titles="titles"></items>')($scope);

    $scope.$digest();

    const displayedTitle = element.find('item').text();

    expect(displayedTitle).toBe('Title: A Title');
  });

});

Estos componentes nos hacen notar que ciertas cosas en la manera de probarlos.

  • Hay un patrón al escribir las pruebas que se repite. Podemos reducirlas mediante su configuración con el método beforeEach. Esto nos podría llevar a tener más contextos describe en los cuales agrupar las pruebas según los atributos usados durante la configuración del componente; por ejemplo, probar el comportamiento del componente cuando la lista de títulos está vacía.
  • Si dependemos de un componente de una library externa, tenemos que crear un mock para poder probar los componentes que la requieren.

Esto último podría hacer que necesitemos conocer el funcionamiento del framework a un nivel de especialización para poder lograr la prueba. Sin embargo, disponemos de algunos patrones que evitan esta complejidad. El patrón PageObject descrito por Selenium es una buena alternativa que ataca este problema. Pero esto es material para un próximo artículo. 🙂

 

1.   Axioma de Unión

Es natural pensar que, dado un conjunto de conjuntos {\mathcal C}, haya un conjunto mayor para el cual todo conjunto de {\mathcal C} es un subconjunto.

Axioma 1 (Axioma de Unión). Sea {\mathcal C} un conjunto de conjuntos. Entonces hay un conjunto {C} tal que para cualquier conjunto {X} se cumple {X \in \mathcal C \iff X \subseteq C}.

Alternativamente, podríamos relajar la condición de equivalencia y usar la Especificación para crear la misma unión.

Ahora veamos algo de notación.

Definición 2 (Unión). Sea {\mathcal C} un conjunto de conjuntos. Sea {C} el conjunto garantizado por el Axioma de Unión. Entonces diremos que {C} es la unión de los conjuntos de {\mathcal C} y escribimos

\displaystyle  C = \bigcup\mathcal C = \bigcup_{X\in C} X.

Establecemos dos hechos triviales sobre las uniones.

Proposición 3.
1.

\displaystyle  \bigcup\emptyset = \bigcup_{X\in\emptyset}X = \emptyset.

2.

\displaystyle  \bigcup\{Y\} = \bigcup_{X\in\{Y\}}X = Y.

Demostración. La primera es un argumento trivial y la segunda es sólo la aplicación del axioma de extensión.\Box

Supongamos que tenemos un par de conjuntos. La unión de estos puede escribirse de una manera particular. Notemos que esta definición no es un excepción del axioma del par, sino, uno caso especial.

Definición 4 (Unión de un par). Sea {\mathcal C = \{X, Y\}}. Escribiremos

\displaystyle  \bigcup\mathcal C = X\cup Y = \{x : x\in X \lor x\in B\}.

Ahora veamos unos hechos elementales de las uniones de pares. Dejaré las pruebas para el lector (son la aplicación de las definiciones).

Proposición 5 (Propiedades de la unión de un par). Sean {X, Y, Z} conjuntos. Entonces
1.

\displaystyle  X\cup\emptyset = X.

2.

\displaystyle  X\cup Y = Y\cup A.

3.

\displaystyle  X\cup(Y\cup Z) = (X\cup Y)\cup Z.

4.

\displaystyle  X\cup X = X.

5.

\displaystyle  X \subseteq Y \iff X\cup Y = Y.

Volvemos con la serie sobre teoría de conjuntos. 🙂 Disculpad la demora.

1.   Axioma del Par

Supongamos que tenemos dos conjuntos {X} e {Y}. ¿Son elemeneots de algún conjunto? La teoría que tenemos hasta el momento no permite resolver esta cuestión. Lo que motiva el siguiente axioma:

Axioma 1 (Axioma del Par). Para cualquier par de conjuntos {X} e {Y}, hay un conjunto {A} que contiene a {X} e {Y} como elementos.

Mediante el uso de la Especificación, podemos hallar un conjunto {B \subseteq A} tal que {B = \{X,Y\}}. Consideremos {S(x) \equiv x = X \lor x = Y}.

Nombraremos dos tipos especiales de conjuntos:

Definición 2 (Par no ordenado). Con {X = \{a, b\}}, diremos que {X} es un par no ordenado.

Definición 3 (Conjunto unitario). Llamaremos al conjunto {X = \{x\}} conjunto unitario.

 

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

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

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

cuadrantes

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

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

tiempo

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

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

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

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

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

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

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

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

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

Categorías