昨天我們介紹了怎麼在 .NET Web API 的專案裡實現依賴注入,但是昨天我們注入的是一個指定的 Service。這樣的情況下我們其實無法感受到 DI 的好處,用起來的感覺只是把 new 出 Service 實體的程式碼集中到建構式,如果哪天這個 Service 有變動,依賴這個 Service 的地方還是都要改。比較好的作法是讓我們的 Controller 依賴介面(interface)而不是依賴固定的 Service 類別,今天我們要來修改我們的程式,發揮 DI 的好處!
在開始之前,我們先介紹一下介面,以下節錄微軟官網對介面的解釋
An interface contains definitions for a group of related functionalities that a non-abstract class or a struct must implement.
介面包含了一組相關的功能定義,非抽象的類別或結構必須實作這些定義。
(官網的繁體中文翻譯真的很爛,所以我自己翻了一下 = =)
不過我想除非真的實際定過、實作過介面,不然還是無法明白這句話在說什麼。這裡,筆者提供一個自己的理解給大家參考:「介面定義了一組相關的方法(method),一個類別如果實作這個介面,那麼這個類別就必須依照介面的定義完成所有的方法」
舉個例子來說,我們定義了一個叫「貓咪」的介面,包含吃罐頭()、睡覺()、呼嚕嚕()三個方法,然後我們讓一個「橘貓」類別實作這個介面,那麼「橘貓」就一定至少要有吃罐頭()、睡覺()、呼嚕嚕()三個公開的(public)方法,而且參數、回傳的資料型態一定要與介面的定義相同
public interface ICat // 介面的命名慣例會在最開頭加大寫 I
{
void Eat(); // 吃罐頭
void Hoolulu(); // 呼嚕嚕
int Sleep(int hour); // 睡覺
}
public class OrangeCat : ICat
{
public void Eat()
{
Console.WriteLine("橘貓開始吃罐頭");
}
public void Hoolulu()
{
Console.WriteLine("橘貓開始呼嚕嚕");
}
public int Sleep(int hour)
{
Console.WriteLine("橘貓睡了" + hour + "小時");
return hour;
}
}
public class ScottishFold : ICat
{
public void Eat()
{
Console.WriteLine("摺耳貓開始吃罐頭");
}
public void Hoolulu()
{
Console.WriteLine("摺耳貓開始呼嚕嚕");
}
public int Sleep(int hour)
{
Console.WriteLine("摺耳貓睡了" + hour + "小時");
return hour;
}
}
讓我們的類別實作介面有幾個直接的好處
var rand = new Random();
var cats = new List<ICat>();
for (int i = 0; i < 10; i++)
{
if (rand.Next() % 2 == 0)
{
cats.Add(new OrangeCat());
}
else
{
cats.Add(new ScottishFold());
}
}
foreach (ICat cat in cats)
{
cat.Hoolulu();
cat.Eat();
cat.Sleep(1);
}
public interface IUserCRUD
{
public List<User> GetAllUsers();
public User GetUserById(int id);
public void CreateUser(User model);
public void UpdateUser(int id, User model);
public void DeleteUser(int id);
}
然後,讓我們的 Service 實作這個介面
public class UserService : IUserCRUD
{
// 內容不變
}
services.AddScoped<IUserCRUD, UserService>();
public class UserController : ControllerBase
{
private readonly IUserCRUD _user;
public UserController(IUserCRUD user)
{
_user = user;
}
// 其他的程式不變
}
這樣一來,我們就成功的讓 Controller 依賴介面而不是明確指定的類別了!執行程式可以發現,API 用起來完全一樣。
現在,假設我們新增一個實作 IUserCRUD 介面的 Service,這個 Service 從本地端的檔案讀寫 User 資料
public class UserServiceWithFile : IUserCRUD
{
private readonly string _fileName = "D:/TestUsers.csv";
private readonly List<User> _users;
private List<User> ReadUsersFromFile(string fileName)
{
if (!File.Exists(_fileName))
{
var file = File.Create(_fileName);
file.Close();
}
var users = new List<User>();
using (var reader = new StreamReader(fileName))
{
while (!reader.EndOfStream)
{
var values = reader.ReadLine().Split(",");
users.Add(new User()
{
UserId = Convert.ToInt32(values[0]),
UserName = values[1],
Email = values[2]
});
}
}
return users;
}
private void SaveUsersToFile(string fileName)
{
using (var writer = new StreamWriter(fileName))
{
foreach (var user in _users)
{
writer.WriteLine($"{user.UserId},{user.UserName},{user.Email}");
}
}
}
public UserServiceWithFile()
{
_users = ReadUsersFromFile(_fileName);
}
public List<User> GetAllUsers()
{
return _users;
}
public User GetUserById(int id)
{
return _users.FirstOrDefault(x => x.UserId == id);
}
public void CreateUser(User model)
{
if (_users.Count == 0)
{
model.UserId = 1;
}
else
{
model.UserId = _users.Max(x => x.UserId) + 1;
}
_users.Add(model);
SaveUsersToFile(_fileName);
}
public void UpdateUser(int id, User model)
{
var existingUser = _users.FirstOrDefault(x => x.UserId == id);
if (existingUser != null)
{
existingUser.UserName = model.UserName;
existingUser.Email = model.Email;
}
SaveUsersToFile(_fileName);
}
public void DeleteUser(int id)
{
var existingUser = _users.FirstOrDefault(x => x.UserId == id);
if (existingUser != null)
{
_users.Remove(existingUser);
}
SaveUsersToFile(_fileName);
}
}
接著,把註冊 Service 的地方改成
services.AddScoped<IUserCRUD, UserServiceWithFile>();
執行程式一樣會覺得跑起來一模一樣!而且只要在註冊 Service 的地方稍作修改,我們就能隨時替換真正使用的 Service,依賴這個介面的其他程式完全不用改!我們在後面的文章會使用 MySQL 來管理資料,到時候只要再新增一個 UserServiceWithMySQL 實作 IUserCRUD 介面,然後註冊的地方換一下,就能無痛把用檔案管理使用者的 Service 替換掉!沒騙你吧!學 DI 的 CP 值真的很高!
明天我們將繼續使用 DI 的方法,把程式需要的組態設定注入給 Controller。
弱弱地舉手問:「依賴介面而不是特定的 Service」 ...這就是傳說中的「依賴反轉原則」嗎?~
弱弱的回答:是滴~~ DI 的目的就是為了達到 IoC
(竟然要先解新手任務才能回覆...)