الإفراط في هندسة الدماغ

صادفت مهمة مسلية بسيطة: جمع البيانات عن درجة حرارة الماء والهواء من صفحتين من صفحات HTML وإرجاع النتيجة في JSON من واجهة برمجة التطبيقات. المهمة تافهة ، يتم حلها بواسطة رمز من 40 سطر (أو نحو ذلك) مع التعليقات. بالطبع ، إذا كنت تكتب مسترشدًا بمبدأ Quick & Dirty. بعد ذلك ستكون الشفرة المكتوبة كريهة الرائحة ولن تتوافق مع معايير البرمجة الحديثة.






لنأخذ التنفيذ البسيط كأساس ونرى ما سيحدث إذا قمنا بإعادة تشكيله ( كود مع الالتزامات )





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. معالجة الخطأ





- , .





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



أدت الإصلاحات السابقة إلى عدم جدوى التعداد بنوع البعد ، وكان علينا التخلص من الكود المحدد في النقطة 3 ، وهو أمر إيجابي إلى حد ما ، لأنه يقلل من درجة تفريع الكود ويساعد على تجنب الأخطاء.





النتيجة

نتيجة لذلك ، نمت طريقة الصفحة الواحدة إلى مشروع لائق





بالطبع ، المشروع مرن ، ومدعوم ، وما إلى ذلك ، وما إلى ذلك ، ولكن هناك شعور بأنه كبير جدًا. وفقًا لتحليلات VS ، من بين 277 سطرًا من التعليمات البرمجية ، يمكن تنفيذ 67 سطرًا فقط.





ربما لم يكن المثال صحيحًا ، لأن الوظيفة ليست واسعة جدًا ، أو أن إعادة الهيكلة تمت بشكل غير صحيح ، وليس بالكامل.





شارك خبرتك.








All Articles