iOS代碼耦合的處理

耦合是每個(gè)程序員都必須面對的話題犀填,也是容易被忽視的存在,怎么處理耦合關(guān)系到我們最后的代碼質(zhì)量。今天Peak君和大家聊聊耦合這個(gè)基本功話題苛蒲,一起捋一捋iOS代碼中處理耦合的種種方式及差異爽丹。

簡化場景

耦合的話題可大可小筑煮,但原理都是相通的。為了方便討論粤蝎,我們先將場景進(jìn)行抽象和簡化真仲,只討論兩個(gè)類之間的耦合。

假設(shè)我們有個(gè)類Person初澎,需要喝水秸应,根據(jù)職責(zé)劃分,我們需要另一個(gè)類Cup來完成喝水的動(dòng)作碑宴,代碼如下:

//Person.h
@interface Person : NSObject
- (void)drink;
@end
 
//Cup.h
@interface Cup : NSObject
- (id)provideWater;
@end

很明顯软啼,Person和Cup之間要配合完成喝水的動(dòng)作,是無論如何都會(huì)產(chǎn)生耦合的延柠,我們來看看在Objective C下都有哪些耦合的方式祸挪,以及不同耦合方式對以后代碼質(zhì)量變化的影響。

方式一:.m引用

這種方式直接在.m文件中導(dǎo)入Cup.h捕仔,同時(shí)生成臨時(shí)的Cup對象來調(diào)用Cup中的方法匕积。代碼如下:

#import "Person.h"
#import "Cup.h"

@implementation Person

- (void)drink {
    Cup* c = [Cup new];
    
    id water = [c provideWater];
    [self sip:water];
}

- (void)sip:(id)water
{
    //sip water
}

@end

這應(yīng)該是不少同學(xué)會(huì)選擇的做法,要用到某個(gè)類的功能榜跌,就import該類闪唆,再調(diào)用方法,功能完成提交測試一氣呵成钓葫。

這種方式初看起來沒什么毛病悄蕾,但有個(gè)弊端:Person與Cup的耦合被埋進(jìn)了Person.m文件的方法實(shí)現(xiàn)中,而.m文件一般都是業(yè)務(wù)邏輯代碼的重災(zāi)區(qū)础浮,當(dāng)Person.m的代碼量膨脹之后帆调,如果Person類交由另一位工程師來維護(hù),那這位新接手的同學(xué)無法從Person.h中一眼看出Person類和哪些類之間有交互豆同,即使在Person.m中看drink的聲明也沒有任何線索番刊,要理清楚的話,只能把Person.m文件從頭到尾讀一遍影锈,對團(tuán)隊(duì)效率的影響可想而知芹务。

方式二:.h Property

既然直接在.m中引用會(huì)導(dǎo)致耦合不清晰蝉绷,我們可以將耦合的部分放入Property中,代碼如下:

//Person.h
@interface Person : NSObject
@property (nonatomic, strong) Cup*                 cup;
- (void)drink;
@end
  
//Person.m
@implementation Person
- (void)drink {
    id water = [self.cup provideWater];
    [self sip:water];
}

- (void)sip:(id)water
{
    //sip water
}
@end

這樣枣抱,我們只需要掃一眼Person.h就能明白熔吗,Person類對哪些類產(chǎn)生了依賴,比直接在.m中引用清晰多了佳晶。

不知道大家有沒有好奇過桅狠,為什么在Objective C中會(huì)有.h文件的存在,為什么不像Java轿秧,Swift一樣一個(gè)文件代表一個(gè)類中跌?使用.h文件有利有弊。

.h文件最大的意義在于將聲明實(shí)現(xiàn)相隔離淤刃。聲明是告訴外部我支持哪些功能晒他,實(shí)現(xiàn)是支撐這些功能背后的代碼邏輯。在我們閱讀一個(gè)類的.h文件的時(shí)候逸贾,它最主要的作用是透露兩個(gè)信息:

  • 我(Person類)依賴了哪些外部元素
  • 我(Person類)提供哪些接口供外部調(diào)用

所以.h文件應(yīng)該是我們代碼耦合的關(guān)鍵所在陨仅,當(dāng)我們猶豫一個(gè)類的Property要不要放到.h文件中去聲明時(shí),要思考這個(gè)Property是不是必須暴露給外部铝侵。一旦暴露到.h文件中灼伤,就增加了依賴和耦合的幾率。有時(shí)候Review代碼咪鲜,只要看.h文件是否清晰狐赡,就大概能猜測這個(gè)類設(shè)計(jì)者的水平。

