El primer paso (incremento) en este camino de aprender sobre diseño haciendo nodebots, es hacer un auto a controlado manualmente (usando un teclado) y remotamente (bluetooth). Veamos la primera versión del código:

const { Board, Motor } = require('johnny-five');
const keypress = require('keypress');

const board = new Board({
  port: '/dev/rfcomm0',
  repl: false,
  debug: true,
});

keypress(process.stdin);
process.stdin.resume();
process.stdin.setEncoding('utf8');
process.stdin.setRawMode(true);

board.on('ready', () => {
  const configs = Motor.SHIELD_CONFIGS.ADAFRUIT_V2;
  const motorL = new Motor(configs.M2);
  const motorR = new Motor(configs.M1);
  // set 80% speed
  const speed = 255 * 0.8;

  process.stdin.on('keypress', (ch, key) => {
    if (!key) {
      return;
    } else if (key.name === 'q') {
      process.exit();
    } else if (key.name === 'up') {
      // forward
      motorL.forward(speed);
      motorR.forward(speed);
    } else if (key.name === 'down') {
      // reverse
      motorL.reverse(speed);
      motorR.reverse(speed);
    } else if (key.name === 'left') {
      // left
      motorL.reverse(speed);
      motorR.forward(speed);
    } else if (key.name === 'right') {
      // right
      motorL.forward(speed);
      motorR.reverse(speed);
    } else if (key.name === 'space') {
      // stop
      motorL.stop();
      motorR.stop();
    }
  });

  board.on('exit', () => {
    // stop
    motorL.stop();
    motorR.stop();
  });

});

Command Pattern

Ahora bien, por donde empezar a hacer el código más simple? Una manera de hacerlo sería aplicar algún patrón o principio de diseño. Intentemos con lo primero. Hay un patrón conocido como command, que nos permite separar al objeto que invoca una operación (mover adelante) de los objetos que la ejecutan (motores). Para este ejemplo la forma de organizar el código propuesta por el patrón sería:

  • cliente: main.js
    1. crea los comandos
    2. asigna los comandos al invoker
    3. le pide al invoker que ejecute los comandos
  • invoker: teclado
  • comandos: adelante, atras, izquierda, derecha, etc.
  • receiver: motores

Veamos como quedaría la segunda versión. El cliente:

const { Board, Motor } = require('johnny-five');
const KeyboardInvoker = require('./keyboardInvoker.js');

const board = new Board({
  port: '/dev/rfcomm0',
  repl: false,
  debug: true,
});

board.on('ready', () => {
  const configs = Motor.SHIELD_CONFIGS.ADAFRUIT_V2;
  const motorL = new Motor(configs.M2);
  const motorR = new Motor(configs.M1);
  // set 80% speed
  const speed = 255 * 0.8;

  const commands = {
    'q': () => process.exit(),
    'up': () => {
      motorL.forward(speed);
      motorR.forward(speed);
    },
    'down': () => {
      motorL.reverse(speed);
      motorR.reverse(speed);
    },
    'left': () => {
      motorL.reverse(speed);
      motorR.forward(speed);
    },
    'right': () => {
      motorL.forward(speed);
      motorR.reverse(speed);
    },
    'space': () => {
      // stop
      motorL.stop();
      motorR.stop();
    }
  };
  const keyboardInvoker = new KeyboardInvoker(commands);

  keyboardInvoker.listen();

  board.on('exit', () => {
    keyboardInvoker.execute('space');
  });

});

y el invoker:

const keypress = require('keypress');

class KeyboardInvoker {
  constructor(commands) {
    this.commands = commands;
    keypress(process.stdin);
    process.stdin.resume();
    process.stdin.setEncoding('utf8');
    process.stdin.setRawMode(true);
  }

  listen() {
    process.stdin.on('keypress', (ch, key) => {
      this.execute(key.name);
    })
  }

  execute(commandName) {
    if (this.commands[commandName]) {
      this.commands[commandName]();
    }
  }

}

module.exports = KeyboardInvoker;

Y donde están los principios?

Anteriormente había comentado que detrás de los patrones están (como escondidos) los principios de diseño. Cuáles son los principios de diseño en este caso? Podemos encontrar varios, aquí algunos:

  • DRY (Don’t repeat yourself)
    Al tener las operaciones encapsuladas en comandos, podemos ejecutarlas desde distintos lugares sin repetirlas. Por ejemplo en board.on('exit' ejecutamos el comando definido como space (antes lo teníamos duplicado). Si bien este principio está expresado desde el no (no repetir), de forma casi escondida le está diciendo que si a algo muy importante: que cada pieza de conocimiento tenga su representación.
  • Responsabilidad única (single responsibility)
    El patrón command nos propone organizar (separar) el código en cuatro responsabilidades:

    • cliente
    • invoker
    • command
    • receiver
  • Abierto para extensión / cerrado para modificación (open/closed)
    El invoker es abierto para extensión, podemos agregar los comandos que necesitemos, sin necesidad de modificarlo. Esto último es una gran ventaja ya que eliminamos la posibilidad de introducir bugs. Si bien es imposible asegurar que nunca vamos a necesitar cambiarlo, la idea de que sea cerrado para modificación tiene que ver con que es muy poco probable que necesitemos cambiarlo.

En los próximos post haré el intento de mejorar el código desde los principios de diseño, profundizando en el principio de responsabilidad única y ortogonalidad. También introducir tdd, no solo la técnica, sino también algunos tips de como encontrar oportunidades para aplicarla.

saludos!

compartir...Tweet about this on TwitterShare on FacebookShare on Google+Email this to someone