為什麼推薦你用 C# 構建大型後端應用?(二)

語言: CN / TW / HK

不畏浮雲遮望眼,只緣身在此山中。--王安石《登飛來峰》

筆者在上一篇 《為什麼推薦你用 C# 構建大型後端應用?- Part 1》 展示了 C# 的受歡迎程度,並介紹了一些 C# 特有的語法特性,並以此為引子分析 C# 對於構建後端應用的優勢。在本篇文章中,我們將繼續探究 C# 的其他一些特性,進一步展示用 C# 做後端開發的優勢和適用場景。本篇文章是 C# 系列文章的第二篇,我們將以 C# 跨平臺框架 .NET Core 為例,介紹如何用 C# 編寫大型後端應用專案。

.NET Core 簡介

在前一篇文章我們提到,C# 是 .NET 的主要支援語言,.NET 是微軟開發並開源的平臺開發框架,跟 Java 的 Spring 類似,是非常全面的軟體開發框架,能夠開發 Web 應用、API、雲函式、移動端等,集成了開發者所需要的各種套件。而 .NET 之前一直為人詬病的原因之一就是它只支援在 Windows 作業系統上部署,這為普及 C# 和 .NET 帶來了麻煩。所幸的是,微軟在 2016 年釋出了 .NET Core,支援跨平臺執行 C# 應用,不僅能在 Windows 上部署,還可以在 MacOS 和 Linux 上執行 C# 開發的程式。.NET Core 的釋出,讓 C# 從 Windows 獨立出來,成為不受作業系統限制的企業級程式語言,讓其能夠與基於 JVM 跨平臺執行的 Java 平起平坐。甚至,有很多地方 C# 比 Java 更有優勢,例如語法特性方面。

現在很多企業都已經採用 .NET Core 來作為後端開發框架,因為它足夠簡單、全面,很多功能模組能開箱即用,還能夠部署在 CentOS、Ubuntu 等作業系統,或以容器部署於分散式叢集中。隨著容器編排技術、微服務等技術的發展,.NET Core 作為一個現代後端開發框架,可以被用於開發可擴充套件的、易維護的大型後端系統,而且相應的生態技術也能夠讓其適用於更多的業務場景。

後端框架概覽

前後端分離是現代 Web 應用的主流架構設計,前端一般是 React、Vue 等前端框架開發的單頁應用(SPA),而後端一般是基於 HTTP 的 RESTful 風格 API,前後端資料互動通過 AJAX 請求來完成。而後端 API 開發正是 .NET Core 擅長的應用領域。

下面是 Web API 開發中常規的模組或功能,這裡只列出了後端應用常用的模組功能。

  • 路由

  • 中介軟體

  • 鑑權/授權

  • 資料庫操作

  • 配置

  • 依賴注入

  • 日誌

  • 錯誤處理

  • 單元測試

  • RPC

可以看到,成熟後端 Web 框架還是需要涵蓋不少內容的。而 .NET Core 作為現代化後端框架,包含所有這些常規的模組和功能。因此,如果直接採用 .NET Core 來開發後端 Web API,會簡化不少搭建工作,因為這些功能都能開箱即用,很容易被應用到專案中來。另外,微軟為 .NET Core 編寫了非常詳盡的文件,對於首次接觸或不熟悉 .NET Core 的入門開發者來說,可以通過閱讀相關的文件解釋說明,來幫助自己迅速掌握基礎知識和相關 API,以加速入門使用 .NET Core 開發後端 API 應用。

其他程式語言的 Web 框架(例如 Spring Boot、Gin)也包含類似的功能模組,一些基礎模組是非常類似的。不過,筆者認為 .NET Core 在某些方面有提升開發體驗的一定優勢,例如資料庫操作、鑑權/授權、依賴注入等。本篇文章將就這些模組來介紹 .NET Core 的主要特性。

下面,筆者將著重介紹 .NET Core 用於開發後端 API 的這些主要特性。

.NET Core 主要特性

這裡我們主要介紹一些 .NET Core 中對開發者比較友好的特性,同時也是提升開發體驗和效率的主要特性。我們將略過路由、中介軟體、單元測試等常規特性,這些各個框架都大同小異,大家可以參考官方文件來檢視詳情。

資料庫操作

