先前 ViewComponent 範例程式為求單純避免失焦,有幾段程式寫法不符合 ASP.NET Core 規範留下小尾巴,現在再來收拾它。

幾個明顯問題包含:

  • 在 ASP.NET Core 中使用服務(如:SimpleWeatherService)應採用依賴注入 (Dependency Injection, DI) 機制,不建議直接 new SimpleWeatherService
    (延伸閱讀:不可不知的 ASP.NET Core 依賴注入)
  • OpenData API URL 寫死在程式,移至 appSetting 才是正確姿勢
  • 方法呼叫應採非同步方式(例如:Invoke() 應改成 InvokeAsync()。像 HttpClient 本質都是走非同步,配合 Invoke() 要寫成 httpClient.GetAsync(openDataApiUrl).Result.Content.ReadAsStringAsync().Result; 轉得蠻硬。
    (延伸閱讀:ASP.NET async 基本心法)

讓我們來動手改造它。

首先看如何將 SimpleWeatherService 改為依賴注入,原本在 WeatherBlockViewComponent.Invoke() 是用 new SimpleWeatherService() 建立物件並呼叫 GetWeatherFromOpenDataApi() 方法。為了除去對 SimpleWetherService 的依賴,我們另外定義一個提供 GetWeatherData(string zoneName) 的介面 - IWeatherService,在 WeatherBlockViewComponent 改使用 IWeatherService 而非 SimpleWeatherService,未來如果要從 OpenData Web API 換成其他資料來源,WeatherBlockViewComponent 可以不需要修改。WeatherData 原本是 SimpleWeatherService 的子類別,配合抽出介面要拉出來成為為獨立類別。(註:抽取介面請善用 Visual Studio 的擷取介面重構功能,不要傻傻自己寫)

public class WeatherData
{
    public string ZoneName { get; set; }
    public string Status { get; set; }
    public string MaxTemp { get; set; }
    public string MinTemp { get; set; }
}

public interface IWeatherService
{
    WeatherData GetWeatherData(string zoneName);
}

SimpleWeatherService 繼承 IWeatherService,實作 GetWeatherData(string zoneName),底層仍呼叫 GetWeatherFromOpenDataApi(zoneName) 取得資料:(由於 WeatherData 從 SimpleWeatherService 的子類別拉高到與 SimpleWeatherService 平行,Views/Shared/Components/WeatherBlock/Default.html 的 Model 型別要記得修改)

public class SimpleWeatherService : IWeatherService
{
    public WeatherData GetWeatherData(string zoneName)
    {
        return GetWeatherFromOpenDataApi(zoneName);
    }

    //...其餘部分不變...

WeatherBlockViewComponent 則改用 IWeatherService 並透過 DI 容器取得物件,ASP.NET Core 的標準做法是改由建構式接收 IWeatherService 型別參數保存,將 IWeatherService 實體物件要用哪一種、如何產生則全權交由 ASP.NET Core 的 DI 機制管理:

public class WeatherBlockViewComponent : ViewComponent
{
    readonly IWeatherService _weatherService;
    public WeatherBlockViewComponent(IWeatherService weatherService)
    {
        _weatherService = weatherService;
    }

    public IViewComponentResult Invoke(string zoneName)
    {
        var data = _weatherService.GetWeatherData(zoneName);
        return View(data);
    }
}

改完後程式會出錯:

這是因為我們還沒向 ASP.NET Core 註冊 IWeatherService 服務,DI 機制不知道如何變出 IWeatherService 給 WeatherBlockViewComponent 用。

ASP.NET Core 的服務註冊要寫在 Startup.cs 的 ConfigureServices() 方法,我們加入一行 services.AddTransient<SimpleWeatherService>(): (依據物件生命周期不同,註冊時有 Singleton、Scoped、Transient 三種選擇,詳情請參考不可不知的 ASP.NET Core 依賴注入)

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IWeatherService, SimpleWeatherService>();
    services.AddControllersWithViews();
}

註冊後,程式顯示結果相同,顯示台北、南投、高雄三地的氣象,但底層已經改用 DI 方式決定天氣資訊來源。接著來看看改成 DI 有什麼優點? 假設我們想測試特定文字、數值的呈現效果,則可以做一顆模擬資料的 IWeatherService 類別 - FakeWeatherService:

public class FakeWeatherService : IWeatherService
{
    public WeatherData GetWeatherData(string zoneName)
    {
        return new WeatherData
        {
            ZoneName = zoneName,
            Status = "冰雹龍捲風",
            MinTemp = "-18",
            MaxTemp = "38"
        };
    }
}

下一步,我們將 Startup.ConfigureServices() 的註冊部分改成 services.AddTransient<IWeatherService, FakeWeatherService>();,其餘程式不用修改,網頁變成:

提取介面並使用 DI 容器,我們可以在幾乎不修改程式的前題下輕鬆抽換底層實作,寫出結構分明容易維護的好程式。ASP.NET Core 的範例程式幾乎都已採行這種做法,大家在寫 ASP.NET Core 網站時就從善如流吧!

Example of using DI to inject service in ASP.NET Core.


Comments

Be the first to post a comment

Post a comment


75 + 17 =