如何重構“箭頭型”代碼

本文主要起因是,一次在微博上和朋友關于嵌套好幾層的if-else語句的代碼重構的討論(微博原文),在微博上大家有各式各樣的問題和想法。按道理來說這些都是編程的基本功,似乎不太值得寫一篇文章,不過我覺得很多東西可以從一個簡單的東西出發,到達本質,所以,我覺得有必要在這里寫一篇的文章。不一定全對,只希望得到更多的討論,因為有了更深入的討論才能進步。

文章有點長,我在文章最后會給出相關的思考和總結陳詞,你可以跳到結尾。

所謂箭頭型代碼,基本上來說就是下面這個圖片所示的情況。

那么,這樣“箭頭型”的代碼有什么問題呢?看上去也挺好看的,有對稱美。但是……

關于箭頭型代碼的問題有如下幾個:

1)我的顯示器不夠寬,箭頭型代碼縮進太狠了,需要我來回拉水平滾動條,這讓我在讀代碼的時候,相當的不舒服。

2)除了寬度外還有長度,有的代碼的if-else里的if-else里的if-else的代碼太多,讀到中間你都不知道中間的代碼是經過了什么樣的層層檢查才來到這里的。

總而言之,“箭頭型代碼”如果嵌套太多,代碼太長的話,會相當容易讓維護代碼的人(包括自己)迷失在代碼中,因為看到最內層的代碼時,你已經不知道前面的那一層一層的條件判斷是什么樣的,代碼是怎么運行到這里的,所以,箭頭型代碼是非常難以維護和Debug的

微博上的案例 與 Guard Clauses

OK,我們先來看一下微博上的那個示例,代碼量如果再大一點,嵌套再多一點,你很容易會在條件中迷失掉(下面這個示例只是那個“大箭頭”下的一個小箭頭)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FOREACH(Ptr<WfExpression>, argument, node->arguments) {
????int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj());
????if (index != -1) {
????????auto type = manager->expressionResolvings.Values()[index].type;
????????if (! types.Contains(type.Obj())) {
????????????types.Add(type.Obj());
????????????if (auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L"CastResult", true)) {
????????????????int count = group->GetMethodCount();
????????????????for (int i = 0; i < count; i++) { auto method = group->GetMethod(i);
????????????????????if (method->IsStatic()) {
????????????????????????if (method->GetParameterCount() == 1 &&
????????????????????????????method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
????????????????????????????method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
????????????????????????????symbol->typeInfo = CopyTypeInfo(method->GetReturn());
????????????????????????????break;
????????????????????????}
????????????????????}
????????????????}
????????????}
????????}
????}
}

上面這段代碼,可以把條件反過來寫,然后就可以把箭頭型的代碼解掉了,重構的代碼如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FOREACH(Ptr<WfExpression>, argument, node->arguments) {
????int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj());
????if (index == -1)? continue;
?????
????auto type = manager->expressionResolvings.Values()[index].type;
????if ( types.Contains(type.Obj()))? continue;
?????
????types.Add(type.Obj());
????auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L"CastResult", true);
????if? ( ! group ) continue;
??
????int count = group->GetMethodCount();
????for (int i = 0; i < count; i++) { auto method = group->GetMethod(i);
????????if (! method->IsStatic()) continue;
????????
????????if ( method->GetParameterCount() == 1 &&
???????????????method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
???????????????method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
????????????symbol->typeInfo = CopyTypeInfo(method->GetReturn());
????????????break;
????????}
????}
}

這種代碼的重構方式叫 Guard Clauses

  • Martin Fowler 的 Refactoring 的網站上有相應的說明《Replace Nested Conditional with Guard Clauses》。
  • Coding Horror 上也有一篇文章講了這種重構的方式 —— 《Flattening Arrow Code》
  • StackOverflow 上也有相關的問題說了這種方式 —— 《Refactor nested IF statement for clarity》

