【《代碼整潔之道》精讀與演繹】之三 整潔代碼的函數書寫準則



這篇文章,是關於整潔代碼函數書寫的一些準則。

 



一、引言



以下引言的內容,有必要伴隨這個系列的每一次更新,這次也不例外。

 

《代碼整潔之道》這本書提出了一個觀點:代碼質量與其整潔度成正比,乾淨的代碼,既在質量上可靠,也爲後期維護、升級奠定了良好基礎。書中介紹的規則均來自作者多年的實踐經驗,涵蓋從命名到重構的多個編程方面,雖爲一“家”之言,然誠有可資借鑑的價值。

 

但我們知道,很多時候,理想很豐滿,現實很骨感,也知道人在江湖,身不由己。因爲項目的緊迫性,需求的多樣性,我們無法時時刻刻都寫出整潔的代碼,保持自己輸出的都是高質量、優雅的代碼。

 

但若我們理解了代碼整潔之道的精髓,我們會知道怎樣讓自己的代碼更加優雅、整潔、易讀、易擴展,知道真正整潔的代碼應該是怎麼樣的,也許就會漸漸養成持續輸出整潔代碼的習慣。

 

而且或許你會發現,若你一直保持輸出整潔代碼的習慣,長期來看,會讓你的整體效率和代碼質量大大提升。

 




二、本文涉及知識點思維導圖



國際慣例,先放出這篇文章所涉及內容知識點的一張思維導圖,就開始正文。大家若是疲於閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點的大概。

 






三、整潔代碼的函數書寫準則

 


短小


函數的第一規則是要短小。第二規則還是要短小。

《代碼整潔之道》一書作者Bob大叔寫道,“近40年來,我寫過各種長度不同的函數。我寫過令人憎惡的長達3000行的厭物,也寫過許多100行到300行的函數,還寫過20行到30行的。經過漫長的試錯,經驗告訴我,函數就該短小”。

那麼函數應該有多短小合適呢?通常來說,應該短於如下這個函數:

  1. public static StringrenderPageWithSetupsAndTeardowns  
  2. (PageData pageData, boolean isSuite  
  3.        )throws Exception  
  4. {  
  5.        booleanisTestPage = pageData.hasAttribute("Test");  
  6.        if(isTestPage) {  
  7.               WikiPagetestPage = pageData.getWikiPage( );  
  8.               StringBuffernewPageContent = new StringBuffer( );  
  9.               includeSetupPages(testPage,newPageContent, isSuite);  
  10.               newPageContent.append(pageData.getContent());  
  11.               includeTeardownPages(testPage,newPageContent, isSuite);  
  12.               pageData.setContent(newPageContent.toString());  
  13.        }  
  14.        returnpageData.getHtml( );  
  15. }  

而其實,最好應該縮短成如下的樣子:

[csharp] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. public static StringrenderPageWithSetupsAndTeardowns(  
  2.        PageDatapageData, boolean isSuite) throws Exception   
  3. {  
  4.        if(isTestPage(pageData))  
  5.               includeSetupAndTeardownPages(pageData,isSuite);  
  6.        returnpageData.getHtml( );  
  7. }  

總之,十行以內是整潔的函數比較合適的長度,若沒有特殊情況,我們最好將單個函數控制在十行以內。


評論區有一些討論,也放到正文來吧。

函數是否應該足夠短小,算是《代碼整潔之道》中最具爭議的議題之一。

書寫短小函數的時候,其實我們不要忽略一點,那就是,函數名名稱本身就具描述性。短小的函數構成,如果要追根溯源瞭解內部實現,自然需要一層層找到最終的實現。但若是想大致知道這個函數到底做了什麼,結合這個短小函數體內具描述性的一些函數名,應該也就一目瞭然了。試想,當你眼前的這個函數是幾十上百上千行的龐然大物的時候,你能做到一眼就一目瞭然,將其大概做了什麼瞭然於心嗎?函數短小的一方面優點,在這裏就體現出來了。

函數應該短小這個議題,仁者見仁智者見智,在實際編碼過程中任何人都很難做到嚴格遵守,但大的方向,若想寫出整潔的代碼,應該去向短小的函數靠攏,對吧?”




 

2 單一職責

 

