2022-01-01設(shè)計原則--單一職責(zé)與接口隔離原則總結(jié)

單一職責(zé)(SRP)

  • 如何理解單一職責(zé)原則(SRP)?

    單一職責(zé)原則的英文是 Single Responsibility Principle我磁,縮寫為 SRP。這個原則的英文描述是這樣的:A class or module should have a single responsibility驻襟。如果我們把它翻譯成中文夺艰,那就是:一個類或者模塊只負責(zé)完成一個職責(zé)(或者功能)。

    注意沉衣,這個原則描述的對象包含兩個郁副,一個是類(class),一個是模塊(module)豌习。關(guān)于這兩個概念存谎,有兩種理解方式。一種理解是:把模塊看作比類更加抽象的概念肥隆,類也可以看作模塊愕贡。另一種理解是:把模塊看作比類更加粗粒度的代碼塊,模塊中包含多個類巷屿,多個類組成一個模塊固以,不管哪種理解道理是想通的,下面以類作為分析對象,模塊自行引申即可憨琳。

    一個類只負責(zé)完成一個職責(zé)或者功能诫钓。不要設(shè)計大而全的類,要設(shè)計粒度小篙螟、功能單一的類菌湃。單一職責(zé)原則是為了實現(xiàn)代碼高內(nèi)聚、低耦合遍略,提高代碼的復(fù)用性惧所、可讀性、可維護性绪杏。

  • 如何判斷類的職責(zé)是否足夠單一下愈?

不同的應(yīng)用場景、不同階段的需求背景蕾久、不同的業(yè)務(wù)層面势似,對同一個類的職責(zé)是否單一,可能會有不同的判定結(jié)果僧著。所以我們可以先寫一個粗粒度的類履因,滿足業(yè)務(wù)需求。隨著業(yè)務(wù)的發(fā)展盹愚,如果粗粒度的類越來越龐大栅迄,代碼越來越多,這個時候皆怕,我們就可以將這個粗粒度的類毅舆,拆分成幾個更細粒度的類(持續(xù)重構(gòu))。

實際上端逼,一些側(cè)面的判斷指標更具有指導(dǎo)意義和可執(zhí)行性,比如污淋,出現(xiàn)下面這些情況就有可能說明這類的設(shè)計不滿足單一職責(zé)原則:

  1. 類中的代碼行數(shù)顶滩、函數(shù)或者屬性過多;
  2. 類依賴的其他類過多寸爆,或者依賴類的其他類過多礁鲁;
  3. 私有方法過多;
  4. 比較難給類起一個合適的名字赁豆;
  5. 類中大量的方法都是集中操作類中的某幾個屬性仅醇。
/**
 * UserInfo類
 *
 * 該類是否滿足單一職責(zé)?
 *
 * 分析問題要結(jié)合實際的應(yīng)用場景:如果在這個社交產(chǎn)品中魔种,用戶的地址信息跟其他信息一樣析二,只是單純地用來展示,那 UserInfo 現(xiàn)在的設(shè)計就是合理的。
 * 但是叶摄,如果這個社交產(chǎn)品發(fā)展得比較好属韧,之后又在產(chǎn)品中添加了電商的模塊,用戶的地址信息還會用在電商物流中蛤吓,那我們最好將地址信息從 UserInfo 中拆分出來宵喂,獨立成用戶物流信息(或者叫地址信息、收貨信息等)会傲。
 *
 */
@Getter
@Setter
public class UserInfo {

    private long userId;
    private String username;
    private String email;
    private String telephone;
    private long createTime;
    private long lastLoginTime;
    private String avatarUrl;
    private String provinceOfAddress; // 省
    private String cityOfAddress; // 市
    private String regionOfAddress; // 區(qū)
    private String detailedAddress; // 詳細地址

}
  • 類的職責(zé)是否設(shè)計得越單一越好锅棕?

    單一職責(zé)原則通過避免設(shè)計大而全的類,避免將不相關(guān)的功能耦合在一起淌山,來提高類的內(nèi)聚性裸燎。同時,類職責(zé)單一艾岂,類依賴的和被依賴的其他類也會變少顺少,減少了代碼的耦合性,以此來實現(xiàn)代碼的高內(nèi)聚王浴、低耦合脆炎。但是,如果拆分得過細氓辣,實際上會適得其反秒裕,反倒會降低內(nèi)聚性,也會影響代碼的可維護性钞啸。

