
Obtive uma tarefa simples e divertida: coletar dados sobre as temperaturas da água e do ar de algumas páginas HTML e retornar o resultado em JSON da API. A tarefa é trivial, é resolvida por um código de linhas de 40 (ou mais) com comentários. Claro, se você escrever guiado pelo princípio Quick & Dirty. Então, o código escrito será fedorento e não corresponderá aos padrões de programação modernos.
Vamos tomar uma execução simples como base e ver o que acontece se a refatorarmos ( código com commits )
public async Task<ActionResult<MeasurementSet>>
Get([FromQuery]DateTime? from = null,
[FromQuery]DateTime? to = null)
{
// Defaulting values
from ??= DateTime.Today.AddDays(-1);
to ??= DateTime.Today;
// Defining URLs
var query = $"beginn={from:dd.MM.yyyy}&ende={to:dd.MM.yyyy}";
var baseUrl = new Uri("https://BaseURL/");
using (var client = new HttpClient { BaseAddress = baseUrl })
{
// Collecting data
return Ok(new MeasurementSet
{
Temperature = await GetMeasures(query, client, "wassertemperatur"),
Level = await GetMeasures(query, client, "wasserstand"),
});
}
}
private static async Task<IEnumerable<Measurement>>
GetMeasures(string query, HttpClient client, string apiName)
{
// Retrieving the data
var response = await client.GetAsync($"{apiName}/some/other/url/part/?{query}");
var html = await response.Content.ReadAsStringAsync();
// Parsing HTML response
var bodyMatch = Regex.Match(html, "<tbody>(.*)<\\/tbody>");
var rowsHtml = bodyMatch.Groups.Values.Last();
return Regex.Matches(rowsHtml.Value, "<tr class=\"row2?\"><td >([^<]*)<\\/td><td class=\"whocares\">([^<]*)<\\/td>")
// Building the results
.Select(match => new Measurement
{
Date = DateTime.Parse(match.Groups[1].Value),
Value = decimal.Parse(match.Groups[2].Value)
});
}
1. Tratamento de erros
- , .
if (response.IsSuccessStatusCode)
{
throw new Exception($"{apiName} gathering failed with. [{response.StatusCode}] {html}");
}
// Parsing HTML response
var bodyMatch = Regex.Match(html, "<tbody>(.*)<\\/tbody>");
if (!bodyMatch.Success)
{
throw new Exception($"Failed to define data table body. Content: {html}");
}
2.
, , , . , MeasureParser RawMeasuresCollector . , , .
3.
enum, , :
var apiName = measure switch
{
MeasureType.Temperature => "wassertemperatur",
MeasureType.Level => "wasserstand",
_ => throw new NotImplementedException($"Measure type {measure} not implemented")
};
, . :
public class UrlQueryBuilder
{
public DateTime From { get; set; } = DateTime.Today.AddDays(-1);
public DateTime To { get; set; } = DateTime.Today;
public string Build(MeasureType measure)
{
var query = $"beginn={From:dd.MM.yyyy}&ende={To:dd.MM.yyyy}";
var apiName = measure switch
{
MeasureType.Temperature => "wassertemperatur",
MeasureType.Level => "wasserstand",
_ => throw new NotImplementedException($"Measure type {measure} not implemented")
};
return $"{apiName}/some/other/url/part/?{query}";
}
}
4. (coupling)
. , , . URL :
var settings = Configuration.GetSection("AppSettings").Get<AppSettings>();
services.AddHttpClient(Constants.ClientName, client =>
{
client.BaseAddress = new Uri(settings.BaseUrl);
});
services.AddTransient<IRawMeasuresCollector, RawMeasuresCollector>();
5.
. , :
[TestMethod]
public async Task TestHtmlTemperatureParsing()
{
var collector = new Mock<IRawMeasuresCollector>();
collector
.Setup(c => c.CollectRawMeasurement(MeasureType.Temperature))
.Returns(Task.FromResult(_temperatureDataA));
var actual = (await new MeasureParser(collector.Object)
.GetMeasures(MeasureType.Temperature)
).ToArray();
Assert.AreEqual(165, actual.Length);
Assert.AreEqual(7.1M, actual
.First(l => l.Date == DateTime.Parse("24.11.2020 10:15")).Value);
}
6.
, , . DOM , . .
:
public async Task<ActionResult<MeasurementSet>> Get
([FromQuery] DateTime? from = null, [FromQuery] DateTime? to = null)
{
var parser = new MeasureParser(_collectorFactory.CreateCollector(from, to));
return Ok(new MeasurementSet
{
Temperature = await parser.GetTemperature(),
Level = await parser.GetLevel(),
});
}
As correções anteriores levaram à inutilidade do enum com o tipo de dimensão, e tivemos que nos livrar do código especificado no ponto 3, o que é bastante positivo, pois reduz o grau de ramificação do código e ajuda a evitar erros.
Resultado
Como resultado, o método de uma página cresceu e se tornou um projeto decente
Claro, o projeto é flexível, com suporte, etc., etc. Mas há uma sensação de que é muito grande. De acordo com o VS analytics, das 277 linhas de código, apenas 67 são executáveis.
Talvez o exemplo não esteja correto, pois a funcionalidade não é tão ampla, ou a refatoração foi feita incorretamente, não completamente.
Compartilhe sua experiência.