.NET Core 主要使用的資料庫操作框架是 「Entity Framework」 (簡稱 EF,中文名為 實體框架 )。EF 是 C# 專屬現代化 ORM 框架,主要用於操作關係型資料庫中的資料、表、列、欄位等資料庫物件。Entity Framework 可以配合 LINQ(一種類似 SQL 的查詢語言,在前一篇文章 《為什麼推薦你用 C# 構建大型後端應用?- Part 1》 詳細介紹過)在程式碼中輕鬆完成 ORM 查詢和操作。

下面是一個 EF 簡單的查詢語言例子。

using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Where(b => b.Url.Contains("dotnet"))
.ToList();
}

其中 BloggingContext 是 EF 中對應的資料庫上下文類,其中包含了相關的資料庫表、列、欄位、索引、外來鍵等資訊,可以直接在 C# 程式碼中對其進行操作。這裡 context.Blogs 是代表部落格文章對應的表, Where 表示對該表進行篩選,其中用 b => b.Url.Contains("dotnet") 這樣的匿名函式來定義篩選條件,函式引數 b 是表中每一行記錄,其包含 Url 這個欄位。

當然,Entity Framework 的查詢能力遠不限於此,它能夠做 SQL 能做的幾乎所有事情。

下面是一個包含 JOIN 相對複雜的 LINQ 查詢語句。

var query = from photo in context.Set<PersonPhoto>()
join person in context.Set<Person>()
on new { Id = (int?)photo.PersonPhotoId, photo.Caption }
equals new { Id = person.PhotoId, Caption = "SN" }
select new { person, photo };

在這個查詢語句裡, PersonPhotoPerson 這兩個表在 PersonPhotoIdPhotoId 這兩個欄位上進行了 JOIN,而且還篩選出 Caption 等於 SN 的記錄,最後將 personphoto 這兩個模型輸出出來。最後的結果是一個列表,列表項包含 personphoto 這兩個模型。

可以看到, 「在 EF 中配合 LINQ 對關係型資料庫進行查詢操作是非常簡單的,就跟寫 SQL 一樣簡單」 。而且, 「我們在 C# 程式碼中用 ORM 的方式操作資料庫的主要目的是為了約束變數型別,以靜態型別的方式保證程式碼的健壯性」

對於一個通用的 Web API 來說,增刪改查(CURD)模組幾乎是不可或缺的,前面介紹了 EF 在查詢方面的便捷性,現在我們介紹 EF 其他的資料操作方式,也就是增、刪、改。

下面是 EF 中簡單的增刪改例子。

// 新增
using (var context = new BloggingContext())
{
var blog = new Blog { Url = "http://example.com" };
context.Blogs.Add(blog);
context.SaveChanges();
}

// 刪除
using (var context = new BloggingContext())
{
var blog = context.Blogs.First();
context.Blogs.Remove(blog);
context.SaveChanges();
}

// 更新
using (var context = new BloggingContext())
{
var blog = context.Blogs.First();
blog.Url = "http://example.com/blog";
context.SaveChanges();
}

上面的增刪改操作是通過 context.BlogsAddRemove 以及屬性賦值( blog.Url = ... )來實現的,相對來說也非常簡潔。

語法一致性

請注意,上面提到的這些是 C# 中 ICollection 介面以及型別例項的基礎語法,沒有任何附加的東西,也就是說,Entity Framework 是完全相容 C# 基礎型別的,具有很強的 「語法一致性」

為什麼說語法型別一致性是提升開發體驗的重要特性呢?第一,從大腦工作的角度來看,這讓開發者減少了不必要的工作記憶開銷,因為你不需要更多的工作記憶區域(例如新的語法知識)來協助完成工作,因而能提高開發效率,避免工作記憶切換帶來的效率問題;第二,語法一致性讓開發者能夠更合理的規範程式碼,能夠利用基礎語法中的程式設計模式來規範 ORM 中的程式碼;第三,學習成本降低,因為開發者不需要學習新的知識。

相反,如果你去學習 Java 的 MyBatis、JPA 這樣的 ORM 框架,需要掌握很多額外的知識,才能有效完成資料庫的增刪改查操作。

筆者推薦用 C# 寫後端應用的其中一個重要原因就是 Entity Framework。在 .NET Core 中用 EF 操作資料庫會很好的滿足語法一致性,提高開發體驗以及工作效率。

依賴注入

