Go to English Blog

Um pouco mais sobre JavaScript modular

Leia em 3 minutos

No último artigo do Simples Ideias mostrei como uma implementação “tradicional” poderia ser implementada de um modo um pouco mais modular. O feedback positivo me motivou a escrever esta continuação, em um exemplo de refatoração de uma função que converte timestamps em objetos do tipo Date.

Este método pode receber diferentes tipos de objetos como argumento.

A primeira implementação que eu tinha era apenas uma função que fazia tudo isso.

Utils.parseDate = function(timestamp) {
  var matches, date;
  var diff = 0;

  // we have a date, so just return it.
  if (timestamp.constructor === Date) {
    return timestamp;
  };

  matches = timestamp.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|[+-]\d+)?/);

  if (matches) {
    for (var i = 1; i <= 6; i++) {
      matches[i] = parseInt(matches[i], 10) || 0;
    }

    // month starts on 0
    matches[2] -= 1;

    date = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6]));

    // Check if timezone is available.
    matches = matches[7] && timezone.match(/^([+-])(\d{2})(\d{2})/);

    if (matches) {
      diff += parseInt(matches[2], 10) * 60 * 60 * 1000;
      diff += parseInt(matches[3], 10) * 60 * 1000;
      diff *= matches[1] === "-" ? 1 : -1;
      date.setTime(date.getTime() + diff);
    }
  } else if (typeof(timestamp) == "number") {
    // UNIX timestamp
    date = new Date();
    date.setTime(timestamp);
  } else if (timestamp.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
    // a valid javascript format with timezone info
    date = new Date();
    date.setTime(Date.parse(timestamp))
  } else {
    // an arbitrary javascript string
    date = new Date();
    date.setTime(Date.parse(timestamp));
  }

  return date;
};

Embora a função não seja extremamente complicada, está fazendo mais do que deveria. A ideia aqui será extrair cada uma das condições em sua própria função construtora. Decidi criar um namespace chamado DateParser, que irá conter cada um dos adapters. O parsing do timestamp será feito na função parse(). Cada adapter pode estar em seu próprio arquivo, mas para facilitar este exemplo coloquei tudo junto.

Module("DateParser.Date", function(Adapter){
  Adapter.fn.initialize = function(timestamp) {
    this.timestamp = timestamp;
  };

  // The DateParse.Date adapter will receive a Date instance,
  // so we just have to return it.
  Adapter.fn.parse = function() {
    return this.timestamp;
  };
});

Module("DateParser.Timestamp", function(Adapter){
  Adapter.fn.initialize = function(timestamp) {
    this.timestamp = timestamp;
  };

  // We received a UNIX time, so we can just set it
  // through Date#setTime function.
  Adapter.fn.parse = function() {
    var date = new Date();
    date.setTime(this.timestamp);
  };
});

Module("DateParser.ISO8601", function(Adapter){
  Adapter.FORMAT_REGEX = /(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|0000)?/;

  Adapter.fn.initialize = function(timestamp) {
    this.timestamp = timestamp;
  };

  Adapter.fn.parse = function() {
    var date;
    var matches = this.timestamp.match(Adapter.FORMAT_REGEX);
    var diff = 0;

    for (var i = 1; i <= 6; i++) {
      matches[i] = parseInt(matches[i], 10) || 0;
    }

    var year     = matches[1]
      , month    = matches[2] - 1 // remember that months starts on zero
      , day      = matches[3]
      , hours    = matches[4]
      , minutes  = matches[5]
      , seconds  = matches[6]
      , timezone = matches[7]
    ;

    // Always set the date as UTC. We'll consider the
    // timezone later on when is available.
    date = new Date(Date.UTC(year, month, day, hours, minutes, seconds));

    // Check if timezone is available.
    matches = timezone && timezone.match(/^([+-])(\d{2})(\d{2})/);

    if (matches) {
      diff += parseInt(matches[2], 10) * 60 * 60 * 1000;
      diff += parseInt(matches[3], 10) * 60 * 1000;
      diff *= matches[1] === "-" ? 1 : -1;
      date.setTime(date.getTime() + diff);
    }

    return date;
  };
});

