重構(gòu)改善既有代碼的設(shè)計-對于函數(shù)的重構(gòu)手法總結(jié)上

在《重構(gòu)改善既有代碼設(shè)計》一書中,作者的經(jīng)驗是霞捡,重構(gòu)的大多數(shù)手法都是源自對于函數(shù)進(jìn)行的處理坐漏,絕大多數(shù)是從過長函數(shù)開始。結(jié)合我的實際處理情況碧信,不外如是仙畦。

過長函數(shù),確實很討厭音婶。因為他們往往向書中所說慨畸,會包含太多的信息,這些信息又會被函數(shù)錯綜復(fù)雜的邏輯衣式。不易鑒別寸士。如何對付過長函數(shù),作者系統(tǒng)化的總結(jié)出了一套經(jīng)驗:

對付過長函數(shù)碴卧,一項重要的重構(gòu)手法就是.Extract Method (110), 它把一段代碼從原先函 數(shù)中提取出來弱卡,放進(jìn)一個單獨函數(shù)中。而Inline Method(117)正好相反:將一個函數(shù)調(diào)用動作替換為該函數(shù)本體住册。如果在進(jìn)行多次提煉之后婶博,意識到提煉所得的某些函數(shù) 并沒有做任何實質(zhì)事情,或如果需要回溯恢復(fù)原先函數(shù)荧飞,我就需要Inline Method (117)凡人。

Extract Method (110)最大的困難就是處理局部變量,而臨時變量則是其中一個 主要的困難源頭叹阔。處理一個函數(shù)時挠轴,我喜歡運用Replace Temp with Query (120)去掉 所有可去掉的臨時變量。如果很多地方使用了某個臨時變量耳幢,我就會先運用既Split Temporary Variable (128)將它變得比較容易替換岸晦。

但有時候臨時變量實在太混亂,難以替換。這時候我就需要使用Replace Method with Method Object(135) 它讓我可以分解哪怕最混亂的函數(shù)启上,代價則是引入一個新
類邢隧。

注釋:重構(gòu)手法后面的括號是指書中的對應(yīng)處理手法的頁數(shù),之所以列出來也是為了讀者可以方便去書中直接找對應(yīng)的處理方法冈在。

image.png

下面會一一介紹包括使用邏輯以及注意點:

一府框、Extract Method (提煉函數(shù))

你有一段代碼可以被組織在一起并獨立出來。將這段代碼放進(jìn)一個獨立函數(shù)中讥邻,并讓函數(shù)名稱解釋該函數(shù)的用途迫靖。

image.png

為什么需要提煉函數(shù)呢? 首先兴使,如果每個函數(shù)的粒度都很小系宜,那么函數(shù)被復(fù)用的機會就更大;其次发魄,這會使高層函數(shù)讀起來就像一系列注釋盹牧;再次,如果函數(shù)都是細(xì)粒度励幼,那么函數(shù)的覆寫也會更容易些汰寓。

做法:

1、創(chuàng)造一個新函數(shù)苹粟,根據(jù)這個函數(shù)的意圖來對它命名(以它"做什么”來命名有滑, 而不是以它“怎樣做”命名)。
2嵌削、將提煉出的代碼從源函數(shù)復(fù)制到新建的目標(biāo)函數(shù)中毛好。
3、仔細(xì)檢査提煉出的代碼苛秕,看看其中是否引用了"作用域限于源函數(shù)”的變量
(包括局部變量和源函數(shù)參數(shù))肌访。
4、檢査被提煉代碼段艇劫,看看是否有任何局部變量的值被它改變吼驶。如果一個臨時 變量值被修改了,看看是否可以將被提煉代碼段處理為一個查詢店煞,并將結(jié)果值賦值給相關(guān)變量蟹演。

如果很難這樣做,或者如果被修改的變量不止一個浅缸,你就不能僅僅將這段代碼原封不動地提煉出來轨帜。你可能需要先使用Splite Temporary Variable (128),然后再嘗試提煉魄咕。也可以使用Temp with Query (120) 將臨時變量消滅掉衩椒。

5、將被提煉代碼段中需要讀取的局部變量,當(dāng)作參數(shù)傳給目標(biāo)函數(shù)毛萌。
6苟弛、處理完所有局部變量之后,進(jìn)行編譯阁将。
7膏秫、在源函數(shù)中,將被提煉代碼段替換為對目標(biāo)函數(shù)的調(diào)用做盅。
8缤削、如果你將任何臨時變量移到目標(biāo)函數(shù)中,請檢查它們原本的聲明式是否在被提煉代碼段的外圍吹榴。如果是亭敢,現(xiàn)在你可以刪除這些聲明式了。
9图筹、編譯帅刀,測試。

如果你發(fā)現(xiàn)源函數(shù)的參數(shù)被賦值远剩,應(yīng)該馬上使用Remove Assignments to Parameters (131)就是將入?yún)⒌木植孔兞吭儋x值給一個同類型的參數(shù)扣溺。