這里的思路其實就是,讓出錯的代碼先返回,前面把所有的錯誤判斷全判斷掉,然后就剩下的就是正常的代碼了

抽取成函數

微博上有些人說,continue 語句破壞了閱讀代碼的通暢,我覺得他們一定沒有好好讀這里面的代碼,其實,我們可以看到,所有的 if 語句都是在判斷是否出錯的情況,所以,在維護代碼的時候,你可以完全不理會這些 if 語句,因為都是出錯處理的,而剩下的代碼都是正常的功能代碼,反而更容易閱讀了。當然,一定有不是上面代碼里的這種情況,那么,不用continue ,我們還能不能重構呢?

當然可以,抽成函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
bool CopyMethodTypeInfo(auto &method, auto &group, auto &symbol)
{
????if (! method->IsStatic()) {
????????return true;
????}
????if ( method->GetParameterCount() == 1 &&
???????????method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() &&
???????????method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor<void>() ) {
????????symbol->typeInfo = CopyTypeInfo(method->GetReturn());
????????return false;
????}
????return true;
}
void ExpressionResolvings(auto &manager, auto &argument, auto &symbol)
{
????int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj());
????if (index == -1) return;
?????
????auto type = manager->expressionResolvings.Values()[index].type;
????if ( types.Contains(type.Obj())) return;
????types.Add(type.Obj());
????auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L"CastResult", true);
????if? ( ! group ) return;
????int count = group->GetMethodCount();
????for (int i = 0; i < count; i++) { auto method = group->GetMethod(i);
????????if ( ! CopyMethodTypeInfo(method, group, symbol) ) break;
????}
}
...
...
FOREACH(Ptr<WfExpression>, argument, node->arguments) {
????ExpressionResolvings(manager, arguments, symbol)
}
...
...

你發出現,抽成函數后,代碼比之前變得更容易讀和更容易維護了。不是嗎?

有人說:“如果代碼不共享,就不要抽取成函數!”,持有這個觀點的人太死讀書了。函數是代碼的封裝或是抽象,并不一定用來作代碼共享使用,函數用于屏蔽細節,讓其它代碼耦合于接口而不是細節實現,這會讓我們的代碼更為簡單,簡單的東西都能讓人易讀也易維護。這才是函數的作用。

嵌套的 if 外的代碼

微博上還有人問,原來的代碼如果在各個 if 語句后還有要執行的代碼,那么應該如何重構。比如下面這樣的代碼。

原版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(....) {
????do_before_cond1()
????if (cond1) {
????????do_before_cond2();
????????if (cond2) {
????????????do_before_cond3();
????????????if (cond3) {
????????????????do_something();
????????????}
????????????do_after_cond3();
????????}
????????do_after_cond2();
????}
????do_after_cond1();
}

上面這段代碼中的那些 do_after_condX() 是無論條件成功與否都要執行的。所以,我們拉平后的代碼如下所示:

重構第一版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
for(....) {
????do_before_cond1();
????if ( !cond1 ) {
????????do_after_cond1();
????????continue
????}
????do_after_cond1();
????do_before_cond2();
????if ( !cond2 ) {
????????do_after_cond2();
????????continue;
????}
????do_after_cond2();
????do_before_cond3();
????if ( !cond3 ) {
????????do_after_cond3();
????????continue;
????}
????do_after_cond3();
????do_something();?
}

你會發現,上面的 do_after_condX 出現了兩份。如果 if 語句塊中的代碼改變了某些do_after_condX依賴的狀態,那么這是最終版本。

但是,如果它們之前沒有依賴關系的話,根據 DRY 原則,我們就可以只保留一份,那么直接掉到 if 條件前就好了,如下所示:

重構第二版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for(....) {
????do_before_cond1();
????do_after_cond1();
????if ( !cond1 ) continue;
??
????do_before_cond2();
????do_after_cond2();
????if ( !cond2 ) continue;
????do_before_cond3();
????do_after_cond3();
????if ( !cond3 ) continue;
????do_something();?
}

