Sobreingeniería cerebral

Me encontré con una tarea sencilla y entretenida: recopilar datos sobre la temperatura del agua y el aire de un par de páginas HTML y devolver el resultado en JSON desde la API. La tarea es trivial, se resuelve mediante un código de líneas de 40 (más o menos) con comentarios. Por supuesto, si escribe guiado por el principio Quick & Dirty. Entonces el código escrito olerá mal y no corresponderá a los estándares de programación modernos.






Tomemos una ejecución simple como base y veamos qué sucede si la refactorizamos ( código con confirmaciones )





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. Manejo de errores





- , .





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(),
  });
}
      
      



Las correcciones anteriores llevaron a la inutilidad de la enumeración con el tipo de dimensión, y tuvimos que deshacernos del código especificado en el punto 3, que es bastante positivo, ya que reduce el grado de ramificación del código y ayuda a evitar errores.





Salir

Como resultado, el método de una página se ha convertido en un proyecto decente.





Por supuesto, el proyecto es flexible, apoyado, etc., etc. Pero existe la sensación de que es demasiado grande. Según VS Analytics, de 277 líneas de código, solo 67 son ejecutables.





Quizás el ejemplo no sea correcto, ya que la funcionalidad no es tan amplia, o la refactorización se realizó de forma incorrecta, no completa.





Comparte tu experiencia.








All Articles