當(dāng)我們把Cup類做為Person的Property聲明時(shí)疟丙,就表明Person與Cup之間存在必要的依賴颖侄,我們把這種依賴放到頭文件中來,起到一目了然的效果享郊。這比方式一清晰了不少览祖,但有另一個(gè)問題,Cup暴露出去以后炊琉,外部元素可以隨意修改展蒂,當(dāng)內(nèi)部執(zhí)行drink的時(shí)候,可能另一個(gè)線程將cup置空了苔咪,影響正常的業(yè)務(wù)流程锰悼。

方式三:.h ReadOnly Property

方式二中,Person類在對Cup產(chǎn)生依賴的同時(shí)团赏,也承擔(dān)了cup隨時(shí)被外部修改的風(fēng)險(xiǎn)箕般。當(dāng)然做直觀的做法是將Cup類作為ReadOnly的property,同時(shí)提供一個(gè)對外的setter:

//Person.h
@interface Person : NSObject
@property (nonatomic, strong, readonly) Cup*                 cup;
- (void)setPersonCup:(Cup*)cup;
- (void)drink;
@end

有同學(xué)可能會(huì)問舔清,這和上面的做法有什么區(qū)別丝里,不一樣都有讀寫的接口嗎可柿?最大的區(qū)別是增加了檢查和干擾的入口。

當(dāng)我Debug的時(shí)候丙者,經(jīng)常需要檢查某個(gè)Propery到底是被誰修改了,Setter中設(shè)置一個(gè)斷點(diǎn)調(diào)試起來方便不少营密。同時(shí)械媒,我們還可以使用Xcode的Caller機(jī)制,查看當(dāng)前Setter都被那些外部類調(diào)用了评汰,分析類與類之間的關(guān)聯(lián)是很有幫助纷捞。

Person.m中Setter方法還提供了我們拓展功能的入口,比如我們需要在Setter中增加多線程同步Lock被去,當(dāng)Person.m中的其他方法在使用Cup時(shí)主儡,Setter必須等待完成才能執(zhí)行。又比如我們可以在Setter中實(shí)現(xiàn)Copy On Write機(jī)制:

//Person.m
- (void)setPersonCup:(Cup*)cup {
    Cup* anotherCup = [cup copy];
    _cup = anotherCup;
}

這樣惨缆,Person類就可以避免和外部類共享同一個(gè)Cup糜值,杜絕使用同一個(gè)水杯的衛(wèi)生問題 ;)

總之,單獨(dú)的Setter方法讓我們對代碼有更大的掌控能力坯墨,也為后續(xù)接手維護(hù)你代碼的同學(xué)帶來了方便寂汇,利己利人。

方式四:init 注入

使用帶Setter的Property雖然看上去好了不少捣染,但Setter方法可以被任意外部類隨時(shí)隨刻調(diào)用骄瓣,對于Person.m中使用Cup的方法來說,多少有些不安心耍攘,萬一用著用著被別人改了呢榕栏?

為了避免被隨意修改,我們可以采用init注入的方式蕾各,Objective C中的designated initializer正是為此而生:

//Person.h
@interface Person : NSObject
- (instancetype)initWithCup:(Cup*)cup;
- (void)drink;
@end

去掉Property扒磁,將Cup的設(shè)置放入init方法中,這樣Person類對外就只提供一次機(jī)會(huì)來設(shè)置Cup示损,init之后渗磅,外部類就沒有其他機(jī)會(huì)來修改Cup了。

這是使用最多检访,也是比較推薦的方式始鱼。只在對象被創(chuàng)建的時(shí)候,去建立與其他對象的關(guān)系脆贵,把可變性降低到一定程度医清。那這種方式是否也有什么缺點(diǎn)呢?

通過init的方式設(shè)置cup卖氨,杜絕了外部因素的影響会烙,但如果內(nèi)部持有了cup對象负懦,那么內(nèi)部的函數(shù)調(diào)用依然可以通過各種姿勢與Cup類產(chǎn)生耦合,比如:

//Person.m
@interface Person ()
@property (nonatomic, strong) Cup*                 myCup;
@end

@implementation Person
- (instancetype)initWithCup:(Cup*)cup {
    self = [super init];
    if (self) {
        self.myCup = cup;
    }
    return self;
}

