淺談依賴註入

来源:https://www.cnblogs.com/xiaomowang/archive/2019/04/02/10640432.html
-Advertisement-
Play Games

1 IGame游戲公司的故事 1.1 討論會 話說有一個叫IGame的游戲公司,正在開發一款ARPG游戲(動作&角色扮演類游戲,如魔獸世界、夢幻西游這一類的游戲)。一般這類游戲都有一個基本的功能,就是打怪(玩家攻擊怪物,藉此獲得經驗、虛擬貨幣和虛擬裝備),並且根據玩家角色所裝備的武器不同,攻擊效果也 ...


1 IGame游戲公司的故事

1.1 討論會

話說有一個叫IGame的游戲公司,正在開發一款ARPG游戲(動作&角色扮演類游戲,如魔獸世界、夢幻西游這一類的游戲)。一般這類游戲都有一個基本的功能,就是打怪(玩家攻擊怪物,藉此獲得經驗、虛擬貨幣和虛擬裝備),並且根據玩家角色所裝備的武器不同,攻擊效果也不同。這天,IGame公司的開發小組正在開會對打怪功能中的某一個功能點如何實現進行討論,他們面前的大屏幕上是這樣一份需求描述的ppt:

圖1.1 需求描述ppt

各個開發人員,面對這份需求,展開了熱烈的討論,下麵我們看看討論會上都發生了什麼。

1.2 實習生小李的實現方式

在經過一番討論後,項目組長Peter覺得有必要整理一下各方的意見,他首先詢問小李的看法。小李是某學校電腦系大三學生,對游戲開發特別感興趣,目前是IGame公司的一名實習生。

經過短暫的思考,小李闡述了自己的意見:

“我認為,這個需求可以這麼實現。HP當然是怪物的一個屬性成員,而武器是角色的一個屬性成員,類型可以使字元串,用於描述目前角色所裝備的武器。角色類有一個攻擊方法,以被攻擊怪物為參數,當實施一次攻擊時,攻擊方法被調用,而這個方法首先判斷當前角色裝備了什麼武器,然後據此對被攻擊怪物的HP進行操作,以產生不同效果。”