/**
 * Serialization類
 *
 * 拆分過度問題:以序列化為例經(jīng)過拆分之后几蜻,Serializer 類和 Deserializer 類的職責(zé)更加單一了,
 * 但也隨之帶來了新的問題体斩。如果我們修改了協(xié)議的格式梭稚,數(shù)據(jù)標識從“UEUEUE”改為“DFDFDF”,或者序列
 * 化方式從 JSON 改為了 XML絮吵,那 Serializer 類和 Deserializer 類都需要做相應(yīng)的修改弧烤,代碼的
 * 內(nèi)聚性顯然沒有原來 Serialization 高了。而且蹬敲,如果我們僅僅對 Serializer 類做了協(xié)議修改暇昂,而
 * 忘記了修改 Deserializer 類的代碼,那就會導(dǎo)致序列化伴嗡、反序列化不匹配急波,程序運行出錯,也就是說瘪校,
 * 拆分之后澄暮,代碼的可維護性變差了。
 *
 *
 */
public class Serialization {
    private static final String IDENTIFIER_STRING = "UEUEUE;";

    public String serialze(Map<String, String> object) {
        StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
        textBuilder.append(JSON.toJSONString(object));
        return textBuilder.toString();
    }

    public Map<String, String> deserialize(String text){
        if(!text.startsWith(IDENTIFIER_STRING)){
            return Collections.emptyMap();
        }

        text = text.substring(IDENTIFIER_STRING.length());

        return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
    }
}

public class Serializer {

    private static final String IDENTIFIER_STRING = "UEUEUE;";

    public String serialze(Map<String, String> object) {
        StringBuilder textBuilder = new StringBuilder(IDENTIFIER_STRING);
        textBuilder.append(JSON.toJSONString(object));
        return textBuilder.toString();
    }

}

public class Deserializer {
    private static final String IDENTIFIER_STRING = "UEUEUE;";


    public Map<String, String> deserialize(String text){
        if(!text.startsWith(IDENTIFIER_STRING)){
            return Collections.emptyMap();
        }

        text = text.substring(IDENTIFIER_STRING.length());

        return JSON.parseObject(text,new TypeReference<HashMap<String,String>>(){});
    }
}