- (void)drinkWater {
    id water = [self.myCup provideWater];
    [self sip:water];
}

- (void)drinkMilk {
    id milk = [self.myCup provideMilk];
    [self sip:milk];
}

@end

Person內(nèi)部的方法可以通過Cup所有對外的接口來產(chǎn)生耦合柏腻,此時(shí)我們對于兩個(gè)類之間的耦合纸厉,就主要靠對Cup.h頭文件來解讀了。如果Cup類設(shè)計(jì)合理五嫂,頭文件結(jié)構(gòu)清晰的話颗品,這其實(shí)不算太糟糕的場景。那還有沒有其他方式呢沃缘?

方式五:parameter 注入

用Property持有的方式躯枢,在Person對象的整個(gè)生命周期內(nèi),耦合的可能性一直存在槐臀,原因在于Property對于.m文件來說是全局可見的锄蹂。我們可以用另一種方式讓耦合只發(fā)生在單個(gè)方法內(nèi)部,即parameter injection:

//Person.h
@interface Person : NSObject
- (void)drink:(Cup*)cup;
@end
  
//Person.m
- (void)drink:(Cup*)cup {
    id water = [cup provideWater];
    [self sip:water];
}

這種方式的好處在于:Person和Cup的耦合只發(fā)生在drink函數(shù)的內(nèi)部水慨,一旦函數(shù)調(diào)用結(jié)束得糜,Person和Cup之間就結(jié)束了依賴關(guān)系。從時(shí)間和空間的跨度上來說晰洒,這種方式比持有Property風(fēng)險(xiǎn)更小掀亩。

可要是在Person中存在多處Cup的依賴,比如有drinkWater,drinkMilk,drinkCoffee等等欢顷,反而又不如Property直觀方便了槽棍。

方式六:單例引用

單例的優(yōu)劣有很多優(yōu)秀的技術(shù)文章分析過了,Peak君只強(qiáng)調(diào)其中一點(diǎn)抬驴,也是平時(shí)review代碼和Debug發(fā)現(xiàn)最多的問題緣由:單例中的狀態(tài)共享炼七。

上面的例子中,我們可以把Cup做成單例布持,代碼如下:

//Person.m
- (void)drink {
    id water = [[Cup sharedInstance] provideWater];
    [self sip:water];
}

這種方式產(chǎn)生的耦合不但和方式一同樣隱蔽豌拙,而且是最容易導(dǎo)致代碼降級(jí)的,隨著版本的不停迭代题暖,我們很有可能會(huì)得到下面的一個(gè)類關(guān)聯(lián)圖:

所有的對象都依賴于同一個(gè)對象的狀態(tài)按傅,所有的對象都對這個(gè)對象的狀態(tài)擁有讀寫權(quán)限,最后的結(jié)果很有可能是到處打補(bǔ)丁修Bug胧卤,按下葫蘆浮起瓢唯绍。

使用單例類似的場景很常見,比如我們在單例中持有某個(gè)用戶的信息枝誊,在用戶登出之后况芒,忘記清除之前用戶的信息就會(huì)導(dǎo)致奇怪的bug,而且單例一旦零散的分布在項(xiàng)目的各個(gè)角落叶撒,要逐一處理十分困難绝骚。

方式七:繼承

繼承是一種強(qiáng)耦合關(guān)系耐版,網(wǎng)絡(luò)上有不少關(guān)于繼承(inheritance)和組合(compoisition)之間優(yōu)劣的對比文章了,這里不做贅述压汪。繼承確實(shí)能在初期很方便的建立清晰的對象模型粪牲,重用和多態(tài)看著也很美妙,問題在于這種強(qiáng)耦合關(guān)系在理解上很容易產(chǎn)生分歧止剖,比如什么樣對象之間可以被確立為父子關(guān)系虑瀑,哪些子類的行為可以放到父類中給其他子類使用,在多層繼承的時(shí)候這些問題會(huì)變得更加復(fù)雜滴须。所以Peak君建議盡可能的少用繼承關(guān)系來描述對象,除非是一目了然毫無異議的父子關(guān)系叽奥。

我就不強(qiáng)行來一波父類定義來舉例了扔水,比如什么ObjectWithCup這類。

方式八:runtime依賴

使用runtime來處理耦合是Objective C獨(dú)特的方式朝氓,而且耦合度非常之低魔市,甚至可以說感覺不到耦合的存在,比如:

