為什麼推薦你用 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