函數應該只做一件事情。只做一件事,做好這件事。

設計模式中有單一職責原則,我們可以把這條原則理解爲代碼整潔之道中的函數單一職責原則。

要判斷函數是不是隻做了一件事情,還有一個方法,就是看能否再拆出一個函數,該函數不僅只是單純地重新詮釋其實現。

 

 


命名合適且具描述性

 

“如果每個例程都讓你感到深合己意,那就是整潔的代碼。”要遵循這一原則,大半工作都在於爲只做一件事的小函數取個好名字。函數越短小,功能越集中,就越便於取個好名字。

 

別害怕長名稱。長而具有描述性的名稱,比短而令人費解的名稱好。而且長而具有描述性的名稱,比描述性的長註釋要好。且選擇描述性的名稱能理清你關於模塊的設計思路,並幫你改進之。當然,如果短的名稱已經足夠說明問題,還是越短越好。

 

命名方式要保持一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。比如,includeSetupAndTeardownPages,includeSetupPages, includeSuiteSetupPage, and includeSetupPage等。這些名詞使用了類似的措辭,依序講述一個故事,就是是比較推崇的命名方式了。

 

 

 

參數儘可能少

 

最理想的函數參數形態是零參數,其次是單參數,再次是雙參數,應儘量避免三參數及以上參數的函數,有足夠的理由才能用三個以上參數(多參數函數)。


函數參數中出現標識符參數是非常不推崇的做法。有標識符參數的函數,很有可能不止在做一件事,標示如果標識符爲true將這樣做,標識符爲false將那樣做。正確的做法應該將有標識符參數的函數一分爲二,對標識符爲true和false分別開一個函數來處理。

 

 

 

5 避免重複


重複的代碼會導致模塊的臃腫,整個模塊的可讀性可能會應該重複的消除而得到提升。


其實可以這樣說,重複可能是軟件中一切邪惡的根源,許多原則與實踐規則都是爲控制與消除重複而創建的。仔細想一想,面向對象編程是如何將代碼集中到基類,從而避免了冗餘的。而面向方面編程(Aspect Oriented Programming)、面向組件編程(ComponentOriented Programming)多少也是消除重複的一種策略。這樣看來,自子程序發明以來,軟件開發領域的所有創新都是在不斷嘗試從源代碼中消滅重複。


重複而囉嗦的代碼,乃萬惡之源,我們要盡力避免。

 




四、範例



有必要貼出一段書中推崇的整潔代碼作爲本次函數書寫準則的範例。