接口隔離原則(ISP)

  1. 如何理解“接口隔離原則”?

    接口隔離原則的英文翻譯是“ Interface Segregation Principle”赏寇,縮寫為 ISP吉嫩。Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use⌒岫ǎ”直譯成中文的話就是:客戶端不應(yīng)該被強迫依賴它不需要的接口自娩。其中的“客戶端”,可以理解為接口的調(diào)用者或者使用者渠退。

    理解“接口隔離原則”的重點是理解其中的“接口”二字忙迁。這里有三種不同的理解。

    • 如果把“接口”理解為一組接口集合碎乃,可以是某個微服務(wù)的接口姊扔,也可以是某個類庫的接口等。如果部分接口只被部分調(diào)用者使用梅誓,我們就需要將這部分接口隔離出來恰梢,單獨給這部分調(diào)用者使用,而不強迫其他調(diào)用者也依賴這部分不會被用到的接口梗掰。
    /**
     * UserService接口
     *
     *  場景:用戶系統(tǒng)提供了一組跟用戶相關(guān)的 API 給其他系統(tǒng)使用嵌言,比如:注冊、登錄及穗、獲取用戶信息等〈蒈睿現(xiàn)在,
     *  我們的后臺管理系統(tǒng)要實現(xiàn)刪除用戶的功能埂陆,希望用戶系統(tǒng)提供一個刪除用戶的接口苛白。這個時候我們該如何來做呢?
     *
     *  分析:方案一:在 UserService 中新添加一個 deleteUserByCellphone() 或 deleteUserById() 接口就可以了焚虱。
     *  這個方法可以解決問題购裙,但是也隱藏了一些安全隱患,刪除用戶是一個非常慎重的操作鹃栽,我們只希望通過后臺管理系統(tǒng)來執(zhí)行躏率,
     *  所以這個接口只限于給后臺管理系統(tǒng)使用,如果在沒有鑒權(quán)的情況下谍咆,加限制地被其他業(yè)務(wù)系統(tǒng)調(diào)用禾锤,就有可能導(dǎo)致誤刪用戶私股。
     *
     *  方案二:在沒有鑒權(quán)情況下可以從代碼層面規(guī)避上述風(fēng)險,具體可以參照接口隔離原則摹察,調(diào)用者不應(yīng)該強迫依賴它不需要的接口,
     *  將刪除接口單獨放到另外一個接口 RestrictedUserService 中倡鲸,然后將 RestrictedUserService 只打包提供給后臺
     *  管理系統(tǒng)來使用供嚎。
     *
     */
    
    public interface UserService {
    
        boolean register(String cellphone, String password);
    
        boolean login(String cellphone, String password);
    
        UserInfo getUserInfoById(long id);
    
        UserInfo getUserInfoByCellphone(String cellphone);
    }
    
    public interface RestrictedUserService {
    
        boolean deleteUserByCellphone(String cellphone);
    
        boolean deleteUserById(long id);
    }
    
    public class BackgroundUserServiceImpl implements UserService, RestrictedUserService {
    
        @Override
        public boolean deleteUserByCellphone(String cellphone) {
            return false;
        }
    
        @Override
        public boolean deleteUserById(long id) {
            return false;
        }
    
        @Override
        public boolean register(String cellphone, String password) {
            return false;
        }
    
        @Override
        public boolean login(String cellphone, String password) {
            return false;
        }
    
        @Override
        public UserInfo getUserInfoById(long id) {
            return null;
        }
    
        @Override
        public UserInfo getUserInfoByCellphone(String cellphone) {
            return null;
        }
    }
    
    • 如果把“接口”理解為單個 API 接口或函數(shù),函數(shù)的設(shè)計要功能單一,不要將多個不同的功能邏輯在一個函數(shù)中實現(xiàn)克滴。部分調(diào)用者只需要函數(shù)中的部分功能逼争,那我們就需要把函數(shù)拆分成粒度更細的多個函數(shù),讓調(diào)用者只依賴它需要的那個細粒度函數(shù)劝赔。
    /**
     * Statistics類
     *
     * 接口設(shè)計分析:count() 函數(shù)的功能不夠單一誓焦,包含很多不同的統(tǒng)計功能,比如着帽,求最大值杂伟、最小值、平均值等
     * 場景一:如果在項目中仍翰,對每個統(tǒng)計需求赫粥,Statistics 定義的那幾個統(tǒng)計信息都有涉及,那 count()
     * 函數(shù)的設(shè)計就是合理的予借。
     *
     * 場景二:如果每個統(tǒng)計需求只涉及 Statistics 羅列的統(tǒng)計信息中一部分越平,比如,有的只需要用到 max灵迫、
     * min秦叛、average 這三類統(tǒng)計信息,在這個應(yīng)用場景下龟再,count() 函數(shù)的設(shè)計就有點不合理了书闸,這種場景下
     * 需要將其拆分成粒度更細的多個統(tǒng)計函數(shù)。
     *
     * 總結(jié):ISP提供了一種判斷接口是否職責(zé)單一的標準:通過調(diào)用者如何使用接口來間接地判定利凑。如果調(diào)用者只
     * 使用部分接口或接口的部分功能浆劲,那接口的設(shè)計就不夠職責(zé)單一。
     *
     */
    @Getter
    public class Statistics {
    
        private Long max;
        private Long min;
        private Long average;
        private Long sum;
        private Long percentile99;
        private Long percentile999;
    
        /**
         * 場景一下合理
         */
        public Statistics count(Collection<Long> dataSet) {
            Statistics statistics = new Statistics();
            //求最大值
            statistics.setMax(2L);
            // 最小值
            statistics.setMin(0L);
            // 平均值
            statistics.setAverage(1L);
            return statistics;
        }
    
        /**
         * 場景二下合理
         *
         * @param dataSet
         * @return
         */
        public Long max(Collection<Long> dataSet) {
            return 2L;
        }
    
        public Long min(Collection<Long> dataSet) {
            return 0L;
        }
    
        public Long average(Collection<Long> dataSet) {
            return 1L;
        }
    
        public void setMax(Long max) {
            this.max = max;
        }
    
        public void setMin(Long min) {
            this.min = min;
        }
    
        public void setAverage(Long average) {
            this.average = average;
        }
    }
    
    • 如果把“接口”理解為 面向?qū)ο缶幊?OOP) 中的接口哀澈,也可以理解為面向?qū)ο缶幊陶Z言中的接口語法牌借。那接口的設(shè)計要盡量單一,不要讓接口的實現(xiàn)類和調(diào)用者割按,依賴不需要的接口函數(shù)膨报。
    /**
     * Application類
     *
     * 背景:假設(shè)我們的項目中用到了三個外部系統(tǒng):Redis、MySQL适荣、Kafka现柠。每個系統(tǒng)都對應(yīng)一系列配置信息,
     *      比如地址弛矛、端口够吩、訪問超時時間等。為了在內(nèi)存中存儲這些配置信息丈氓,供項目中的其他模塊來使用周循,我
     *      們分別設(shè)計實現(xiàn)了三個 Configuration 類:RedisConfig强法、MysqlConfig、KafkaConfig
     *
     * 需求:
     *      1.希望支持 Redis 和 Kafka 配置信息的熱更新湾笛。所謂“熱更新(hot update)”就是饮怯,如果在配
     *      置中心中更改了配置信息,我們希望在不用重啟系統(tǒng)的情況下嚎研,能將最新的配置信息加載到內(nèi)存中(也就
     *      是 RedisConfig蓖墅、KafkaConfig 類中)。
     *
     *      2.監(jiān)控功能需求临扮。通過命令行來查看 Zookeeper 中的配置信息是比較麻煩的置媳。所以,我們希望能有一
     *      種更加方便的配置信息查看方式公条。我們可以在項目中開發(fā)一個內(nèi)嵌的 SimpleHttpServer拇囊,輸出項目的
     *      配置信息到一個固定的 HTTP 地址,比如:http://127.0.0.1:2389/config 靶橱。我們只需要在瀏覽
     *      器中輸入這個地址寥袭,就可以顯示出系統(tǒng)的配置信息。不過关霸,出于某些原因传黄,我們只想暴露 MySQL 和 Redis
     *      的配置信息。
     *
     */
    public class Application {
    
        private static ConfigSource configSource = new ZookeerConfigSource();
    
        private static final RedisConfig redisConfig = new RedisConfig(configSource);
        private static final KafkaConfig kafkaConfig = new KafkaConfig(configSource);
        private static final MysqlConfig mySqlConfig = new MysqlConfig(configSource);
    
    
        public static void main(String[] args) {
            ScheduledUpdater redisConfigUpdater = new ScheduledUpdater(redisConfig,300,300);
            redisConfigUpdater.run();
    
            ScheduledUpdater kafkaConfigUpdater = new ScheduledUpdater(kafkaConfig,60,60);
            kafkaConfigUpdater.run();
    
            SimpleHttpServer simpleHttpServer = new SimpleHttpServer("127.0.0.1",2389);
            simpleHttpServer.addViewer("/config",redisConfig);
            simpleHttpServer.addViewer("/config",mySqlConfig);
        }
     
    }
    
    /**
     * Updater熱更新接口
     */
    public interface Updater {
        /**
         * 熱部署,從configSource加載配置到address/timeout/maxTotal
         */
        void update();
    }
    /**
     * Viewer監(jiān)控接口
     */
    public interface Viewer {
        /**
         * 監(jiān)控-輸出文本信息
         */
        String outputInPlainText();
    
        /**
         * 監(jiān)控-輸出監(jiān)控項
         */
        Map<String,String> output();
    }
    
    //接口處理類
    public class ScheduledUpdater {
        private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
    
        private long initialDelayInSeconds;
    
        private long periodInSeconds;
    
        private Updater updater;
    
        public ScheduledUpdater(Updater updater, long initialDelayInSeconds, long periodInSeconds) {
            this.initialDelayInSeconds = initialDelayInSeconds;
    
            this.periodInSeconds = periodInSeconds;
    
            this.updater = updater;
        }
    
        public void run(){
            executor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    updater.update();
                }
            },this.initialDelayInSeconds,this.periodInSeconds, TimeUnit.SECONDS);
        }
    }
    
    public class SimpleHttpServer {
    
        private String host;
    
        private int port;
        private Map<String, List<Viewer>> viewerMap = new HashMap<>();
    
        public SimpleHttpServer(String host, int port) {
            this.host = host;
            this.port = port;
        }
    
        public void addViewer(String urlDirectory, Viewer viewer) {
            if (!viewerMap.containsKey(urlDirectory)) {
                viewerMap.put(urlDirectory, new ArrayList<Viewer>());
            }
    
            viewerMap.get(urlDirectory).add(viewer);
    
        }
    
        public void run(){
            // 輸出項目的配置信息到一個固定的 HTTP 地址
            // 比如:http://127.0.0.1:2389/config 队寇。
            // 我們只需要在瀏覽器中輸入這個地址膘掰,就可以顯示出系統(tǒng)的配置信息。
        }
    }
    
    //config配置類
    @Getter
    public abstract class AbstractConfig {
        /**
         * 配置中心(比如zookeeper)
         */
        protected ConfigSource configSource;
    
        protected String address;
    
        protected int timeout;
    
        protected int maxTotal;
    
    }
    
    public class RedisConfig extends AbstractConfig implements Updater, Viewer {
    
        public RedisConfig(ConfigSource configSource) {
            super();
            super.configSource = configSource;
        }
    
        /**
         * 熱部署佳遣,從configSource加載配置到address/timeout/maxTotal
         */
        @Override
        public void update() {
            super.address = configSource.getAddress();
            super.timeout = configSource.getTimeout();
            super.maxTotal = configSource.getMaxTotal();
        }
    
        /**
         * 監(jiān)控-輸出文本信息
         */
        @Override
        public String outputInPlainText() {
            return JSON.toJSONString(this);
        }
    
        /**
         * 監(jiān)控-輸出監(jiān)控項
         */
        @Override
        public Map<String, String> output() {
            return JSON.parseObject(this.outputInPlainText(),
                new TypeReference<HashMap<String, String>>(){});
        }
    }
    
    public class MysqlConfig extends AbstractConfig implements Viewer {
    
        public MysqlConfig(ConfigSource configSource) {
            super();
            super.configSource = configSource;
        }
      
        @Override
        public String outputInPlainText() {
            return JSON.toJSONString(this);
        }
    
        @Override
        public Map<String, String> output() {
            return JSON.parseObject(this.outputInPlainText(),
                new TypeReference<HashMap<String, String>>(){});
        }
    }
    
    public class KafkaConfig extends AbstractConfig implements Updater {
    
        public KafkaConfig(ConfigSource configSource) {
            super();
            super.configSource = configSource;
        }
    
        @Override
        public void update() {
            super.address = configSource.getAddress();
            super.timeout = configSource.getTimeout();
            super.maxTotal = configSource.getMaxTotal();
        }
    }
    
  2. 接口隔離原則與單一職責(zé)原則的區(qū)別

    單一職責(zé)原則針對的是模塊识埋、類、接口的設(shè)計零渐。接口隔離原則相對于單一職責(zé)原則窒舟,一方面更側(cè)重于接口的設(shè)計,另一方面它的思考角度也是不同的诵盼。接口隔離原則提供了一種判斷接口的職責(zé)是否單一的標準:通過調(diào)用者如何使用接口來間接地判定惠豺。如果調(diào)用者只使用部分接口或接口的部分功能,那接口的設(shè)計就不夠職責(zé)單一风宁。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載洁墙,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末戒财,一起剝皮案震驚了整個濱河市热监,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌固翰,老刑警劉巖狼纬,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異骂际,居然都是意外死亡疗琉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門歉铝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盈简,“玉大人,你說我怎么就攤上這事太示∧停” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵类缤,是天一觀的道長臼勉。 經(jīng)常有香客問我,道長餐弱,這世上最難降的妖魔是什么宴霸? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮膏蚓,結(jié)果婚禮上瓢谢,老公的妹妹穿的比我還像新娘。我一直安慰自己驮瞧,他們只是感情好氓扛,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著论笔,像睡著了一般采郎。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上狂魔,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天尉剩,我揣著相機與錄音,去河邊找鬼毅臊。 笑死理茎,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的管嬉。 我是一名探鬼主播皂林,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蚯撩!你這毒婦竟也來了础倍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤胎挎,失蹤者是張志新(化名)和其女友劉穎沟启,沒想到半個月后忆家,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡德迹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年芽卿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胳搞。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡卸例,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出肌毅,到底是詐尸還是另有隱情筷转,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布悬而,位于F島的核電站呜舒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏笨奠。R本人自食惡果不足惜阴绢,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望艰躺。 院中可真熱鬧呻袭,春花似錦、人聲如沸腺兴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽页响。三九已至篓足,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間闰蚕,已是汗流浹背栈拖。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留没陡,地道東北人涩哟。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像盼玄,于是被迫代替她去往敵國和親贴彼。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345

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