صادفت مهمة مسلية بسيطة: جمع البيانات عن درجة حرارة الماء والهواء من صفحتين من صفحات 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 سطرًا فقط.
ربما لم يكن المثال صحيحًا ، لأن الوظيفة ليست واسعة جدًا ، أو أن إعادة الهيكلة تمت بشكل غير صحيح ، وليس بالكامل.
شارك خبرتك.