實(shí)戰(zhàn)組合式設(shè)計(jì)

需求

leetcode 上有一道關(guān)于強(qiáng)密碼校驗(yàn)器的練習(xí)題,如下所述:

一個(gè)強(qiáng)密碼應(yīng)滿足以下所有條件:

  • 由至少6個(gè)气笙,至多20個(gè)字符組成祸轮。
  • 至少包含一個(gè)小寫字母黔姜,一個(gè)大寫字母,和一個(gè)數(shù)字晦款。
  • 同一字符不能連續(xù)出現(xiàn)三次 (比如 "...aaa..." 是不允許的, 但是 "...aa...a..." 是可以的)贷洲。

編寫函數(shù) strongPasswordChecker(s)筒饰,s 代表輸入字符串,如果 s 已經(jīng)符合強(qiáng)密碼條件,則返回0骂际;否則返回要將 s 修改為滿足強(qiáng)密碼條件的字符串所需要進(jìn)行修改的最小步數(shù)(插入疗琉、刪除、替換任一字符都算作一次修改)歉铝。

需求分析

需求中有兩個(gè)要點(diǎn):

  • 強(qiáng)密碼考慮了三個(gè)維度:長度盈简、連續(xù)性、字符種類
  • 允許使用三種修改方法:插入犯戏、刪除送火、替換

我們下面使用表格來分析一下這幾種修改方法對(duì)這幾個(gè)緯度的影響:

修改方法 長度 字符種類 連續(xù)性
插入 +1 +1 (if < 3) 連續(xù)性長度-2
刪除 -1 不影響 連續(xù)性長度-1
(連續(xù)性長度%3的值越小優(yōu)先級(jí)越高)
替換 不影響 +1 (if < 3) 連續(xù)性長度-3

說明如下:

  • 修改長度:對(duì)長度的修改是確定的(長了就刪除,短了就插入)先匪,但是要注意插入和刪除也會(huì)修改連續(xù)性和字符種類种吸,所以要先讓密碼滿足長度的要求,然后使用替換來改進(jìn)連續(xù)性和字符種類
  • 修改字符種類:在修改長度和修改連續(xù)性后呀非,如果字符種類還不是3坚俗,則應(yīng)該通過替換增加字符種類
  • 修改連續(xù)性:對(duì)于插入來說,密碼長度肯定小于6岸裙,那么只可能有一個(gè)連續(xù)性字符子串(3<=子串長度<=5)猖败,而插入一個(gè)字符會(huì)使得該子串長度減2,這時(shí)要么已沒有連續(xù)性字符子串降允,要么還有一個(gè)長度為3的連續(xù)性字符子串(再做一次替換即可)恩闻;對(duì)于刪除來說,密碼長度肯定大于20剧董,那么連續(xù)性字符子串可能有多個(gè)幢尚,這時(shí)應(yīng)該選擇子串長度模3的值最小的子串,然后將該子串長度減1

軟件設(shè)計(jì)

組合式設(shè)計(jì)概述

七巧板翅楼,是大家熟知的一種源自中國的古老智力游戲:

七巧板.png

由這么七塊簡單的小素材尉剩,可以拼出變化無窮的圖案,受限的只是你的想象力:


靈活的七巧板組合形狀.png

這個(gè)簡單的游戲毅臊,蘊(yùn)含這一種極具價(jià)值的設(shè)計(jì)思想:組合理茎。

基本語義規(guī)則都是小素材,基本語義規(guī)則通過組合形成新的對(duì)用戶有價(jià)值的語義規(guī)則就是圖案管嬉。對(duì)于強(qiáng)密碼校驗(yàn)器這個(gè)需求來說皂林,要實(shí)踐組合式設(shè)計(jì),首先要拆出小素材宠蚂,然后再將小素材層層組合成強(qiáng)密碼校驗(yàn)器圖案式撼。其實(shí),軟件設(shè)計(jì)就是一門研究系統(tǒng)“如何分”和“怎么合”的學(xué)問求厕,目的是為了在讓軟件在長期范圍內(nèi)容易應(yīng)對(duì)變化著隆,而組合式設(shè)計(jì)就是將系統(tǒng)正交拆分且低成本組合的最佳軟件設(shè)計(jì)實(shí)踐扰楼。

于是,問題來了美浦,系統(tǒng)究竟該如何分弦赖?在回答這個(gè)問題之前,我們先看看什么是好的軟件設(shè)計(jì)浦辨。