[csharp] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. using System;  
  2.   
  3. public class SetupTeardownIncluder  
  4. {  
  5.     private PageData pageData;  
  6.     private boolean isSuite;  
  7.     private WikiPage testPage;  
  8.     private StringBuffer newPageContent;  
  9.     private PageCrawler pageCrawler;  
  10.   
  11.     public static String render(PageData pageData) throws Exception  
  12.     {  
  13.         return render(pageData, false);  
  14.     }  
  15.     public static String render(PageData pageData, boolean isSuite)throws Exception  
  16.     {  
  17.         return new SetupTeardownIncluder(pageData).render(isSuite);  
  18.     }  
  19.     private SetupTeardownIncluder(PageData pageData)  
  20.     {  
  21.         this.pageData = pageData;  
  22.         testPage = pageData.getWikiPage();  
  23.         pageCrawler = testPage.getPageCrawler();  
  24.         newPageContent = new StringBuffer();  
  25.     }  
  26.   
  27.     private String render(boolean isSuite) throws Exception  
  28.     {  
  29.         this.isSuite = isSuite;  
  30.         if (isTestPage())  
  31.         includeSetupAndTeardownPages();  
  32.         return pageData.getHtml();  
  33.     }  
  34.   
  35.     private boolean isTestPage() throws Exception  
  36.     {  
  37.         return pageData.hasAttribute("Test");  
  38.     }  
  39.   
  40.     private void includeSetupAndTeardownPages() throws Exception  
  41.     {  
  42.         includeSetupPages();  
  43.         includePageContent();  
  44.         includeTeardownPages();  
  45.         updatePageContent();  
  46.     }  
  47.   
  48.     private void includeSetupPages() throws Exception  
  49.     {  
  50.         if (isSuite)  
  51.             includeSuiteSetupPage();  
  52.         includeSetupPage();  
  53.     }  
  54.   
  55.     private void includeSuiteSetupPage() throws Exception  
  56.     {  
  57.         include(SuiteResponder.SUITE_SETUP_NAME, "-setup");  
  58.     }  
  59.   
  60.     private void includeSetupPage() throws Exception  
  61.     {  
  62.         include("SetUp""-setup");  
  63.     }  
  64.   
  65.     private void includePageContent() throws Exception  
  66.     {  
  67.         newPageContent.append(pageData.getContent());  
  68.     }  
  69.   
  70.     private void includeTeardownPages() throws Exception  
  71.     {  
  72.         includeTeardownPage();  
  73.         if (isSuite)  
  74.         includeSuiteTeardownPage();  
  75.     }  
  76.   
  77.     private void includeTeardownPage() throws Exception  
  78.     {  
  79.         include("TearDown""-teardown");  
  80.     }  
  81.   
  82.     private void includeSuiteTeardownPage() throws Exception  
  83.     {  
  84.         include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");  
  85.     }  
  86.   
  87.     private void updatePageContent() throws Exception  
  88.     {  
  89.         pageData.setContent(newPageContent.toString());  
  90.     }  
  91.   
  92.     private void include(String pageName, String arg) throws Exception  
  93.     {  
  94.         WikiPage inheritedPage = findInheritedPage(pageName);  
  95.         if (inheritedPage != null)   
  96.         {  
  97.             String pagePathName = getPathNameForPage(inheritedPage);  
  98.             buildIncludeDirective(pagePathName, arg);  
  99.         }  
  100.     }  
  101.   
  102.     private WikiPage findInheritedPage(String pageName) throws Exception  
  103.     {  
  104.         return PageCrawlerImpl.getInheritedPage(pageName, testPage);  
  105.     }  
  106.   
  107.     private String getPathNameForPage(WikiPage page) throws Exception  
  108.     {  
  109.         WikiPagePath pagePath = pageCrawler.getFullPath(page);  
  110.         return PathParser.render(pagePath);  
  111.     }  
  112.   
  113.     private void buildIncludeDirective(String pagePathName, String arg)  
  114.     {  
  115.         newPageContent  
  116.         .append("\n!include ")  
  117.         .append(arg)  
  118.         .append(" .")  
  119.         .append(pagePathName)  
  120.         .append("\n");  
  121.     }  
  122. }  
上面這段代碼,滿足了函數書寫短小、單一職責、命名合適、參數儘可能少、不重複囉嗦這幾條準則。整潔的函數代碼大致如此。




 

五、小結



大師級程序員把系統當作故事來講,而不是當做程序來寫。這是之前已經提到過的一個觀點。

 

本文講述瞭如何編寫良好函數的一些準則,如果你遵循這些準則,函數就會短小,有個好名字,而且被很好的歸置。不過永遠不要忘記,我們真正的目標在於講述系統的故事,而你編寫的函數必須乾淨利落的拼裝到一起,形成一種精確而清晰的語言,幫助你講故事。

 

程序員,其實是故事家。

 




六、本文涉及知識點提煉整理

 


整潔代碼的函數書寫,可以遵從如下幾個原則:

  • 第一原則:短小。若沒有特殊情況,最好將單個函數控制在十行以內。
  • 第二原則:單一職責。函數應該只做一件事情。只做一件事,做好這件事。
  • 第三原則:命名合適且具描述性。長而具有描述性的名稱,比短而令人費解的名稱好。當然,如果短的名稱已經足夠說明問題,還是越短越好。
  • 第四原則:參數儘可能少。最理想的函數參數形態是零參數,其次是單參數,再次是雙參數,應儘量避免三參數及以上參數的函數,有足夠的理由才能用三個以上參數。
  • 第五原則:盡力避免重複。重複的代碼會導致模塊的臃腫,整個模塊的可讀性可能會應該重複的消除而得到提升。

 


本文就此結束。

下篇文章,我們將繼續《代碼整潔之道》的精讀與演繹,探討更多的內容。

Best Wish~

這篇文章,是關於整潔代碼函數書寫的一些準則。

 



一、引言



