Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
.NET com vários paradigmas, Parte 10: escolhendo uma abordagem
Ted Neward
Em minha última coluna (Parte 9 desta série), eu disse que sempre que uma série de artigos chega perto de dois dígitos é porque o autor é pretensioso o bastante para achar que seus leitores estão realmente interessados naquele assunto tantas vezes consecutivas ou ele simplesmente é incapaz de sugerir um novo tópico. Deixarei que os leitores especulem qual desses dois casos se encaixa melhor aqui.
Entretanto, os comentários sobre o assunto deixaram claro que, apesar dos perigos de ultrapassar o território dos dois dígitos, mais um artigo sobre a criação com vários paradigmas seria necessário para tentar amarrar todos os elementos individuais, a fim de demonstrar como usar cada um desses paradigmas diferentes e escolher um deles para um problema real. Para os nossos fins, “real” significa incomum o bastante para supor como a abordagem pode ser útil para problemas que não são tão simples como aqueles escolhidos para serem resolvidos em um artigo de revista.
Tentar criar um problema desse tipo é mais difícil do que pode parecer; ou a ideia é muito complicada e tem muitas distrações que impedem a obtenção de uma visão clara das soluções utilizadas ou a ideia é muito simples e possibilita pouquíssimas variações em sua implementação para ilustrar como cada um dos diferentes paradigmas pode ser usado para resolver o problema. Felizmente, para começar, podemos usar algum trabalho que já foi realizado — como os “Code Katas” de Dave Thomas.
Code Katas
Em seu site (http://codekata.pragprog.com), Thomas descreve como foi levar seu filho Zachary às aulas de caratê e descobrir que não havia espaço para ele na área de observação do dojo reservada para os pais. Isso permitiu que ele tivesse 45 minutos para si próprio e, por isso, começou a se distrair com um código sobre o qual já vinha, em vão, considerando implicações de desempenho. Ele escreveu:
Eu apenas queria me distrair com algum código e experimentar alguma técnica que ainda não havia utilizado. Eu fiz isso em um ambiente simples e controlado e tentei muitas variações diferentes (mais do listei aqui). E ainda tenho mais algumas para tentar…
O que fez disso uma sessão de exercício? Bem, eu tive um tempo sem interrupções. Havia algo que eu queria experimentar, e tentei várias vezes. Busquei por respostas todas as vezes e, por isso, pude trabalhar para melhorar. Não havia pressão: o código era efetivamente descartável. Era divertido. Eu continuava fazendo pequenos progressos, o que me motivava a prosseguir. Por fim, eu saí de lá sabendo mais do que quando entrei.
Essencialmente, ter tempo livre foi o que me permitiu praticar.Se houvesse pressão ou um prazo para entregar a funcionalidade de pesquisa do blog, a execução existente seria aceitável e a prática nunca teria ocorrido. Mas os 45 minutos sem nenhuma pressão me permitiram brincar.
Portanto, este é o meu desafio do dia: tente encontrar de 45 a 60minutos para brincar com um pequeno trecho de código. Você não precisa, necessariamente, voltar-se ao desempenho; talvez, você possa se distrair com a estrutura, o uso de memória ou a interface. No fim, isso não é o mais importante. Experimente, avalie, melhore.
Em outras palavras, o code kata é um problema (relativamente) simples — não tão difícil de compreender conceitualmente — que oferece uma estrutura para exploração. Em nosso caso específico, o objetivo será a criação. Nós vamos usar como base de nossa exploração o “Kata 4: mudanças irreversíveis em dados” de Thomas.
Kata 4: mudanças irreversíveis em dados
Neste caso, o code kata possui três partes, que veremos em etapas separadas, uma de cada vez, criando conforme avançamos e considerando cada uma das linhas centrais disponíveis para nós no C# (ainda que, novamente, as soluções no Visual Basic possam ser igualmente abordadas).
Etapa Um
A etapa um é a seguinte:
Em weather.dat (http://bit.ly/ksbVPs), você encontrará dados climáticos diários deMorristown, N.J., referentes a junho de 2002. Baixe esse arquivo de texto e, em seguida, escreva um programa para obter o número do dia (coluna um) com a menor diferença de temperatura (a temperatura máxima está na segunda coluna e a temperatura mínima está na terceira coluna).
O arquivo weather.dat é semelhante ao exibido na Figura 1 (exceto pelo fato de que ele se estende por todos os 30 dias).
Figura 1 O arquivo de texto weather.dat
MMU de junho de 2002 | ||||||||||||||||
Dy | MxT | MnT | AvT | HDDay | AvDP | 1HrP | TPcpn | WxType | PDir | AvSp | Dir | MxS | SkyC | MxR | MnR | AvSLP |
1 | 88 | 59 | 74 | 53.8 | 0.00 | F | 280 | 9.6 | 270 | 17 | 1.6 | 93 | 23 | 1004.5 | ||
2 | 79 | 63 | 71 | 46.5 | 0.00 | 330 | 8.7 | 340 | 23 | 3.3 | 70 | 28 | 1004.5 | |||
3 | 77 | 55 | 66 | 39.6 | 0.00 | 350 | 5.0 | 350 | 9 | 2.8 | 59 | 24 | 1016.8 | |||
4 | 77 | 59 | 68 | 51.1 | 0.00 | 110 | 9.1 | 130 | 12 | 8.6 | 62 | 40 | 1021.1 | |||
. | .. | |||||||||||||||
28 | 84 | 68 | 76 | 65.6 | 0.00 | RTFH | 280 | 7.6 | 340 | 16 | 7.0 | 100 | 51 | 1011.0 | ||
29 | 88 | 66 | 77 | 59.7 | 0.00 | 040 | 5.4 | 020 | 9 | 5.3 | 84 | 33 | 1020.6 | |||
30 | 90 | 45 | 68 | 63.6 | 0.00 | H | 240 | 6.0 | 220 | 17 | 4.8 | 200 | 41 | 1022.7 | ||
mo | 82.9 | 60.5 | 71.7 | 16 | 58.8 | 0.00 | 6.9 | 5.3 |
É imediatamente claro que o arquivo não é separado por vírgulas, mas separado por posição — a coluna “MxT” (temperatura máxima) sempre começa no mesmo lugar e a coluna “MnT” (temperatura mínima) começa em outra localização fixa. Em uma visualização rápida do arquivo, o Visual Studio revela que cada linha possui precisamente 90 caracteres de comprimento. A análise desse arquivo em uma combinação por linhas e por cadeias de caracteres será comum, pois é possível dividir em novas linhas ou em comprimentos de 90 caracteres.
Depois disso, no entanto, as coisas ficam menos claras. Embora o exercício solicite que obtenhamos apenas a menor diferença de temperatura do mês por meio da codificação de valores de posições e executando String.Subset em cada linha, isso não abrange todos os nossos objetivos. Por isso, vamos dar um passo além e refinar o problema para também exigir a capacidade de examinar quaisquer elementos de dados de qualquer dia do mês.
Lembre-se de que o ponto principal da criação com vários paradigmas é identificar o eixo de semelhança/variabilidade, e para este problema, esse eixo é pouco intuitivo — embora seja fácil perceber que devemos analisar um arquivo de texto e examinar os resultados. Esses dados são de natureza essencialmente tabular e podem ser visualizados de várias maneiras diferentes com base nos paradigmas que investigamos.
A partir de uma perspectiva de procedimento, a estrutura de dados é a definição de cada linha, e uma solução orientada a objetos não nos oferece muito mais. É bastante simples apenas obter a estrutura de dados de procedimento e lançar os métodos para analisar os dados na classe. Essencialmente, voltamos aos “dados inteligentes” que, apesar de não serem particularmente de objetos, funcionam. Por enquanto.
Nada aqui sugere o uso prático de táticas metaprogramação, nem de programação dinâmica, exceto, talvez, a estrutura de dados que permite algum tipo de pesquisa com base em nome de coluna, de forma que day1[“MxT”] retornaria “88”. A abordagem funcional poderia ser interessante, pois a análise pode ser vista como uma coleção de funções, executando uma após a outra e obtendo entradas e retornando cadeias de caracteres (ou outros dados analisados), além de mais funções para analisar o restante do arquivo como resultado. Essa técnica é conhecida como combinadores de análise e é uma discussão muito além do escopo deste artigo em particular.
Nenhuma dessas abordagens parece ajudar muito; até o momento, a melhor solução parece ser uma abordagem de procedimentos, realizando algo semelhante ao exibido na Figura 2.
Figura 2 Utilizando uma abordagem de procedimentos
namespace DataMunger
{
public struct WeatherData
{
public int Day;
public float MxT;
public float MnT;
// More columns go here
}
class Program
{
static void Main(string[] args)
{
TextReader reader = new StreamReader("weather.dat");
// Read past first four lines
reader.ReadLine();
reader.ReadLine();
reader.ReadLine();
reader.ReadLine();
// Start reading data
List<WeatherData> weatherInfo = new List<WeatherData>();
while (reader.Peek() > 0)
{
string line = reader.ReadLine();
// Guard against "mo" summation line
if (line.Substring(0, 4) == " mo")
continue;
WeatherData wd = new WeatherData();
wd.Day = Int32.Parse(line.Substring(0, 4).Replace("*", " "));
wd.MxT = Single.Parse(line.Substring(5, 6).Replace("*", " "));
wd.MnT = Single.Parse(line.Substring(12, 6).Replace("*", " "));
// More parsing goes here
weatherInfo.Add(wd);
}
Console.WriteLine("Max spread: " +
weatherInfo.Select((wd) => wd.MxT - wd.MnT).Max());
}
}
}
Observe que esse código já apresenta um problema: quando a análise alcança o dia 9, um asterisco é exibido na coluna MnT, indicando que esse é o valor “mais baixo” do mês, da mesma maneira que o dia 26 é o valor “mais alto” do mês. Isso pode ser resolvido removendo o “*” da cadeia de caracteres, e chega-se ao ponto desejado: o eixo de procedimento concentra-se em estabelecer a estrutura de dados e operar nela — nesse caso, analisá-la.
Além disso, observe o uso de List<> aqui. Apenas porque estamos usando uma abordagem de procedimentos para analisar o arquivo não significa que não podemos tirar vantagem da utilização de classes úteis em qualquer lugar da Biblioteca de Classes Base. Isso torna comum descobrir a menor diferença — uma única consulta LINQ-to-Objects fornecerá o resultado desejado. (Nós devemos, obviamente, obter o dia em que a diferença ocorreu, para ser fiel ao problema, mas essa é apenas uma questão de retorno e aplicação de Max() em algo que não seja uma flutuação bruta; considere isso um exercício para o leitor.)
Etapa Dois
Poderíamos gastar bastante tempo projetando como seria possível escrever um código “reutilizável” para isso, mas fazer isso seria como tentar prever o futuro; esse código funciona e, portanto, vamos passar para a próxima etapa do kata:
O arquivo football.dat (http://bit.ly/lyNLya) contém os resultados doCampeonato Inglês de Futebol de 2001/2002. As colunas “F” e “A” contêm o número total de gols marcados e sofridos de cada time naquela temporada (portanto, o Arsenal marcou 79 gols e sofreu 36 gols). Escreva um programa para imprimir o nome do time com a menor diferença entre gols “marcados” e “sofridos”.
Mais análise de texto. Diversão. O arquivo football.dat é exibido na Figura 3. Ele é semelhante em vários aspectos ao arquivo weather.dat (Figura 1); no entanto, é diferente o bastante para merecer um código de análise diferente.
Figura 3 O arquivo de texto football.dat
Equipe | P | N | S | B | P | Uma | Pts | ||
1. | Arsenal | 38 | 26 | 9 | 3 | 79 | - | 36 | 87 |
2. | Liverpool | 38 | 24 | 8 | 6 | 67 | - | 30 | 80 |
3. | Manchester_U | 38 | 24 | 5 | 9 | 87 | - | 45 | 77 |
4. | Newcastle | 38 | 21 | 8 | 9 | 74 | - | 52 | 71 |
5. | Leeds | 38 | 18 | 12 | 8 | 53 | - | 37 | 66 |
6. | Chelsea | 38 | 17 | 13 | 8 | 66 | - | 38 | 64 |
7. | West_Ham | 38 | 15 | 8 | 15 | 48 | - | 57 | 53 |
8. | Aston_Villa | 38 | 12 | 14 | 12 | 46 | - | 47 | 50 |
9. | Tottenham | 38 | 14 | 8 | 16 | 49 | - | 53 | 50 |
10. | Blackburn | 38 | 12 | 10 | 16 | 55 | - | 51 | 46 |
11. | Southampton | 38 | 12 | 9 | 17 | 46 | - | 54 | 45 |
12. | Middlesbrough | 38 | 12 | 9 | 17 | 35 | - | 47 | 45 |
13. | Fulham | 38 | 10 | 24 | 14 | 36 | - | 44 | 44 |
14. | Charlton | 38 | 10 | 14 | 14 | 38 | - | 49 | 44 |
15. | Everton | 38 | 11 | 10 | 17 | 45 | - | 57 | 43 |
16. | Bolton | 38 | 9 | 13 | 16 | 44 | - | 62 | 40 |
17. | Sunderland | 38 | 10 | 10 | 18 | 29 | - | 51 | 40 |
18. | Ipswich | 38 | 9 | 9 | 20 | 41 | - | 64 | 36 |
19. | Derby | 38 | 8 | 6 | 24 | 33 | - | 63 | 30 |
20. | Leicester | 38 | 5 | 13 | 20 | 30 | - | 64 | 28 |
Se considerarmos esses dois programas de forma independente, é bem fácil perceber que, individualmente, cada um é resolvido de maneira muito semelhante — não há muito motivo para refazer o hash para o exercício de futebol.
Etapa Três
A última etapa do kata, entretanto, nos leva a uma valiosa descoberta no exercício:
Obtenha os dois programas escritos anteriormente e extraia o máximo de código em comum entre eles possível, o que fornecerá a você dois programas menores e um tipo de funcionalidade compartilhada.
Isso é intrigante, e inclui como fator a análise de semelhança/variabilidade.
Semelhança/variabilidade
Como agora temos mais do que apenas um problema para examinar (ou seja, uma família de preocupações), é mais fácil revelar o comum e o variável entre os dois problemas. Essa análise, começando pela análise dos arquivos de texto, revela que o exercício essencialmente se resume a duas etapas: analisar o arquivo em uma lista de dados de linha e, em seguida, examinar esses dados em termos de algum tipo de cálculo ou análise.
Existem algumas questões a serem consideradas durante a análise dos arquivos de texto:
- O formato dos dois arquivos tem um “cabeçalho” que deve ser ignorado.
- O formato dos dois arquivos baseia-se em posições.
- O formato dos dois arquivos tem linhas que deverão ser ignoradas (a linha de soma “mo” no arquivo weather.dat e o marcador visual “------” no arquivo football.dat).
- O arquivo weather.dat possui algumas colunas vazias, diferentemente do arquivo football.dat.
- O formato dos dois arquivos oferece suporte principalmente a colunas numéricas e de cadeias de caracteres (com exceção do arquivo weather.dat que também inclui valores “*” que talvez tenham de ser capturados de alguma maneira).
O cálculo dos resultados depende muito de como terminam os dados analisados; por isso, parece razoável começar por aí. Existem várias abordagens, com base em cada paradigma, por onde podemos começar:
Procedimentos Esse eixo concentra-se em capturar a semelhança em estruturas de dados. Entretanto, com dois formatos de arquivos diferentes, fica claro onde queremos capturar a variabilidade; dessa maneira, o procedimento parece não ser o adequado. Pode ser possível criar algum tipo de estrutura de dados que capture os dois formatos de arquivo, mas é provável que seja estranho em comparação a outros paradigmas.
Orientada a objetos A semelhança entre os dois arquivos sugere o uso de uma classe base abstrata “TextParser” para fornecer a funcionalidade de análise de base, incluindo a capacidade de ignorar linhas. A variabilidade é proveniente da análise de cada linha, o que significa que as subclasses devem substituir algum tipo de método “ParseLine” para fazer a análise entre linhas real. Entretanto, a maneira como os valores analisados são recuperados do subtipo TextParser — a fim de realizar a comparação entre mínimo e máximo — poderia ser complicada, pois os tipos de colunas também podem variar. Historicamente, o Microsoft .NET Framework resolve isso (com conjuntos de dados SQL) retornando objetos — o que nós poderíamos usar, se necessário. Mas isso introduz potenciais erros de segurança de tipos, pois os objetos precisariam ser rebaixados para serem úteis, e isso seria perigoso.
Meta Várias soluções diferentes estão no intervalo de metaobjeto/metaprogramação. A abordagem de atributos sugere que a classe TextParser pode obter um tipo “Record”, no qual cada uma das colunas descritas possui um atributo de início/comprimento personalizado que descreve como analisar as alinhas, desta maneira:
public struct WeatherData
{
[Parse(0, 4)]
public int Day;
[Parse(5, 6)]
public float MxT;
[Parse(12, 6)]
public float MnT;
}
A classe TextParser poderia, então, ser parametrizada para obter o tipo Record (TextParser<RecordT>) e usar os atributos personalizados em tempo de execução a fim de descobrir como analisar cada uma das linhas. Isso retornaria uma lista de registros (List<RecordT>), a partir da qual seria possível realizar os cálculos mencionados anteriormente.
Como alternativa, a programação de geração sugere que um formato de origem de algum tipo descreve o arquivo de texto, e o analisador de cada tipo de arquivo é gerado com base nesse formato de origem.
Dinâmica Utilizar uma abordagem dinâmica é um pouco estranho, considerando que é uma abordagem mais nova do .NET, mas aqui nós podemos imaginar uma classe TextParser obtendo uma cadeia de caracteres que descreve como analisar cada arquivo. Seria quase como uma linguagem de programação muito pequena independente, talvez incorporando expressões regulares ou (em nosso caso) apenas usando dados de posições, pois isso é tudo o que é necessário para esse problema:
string parseCommands =
"Ignore; Ignore; Ignore; Ignore; Repeat(Day:0-4, MxT:5-6, MnT:12-6)";
TextParser parser = new TextParser(parseCommands);
List<string> MxTData = parser["MxT"];
List<string> MnTData = parser["MnT"];
Isso é claramente diferente da abordagem meta, pois ela não apresenta a segurança de tipos que a abordagem meta poderia fornecer. Entretanto, uma abordagem meta exige mudanças em tempo de compilação se o formato do arquivo for alterado, ao passo que uma abordagem dinâmica, por agir em relação à variabilidade com base em nomes, poderia ocorrer com as alterações. A abordagem dinâmica oferece mais flexibilidade ao custo de mais complexidade internamente.
O formato de origem utilizado pela abordagem meta de geração poderia ser o que é transmitido em tempo de execução, criando o analisador em tempo de execução, em vez de em tempo de origem.
Funcional De algumas maneiras, a abordagem funcional é a mais curiosa, pois ela sugere que é no algoritmo — em análise — que reside a variabilidade e, por isso, a classe TextParser deveria obter uma ou mais funções que descrevem como analisar o texto, independentemente de ser entre linhas, colunas ou ambas. Por exemplo, duas coisas que a classe TextParser precisa saber como fazer são ignorar linhas que não podem ser analisadas e quebrar uma linha em partes constituintes. Isso poderia ser estabelecido como instâncias de função na própria classe TextParser, transmitido por meio de um construtor ou definido por meio de propriedades, conforme exibido na Figura 4.
Figura 4 Uma abordagem funcional
TextParser<WeatherData> parser = new TextParser<WeatherData>();
parser.LineParseVerifier =
(line) =>
{
if ( (line.Trim().Length == 0) || // Empty line
(line.Contains("MMU")) || // First header line
(line.Contains("Dy")) || // Second header line
(line.Contains("mo")))
return false;
else
return true;
};
parser.ColumnExtracter =
(line) =>
{
WeatherData wd = new WeatherData();
wd.Day = line.Substring(0, 4);
wd.MxT = line.Substring(5, 6);
wd.MnT = line.Substring(12, 6);
return wd;
};
List<WeatherData> results = parser.Parse("weather.dat");
TextParser utiliza o tipo parametrizado exclusivamente para capturar o tipo WeatherData para uma maior segurança de tipos; isso poderia ser escrito para retornar System.Object genérico, se desejado ou mais fácil.
Conclusão
Claramente, não existe nenhuma “maneira única” real de abordar o problema — ao passo que surgem outros requisitos, nós obtemos um senso melhor de onde estão as variabilidades e, com isso, um senso melhor das semelhanças que devem ser capturadas. Isso, a propósito, destaca o verdadeiro poder da refatoração: como não pode prever o futuro, a refatoração de códigos significa que podemos estar errados sobre nossas análises de semelhança/variabilidade e, mesmo assim, não termos de “jogar tudo fora e começar de novo” quando esses erros são descobertos.
A criação com vários paradigmas não é um assunto fácil de compreender, principalmente não de uma só vez. Foram necessários 10 artigos para descrevê-la, e é bastante razoável esperar que se leve muito mais tempo para incorporá-la em seu próprio pensamento. Entretanto, fazer isso pode levar a criações muito mais flexíveis e confortáveis com o passar do tempo e até mesmo oferecer soluções para problemas espinhosos que deixarão outros desenvolvedores um pouco espantados sobre como você conseguiu chegar à uma solução entre toda a confusão do problema. Lembre-se, no final, uma abordagem com vários paradigmas começa e termina com a semelhança e a variabilidade — lembre-se disso e muitas coisas começarão a se organizar.
Com essa finalidade, eu altamente recomendo que os leitores obtenham o kata de mudanças irreversíveis em dados e tentem deliberadamente resolver cada um dos dois exercícios de análise de arquivo usando cada um dos cinco paradigmas diferentes separadamente e, em seguida, procurem por maneiras de combiná-los a fim de criar um código robusto e reutilizável. O que ocorre quando uma abordagem orientada a objetos é combinada a uma abordagem funcional ou dinâmica? A solução fica mais fácil ou mais complicada? Em seguida, utilize a abordagem em um formato de arquivo diferente — como um formato de arquivo CSV pode acabar com tudo? Ainda mais importante, faça isso durante momentos livres entre casos e reuniões de projetos; a experiência obtida disso sem dúvida valerá a pena quando você tentar fazer esse tipo de criação sob a pressão de um prazo de produção real.
Apesar de já mencionado, vale repetir: as linguagens com vários paradigmas estão aqui, em uso comum, e parece que vieram para ficar. As várias linguagens do Visual Studio 2010 apresentam algum grau de cada um desses paradigmas; o C++, por exemplo, possui alguns recursos de metaprogramação paramétrica que não são possíveis em códigos gerenciados, devido à maneira como o compilador C++ funciona; e apenas recentemente (no último padrão C++0x), ele obteve expressões lambda. Até mesmo a linguagem ECMAScript/JavaScript/JScript tão terrível pode fazer objetos, procedimentos, metaprogramação e paradigmas dinâmicos e funcionais; na verdade, muito de JQuery é criado com base nessas ideias. Quanto antes essas ideias se enraizarem em seu cérebro, antes você conseguirá escrever seu código pelas mais complexas situações.
Boa codificação.
Ted Neward é diretor na Neward & Associates, uma empresa independente especializada sistemas de plataformas .NET Framework e Java empresariais. Ele já escreveu mais de 100 artigos, é um MVP de C#, palestrante da INETA e autor ou coautor de dezenas de livros, incluindo “Professional F# 2.0” (Wrox, 2010). Ele atua como consultor e mentor regularmente. Entre em contato com ele pelo email ted@tedneward.com se desejar que ele venha trabalhar com sua equipe ou leia seu blog em http://blogs.tedneward.com.
Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Anthony D. Green