此時,你會說,我靠,居然,改變了執行的順序,把條件放到 do_after_condX() 后面去了。這會不會有問題啊?

其實,你再分析一下之前的代碼,你會發現,本來,cond1 是判斷 do_before_cond1() 是否出錯的,如果有成功了,才會往下執行。而 do_after_cond1() 是無論如何都要執行的。從邏輯上來說,do_after_cond1()其實和do_before_cond1()的執行結果無關,而 cond1 卻和是否去執行 do_before_cond2() 相關了。如果我把斷行變成下面這樣,反而代碼邏輯更清楚了。

重構第三版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for(....) {
????do_before_cond1();
????do_after_cond1();
????if ( !cond1 ) continue;? // <-- cond1 成了是否做第二個語句塊的條件
????do_before_cond2();
????do_after_cond2();
????if ( !cond2 ) continue; // <-- cond2 成了是否做第三個語句塊的條件
????do_before_cond3();
????do_after_cond3();
????if ( !cond3 ) continue; //<-- cond3 成了是否做第四個語句塊的條件
????do_something();
??
}

于是乎,在未來維護代碼的時候,維護人一眼看上去就明白,代碼在什么時候會執行到哪里。 這個時候,你會發現,把這些語句塊抽成函數,代碼會干凈的更多,再重構一版:

重構第四版
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool do_func3() {
???do_before_cond2();
???do_after_cond2();
???return cond3;
}
bool do_func2() {
???do_before_cond2();
???do_after_cond2();
???return cond2;
}
bool do_func1() {
???do_before_cond1();
???do_after_cond1();
???return cond1;
}
// for-loop 你可以重構成這樣
for (...) {
????bool cond = do_func1();
????if (cond) cond = do_func2();
????if (cond) cond = do_func3();
????if (cond) do_something();
}
// for-loop 也可以重構成這樣
for (...) {
????if ( ! do_func1() ) continue;
????if ( ! do_func2() ) continue;
????if ( ! do_func3() ) continue;
????do_something();
}

上面,我給出了兩個版本的for-loop,你喜歡哪個?我喜歡第二個。這個時候,因為for-loop里的代碼非常簡單,就算你不喜歡 continue ,這樣的代碼閱讀成本已經很低了。

狀態檢查嵌套

接下來,我們再來看另一個示例。下面的代碼的偽造了一個場景——把兩個人拉到一個一對一的聊天室中,因為要檢查雙方的狀態,所以,代碼可能會寫成了“箭頭型”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager)
{
????if ( pA->isConnected() ) {
????????manager->Prepare(pA);
????????if ( pB->isConnected() ) {
????????????manager->Prepare(pB);
????????????if ( manager->ConnectTogther(pA, pB) ) {
????????????????pA->Write("connected");
????????????????pB->Write("connected");
????????????????return S_OK;
????????????}else{
????????????????return S_ERROR;
????????????}
????????}else {
????????????pA->Write("Peer is not Ready, waiting...");
????????????return S_RETRY;
????????}
????}else{
????????if ( pB->isConnected() ) {
????????????manager->Prepare();
????????????pB->Write("Peer is not Ready, waiting...");
????????????return S_RETRY;
????????}else{
????????????pA->Close();
????????????pB->Close();
????????????return S_ERROR;
????????}
????}
????//Shouldn't be here!
????return S_ERROR;
}