以下引言的內容,有必要伴隨這個系列的每一次更新,這次也不例外。

 

《代碼整潔之道》這本書提出了一個觀點:代碼質量與其整潔度成正比,乾淨的代碼,既在質量上可靠,也爲後期維護、升級奠定了良好基礎。書中介紹的規則均來自作者多年的實踐經驗,涵蓋從命名到重構的多個編程方面,雖爲一“家”之言,然誠有可資借鑑的價值。

 

但我們知道,很多時候,理想很豐滿,現實很骨感,也知道人在江湖,身不由己。因爲項目的緊迫性,需求的多樣性,我們無法時時刻刻都寫出整潔的代碼,保持自己輸出的都是高質量、優雅的代碼。

 

但若我們理解了代碼整潔之道的精髓,我們會知道怎樣讓自己的代碼更加優雅、整潔、易讀、易擴展,知道真正整潔的代碼應該是怎麼樣的,也許就會漸漸養成持續輸出整潔代碼的習慣。

 

而且或許你會發現,若你一直保持輸出整潔代碼的習慣,長期來看,會讓你的整體效率和代碼質量大大提升。

 




二、本文涉及知識點思維導圖



國際慣例,先放出這篇文章所涉及內容知識點的一張思維導圖,就開始正文。大家若是疲於閱讀文章正文,直接看這張圖,也是可以Get到本文的主要知識點的大概。

 






三、整潔代碼的函數書寫準則

 


短小


函數的第一規則是要短小。第二規則還是要短小。

《代碼整潔之道》一書作者Bob大叔寫道,“近40年來,我寫過各種長度不同的函數。我寫過令人憎惡的長達3000行的厭物,也寫過許多100行到300行的函數,還寫過20行到30行的。經過漫長的試錯,經驗告訴我,函數就該短小”。

那麼函數應該有多短小合適呢?通常來說,應該短於如下這個函數:

  1. public static StringrenderPageWithSetupsAndTeardowns  
  2. (PageData pageData, boolean isSuite  
  3.        )throws Exception  
  4. {  
  5.        booleanisTestPage = pageData.hasAttribute("Test");  
  6.        if(isTestPage) {  
  7.               WikiPagetestPage = pageData.getWikiPage( );  
  8.               StringBuffernewPageContent = new StringBuffer( );  
  9.               includeSetupPages(testPage,newPageContent, isSuite);  
  10.               newPageContent.append(pageData.getContent());  
  11.               includeTeardownPages(testPage,newPageContent, isSuite);  
  12.               pageData.setContent(newPageContent.toString());  
  13.        }  
  14.        returnpageData.getHtml( );  
  15. }  

而其實,最好應該縮短成如下的樣子:

[csharp] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. public static StringrenderPageWithSetupsAndTeardowns(  
  2.        PageDatapageData, boolean isSuite) throws Exception   
  3. {  
  4.        if(isTestPage(pageData))  
  5.               includeSetupAndTeardownPages(pageData,isSuite);  
  6.        returnpageData.getHtml( );  
  7. }  

總之,十行以內是整潔的函數比較合適的長度,若沒有特殊情況,我們最好將單個函數控制在十行以內。


評論區有一些討論,也放到正文來吧。

函數是否應該足夠短小,算是《代碼整潔之道》中最具爭議的議題之一。

書寫短小函數的時候,其實我們不要忽略一點,那就是,函數名名稱本身就具描述性。短小的函數構成,如果要追根溯源瞭解內部實現,自然需要一層層找到最終的實現。但若是想大致知道這個函數到底做了什麼,結合這個短小函數體內具描述性的一些函數名,應該也就一目瞭然了。試想,當你眼前的這個函數是幾十上百上千行的龐然大物的時候,你能做到一眼就一目瞭然,將其大概做了什麼瞭然於心嗎?函數短小的一方面優點,在這裏就體現出來了。

函數應該短小這個議題,仁者見仁智者見智,在實際編碼過程中任何人都很難做到嚴格遵守,但大的方向,若想寫出整潔的代碼,應該去向短小的函數靠攏,對吧?”




 

2 單一職責

 

函數應該只做一件事情。只做一件事,做好這件事。

設計模式中有單一職責原則,我們可以把這條原則理解爲代碼整潔之道中的函數單一職責原則。

