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.
- Uma instância de
Date
. - Um UNIX Epoch Time.
- Uma string no formato RFC 8601.
- Uma string no formato RFC 2822, formato aceito pela função Date.parse.
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.