重構上面的代碼,我們可以先分析一下上面的代碼,說明了,上面的代碼就是對 PeerA 和 PeerB 的兩個狀態 “連上”, “未連上” 做組合 “狀態” (注:實際中的狀態應該比這個還要復雜,可能還會有“斷開”、“錯誤”……等等狀態), 于是,我們可以把代碼寫成下面這樣,合并上面的嵌套條件,對于每一種組合都做出判斷。這樣一來,邏輯就會非常的干凈和清楚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager)
{
????if ( pA->isConnected() ) {
????????manager->Prepare(pA);
????}
????if ( pB->isConnected() ) {
????????manager->Prepare(pB);
????}
????// pA = YES && pB = NO
????if (pA->isConnected() && ! pB->isConnected()? ) {
????????pA->Write("Peer is not Ready, waiting");
????????return S_RETRY;
????// pA = NO && pB = YES
????}else if ( !pA->isConnected() && pB->isConnected() ) {
????????pB->Write("Peer is not Ready, waiting");
????????return S_RETRY;
????// pA = YES && pB = YES
????}else if (pA->isConnected() && pB->isConnected()? ) {
????????if ( ! manager->ConnectTogther(pA, pB) ) {
????????????return S_ERROR;
????????}
????????pA->Write("connected");
????????pB->Write("connected");
????????return S_OK;
????}
????// pA = NO, pB = NO
????pA->Close();
????pB->Close();
????return S_ERROR;
}

延伸思考

對于 if-else 語句來說,一般來說,就是檢查兩件事:錯誤狀態

檢查錯誤

對于檢查錯誤來說,使用 Guard Clauses 會是一種標準解,但我們還需要注意下面幾件事:

1)當然,出現錯誤的時候,還會出現需要釋放資源的情況。你可以使用 goto fail; 這樣的方式,但是最優雅的方式應該是C++面向對象式的 RAII 方式。

2)以錯誤碼返回是一種比較簡單的方式,這種方式有很一些問題,比如,如果錯誤碼太多,判斷出錯的代碼會非常復雜,另外,正常的代碼和錯誤的代碼會混在一起,影響可讀性。所以,在更為高組的語言中,使用 try-catch 異常捕捉的方式,會讓代碼更為易讀一些。

檢查狀態

對于檢查狀態來說,實際中一定有更為復雜的情況,比如下面幾種情況:

1)像TCP協議中的兩端的狀態變化。

2)像shell各個命令的命令選項的各種組合。

3)像游戲中的狀態變化(一棵非常復雜的狀態樹)。

4)像語法分析那樣的狀態變化。

對于這些復雜的狀態變化,其本上來說,你需要先定義一個狀態機,或是一個子狀態的組合狀態的查詢表,或是一個狀態查詢分析樹。

寫代碼時,代碼的運行中的控制狀態或業務狀態是會讓你的代碼流程變得混亂的一個重要原因,重構“箭頭型”代碼的一個很重要的工作就是重新梳理和描述這些狀態的變遷關系

總結

好了,下面總結一下,把“箭頭型”代碼重構掉的幾個手段如下:

1)使用 Guard Clauses 。 盡可能的讓出錯的先返回, 這樣后面就會得到干凈的代碼。

2)把條件中的語句塊抽取成函數。 有人說:“如果代碼不共享,就不要抽取成函數!”,持有這個觀點的人太死讀書了。函數是代碼的封裝或是抽象,并不一定用來作代碼共享使用,函數用于屏蔽細節,讓其它代碼耦合于接口而不是細節實現,這會讓我們的代碼更為簡單,簡單的東西都能讓人易讀也易維護,寫出讓人易讀易維護的代碼才是重構代碼的初衷

3)對于出錯處理,使用try-catch異常處理和RAII機制。返回碼的出錯處理有很多問題,比如:A) 返回碼可以被忽略,B) 出錯處理的代碼和正常處理的代碼混在一起,C) 造成函數接口污染,比如像atoi()這種錯誤碼和返回值共用的糟糕的函數。

4)對于多個狀態的判斷和組合,如果復雜了,可以使用“組合狀態表”,或是狀態機加Observer的狀態訂閱的設計模式。這樣的代碼即解了耦,也干凈簡單,同樣有很強的擴展性。