要判斷函數是不是隻做了一件事情,還有一個方法,就是看能否再拆出一個函數,該函數不僅只是單純地重新詮釋其實現。

 

 


命名合適且具描述性

 

“如果每個例程都讓你感到深合己意,那就是整潔的代碼。”要遵循這一原則,大半工作都在於爲只做一件事的小函數取個好名字。函數越短小,功能越集中,就越便於取個好名字。

 

別害怕長名稱。長而具有描述性的名稱,比短而令人費解的名稱好。而且長而具有描述性的名稱,比描述性的長註釋要好。且選擇描述性的名稱能理清你關於模塊的設計思路,並幫你改進之。當然,如果短的名稱已經足夠說明問題,還是越短越好。

 

命名方式要保持一致。使用與模塊名一脈相承的短語、名詞和動詞給函數命名。比如,includeSetupAndTeardownPages,includeSetupPages, includeSuiteSetupPage, and includeSetupPage等。這些名詞使用了類似的措辭,依序講述一個故事,就是是比較推崇的命名方式了。

 

 

 

參數儘可能少

 

最理想的函數參數形態是零參數,其次是單參數,再次是雙參數,應儘量避免三參數及以上參數的函數,有足夠的理由才能用三個以上參數(多參數函數)。


函數參數中出現標識符參數是非常不推崇的做法。有標識符參數的函數,很有可能不止在做一件事,標示如果標識符爲true將這樣做,標識符爲false將那樣做。正確的做法應該將有標識符參數的函數一分爲二,對標識符爲true和false分別開一個函數來處理。

 

 

 

5 避免重複


重複的代碼會導致模塊的臃腫,整個模塊的可讀性可能會應該重複的消除而得到提升。


其實可以這樣說,重複可能是軟件中一切邪惡的根源,許多原則與實踐規則都是爲控制與消除重複而創建的。仔細想一想,面向對象編程是如何將代碼集中到基類,從而避免了冗餘的。而面向方面編程(Aspect Oriented Programming)、面向組件編程(ComponentOriented Programming)多少也是消除重複的一種策略。這樣看來,自子程序發明以來,軟件開發領域的所有創新都是在不斷嘗試從源代碼中消滅重複。


重複而囉嗦的代碼,乃萬惡之源,我們要盡力避免。

 




四、範例



有必要貼出一段書中推崇的整潔代碼作爲本次函數書寫準則的範例。