//Person.m
- (void)drink:(id)obj
{
    id water = nil;
    SEL sel = NSSelectorFromString(@"provideWater");
    if ([obj respondsToSelector:sel]) {
        water = [obj performSelector:sel];
    }
    if (water) {
      [self sip:water];
    }
}

既不需要導(dǎo)入Cup的頭文件赵哲,也不需要知道Cup到底支持哪些方法待德。這種方式的問題也正是由于耦合度太低了,讓開發(fā)者感知不到耦合的存在枫夺,感知不到類之間的關(guān)系将宪。如果哪天有人把provideWater改寫成getWater,drink方法如果沒有同步到橡庞,Xcode編譯時(shí)不會(huì)提示你较坛,runtime也不會(huì)crash,但是業(yè)務(wù)流程卻沒有正常往下走了扒最。

這也是為什么我們不推薦用Objective-C runtime的黑魔法去做業(yè)務(wù)丑勤,只是在無副作用的場景下去完成一些數(shù)據(jù)的獲取操作,比如使用AOP去log日志吧趣。

方式九:protocol依賴

這并不是一種獨(dú)立的耦合方式法竞,protocol可以結(jié)合上述各種耦合方式來進(jìn)一步降低耦合,也是在復(fù)雜類關(guān)系設(shè)計(jì)中推薦的方式强挫,比如我們可以定義這樣一個(gè)protocol:

@protocol LiquidContainer <NSObject>

- (id)provideWater;
- (id)provideCoffee;

@end
  
//Person.h
@interface Person : NSObject

- (void)drink:(id<LiquidContainer>)container;

@end

上述的方式中岔霸,無論是Property持有還是parameter注入,都可以使用protocol來降低依賴俯渤,protocol的好處在于他只規(guī)定了方法的聲明秉剑,并不限定具體是那個(gè)類來實(shí)現(xiàn)它,給后期的維護(hù)留下更大的空間和可能性稠诲。有關(guān)protocol的用處和重要性可以單獨(dú)開一篇文章來講侦鹏。

更復(fù)雜的場景

以上是一些常見的類耦合方式诡曙,描述的兩個(gè)類A,B之間的耦合方式略水。從上面的描述中价卤,我們可以大致感知到兩個(gè)類使用不同的方式所導(dǎo)致的耦合的深淺,這種耦合深淺度說白了就是:互相調(diào)用函數(shù)和訪問狀態(tài)的頻次渊涝。理解這種耦的深淺可以幫助我們大致去量化兩個(gè)對象之間的耦合度慎璧,從而在更復(fù)雜的場景中去分析一個(gè)模塊或者一種架構(gòu)方式的耦合度。

在更復(fù)雜的場景中跨释,比如A胸私,B,C三個(gè)類之間也可以采用類似的方法去分析鳖谈,A岁疼,B,C三者可以是如下關(guān)系:

分析三個(gè)類或者更多類之間的耦合關(guān)系的時(shí)候缆娃,也是先拆解成若干個(gè)兩個(gè)類分析捷绒,比如左邊我們分析AB,BC贯要,AC三組耦合暖侨,進(jìn)而去感知ABC作為一個(gè)整體的耦合度。很顯然崇渗,右邊的方式看著比左邊的好字逗,因?yàn)橹恍枰治鯝B和BC。在我們選用設(shè)計(jì)模式重構(gòu)代碼的時(shí)候宅广,也可以依照類似的方式來分析扳肛,從而選擇耦合度最低,最貼合我們業(yè)務(wù)場景的模式乘碑。

我們的原則是:類與類之間調(diào)用的方法挖息,依賴的狀態(tài)要越少越好,在Objective C這門語言環(huán)境下兽肤,書寫分類清晰套腹,接口簡潔的頭文件非常重要。

良性的耦合

前面的分析重在嘗試去量化和感知耦合的深淺资铡,但并不是每一次方法調(diào)用都是有風(fēng)險(xiǎn)的电禀,有些耦合可以稱作是良性的。