5) 重構“箭頭型”代碼其實是在幫你重新梳理所有的代碼和邏輯,這個過程非常值得為之付出。重新整思路去想盡一切辦法簡化代碼的過程本身就可以讓人成長。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/452518.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/452518.shtml
英文地址,請注明出處:http://en.pswp.cn/news/452518.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Django博客--4.開發博客文章詳情頁

文章目錄0.思路引導1.設計文章詳情頁的 URL2.獲取文章的URL3.編寫 detail 視圖函數4.編寫詳情頁模板5.更改主頁中跳轉詳情頁的地址鏈接6.模板繼承--抽取base.html7.模板繼承--修改 index.html使其繼承base.html8.模板繼承--修改detail.html使其繼承base.html9.結果展示0.思路引…

10、并發容器,ConcurrentHashMap

Java 提供了不同層面的線程安全支持。在傳統集合框架內部&#xff0c;除了 Hashtable 等同步容器&#xff0c;還提供了所謂的同步包裝器&#xff08;Synchronized Wrapper&#xff09;&#xff0c;我們可以調用 Collections 工具類提供的包裝方法&#xff0c;來獲取一個同步的包…

程序員的本質

Computers are useless. They can only give you answers. – Picasso計算機沒有什么作用。他們只能告訴你答案。——畢加索很多人&#xff08;包括我岳母&#xff09;認為計算機變得如此智能&#xff0c;所以在不久的未來將不再需要程序員。另外一些人認為程序員是天才&#x…

模式-視圖-控制器模式2.0

1 MVC的實現   1.1 分析應用問題&#xff0c;對系統進行分離   分析應用問題&#xff0c;分離出系統的內核功能、對功能的控制輸入、系統的輸出行為三大部分。設計模型部件使其封裝內核數據和計算功能&#xff0c;提供訪問顯示數據的操作&#xff0c;提供控制內部行為的操作…

總體設計的原理

1 模塊化 2 抽象 3 逐步求精 4 信息隱藏和局部化 5 模塊獨立

android 手動回收對象,Android Studio Studio回收列表中的JSON對象

我想在recyclerview中顯示一些JSON對象&#xff0c;并且希望它們在日期之后排序&#xff0c;我該如何實現&#xff1f;下面是下載從JSON URL的數據的方法&#xff1a;Android Studio Studio回收列表中的JSON對象public void downloadFromSkistar(){try{URL url new URL("…

剖析管理所有大數據組件的可視化利器:Hue

歡迎關注大數據和人工智能技術文章發布的微信公眾號&#xff1a;清研學堂&#xff0c;在這里你可以學到夜白&#xff08;作者筆名&#xff09;精心整理的筆記&#xff0c;讓我們每天進步一點點&#xff0c;讓優秀成為一種習慣&#xff01; 日常的大數據使用都是在服務器命令行中…

Django博客--5.讓博客支持 Markdown 語法和代碼高亮

文章目錄0.前言1.安裝 Python Markdown2.在 detail 視圖中解析 Markdown3.safe 標簽4.代碼高亮5.效果展示0.前言 Markdown 是一種 HTML 文本標記語言&#xff0c;只要遵循它約定的語法格式&#xff0c;Markdown 的解析工具就能夠把 Markdown 文檔轉換為標準的 HTML 文檔&#…

耦合

模塊的獨立性很重要&#xff0c;因為有效的模塊化(即具有獨立的模塊)的軟件比較容易開發出來。 獨立的模塊比較容易測試和維護。 模塊的獨立程度可以由兩個定性標準度量&#xff0c;這兩個標準分別稱為內聚和耦合。 耦合 耦合是對一個軟件結構內不同模塊之間互連程度的度量。…

成為更優秀的開發人員:第二步-知道你的核心競爭力

編者按&#xff1a;原文作者羅布沃林&#xff08;Rob Walling&#xff09;從事Web應用開發10年之久&#xff0c;擔任過業內顧問、自由開發人員和全球最大的信用卡預付公司City of Pasadena的開發經理。現居住于加州中部城市弗雷斯諾&#xff08;Fresno&#xff09;。關注并指導…