而在闡述完後,小李也飛快的在自己的電腦上寫了一個Demo,來演示他的想法,Demo代碼如下。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLi
 7 {
 8     /// <summary>
 9     /// 怪物
10     /// </summary>
11     internal sealed class Monster
12     {
13         /// <summary>
14         /// 怪物的名字
15         /// </summary>
16         public String Name { get; set; }
17   
18         /// <summary>
19         /// 怪物的生命值
20         /// </summary>
21         public Int32 HP { get; set; }
22   
23         public Monster(String name,Int32 hp)
24         {
25             this.Name = name;
26             this.HP = hp;
27         }
28     }
29 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLi
 7 {
 8     /// <summary>
 9     /// 角色
10     /// </summary>
11     internal sealed class Role
12     {
13         private Random _random = new Random();
14   
15         /// <summary>
16         /// 表示角色目前所持武器的字元串
17         /// </summary>
18         public String WeaponTag { get; set; }
19   
20         /// <summary>
21         /// 攻擊怪物
22         /// </summary>
23         /// <param name="monster">被攻擊的怪物</param>
24         public void Attack(Monster monster)
25         {
26             if (monster.HP <= 0)
27             {
28                 Console.WriteLine("此怪物已死");
29                 return;
30             }
31   
32             if ("WoodSword" == this.WeaponTag)
33             {
34                 monster.HP -= 20;
35                 if (monster.HP <= 0)
36                 {
37                     Console.WriteLine("攻擊成功!怪物" + monster.Name + "已死亡");
38                 }
39                 else
40                 {
41                     Console.WriteLine("攻擊成功!怪物" + monster.Name + "損失20HP");
42                 }
43             }
44             else if ("IronSword" == this.WeaponTag)
45             {
46                 monster.HP -= 50;
47                 if (monster.HP <= 0)
48                 {
49                     Console.WriteLine("攻擊成功!怪物" + monster.Name + "已死亡");
50                 }
51                 else
52                 {
53                     Console.WriteLine("攻擊成功!怪物" + monster.Name + "損失50HP");
54                 }
55             }
56             else if ("MagicSword" == this.WeaponTag)
57             {
58                 Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
59                 monster.HP -= loss;
60                 if (200 == loss)
61                 {
62                     Console.WriteLine("出現暴擊!!!");
63                 }
64   
65                 if (monster.HP <= 0)
66                 {
67                     Console.WriteLine("攻擊成功!怪物" + monster.Name + "已死亡");
68                 }
69                 else
70                 {
71                     Console.WriteLine("攻擊成功!怪物" + monster.Name + "損失" + loss + "HP");
72                 }
73             }
74             else
75             {
76                 Console.WriteLine("角色手裡沒有武器,無法攻擊!");
77             }
78         }
79     }
80 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLi
 7 {
 8     class Program
 9     {
10         static void Main(string[] args)
11         {
12             //生成怪物
13             Monster monster1 = new Monster("小怪A", 50);
14             Monster monster2 = new Monster("小怪B", 50);
15             Monster monster3 = new Monster("關主", 200);
16             Monster monster4 = new Monster("最終Boss", 1000);
17   
18             //生成角色
19             Role role = new Role();
20   
21             //木劍攻擊
22             role.WeaponTag = "WoodSword";
23             role.Attack(monster1);
24   
25             //鐵劍攻擊
26             role.WeaponTag = "IronSword";
27             role.Attack(monster2);
28             role.Attack(monster3);
29   
30             //魔劍攻擊
31             role.WeaponTag = "MagicSword";
32             role.Attack(monster3);
33             role.Attack(monster4);
34             role.Attack(monster4);
35             role.Attack(monster4);
36             role.Attack(monster4);
37             role.Attack(monster4);
38   
39             Console.ReadLine();
40         }
41     }
42 }
View Code

程式運行結果如下:

圖1.2 小李程式的運行結果

1.3 架構師的建議

小李闡述完自己的想法並演示了Demo後,項目組長Peter首先肯定了小李的思考能力、編程能力以及初步的面向對象分析與設計的思想,並承認小李的程式正確完成了需求中的功能。但同時,Peter也指出小李的設計存在一些問題,他請小於講一下自己的看法。

小於是一名有五年軟體架構經驗的架構師,對軟體架構、設計模式和麵向對象思想有較深入的認識。他向Peter點了點頭,發表了自己的看法:

“小李的思考能力是不錯的,有著基本的面向對象分析設計能力,並且程式正確完成了所需要的功能。不過,這裡我想從架構角度,簡要說一下我認為這個設計中存在的問題。

首先,小李設計的Role類的Attack方法很長,並且方法中有一個冗長的if…else結構,且每個分支的代碼的業務邏輯很相似,只是很少的地方不同。

再者,我認為這個設計比較大的一個問題是,違反了OCP原則。在這個設計中,如果以後我們增加一個新的武器,如倚天劍,每次攻擊損失500HP,那麼,我們就要打開Role,修改Attack方法。而我們的代碼應該是對修改關閉的,當有新武器加入的時候,應該使用擴展完成,避免修改已有代碼。

一般來說,當一個方法裡面出現冗長的if…else或switch…case結構,且每個分支代碼業務相似時,往往預示這裡應該引入多態性來解決問題。而這裡,如果把不同武器攻擊看成一個策略,那麼引入策略模式(Strategy Pattern)是明智的選擇。

最後說一個小的問題,被攻擊後,減HP、死亡判斷等都是怪物的職責,這裡放在Role中有些不當。”

Tip:OCP原則,即開放關閉原則,指設計應該對擴展開放,對修改關閉。

Tip:策略模式,英文名Strategy Pattern,指定義演算法族,分別封裝起來,讓他們之間可以相互替換,此模式使得演算法的變化獨立於客戶。

小於邊說,邊畫了一幅UML類圖,用於直觀表示他的思想。

圖1.3 小於的設計

Peter讓小李按照小於的設計重構Demo,小李看了看小於的設計圖,很快完成。相關代碼如下:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     internal interface IAttackStrategy
 9     {
10         void AttackTarget(Monster monster);
11     }
12 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     internal sealed class WoodSword : IAttackStrategy
 9     {
10         public void AttackTarget(Monster monster)
11         {
12             monster.Notify(20);
13         }
14     }
15 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     internal sealed class IronSword : IAttackStrategy
 9     {
10         public void AttackTarget(Monster monster)
11         {
12             monster.Notify(50);
13         }
14     }
15 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     internal sealed class MagicSword : IAttackStrategy
 9     {
10         private Random _random = new Random();
11   
12         public void AttackTarget(Monster monster)
13         {
14             Int32 loss = (_random.NextDouble() < 0.5) ? 100 : 200;
15             if (200 == loss)
16             {
17                 Console.WriteLine("出現暴擊!!!");
18             }
19             monster.Notify(loss);
20         }
21     }
22 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     /// <summary>
 9     /// 怪物
10     /// </summary>
11     internal sealed class Monster
12     {
13         /// <summary>
14         /// 怪物的名字
15         /// </summary>
16         public String Name { get; set; }
17   
18         /// <summary>
19         /// 怪物的生命值
20         /// </summary>
21         private Int32 HP { get; set; }
22   
23         public Monster(String name,Int32 hp)
24         {
25             this.Name = name;
26             this.HP = hp;
27         }
28   
29         /// <summary>
30         /// 怪物被攻擊時,被調用的方法,用來處理被攻擊後的狀態更改
31         /// </summary>
32         /// <param name="loss">此次攻擊損失的HP</param>
33         public void Notify(Int32 loss)
34         {
35             if (this.HP <= 0)
36             {
37                 Console.WriteLine("此怪物已死");
38                 return;
39             }
40   
41             this.HP -= loss;
42             if (this.HP <= 0)
43             {
44                 Console.WriteLine("怪物" + this.Name + "被打死");
45             }
46             else
47             {
48                 Console.WriteLine("怪物" + this.Name + "損失" + loss + "HP");
49             }
50         }
51     }
52 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     /// <summary>
 9     /// 角色
10     /// </summary>
11     internal sealed class Role
12     {
13         /// <summary>
14         /// 表示角色目前所持武器
15         /// </summary>
16         public IAttackStrategy Weapon { get; set; }
17   
18         /// <summary>
19         /// 攻擊怪物
20         /// </summary>
21         /// <param name="monster">被攻擊的怪物</param>
22         public void Attack(Monster monster)
23         {
24             this.Weapon.AttackTarget(monster);
25         }
26     }
27 }
View Code
 1 using System;
 2 using System.Collections.Generic;
 3 using System.Linq;
 4 using System.Text;
 5   
 6 namespace IGameLiAdv
 7 {
 8     class Program
 9     {
10         static void Main(string[] args)
11         {
12             //生成怪物
13             Monster monster1 = new Monster("小怪A", 50);
14             Monster monster2 = new Monster("小怪B", 50);
15             Monster monster3 = new Monster("關主", 200);
16             Monster monster4 = new Monster("最終Boss", 1000);
17   
18             //生成角色
19             Role role = new Role();
20   
21             //木劍攻擊
22             role.Weapon = new WoodSword();
23             role.Attack(monster1);
24   
25             //鐵劍攻擊
26             role.Weapon = new IronSword();
27             role.Attack(monster2);
28             role.Attack(monster3);
29   
30             //魔劍攻擊
31             role.Weapon = new MagicSword();
32             role.Attack(monster3);
33             role.Attack(monster4);
34             role.Attack(monster4);
35             role.Attack(monster4);
36             role.Attack(monster4);
37             role.Attack(monster4);
38   
39             Console.ReadLine();
40         }
41     }
42 }
View Code

編譯運行以上代碼,得到的運行結果與上一版本代碼基本一致。

1.4 小李的小結

Peter顯然對改進後的代碼比較滿意,他讓小李對照兩份設計和代碼,進行一個小結。小李簡略思考了一下,並結合小於對一次設計指出的不足,說道:

“我認為,改進後的代碼有如下優點:

第一,雖然類的數量增加了,但是每個類中方法的代碼都非常短,沒有了以前Attack方法那種很長的方法,也沒有了冗長的if…else,代碼結構變得很清晰。

第二,類的職責更明確了。在第一個設計中,Role不但負責攻擊,還負責給怪物減少HP和判斷怪物是否已死。這明顯不應該是Role的職責,改進後的代碼將這兩個職責移入Monster內,使得職責明確,提高了類的內聚性。

第三,引入Strategy模式後,不但消除了重覆性代碼,更重要的是,使得設計符合了OCP。如果以後要加一個新武器,只要新建一個類,實現IAttackStrategy介面,當角色需要裝備這個新武器時,客戶代碼只要實例化一個新武器類,並賦給Role的Weapon成員就可以了,已有的Role和Monster代碼都不用改動。這樣就實現了對擴展開發,對修改關閉。”

Peter和小於聽後都很滿意,認為小李總結的非常出色。

IGame公司的討論會還在進行著,內容是非常精彩,不過我們先聽到這裡,因為,接下來,我們要對其中某些問題進行一點探討。別忘了,本文的主題可是依賴註入,這個主角還沒登場呢!讓主角等太久可不好。

2 探究依賴註入

2.1 故事的啟迪

我們現在靜下心來,再回味一下剛纔的故事。因為,這個故事裡面隱藏著依賴註入的出現原因。我說過不只一次,想真正認清一個事物,不能只看“它是什麼?什麼樣子?”,而應該先弄清楚“它是怎麼來的?是什麼樣的需求和背景促使了它的誕生?它被創造出來是做什麼用的?”。

回想上面的故事。剛開始,主要需求是一個打怪的功能。小李做了一個初步面向對象的設計:抽取領域場景中的實體(怪物、角色等),封裝成類,併為各個類賦予屬性與方法,最後通過類的交互完成打怪功能,這應該算是面向對象設計的初級階段。

在小李的設計基礎上,架構師小於指出了幾點不足,如不符合OCP,職責劃分不明確等等,並根據情況引入策略模式。這是更高層次的面向對象設計。其實就核心來說,小於只做了一件事:利用多態性,隔離變化。它清楚認識到,這個打怪功能中,有些業務邏輯是不變的,如角色攻擊怪物,怪物減少HP,減到0怪物就會死;而變化的僅僅是不同的角色持有不同武器時,每次攻擊的效用不一樣。於是他的架構,本質就是把變化的部分和不變的部分隔離開,使得變化部分發生變化時,不變部分不受影響。

我們再仔細看看小於的設計圖,這樣設計後,有個基本的問題需要解決:現在Role不依賴具體武器,而僅僅依賴一個IAttackStrategy介面,介面是不能實例化的,雖然Role的Weapon成員類型定義為IAttackStrategy,但最終還是會被賦予一個實現了IAttackStrategy介面的具體武器,並且隨著程式進展,一個角色會裝備不同的武器,從而產生不同的效用。賦予武器的職責,在Demo中是放在了測試代碼里。

這裡,測試代碼實例化一個具體的武器,並賦給Role的Weapon成員的過程,就是依賴註入!這裡要清楚,依賴註入其實是一個過程的稱謂!

2.2 正式定義依賴註入

下麵,用稍微正式一點的語言,定義依賴註入產生的背景緣由和依賴註入的含義。在讀的過程中,讀者可以結合上面的例子進行理解。

依賴註入產生的背景:

隨著面向對象分析與設計的發展,一個良好的設計,核心原則之一就是將變化隔離,使得變化部分發生變化時,不變部分不受影響(這也是OCP的目的)。為了做到這一點,要利用面向對象中的多態性,使用多態性後,客戶類不再直接依賴服務類,而是依賴於一個抽象的介面,這樣,客戶類就不能在內部直接實例化具體的服務類。但是,客戶類在運作中又客觀需要具體的服務類提供服務,因為介面是不能實例化去提供服務的。就產生了“客戶類不准實例化具體服務類”和“客戶類需要具體服務類”這樣一對矛盾。為瞭解決這個矛盾,開發人員提出了一種模式:客戶類(如上例中的Role)定義一個註入點(Public成員Weapon),用於服務類(實現IAttackStrategy的具體類,如WoodSword、IronSword和MagicSword,也包括以後加進來的所有實現IAttackStrategy的新類)的註入,而客戶類的客戶類(Program,即測試代碼)負責根據情況,實例化服務類,註入到客戶類中,從而解決了這個矛盾。

依賴註入的正式定義:

依賴註入(Dependency Injection),是這樣一個過程:由於某客戶類只依賴於服務類的一個介面,而不依賴於具體服務類,所以客戶類只定義一個註入點。在程式運行過程中,客戶類不直接實例化具體服務類實例,而是客戶類的運行上下文環境或專門組件負責實例化服務類,然後將其註入到客戶類中,保證客戶類的正常運行。

3 依賴註入那些事兒

上面我們從需求背景的角度,講述了依賴註入的來源和定義。但是,如果依賴註入僅僅就只有這麼點東西,那也沒有什麼值得討論的了。但是,上面討論的僅僅是依賴註入的內涵,其外延還是非常廣泛的,從依賴註入衍生出了很多相關的概念與技術,下麵我們討論一下依賴註入的“那些事兒”。

3.1 依賴註入的類別

依賴註入有很多種方法,上面看到的例子中,只是其中的一種,下麵分別討論不同的依賴註入類型。

3.1.1 Setter註入

第一種依賴註入的方式,就是Setter註入,上面的例子中,將武器註入Role就是Setter註入。正式點說:

Setter註入(Setter Injection)是指在客戶類中,設置一個服務類介面類型的數據成員,並設置一個Set方法作為註入點,這個Set方法接受一個具體的服務類實例為參數,並將它賦給服務類介面類型的數據成員。

圖3.1 Setter註入示意

上圖展示了Setter註入的結構示意圖,客戶類ClientClass設置IServiceClass類型成員_serviceImpl,並設置Set_ServiceImpl方法作為註入點。Context會負責實例化一個具體的ServiceClass,然後註入到ClientClass里。

下麵給出Setter註入的示例代碼。

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 之前css學的一直不精緻而且沒有細節,為了成為一個完美的前端工作人員,所以決定重新學習css的屬性。當然會借鑒MDZ文檔( "MDZ文檔" )或其他博主的經驗來總結。在這裡會註明借鑒或引用文章的出處。侵權即刪。 position屬性值包括 static,relative,absolute,fixed ...
  • 一、HTML代碼如下: 二、jQuery代碼如下: ...
  • 剛看完JS中的深淺拷貝,來記錄分享一番,一起來開心的掉發吧。 首先瞭解深淺拷貝之前來看看JS中的幾種數據類型,分別有String、Number、Boolean、undefined、null、Object。es6還多了一種symbol,我們暫且先把他放一邊。前面提到的幾種數據類型前五種也就是Strin ...
  • <!DOCTYPE html><html> <head> <title></title> <style type="text/css"> html, body { height: 100%; width: 100%; margin: 0; padding: 0; position: relative ...
  • 在b站上看到一個大神用p5.js寫的,我覺得吧,原生是無敵的存在(其實是因為我不會),所以……效果自然沒有人家的好咯 js代碼: 請不要在意裡面的英文,當時只是覺得逼格很高 效果我一直不知道怎麼放 HTML中只要有個canvas就好了 ...
  • 學習web的第五天 CSS層疊樣式表,也稱為級聯樣式表,用來設計網頁風格。 老師說內聯樣式表不介意我們使用,我們只要瞭解並掌握就行了,重點給我們講解了外部樣式表。 css的優勢:1、表現和內容相分離,css通過定義html標記的樣式,使得頁面內容和顯示相分離,簡化了網頁格式設計。 2、加強了網頁的表 ...
  • v-bind:title="message" ...
  • 背景 去年年底的時候,靜兒在團隊會議中提出了自己的對整個服務將來的規劃。靜兒心裡明白自己的架構設想是可實現的,但是遠超目前的架構。被質疑無法落地。於是靜兒將一些概念的東西全都拋去,直接針對具體的項目做領域拆分。項目也在一點點像靜兒原來規劃的演進。 靜兒認為這個不做管理的一個好處:「對技術的挑戰要大的 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...