我們?cè)谲浖_發(fā)中蹬竖,經(jīng)常隨口就說簡單設(shè)計(jì),但簡單設(shè)計(jì)不是一句語義模糊的無法評(píng)判的空洞口號(hào)流酬,而是具有明確的衡量標(biāo)尺币厕。
關(guān)于這個(gè)標(biāo)尺的定義,Kent Beck 給出了清晰的答案:

  • 通過所有測(cè)試(Passes all tests)
  • 盡可能消除重復(fù) (Minimizes duplication)
  • 盡可能清晰表達(dá) (Maximizes clarity)
  • 更少代碼元素 (Has fewer elements)

以上四個(gè)原則的重要程度依次降低芽腾。

這就是簡單設(shè)計(jì)四原則旦装,只有第四條強(qiáng)調(diào)簡單,其他三條分別從需求摊滔、易修改性和可理解性方面對(duì)簡單進(jìn)行了約束:

  • 你不能為了簡單而不去實(shí)現(xiàn)客戶需求
  • 你不能為了簡單而不去消除重復(fù)
  • 你不能為了簡單而不注重清晰表達(dá)

固然前面三條的價(jià)值比簡單這個(gè)價(jià)值更重要阴绢,但是簡單也同樣會(huì)約束前面三條:

  • 為了簡單,不要實(shí)現(xiàn)客戶暫時(shí)還用不到的需求
  • 為了簡單艰躺,不要在變化方向未出現(xiàn)之前就對(duì)類進(jìn)行額外的抽象
  • 為了簡單呻袭,不要增加額外的代碼元素,除非能增強(qiáng)表達(dá)力

我們?cè)诠ぷ髦袘?yīng)按照需求腺兴、易修改性左电、可理解性和復(fù)雜度這四個(gè)緯度的重要程度由高到底對(duì)任務(wù)進(jìn)行排序。比如页响,我們重構(gòu)時(shí)券腔,先消除重復(fù),然后再增強(qiáng)代碼的表達(dá)力拘泞。

如果一個(gè)軟件設(shè)計(jì)滿足簡單設(shè)計(jì)四原則的要求,我們就說這個(gè)軟件設(shè)計(jì)是好的軟件設(shè)計(jì)枕扫。

為了讓簡單設(shè)計(jì)四原則中的第二條(盡可能消除重復(fù))的達(dá)成有章可循陪腌,袁英杰大師提出了正交設(shè)計(jì)四原則

  • 消除重復(fù)
  • 分離不同的變化方向
  • 縮小依賴范圍
  • 向著穩(wěn)定的方向依賴

第一條是重復(fù)出現(xiàn)之后被動(dòng)的消除重復(fù),而后面三條是重復(fù)出現(xiàn)之前的主動(dòng)預(yù)防策略烟瞧。同時(shí)诗鸭,前面兩條是為了驅(qū)動(dòng)模塊內(nèi)向高內(nèi)聚方向演進(jìn),后面兩條是為了驅(qū)動(dòng)模塊間向低耦合方向演進(jìn)参滴。

于是强岸,我們給出組合式設(shè)計(jì)的定義:應(yīng)用正交設(shè)計(jì)四原則,將系統(tǒng)分解成很多單一職責(zé)的小類或函數(shù)(函數(shù)式)砾赔,然后再將它們根據(jù)需要而靈活的組合起來蝌箍。

對(duì)于組合手段來說青灼,最常見且通用的手段是依賴注入,其他手段與編程語言相關(guān)妓盲,比如 C++ 可以使用多重繼承來實(shí)現(xiàn)組合杂拨。

語義模型

我們對(duì)強(qiáng)密碼校驗(yàn)器這個(gè)需求的通用語言進(jìn)行一下梳理:

  1. 強(qiáng)密碼考慮了三個(gè)緯度:長度記作 len,連續(xù)性記作 cont悯衬,字符種類記作 types弹沽;
  2. 允許使用三種修改方法:插入記作 insert,刪除記作 delete筋粗,替換記作 replace策橘;
  3. 原子操作記作 atom,需求中有四個(gè)原子操作娜亿,分別為密碼長度小于 6 的操作 len_less_than_atom丽已, 密碼長度大于 20 的操作,len_more_than_atom暇唾, 字符種類小于 3 的操作 types_atom促脉,同一個(gè)字符連續(xù)出現(xiàn) 3 次的操作 cont_atom
  4. 每個(gè)原子操作 atom 包含兩部分,即匹配器和執(zhí)行器二元組策州,記作(matcher, action)瘸味,那么針對(duì)四個(gè)原子操作,就有(len_less_than_matcher, insert_action)够挂、(len_more_than_matcher, delete_action)旁仿、 (types_matcher, replace_action) 和 (cont_matcher, replace_action)
  5. 需求中有多個(gè)規(guī)則 rule,atom 是最基本的 rule孽糖,repeat(多次執(zhí)行) 是修飾語義的 rule枯冈,allof (“與”的關(guān)系)和 anyof(“或”的關(guān)系)是組合語義的 rule,rule 通過組合可以產(chǎn)生新 rule