.NET Core 跟其他大型後端框架一樣,也有 「依賴注入」 (Dependency Injection,簡稱 DI)的功能。什麼是依賴注入?其實依賴注入不是一個新的概念,是軟體工程中的一個設計模式,主要用於降低模組間依賴關係的複雜度,達到 「解耦」 的目的。簡單來說,依賴注入允許開發者將所有依賴都 “注入”(也可以叫註冊)到容器裡,如果需要某個依賴直接通過型別、介面引用即可,完全不用開發者考慮各模組之間的依賴關係,達到自動載入的目的。這在面向物件程式設計(OOP)語言中是非常有用的。

其實依賴注入不僅限於 OOP。例如前端框架 Angular 就是以依賴注入為模組化的核心功能,在 Go 語言中也有依賴注入的框架。讀者可以參考微軟關於依賴注入的 官方文件 [1] 來對其概念進行深入瞭解。

.NET Core 中提倡用依賴注入的方式規範專案結構。對於大型專案來說,模組化、分層是很重要的,這個可以通過依賴注入來做到。

下面是一個利用依賴注入將負責處理 HTTP 請求響應的控制器層與負責實現核心業務邏輯的服務層分層的例子。

我們首先定義好模型、服務介面、服務類。

// ./Interfaces/IUserService.cs
...
namespace DotNetExamples.Interfaces
{
public interface IUserService
{
IEnumerable<User> GetUserList();
...
}
}

// ./Services/UserService.cs
...
namespace DotNetExamples.Services
{
public class UserService : IUserService
{
public IEnumerable<User> GetUserList()
{
// 獲取資料的核心程式碼
...
}
...
}
}

然後,我們在啟動程式碼中 Startup.cs 註冊這個服務。

// ./Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();

// 註冊服務
services.AddScoped<IUserService, UserService>();
}
...

其中, services.AddScoped<IUserService, UserService>(); 就是註冊使用者服務的程式碼。 AddScoped 是指該依賴的生命週期為 scoped ,也就是在只存在於單次上下文中。.NET Core 一個好處就是可以通過配置依賴的生命週期來進行效能調優。關於依賴注入生命週期這裡限於篇幅原因不打算詳細介紹,感興趣的讀者可以到微軟 .NET Core 官網檢視。

然後,我們就可以在控制器程式碼裡通過介面來引用服務了。下面是控制器 UserController 的程式碼。

// ./Controllers/UserController.cs
...
namespace DotNetExamples.Controllers
{
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{
// 宣告依賴
private readonly IUserService _userService;

public UserController(IUserService userService)
{
// 注入依賴
_userService = userService;
}

[HttpGet]
public IEnumerable<User> GetList()
{
// 呼叫依賴
return _userService.GetUserList();
}
}
}

可以看到,相關的依賴 IUserService 是通過建構函式的形式來注入的。只需要宣告私有變數,然後在建構函式中將依賴注入進去,從而忽略了依賴與依賴之間的細節,直接引用即可,非常直觀方便。

如果您仔細看上面的例子,可以看到控制器 UserController 只是起宣告路由的作用,其核心獲取資料的程式碼在被注入的 UserService 型別的依賴 _userServiceGetUserList 方法中。這樣設計,就輕鬆將負責 HTTP 資料互動的 Controller 層與負責實際業務邏輯的 Service 層剝離開來,各司其職,以實現軟體工程中關於 「低耦合」 的設計理念。

本篇文章主要通過介紹 C# 跨平臺框架 .NET Core 的其中兩個主要特性,來幫助讀者理解用 C# 開發後端專案的優勢。其中,Entity Framework 因為其語法一致性以及配合 LINQ 加持的語法簡潔性,使得用 .NET Core 編寫 CURD 應用變得非常輕鬆和靈活。另外,我們還介紹了 .NET Core 的依賴注入,它讓專案程式碼分層、模組化以及實現低耦合設計變得更容易,Controller 層和 Service 層可以相互分開而不受影響,這增強了可維護性以及可擴充套件性。

本篇文章是 C# 系列文章的第二篇,在後面筆者將介紹更多 C# 構建大型後端應用的實踐內容,以幫助感興趣的讀者更能瞭解和熟悉 C#,幫助大家拓寬知識面以及增加軟體工程特別是後端開發中的知識廣度,讓開發者在後端程式語言中能夠有更多的選擇,例如 C#。

Reference

[1]

官方文件: https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/dependency-injection