如果將我們的代碼進(jìn)行高度抽象笤休,所有的代碼都可以被歸為兩類:Data和Action尖飞。一個(gè)Class中的Property是Data,而Class中的函數(shù)則是Action,我之前寫過的一篇關(guān)于函數(shù)式的文章中提到過政基,真正讓我們代碼變得危險(xiǎn)的是狀態(tài)的變化贞铣,即改變Data。如果一個(gè)函數(shù)是純函數(shù)沮明,既不依賴于外部狀態(tài)辕坝,也不修改外部狀態(tài),那么這個(gè)函數(shù)無論被調(diào)用多少次都是安全的荐健。如果兩個(gè)類酱畅,比如上面舉例的Person和Cup,二者互相調(diào)用的都是純函數(shù)江场,那么二者之間的耦合可以看做是良性的纺酸,并不會(huì)導(dǎo)致程序的狀態(tài)維護(hù)混亂,只是會(huì)讓代碼的重構(gòu)變得困難址否,畢竟耦合的越深餐蔬,重構(gòu)改動(dòng)的代碼就越多。

所以我們在做設(shè)計(jì)的時(shí)候在张,應(yīng)該盡可能使不同元素之間的耦合是良性的,這就涉及到狀態(tài)的維護(hù)問題矮慕,先看下圖中兩種不同的設(shè)計(jì)方式:

圖中紅色的圓圈代表每個(gè)類或者功能單位所持有的狀態(tài)帮匾。依照圖中上方的設(shè)計(jì)方式,每個(gè)單位各自處理自己的狀態(tài)變化痴鳄,這些狀態(tài)之間還互相存在依賴的話瘟斜,耦合越深,開發(fā)調(diào)試和重構(gòu)就越難痪寻,代碼就降級(jí)越厲害螺句。如果按照圖中下方的方式,將狀態(tài)變化的部分全部都集中到一起處理橡类,維護(hù)起來就輕松很多了蛇尚,這也是為什么很多App都有model layer這一設(shè)計(jì)的原因,將App狀態(tài)(各類model)的變化處理獨(dú)立出來作為一個(gè)layer顾画,上層(業(yè)務(wù)層)只是作為model layer的展現(xiàn)和交互的外殼取劫。這種設(shè)計(jì)技巧,大可以應(yīng)用于一個(gè)App架構(gòu)的處理研侣,小可以到一個(gè)小功能模塊的設(shè)計(jì)谱邪。

結(jié)束語

上面總結(jié)了我們常用的一些耦合方式,目的在于分析不同代碼的書寫方式庶诡,對于我們最后耦合所產(chǎn)生的影響惦银。最后值得一提的是,上面有些耦合方式并沒有絕對的優(yōu)劣之分,不同的業(yè)務(wù)場景下可能選擇的方式也不同扯俱,比如有些場景確實(shí)需要持有Property书蚪,有些場景單例更合適,關(guān)鍵在于我們能明白不同方式對于我們代碼后期維護(hù)所產(chǎn)生的影響蘸吓,這篇文章有些地方可能比較抽象善炫,其中很多都是個(gè)人感悟和總結(jié),或有不妥之處库继,請閱讀之后選擇性的吸收箩艺,希望能對大家平常寫代碼處理耦合帶來一些幫助。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宪萄,一起剝皮案震驚了整個(gè)濱河市艺谆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拜英,老刑警劉巖静汤,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異居凶,居然都是意外死亡虫给,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門侠碧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抹估,“玉大人,你說我怎么就攤上這事弄兜∫撸” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵替饿,是天一觀的道長语泽。 經(jīng)常有香客問我,道長视卢,這世上最難降的妖魔是什么踱卵? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮据过,結(jié)果婚禮上颊埃,老公的妹妹穿的比我還像新娘。我一直安慰自己蝶俱,他們只是感情好班利,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著榨呆,像睡著了一般罗标。 火紅的嫁衣襯著肌膚如雪庸队。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天闯割,我揣著相機(jī)與錄音彻消,去河邊找鬼。 笑死宙拉,一個(gè)胖子當(dāng)著我的面吹牛宾尚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谢澈,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼煌贴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了锥忿?” 一聲冷哼從身側(cè)響起牛郑,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎敬鬓,沒想到半個(gè)月后淹朋,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钉答,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年础芍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片数尿。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡仑性,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出砌创,到底是詐尸還是另有隱情虏缸,我是刑警寧澤鲫懒,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布嫩实,位于F島的核電站,受9級(jí)特大地震影響窥岩,放射性物質(zhì)發(fā)生泄漏甲献。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一颂翼、第九天 我趴在偏房一處隱蔽的房頂上張望晃洒。 院中可真熱鬧,春花似錦朦乏、人聲如沸球及。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽吃引。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間镊尺,已是汗流浹背朦佩。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留庐氮,地道東北人语稠。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像弄砍,于是被迫代替她去往敵國和親仙畦。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

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