我們使用統(tǒng)一語言形式化表達(dá)一下需求:

password = newPassword(s)
len_less_than_atom_rule = atom(len_less_than_matcher, insert_action)
len_more_than_atom_rule = atom(len_more_than_matcher, delete_action)
types_atom_rule = atom(types_matcher, replace_action)
cont_atom_rule = atom(cont_matcher, replace_action)

len_rule = anyof(repeat(len_less_than_atom_rule), repeat(len_more_than_atom_rule))
types_rule = repeat(types_atom_rule)
cont_rule = repeat(cont_atom_rule)

rule = allof(len_rule, types_rule, cont_rule)
steps = rule(password)

從上面的形式化描述办悟,可以很容易的得到強(qiáng)密碼校驗(yàn)器的語義模型:

rule: Password -> int
matcher: Password -> bool
action: Password -> int

rule 的幾種形態(tài):

rule: atom | repeat | allof | anyof

我們用類圖表達(dá)一下強(qiáng)密碼校驗(yàn)器的組合式設(shè)計(jì):


rule.png

matcher-action.png

我們?cè)儆帽砀駚肀磉_(dá)一下 Rule 的分類:

分類 Rule
原子語義 LenLessThanAtom
LenMoreThanAtom
TypesAtom
ContAtom
修飾語義 Repeat
組合語義 AnyOf
AllOf

軟件實(shí)現(xiàn)

《聊聊編程范式》一文中尘奏,我們梳闡述了常見的三種編程范式(結(jié)構(gòu)化編程、面向?qū)ο缶幊毯秃瘮?shù)式)的基本設(shè)計(jì)和架構(gòu)風(fēng)格病蛉,梳理了編程范式之間的關(guān)系炫加。

從結(jié)構(gòu)化編程到面向?qū)ο缶幊蹋俚胶瘮?shù)式編程铺然,離圖靈機(jī)模型越來越遠(yuǎn)俗孝,但抽象程度越來越高,與領(lǐng)域問題的距離越來越近魄健。

對(duì)于組合式設(shè)計(jì)的實(shí)現(xiàn)赋铝,我們既可以選擇面向?qū)ο缶幊蹋部梢赃x擇函數(shù)式編程沽瘦。函數(shù)式編程更抽象一些革骨,但代碼更簡潔农尖;面向?qū)ο缶幊滩糠执a稍顯繁雜,但相對(duì)來說易于理解苛蒲。本文選擇以函數(shù)式編程為例來實(shí)踐組合式設(shè)計(jì)卤橄,讀者可以自行使用面向?qū)ο缶幊虂韲L試一下,大同小異臂外。

API 實(shí)現(xiàn)

需求中給出了 API 的定義:

strongPasswordChecker(s)

Golang 代碼的實(shí)現(xiàn)如下:與統(tǒng)一語言的形式化表達(dá)基本一樣

func StrongPasswordChecker(s string) int {
    password := newPassword(s)

    len_less_than_atom_rule := atom(len_less_than_matcher, insert_action)
    len_more_than_atom_rule := atom(len_more_than_matcher, delete_action)
    types_atom_rule := atom(types_matcher, replace_action)
    cont_atom_rule := atom(cont_matcher, replace_action)

    len_rule := anyof(repeat(len_less_than_atom_rule), repeat(len_more_than_atom_rule))
    types_rule := repeat(types_atom_rule)
    cont_rule := repeat(cont_atom_rule)

    rule := allof(len_rule, types_rule, cont_rule)
    steps := rule(password)
    return steps
}

函數(shù)式設(shè)計(jì)的基本方法為:借助閉包的單一接口的標(biāo)準(zhǔn)化和高階函數(shù)的可組合性窟扑,通過規(guī)則串聯(lián)設(shè)計(jì),完成數(shù)據(jù)從源到結(jié)果的映射描述漏健。這里的映射是通過多個(gè)高階函數(shù)的形式化組合完成嚎货,描述就像寫數(shù)學(xué)公式一樣放在那,等源數(shù)據(jù)從一頭傳入蔫浆,然后經(jīng)過層層函數(shù)公式的處理殖属,最后變成你想要的結(jié)果。數(shù)據(jù)在形式化轉(zhuǎn)移的過程中瓦盛,不僅僅包括數(shù)據(jù)本身洗显,還包括規(guī)則的創(chuàng)建、返回和傳遞原环。