void method(Integer a, Integer b) {
    // Remove Assignments to Parameters
    Integer c = a; 
    c += 1;
}

特殊情況

被賦值的臨時變量也分兩種情況。較簡單的情況是:這個變量只在被提煉代碼段中使用瓜晤。果真如此锥余,你可以將這個臨時變量的聲明移到被提煉代碼段中,然后一 起提煉出去痢掠。另一種情況是:被提煉代碼段之外的代碼也使用了這個變量哈恰。這又分 為兩種情況:如果這個變量在被提煉代碼段之后未再被使用,你只需直接在目標(biāo)函數(shù)中修改它就可以了志群;如果被提煉代碼段之后的代碼還使用了這個變量着绷,你就需要讓目標(biāo)函數(shù)返回該變量改變后的值。

代碼示例:(帶有修改局部變量在賦值的情況)

void printowing() (
    Enumeration e = _orders.elements();
    double outstanding = 0.0;
    printBanner()锌云;
    // calculate outstanding while (e.hasMoreElements()) (
    Order each = (Order) e.nextElement();
     outstanding += each.getAmount()荠医;
)
printDetails(outstanding);
)
現(xiàn)在我把“計算”代碼提煉出來:
void printowing() (
    printBanner();
    double outstanding = getOutstanding(); 
    printDetails(outstanding)桑涎;
}

/** Enumeration變量e只在被提煉代碼段中用到彬向,所以可以將它整個搬到新函數(shù)
中。double變量outstanding在被提煉代碼段內(nèi)外都被用到攻冷,所以必須讓提煉出 來的新函數(shù)返回它娃胆。
*/
double getOutstanding() (
    Enumeration e = _orders.elements();
    double outstanding = 0.0;
    while (e.hasMoreElements()) (
          Order each = (Order) e.nextElement();
         outstanding += each.getAmount();
     }
    return outstanding等曼;
}

本例中的outstanding變量只是很單純地被初始化為一個明確初值里烦,所以我可 以只在新函數(shù)中對它初始化凿蒜。如果代碼還對這個變量做了其他處理,就必須將它的 值作為參數(shù)傳給目標(biāo)函數(shù)胁黑。

如果需要返回的變量不止一個废封,又該怎么辦呢???

有幾種選擇。最好的選擇通常是:挑選另一塊代碼來提煉丧蘸。我比較喜歡讓每個 函數(shù)都只返回一個值漂洋,所以會安排多個函數(shù),用以返回多個值力喷。如果你使用的語言 支持“出參數(shù)"(output parameter),可以使用它們帶回多個回傳值刽漂。但我還是盡可 能選擇單一返回值。

臨時變量往往為數(shù)眾多弟孟,甚至?xí)固釤捁ぷ髋e步維艱爽冕。這種情況下,我會嘗試 先運用Replace Temp with Query (120)減少臨時變量披蕉。如果即使這么做了提煉依舊困難重重颈畸,我就會動用Replace Method with Method Object (135),這個重構(gòu)手法不在乎 代碼中有多少臨時變量,"也不在乎你如何使用它們没讲。

二眯娱、Inline Method (內(nèi)聯(lián)函數(shù))

一個函數(shù)的本體與名稱同樣清楚易懂。在函數(shù)調(diào)用點插入函數(shù)本體爬凑,然后移除該函數(shù)徙缴。比起提煉函數(shù),內(nèi)聯(lián)函數(shù)的操作正好相反嘁信,

實例:

int getRatingf) {
return (moreThanFiveLateDeliveries()) ? 2 : 1;
}
boolean moreThanFiveLateDeliveries() (
    return _numberOfLateDeliveries > 5于样;
}
重構(gòu)后:
int getRating() {
    return (_numberOfLateDeliveries > 5) ? 2 : 1;
}

本重構(gòu)方法好像很簡單,但是為了以防萬一導(dǎo)致出現(xiàn)其他問題潘靖,還是羅列出具體的操作動作穿剖。

