簡単な面白いタスクに出くわしました。いくつかのHTMLページから水と空気の温度に関するデータを収集し、その結果をAPIから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で指定されたコードを削除する必要がありました。これは、コードの分岐の程度を減らし、エラーを回避するのに役立つため、かなりポジティブです。
結果
その結果、1ページの方法はまともなプロジェクトに成長しました
もちろん、プロジェクトは柔軟でサポートされているなどですが、大きすぎる気がします。VS分析によると、277行のコードのうち、実行可能なのは67行だけです。
機能がそれほど広くないか、リファクタリングが完全ではなく誤って行われたため、おそらく例は正しくありません。
あなたの経験を共有してください。