數(shù)據(jù)(對(duì)象)實(shí)現(xiàn)

我們下面看看數(shù)據(jù)本身 Password 的設(shè)計(jì)挠唆,該數(shù)據(jù)的表現(xiàn)形式是一個(gè)對(duì)象。

Password 數(shù)據(jù)結(jié)構(gòu)

type Password struct {
    initialStr   string
    Len          int
    Steps        int
    TypesNum     int
    hasDigital   bool
    hasLowerCase bool
    hasUpperCase bool
    contNumbers []*ContNumber
}

type ContNumber struct {
    initialChar  byte
    initialIndex int
    times        int
}

Password 結(jié)構(gòu)中包括了密碼字符串的原始信息嘱吗、預(yù)處理信息和改造過程中的信息玄组。密碼中可能包括多個(gè)連續(xù)性字符組成的數(shù),我們通過 []*ContNumber 來表示谒麦。

Password 對(duì)象的構(gòu)造

func newPassword(s string) *Password {
    pwd := &Password{initialStr: s, Len: len(s), Steps: 0, TypesNum: 0,
        contNumbers: make([]*ContNumber, 0)}
    typesInit(pwd)
    contInit(pwd)
    return pwd
}

說明:共有三步

  • 構(gòu)造 Password 數(shù)據(jù)俄讹,記錄密碼字符串 s 的初始值和長度
  • 遍歷密碼字符串,初始化字符種類
  • 遍歷密碼字符串绕德,初始化連續(xù)性數(shù)的數(shù)組

Password 對(duì)象的方法

下面這些方法都是在 TDD 的實(shí)踐過程中患膛,為了達(dá)成正交設(shè)計(jì)四原則的目標(biāo),通過 extract 重構(gòu)手法逐步提煉出來的:

func (p *Password) increaseLen() {
    p.Len++
}

func (p *Password) decreaseLen() {
    p.Len--
}

func (p *Password) increaseSteps() {
    p.Steps++
}

func (p *Password) increaseTypesNum() {
    if p.TypesNum < maxTypesNum {
        p.TypesNum++
    }
}

func (p *Password) consumeContNumber(quota int) {
    ...
}

func (p *Password) consumeContNumberByPrio() {
    ...
}

rule 的實(shí)現(xiàn)

這一部分是組合式設(shè)計(jì)的核心耻蛇。

數(shù)據(jù)在形式化轉(zhuǎn)移過程中剩瓶,源數(shù)據(jù)是 Password,層層函數(shù)的輸入輸出數(shù)據(jù)都是 Password 城丧,同時(shí)目標(biāo)數(shù)據(jù)也是 Password 的 Steps 字段,所以我們使用 Password 指針類型來表達(dá)數(shù)據(jù)類型豌鹤。

rule 的抽象表達(dá)

我們知道亡哄,rule 包含兩部分,即匹配器和執(zhí)行器二元組:

type Matcher func(pwd *Password) bool
type Action func(pwd *Password) int
type Rule func(mather Matcher, action Action) func(pwd *Password) int

原子語義的實(shí)現(xiàn)

atom 的實(shí)現(xiàn)很簡單布疙,就是返回一個(gè)閉包:當(dāng)匹配器滿足時(shí)蚊惯,就返回執(zhí)行器的結(jié)果(steps)

func atom(matcher Matcher, action Action) func(pwd *Password) int {
    return func(pwd *Password) int {
        if matcher(pwd) {
            return action(pwd)
        }
        return 0
    }
}

原子語義的 rule 有四個(gè)愿卸,分別為 LenLessThanAtom、LenMoreThanAtom截型、TypesAtom 和 ContAtom趴荸,核心是設(shè)計(jì)它們的匹配器(4個(gè))和執(zhí)行器(3個(gè))。

匹配器實(shí)現(xiàn):

func len_less_than_matcher(pwd *Password) bool {
    if pwd.Len < minLen {
        return true
    }
    return false
}

func len_more_than_matcher(pwd *Password) bool {
    if pwd.Len > maxLen {
        return true
    }
    return false
}

func types_matcher(pwd *Password) bool {
    if pwd.TypesNum < 3 {
        return true
    }
    return false
}

func cont_matcher(pwd *Password) bool {
    if len(pwd.contNumbers) > 0 {
        return true
    }
    return false
}

執(zhí)行器實(shí)現(xiàn):