做法:

1、檢査函數(shù)卦溢,確定它不具多態(tài)性糊余。如果子類繼承了這個函數(shù),就不要將此函數(shù)內(nèi)聯(lián)单寂,因為子類無法覆寫一個 根本不存在的函數(shù)贬芥。
2、找出這個函數(shù)的所有被調(diào)用點宣决。
3蘸劈、將這個函數(shù)的所有被調(diào)用點都替換為函數(shù)本體。
4尊沸、編譯威沫,測試贤惯。
5、刪除該函數(shù)的定義壹甥。

三救巷、Inline Temp (內(nèi)聯(lián)臨時變量)

你有一個臨時變量壶熏,只被一個簡單表達(dá)式賦值一次句柠,而它妨礙了其他重構(gòu)手法。
將所有對該變量的引用動作棒假,替換為對它賦值的那個表達(dá)式自身溯职。

示例:

double basePrice = anOrder.basePrice();
return (basePrice > 1000)

重構(gòu)為:
return (anOrder.basePrice() > 1000)

Inline Temp (119)多半是作為Replace Temp with Query (120)的一部分使用的,所 以真正的動機出現(xiàn)在后者那兒帽哑。唯一單獨使用Inline Temp情況是:你發(fā)現(xiàn)某 個臨時變量被賦予某個函數(shù)調(diào)用的返回值谜酒。

實現(xiàn)步驟:

1、檢査給臨時變量賦值的語句妻枕,確保等號右邊的表達(dá)式?jīng)]有副作用僻族。
2、如果這個臨時變量并未被聲明為final,那就將它聲明為final,然后編譯屡谐。=?這可以檢查該臨時變量是否真的只被賦值一次述么。
3、找到該臨時變量的所有引用點愕掏,將它們替換為“為臨時變量賦值”的表達(dá)式度秘。
4、每次修改后饵撑,編譯并測試剑梳。
5、修改完所有引用點之后滑潘,刪除該臨時變量的聲明和賦值語句垢乙。
6、編譯语卤,測試侨赡。

四、Replace Temp with Query (以查詢?nèi)〈R時變量)

你的程序以一個臨時變量保存某一表達(dá)式的運算結(jié)果粱侣。
將這個表達(dá)式提煉到一個獨立函數(shù)中羊壹。將這個臨時變量的所有引用點替換為對新函數(shù)的調(diào)用。此后齐婴,新函數(shù)就可被其他函數(shù)使用油猫。

double basePrice = .quantity * _itemPrice; 

if (basePrice > 1000)
    return basePrice * 0.95; 
else
    return basePrice * 0.98;

重構(gòu)后:
if (basePrice() > 1000)
    return basePrice() * 0.95;
else
    return basePrice() * 0.98;

double basePrice() {
    return _quantity * _itemPrice柠偶;
}

這樣實現(xiàn)的動機為:

臨時變量的問題在于:它們是暫時的情妖,而且只能在所屬函數(shù)內(nèi)使用睬关。由于臨時 變量只在所屬函數(shù)內(nèi)可見,所以它們會驅(qū)使你寫出更長的函數(shù)毡证,因為只有這樣你才 能訪問到需要的臨時變量电爹。如果把臨時變量替換為一個査詢,那么同一個類中的所 有函數(shù)都將可以獲得這份信息料睛。這將帶給你極大幫助丐箩,使你能夠為這個類編寫更清 晰的代碼。

這個重構(gòu)手法較為簡單的情況是:臨時變量只被賦值一次恤煞,或者賦值給臨時變 量的表達(dá)式不受其他條件影響屎勘。其他情況比較棘手,但也有可能發(fā)生居扒。你可能需要 先運用Splite Temporary Variable (128分解臨時變量為多個)或 Separate Query from Modifier (279將查詢函數(shù)和修改函數(shù)分離) 使情況變得簡單一些概漱,然后再替換臨時變量。如果你想替換的臨時變量是用來收集結(jié)果的(例如循環(huán)中的累加值)喜喂,就需要將某些程序邏輯(例如循環(huán))復(fù)制到查詢函數(shù)去瓤摧。

做法:

1、找出只被賦值一次的臨時變量玉吁。
2照弥、今如果某個臨時變量被賦值超過一次,考慮使用splite Temporary Variable 將它分割成多個變量诈茧。
3产喉、將該臨時變量聲明為final。-》確保改變量只被賦值一次
4敢会、編譯曾沈。