Module("DateParser.RFC2822", function(Adapter){
  Adapter.fn.initialize = function(timestamp) {
    this.timestamp = timestamp;
  };

  Adapter.fn.parse = function() {
    var date = new Date();
    date.setTime(Date.parse(this.timestamp));
  };
});

A implementação da função DateParser.parse() vai mudar, pois precisaremos delegar o timestamp para cada um dos adapters. Vamos aproveitar para dar uma refatorada no modo como fazemos as condições.

Module("DateParser", function(DateParser){
  DateParser.parse = function(timestamp) {
    if (timestamp.constructor === Date) {
      return DateParser.Date(timestamp).parse();
    }

    if (typeof(timestamp) === "number") {
      return DateParser.Timestamp(timestamp).parse();
    }

    if (typeof(timestamp) === "string" && DateParser.ISO8601.FORMAT_REGEX.match(timestamp)) {
      return DateParser.ISO8601(timestamp).parse();
    }

    return DateParser.RFC2822(timestamp).parse();
  };
}, {});

Muito mais simples! Mas ainda temos espaço para melhorar essa implementação. Primeiro vamos extrair as condicionais para cada adapter. Note abaixo que estou exibindo apenas a função match().

Module("DateParser.Date", function(Adapter){
  Adapter.fn.match = function() {
    return this.timestamp.constructor === Date;
  };
});

Module("DateParser.Timestamp", function(Adapter){
  Adapter.fn.match = function() {
    return typeof(this.timestamp) === "number";
  };
});

Module("DateParser.ISO8601", function(Adapter){
  var FORMAT_REGEX = /(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|[+-]\d+)?/;

  Adapter.fn.match = function() {
    return this.timestamp.toString().match(FORMAT_REGEX);
  };
});

Module("DateParser.RFC2822", function(Adapter){
  // Always return true, since this will be the default adapter.
  Adapter.fn.match = function() {
    return true;
  };
});

Agora, podemos alterar a implementação do método DateParser.parse para usar a função match() definido em cada um dos adapters. Note que esta função foi definida na instância. Isso significa que vamos ter que instanciar o adapter durante o processo de descoberta que faremos em um loop.

Vamos precisar de um array que define a ordem dos adapters.

Module("DateParser", function(DateParser){
  DateParser.parse = function(timestamp) {
    var adapters = ["Date", "Timestamp", "ISO8601", "RFC2822"];
  };
}, {});

Nós iremos usar a função Array.prototype.some para saber qual o adapter deve ser usado. Essa função para de ser executada quando um dos callbacks retornar true, o que é exatamente o precisamos fazer.

Module("DateParser", function(DateParser){
  DateParser.parse = function(timestamp) {
    var adapters = ["Date", "Timestamp", "ISO8601", "RFC2822"];
    var parser;

    adapters.some(function(name){
      // Retrieve the adapter and instantiate it with the
      // provided timestamp.
      parser = DateParser[name](timestamp);
      return parser.match();
    });

    // Finally return the parsed timestamp.
    return parser.parse();
  };
}, {});

É isso aí. Agora estou satisfeito com esta implementação!

Finalizando

No nosso exemplo, extrair cada parser e unificar a API desses adapters, automatizando o processo de descoberta de qual o adapter mais indicado para o timestamp passado como argumento tornou o nosso código muito mais simples de entender. Você não precisa, por exemplo, ler toda a função para saber como um determinado parser funciona.

Uma outra coisa que você precisa saber é que toda implementação possui espaço para melhorar. Mais importante que saber como fazer este tipo de melhoria é saber quando parar, pois isso pode ser uma coisa sem fim.