func insert_action(pwd *Password) int {
    pwd.increaseLen()
    pwd.increaseTypesNum()
    pwd.consumeContNumber(2)
    pwd.increaseSteps()
    return pwd.Steps
}

func delete_action(pwd *Password) int {
    pwd.decreaseLen()
    pwd.consumeContNumberByPrio()
    pwd.increaseSteps()
    return pwd.Steps
}

func replace_action(pwd *Password) int {
    pwd.increaseTypesNum()
    pwd.consumeContNumber(3)
    pwd.increaseSteps()
    return pwd.Steps
}

修飾語義的實(shí)現(xiàn)

修飾語義的 rule (新)只有一個(gè)宦焦,即 repeat发钝,含義是重復(fù)執(zhí)行被修飾的 rule(舊),直到符合預(yù)期:

func repeat(rule func(pwd *Password) int) func(pwd *Password) int {
    return func(pwd *Password) int {
        for ;; {
            ret :=  rule(pwd)
            if ret == 0 {
                return pwd.Steps
            }
        }
    }
}

組合語義的實(shí)現(xiàn)

組合語義的 rule 有兩個(gè)波闹,即 allof 和 anyof酝豪,分別表達(dá)“與”的關(guān)系和“或”的關(guān)系:

func allof(rules... func(pwd *Password) int) func(pwd *Password) int {
    return func(pwd *Password) int {
        steps := 0
        for _, rule := range rules {
            pwd.Steps = 0
            steps += rule(pwd)
        }
        return steps
    }

}

func anyof(rules... func(pwd *Password) int) func(pwd *Password) int {
    return func(pwd *Password) int {
        steps := 0
        for _, rule := range rules {
            steps += rule(pwd)
            if steps > 0 {
                return steps
            }
        }
        return 0
    }
}

小結(jié)

本文概述了組合式設(shè)計(jì)的要點(diǎn),并通過實(shí)戰(zhàn)強(qiáng)密碼校驗(yàn)器的案例精堕,向讀者展示了組合式設(shè)計(jì)落地的主要過程和步驟孵淘,希望對(duì)讀者有一定的收益。

如果說正交設(shè)計(jì)是軟件設(shè)計(jì)的皇冠歹篓,那么組合式設(shè)計(jì)就是皇冠上的明珠瘫证。應(yīng)用組合式設(shè)計(jì)產(chǎn)出的代碼擴(kuò)展性強(qiáng)且易于理解,你是否深有感觸庄撮?如果你也曾寫過或讀過強(qiáng)密碼校驗(yàn)器的實(shí)現(xiàn)代碼背捌,你都發(fā)現(xiàn)過哪些壞味道?歡迎大家留言探討重窟!

本文實(shí)戰(zhàn)過程中的源碼及 TDD 用例载萌,作者都放到 github 了:https://github.com/agiledragon/strong-password,感興趣的同學(xué)可以查閱巡扇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末扭仁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子厅翔,更是在濱河造成了極大的恐慌乖坠,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刀闷,死亡現(xiàn)場(chǎng)離奇詭異熊泵,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)甸昏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門顽分,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人施蜜,你說我怎么就攤上這事卒蘸。” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵缸沃,是天一觀的道長恰起。 經(jīng)常有香客問我,道長趾牧,這世上最難降的妖魔是什么检盼? 我笑而不...
    開封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮翘单,結(jié)果婚禮上吨枉,老公的妹妹穿的比我還像新娘。我一直安慰自己县恕,他們只是感情好东羹,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著忠烛,像睡著了一般属提。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上美尸,一...
    開封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天冤议,我揣著相機(jī)與錄音,去河邊找鬼师坎。 笑死恕酸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的胯陋。 我是一名探鬼主播蕊温,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼遏乔!你這毒婦竟也來了义矛?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤盟萨,失蹤者是張志新(化名)和其女友劉穎凉翻,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捻激,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡制轰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了胞谭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片垃杖。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丈屹,靈堂內(nèi)的尸體忽然破棺而出缩滨,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布脉漏,位于F島的核電站,受9級(jí)特大地震影響袖牙,放射性物質(zhì)發(fā)生泄漏侧巨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一鞭达、第九天 我趴在偏房一處隱蔽的房頂上張望司忱。 院中可真熱鬧,春花似錦畴蹭、人聲如沸坦仍。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽繁扎。三九已至,卻和暖如春糊闽,著一層夾襖步出監(jiān)牢的瞬間梳玫,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來泰國打工右犹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留提澎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓念链,卻偏偏與公主長得像盼忌,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掂墓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

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