5、將"對該臨時變量賦值”之語句的等號右側(cè)部分提煉到一個獨立函數(shù)中鸥昏。

5.1 首先將函數(shù)聲明為private.日后你可能會發(fā)現(xiàn)有更多類需要使用它塞俱,那時放松對它的保護(hù)也很容易.
5.2 確保提煉出來的函數(shù)無任何副作用,也就是說該函數(shù)并不修改任何對象內(nèi) 容吏垮。如果它有副作用障涯,就對它進(jìn)行 Separate Query from Modifier (279).

6、編譯膳汪,測試唯蝶。
7、在該臨時變量身上實現(xiàn) Inline Temp 遗嗽。

我們常常使用臨時變量保存循環(huán)中的累加信息粘我。在這種情況下,整個循環(huán)都可 以被提煉為一個獨立函數(shù)痹换,這也使原本的函數(shù)可以少掉幾行擾人的循環(huán)邏輯征字。有時 候都弹,你可能會在一個循環(huán)中累加好幾個值,就像本書第26頁的例子那樣匙姜。這種情況下你應(yīng)該針對每個累加值重復(fù)一遍循環(huán)畅厢,這樣就可以將所有臨時變量都替換為查詢。

示例如下:

開始的實例:

首先氮昧,我從一個簡單函數(shù)開始:
double getPrice() (
int basePrice = _quantity * _itemPrice框杜;
double discountFactor;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor郭计;
}
我希望將兩個臨時變量都替換掉霸琴。
先把臨時變量聲明為final,檢查它們是否的確只被賦值一次.
double getPrice() {
final int basePrice = _quantity * _itemPrice;
final double discountFactor椒振;
if (basePrice > 1000) discountFactor = 0.95;
else discountFactor = 0.98;
return basePrice * discountFactor昭伸;
}
如果有任何問題,編譯器就會警告我澎迎。之所以先做這件事庐杨,因為如 果臨時變量不只被賦值一次,我就不該進(jìn)行這項重構(gòu)夹供。

重構(gòu)后:

private int basePrice() (
return _quantity * _itemPrice灵份;
}

private double discountFactor() (
if (basePrice() > 1000) return 0.95;
else return 0.98哮洽;
}

最終填渠,getPrice ()變成了這樣:

double getPrice() {
return basePrice() * discountFactor();
}

五鸟辅、 Introduce Explaining Variable (引入解釋性變量)

你有一個復(fù)雜的表達(dá)式氛什。將該復(fù)雜表達(dá)式(或其中一部分)的結(jié)果放進(jìn)一個臨時變量,以此變量名稱來解釋表達(dá)式用途匪凉。