android 字體間間隔,TextView設置行間距、字體間距

一、設置行間距1、設置行間距&#xff1a;android:lineSpacingExtra&#xff0c;取值范圍&#xff1a;正數、負數和0&#xff0c;正數表示增加相應的大小&#xff0c;負數表示減少相應的大小&#xff0c;0表示無變化2、設置行間距的倍數&#xff1a;android:lineSpacingMultipl…

破解mysql數據庫的密碼

發現的1小問題 語句打錯以后應該退出本語句,再繼續打新語句.也可以打\c,退出本語句. 如何破解數據庫的密碼: 1:通過任務管理器或者服務管理,關掉mysqld(服務進程) 2:通過命令行特殊參數開啟mysqld Mysqld --skip-grant-tables 3:此時,mysqld服務進程已經打開,并且,不需要權限檢…

Diango博客--6.Markdown 文章自動生成目錄

文章目錄0.思路引導1.在文中插入目錄2.在頁面的任何地方插入目錄3.美化標題的錨點 URL0.思路引導 Markdown 在解析內容的同時還可以自動提取整個內容的目錄結構&#xff0c;本文內容將從以下幾個方面展開&#xff1a; 1&#xff09;在文中插入目錄&#xff1b; 2&#xff09;在…

Java中對象和引用的理解

2019獨角獸企業重金招聘Python工程師標準>>> 偶然想起Java中對象和引用的基本概念&#xff0c;為了加深下對此的理解和認識&#xff0c;特地整理一下相關的知識點&#xff0c;通過具體實例從兩者的概念和區別兩方面去更形象的認識理解&#xff0c;再去記憶。12一、對…

android怎樣封裝,如何封裝屬于自己的博客網站安卓APP 源碼家園

說實話我今天在寫這個文章的時候是我使用易語言(E4A\易安卓)的第一天&#xff0c;我也是易小白&#xff0c;但是的確可以用&#xff01;我為什么寫這個文章呢&#xff1f;因為之前我也想封裝自己的網站&#xff0c;然后去網上找的在線封裝生成APP&#xff0c;果然能封裝好了&am…

程序員常犯的5個非技術性錯誤

一個好的軟件開發人員需要培養兩種技能&#xff1a;技術技能和非技術技能。不幸的是一些開發者只注重技術的部分&#xff0c;以致養成一些陋習&#xff0c;下面是最常犯的5個非技術性錯誤&#xff1a; 0. 缺乏自律 Jim Rohn曾經說過&#xff1a;自律是目標和成果之間的橋梁。我…

Redis進階實踐之二十 Redis的配置文件使用詳解

一、引言   寫完上一篇有關redis使用lua腳本的文章&#xff0c;就有意結束Redis這個系列的文章了&#xff0c;當然了&#xff0c;這里的結束只是我這個系列的結束&#xff0c;但是要學的東西還有很多。但是&#xff0c;好多天過去了&#xff0c;總是感覺好像還缺點什么…

web流程設計器 工作流的 整合視頻教程 activiti畫圖 SSM和獨立部署

本視頻為activiti工作流的web流程設計器整合視頻教程整合Acitiviti在線流程設計器(Activiti-Modeler 5.21.0 官方流程設計器&#xff09;本視頻共講了兩種整合方式1. 流程設計器和其它工作流項目分開部署的方式2. 流程設計器和SSM框架項目整合在一起的方式視頻大小 1.13 GB ~【…

Diango博客--7.自動生成文章摘要

文章目錄0.思路引導1.方法一&#xff1a;覆寫 save 方法2.方法二&#xff1a;使用 truncatechars 模板過濾器0.思路引導 博客文章的模型有一個 excerpt 字段&#xff0c;這個字段用于存儲文章的摘要。 若在 django admin 后臺手動為文章輸入摘要&#xff0c;每次手動輸入摘要…