[csharp] view plain copy
 print?在CODE上查看代碼片派生到我的代碼片
  1. using System;  
  2.   
  3. public class SetupTeardownIncluder  
  4. {  
  5.     private PageData pageData;  
  6.     private boolean isSuite;  
  7.     private WikiPage testPage;  
  8.     private StringBuffer newPageContent;  
  9.     private PageCrawler pageCrawler;  
  10.   
  11.     public static String render(PageData pageData) throws Exception  
  12.     {  
  13.         return render(pageData, false);  
  14.     }  
  15.     public static String render(PageData pageData, boolean isSuite)throws Exception  
  16.     {  
  17.         return new SetupTeardownIncluder(pageData).render(isSuite);  
  18.     }  
  19.     private SetupTeardownIncluder(PageData pageData)  
  20.     {  
  21.         this.pageData = pageData;  
  22.         testPage = pageData.getWikiPage();  
  23.         pageCrawler = testPage.getPageCrawler();  
  24.         newPageContent = new StringBuffer();  
  25.     }  
  26.   
  27.     private String render(boolean isSuite) throws Exception  
  28.     {  
  29.         this.isSuite = isSuite;  
  30.         if (isTestPage())  
  31.         includeSetupAndTeardownPages();  
  32.         return pageData.getHtml();  
  33.     }  
  34.   
  35.     private boolean isTestPage() throws Exception  
  36.     {  
  37.         return pageData.hasAttribute("Test");  
  38.     }  
  39.   
  40.     private void includeSetupAndTeardownPages() throws Exception  
  41.     {  
  42.         includeSetupPages();  
  43.         includePageContent();  
  44.         includeTeardownPages();  
  45.         updatePageContent();  
  46.     }  
  47.   
  48.     private void includeSetupPages() throws Exception  
  49.     {  
  50.         if (isSuite)  
  51.             includeSuiteSetupPage();  
  52.         includeSetupPage();  
  53.     }  
  54.   
  55.     private void includeSuiteSetupPage() throws Exception  
  56.     {  
  57.         include(SuiteResponder.SUITE_SETUP_NAME, "-setup");  
  58.     }  
  59.   
  60.     private void includeSetupPage() throws Exception  
  61.     {  
  62.         include("SetUp""-setup");  
  63.     }  
  64.   
  65.     private void includePageContent() throws Exception  
  66.     {  
  67.         newPageContent.append(pageData.getContent());  
  68.     }  
  69.   
  70.     private void includeTeardownPages() throws Exception  
  71.     {  
  72.         includeTeardownPage();  
  73.         if (isSuite)  
  74.         includeSuiteTeardownPage();  
  75.     }  
  76.   
  77.     private void includeTeardownPage() throws Exception  
  78.     {  
  79.         include("TearDown""-teardown");  
  80.     }  
  81.   
  82.     private void includeSuiteTeardownPage() throws Exception  
  83.     {  
  84.         include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");  
  85.     }  
  86.   
  87.     private void updatePageContent() throws Exception  
  88.     {  
  89.         pageData.setContent(newPageContent.toString());  
  90.     }  
  91.   
  92.     private void include(String pageName, String arg) throws Exception  
  93.     {  
  94.         WikiPage inheritedPage = findInheritedPage(pageName);  
  95.         if (inheritedPage != null)   
  96.         {  
  97.             String pagePathName = getPathNameForPage(inheritedPage);  
  98.             buildIncludeDirective(pagePathName, arg);  
  99.         }  
  100.     }  
  101.   
  102.     private WikiPage findInheritedPage(String pageName) throws Exception  
  103.     {  
  104.         return PageCrawlerImpl.getInheritedPage(pageName, testPage);  
  105.     }  
  106.   
  107.     private String getPathNameForPage(WikiPage page) throws Exception  
  108.     {  
  109.         WikiPagePath pagePath = pageCrawler.getFullPath(page);  
  110.         return PathParser.render(pagePath);  
  111.     }  
  112.   
  113.     private void buildIncludeDirective(String pagePathName, String arg)  
  114.     {  
  115.         newPageContent  
  116.         .append("\n!include ")  
  117.         .append(arg)  
  118.         .append(" .")  
  119.         .append(pagePathName)  
  120.         .append("\n");  
  121.     }  
  122. }  
上面這段代碼,滿足了函數書寫短小、單一職責、命名合適、參數儘可能少、不重複囉嗦這幾條準則。整潔的函數代碼大致如此。




 

五、小結



大師級程序員把系統當作故事來講,而不是當做程序來寫。這是之前已經提到過的一個觀點。

 

本文講述瞭如何編寫良好函數的一些準則,如果你遵循這些準則,函數就會短小,有個好名字,而且被很好的歸置。不過永遠不要忘記,我們真正的目標在於講述系統的故事,而你編寫的函數必須乾淨利落的拼裝到一起,形成一種精確而清晰的語言,幫助你講故事。

 

程序員,其實是故事家。

 




六、本文涉及知識點提煉整理

 


整潔代碼的函數書寫,可以遵從如下幾個原則:

  • 第一原則:短小。若沒有特殊情況,最好將單個函數控制在十行以內。
  • 第二原則:單一職責。函數應該只做一件事情。只做一件事,做好這件事。
  • 第三原則:命名合適且具描述性。長而具有描述性的名稱,比短而令人費解的名稱好。當然,如果短的名稱已經足夠說明問題,還是越短越好。
  • 第四原則:參數儘可能少。最理想的函數參數形態是零參數,其次是單參數,再次是雙參數,應儘量避免三參數及以上參數的函數,有足夠的理由才能用三個以上參數。
  • 第五原則:盡力避免重複。重複的代碼會導致模塊的臃腫,整個模塊的可讀性可能會應該重複的消除而得到提升。

 


本文就此結束。

下篇文章,我們將繼續《代碼整潔之道》的精讀與演繹,探討更多的內容。

Best Wish~

發佈了18 篇原創文章 · 獲贊 4 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章