if ( (platform. toUpperCase () . indexOf () > -1) && (browser.toUpperCase().indexOf(nIE") > -1) && waslnitialized() && resize > 0)
{
// do something
}

重構(gòu)后:
final boolean isMacOs = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrowser = browser. toUpperCase () . indexOf ('* IE") > -1; final boolean wasResized = resize > 0;
if (isMacOs && isIEBrowser && waslnitialized().&& wasResized) (
//do something
}

動機:緣由

表達(dá)式有可能非常復(fù)雜而難以閱讀枪眉。這種情況下,臨時變量可以幫助你將表達(dá) 式分解為比較容易管理的形式再层。你可以用這項 重構(gòu)將每個條件子句提煉出來贸铜,以一個良好命名的臨時變量來解釋對應(yīng)條件子句的 意義。代碼可讀性強聂受。

做法

做法
1蒿秦、聲明一個final臨時變量,將待分解之復(fù)雜表達(dá)式中的一部分動作的運算結(jié)果賦值給它蛋济。
2棍鳖、將表達(dá)式中的“運算結(jié)果”這一部分,替換為上述臨時變量瘫俊。
3鹊杖、如果被替換的這一部分在代碼中重復(fù)出現(xiàn)悴灵,你可以毎次一個,逐一替換骂蓖。
4积瞒、編譯,測試登下。
5茫孔、重復(fù)上述過程,處理表達(dá)式的其他部分被芳。

示例

我們從一個簡單計算開始:
double price() (
    // price is base price - quantity discount + shipping 
    return .quantity *  _itemPrice  
                      -Math.max(0, _quantity - 500) * _itemPrice * 0.05 
                      +Math.min(_quantity * _itemPrice * 0.1, 100.0);
}
這段代碼還算簡單缰贝,不過我可以讓它變得更容易理解。首先我發(fā)現(xiàn)畔濒,底價(base price)等于數(shù)量(quantity)乘以單價(item price)o, 于是剩晴,我把這一部分計算的結(jié) 果放進(jìn)一個臨時變量中:

稍后也用上了 '‘?dāng)?shù)量乘以單價"運算結(jié)果,所以我同樣將它替換為basePrice 臨時變量.

批發(fā)折扣(quantity discount)的計算提煉出來侵状,將結(jié)果賦予臨時變量.

最后赞弥,我再把運費(shipping)計算提煉出來,將運算結(jié)果賦予臨時變量 shipping趣兄。同時我還可以刪掉代碼中的注釋绽左,因為現(xiàn)在代碼已經(jīng)可以完美表達(dá)自 己的意義了:

重構(gòu)后:
double price() (
    final double basePrice = _quantity * _itemPrice;
    final double quantityDiscount = Math.rnax(Or _quantity - 500)* _itemPrice * 0.05; final double shipping = Math.min(basePrice * 0.1, 100.0);
    return basePrice - quantityDiscount + shipping;
}

使用 Extract Method處理上述范例
面對上述代碼艇潭,我通常不會以臨時變量來解釋其動作意圖拼窥,我更喜歡使用 Extract Method(110)。

這一次我把底價計算提煉到一個獨立函數(shù)中蹋凝,批發(fā)折扣(quantity discount)的計算提煉出來鲁纠,運費(shipping)計算提煉出來。

最終可以得到如下的代碼:

double price() (
     return basePrice() - quantityDiscount() + shipping()仙粱;
)

// 一開始我會把這些新函數(shù)聲明為private房交; 如果其他對象也需要它們,我可以輕易釋放這些函數(shù)的訪問限制伐割。
private double quantityDiscount() (
    return Math.max(0, .quantity - 500) * _itemPrice * 0.05候味;
)
private double shipping() (
    return Math.min(basePrice() * 0.1, 100.0);
}
private double basePrice() (
    return ^quantity * _itemPrice隔心;
}


說明:那么白群,應(yīng)該在什么時候使用Introduce Explaining Variable (124)呢?答案是:在 Extract Method (110)需要花費更大工作量時硬霍。如果我要處理的是一個擁有大量局部變量的算法帜慢,那么使用Extract Method (110)絕非易事。這種情況下就會使用Introduce Explaining Variable (124)來理清代碼,然后再考慮下一步該怎么辦粱玲。搞清楚代碼邏 輯之后躬柬,我總是可以運用Replace Temp with Query (120)把中間引入的那些解釋性臨 時變量去掉。況且抽减,如果我最終使用Replace Method with Method Object (135)>那么 中間引入的那些解釋性臨時變量也有其價值允青。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市卵沉,隨后出現(xiàn)的幾起案子颠锉,更是在濱河造成了極大的恐慌,老刑警劉巖史汗,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件琼掠,死亡現(xiàn)場離奇詭異,居然都是意外死亡停撞,警方通過查閱死者的電腦和手機瓷蛙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來怜森,“玉大人速挑,你說我怎么就攤上這事谤牡「惫瑁” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵翅萤,是天一觀的道長恐疲。 經(jīng)常有香客問我,道長套么,這世上最難降的妖魔是什么培己? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮胚泌,結(jié)果婚禮上省咨,老公的妹妹穿的比我還像新娘。我一直安慰自己玷室,他們只是感情好零蓉,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著穷缤,像睡著了一般敌蜂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上津肛,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天章喉,我揣著相機與錄音,去河邊找鬼。 笑死秸脱,一個胖子當(dāng)著我的面吹牛落包,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播摊唇,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼妥色,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了遏片?” 一聲冷哼從身側(cè)響起嘹害,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎吮便,沒想到半個月后笔呀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡髓需,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年许师,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片僚匆。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡微渠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咧擂,到底是詐尸還是另有隱情逞盆,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布松申,位于F島的核電站云芦,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏贸桶。R本人自食惡果不足惜舅逸,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望皇筛。 院中可真熱鬧琉历,春花似錦、人聲如沸水醋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽离例。三九已至换团,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間宫蛆,已是汗流浹背艘包。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工的猛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人想虎。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓卦尊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舌厨。 傳聞我的和親對象是個殘疾皇子岂却,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容