软件架构VIII: 基于组件的思考

开发人员实际上以不同的方式打包模块,有时取决于他们的开发平台。 我们称模块组件为物理包装。 大多数语言也支持物理包装:Java中的jar文件,.NET中的dll,Ruby中的gem,等等。 在本章中,我们讨论围绕组件的体系结构注意事项,范围从范围到发现。

组件范围

开发人员发现基于大量因素细分组件的概念很有用,其中一些因素出现在图8-1中。

组件提供了一种特定于语言的机制来将工件分组在一起,通常将它们嵌套以产生分层。 如图8-1所示,最简单的组件以比类(或函数,非面向对象的语言)更高的模块化级别包装代码。 这个简单的包装器通常称为库,它倾向于在与调用代码相同的内存地址中运行,并通过语言函数调用机制进行通信。 库通常是编译时依赖项(有很多例外,例如动态链接库[DLL]多年来一直是Windows用户的祸根)。

image.png

组件还作为体系结构中的子系统或层出现,成为许多事件处理器的可部署工作单元。另一类组件,即服务,通常在其自己的地址空间中运行,并通过低级网络协议(如TCP / IP)或更高级别的格式(如REST或消息队列)进行通信,从而在微服务等体系结构中形成独立的可部署单元。

不需要架构师使用组件—碰巧的是,拥有比该语言提供的最低级别更高的模块化级别通常很有用。例如,在微服务架构中,简单性是架构原则之一。因此,服务可能包含足够的代码以保证组件,或者可能足够简单以仅包含少量代码,如图8-2所示。

组件构成了体系结构中的基本模块化构建块,使它们成为建筑师的重要考虑因素。实际上,架构师必须做出的主要决定之一涉及架构中组件的顶级分区。

image.png

架构师角色

通常,架构师定义,完善,管理和管理架构中的组件。软件架构师与业务分析师,主题专家,开发人员,质量保证工程师,运营和企业架构师合作,创建了软件的初始设计,并结合了第4章中讨论的架构特征和软件系统的要求。

实际上,我们在本书中涵盖的所有细节都独立于团队使用的软件开发过程而存在:体系结构与开发过程无关。该规则的主要例外是在各种敏捷软件开发风格中率先采用的工程实践,尤其是在部署和自动化治理领域。但是,通常,软件体系结构与流程是分开存在的。因此,架构师最终不在乎需求的起源:正式的联合应用设计(JAD)流程,冗长的瀑布式分析和设计,敏捷的故事卡……或这些的任何混合版本。

通常,该组件是架构师直接与之交互的软件系统的最低级别,第6章中讨论的许多代码质量指标会全面影响代码库,但该质量指标除外。组件由类或函数(取决于实现平台)组成,其类设计由技术主管或开发人员负责。并不是说建筑师不应该让自己参与课堂设计(尤其是在发现或应用设计模式时),而是应该避免对系统中从上到下的每个决策进行微观管理。如果架构师从不让其他角色来做出决定,那么组织将在赋予下一代架构师权力方面陷入困境。

架构师必须将组件标识为新项目中的首要任务之一。但是在架构师可以识别组件之前,他们必须知道如何对架构进行分区。

架构分区

《软件体系结构第一定律》指出,软件中的所有事物都是一个权衡,包括架构师如何在体系结构中创建组件。 由于组件代表一种通用的集装箱运输机制,因此架构师可以构建所需的任何类型的分区。 存在几种常见的样式,具有不同的取舍方式。 我们将在第二部分中深入讨论建筑风格。 在这里,我们讨论样式的重要方面,即体系结构中的顶级分区。

考虑图8-3中所示的两种类型的体系结构样式。

image.png

在图8-3中,一种为许多人所熟悉的架构类型是分层的整体结构(在第10章中有详细讨论)。 另一种是西蒙·布朗(Simon Brown)流行的体系结构样式,称为模块化整体结构,即与数据库关联并围绕域而不是技术能力进行分区的单个部署单元。 这两种样式表示对体系结构进行顶层分区的不同方法。 请注意,在每个变体中,每个顶级组件(层或组件)都可能嵌入了其他组件。 顶层分区特别受架构师的关注,因为它定义了基本的体系结构样式和分区代码的方式。

基于技术能力(例如分层整体结构)的组织架构代表了技术顶级分区。 图8-4显示了它的通用版本。

image.png

在图8-4中,架构师将系统的功能划分为技术功能:表示,业务规则,服务,持久性等。这种组织代码库的方式当然很有意义。所有持久性代码都驻留在体系结构的一层中,从而使开发人员可以轻松找到与持久性相关的代码。尽管分层体系结构的基本概念要早于数十年,但Model-View-Controller设计模式与该体系结构模式相匹配,从而使开发人员易于理解。因此,它通常是许多组织中的默认体系结构。

分层体系结构优势的一个有趣的副作用与公司如何担当不同项目角色有关。使用分层体系结构时,将所有后端开发人员放在一起放在一个部门中,将DBA放在另一个部门中,将演示团队放在另一个部门中,这是有意义的。由于康威的法律,在这些组织中这是有道理的。

图8-4中的另一种体系结构变体表示域分区,这受Eric Evan的《域驱动设计》一书的启发,这是一种用于分解复杂软件系统的建模技术。在DDD中,架构师确定相互独立且相互分离的域或工作流。微服务架构风格(在第17章中讨论过)就是基于这种哲学。在模块化的整体中,架构师将架构划分为域或工作流,而不是技术能力。由于组件之间经常相互嵌套,因此域分区中的图8-4中的每个组件(例如,CatalogCheckout)都可以使用持久性库并具有独立的业务规则层,但是顶级分区围绕域进行。

不同体系结构模式之间的根本区别之一是每种支持哪种类型的顶级分区,我们将针对每种单独的模式进行介绍。它还对架构师如何决定如何初始识别组件产生巨大影响-架构师是否要按技术或按领域划分事物?

使用技术分区的架构师通过技术能力来组织系统的组件:演示,业务规则,持久性等。因此,该体系结构的组织原则之一是技术关注点的分离。反过来,这会创建有用的解耦级别:如果服务层仅连接到下面的持久层和上面的业务规则层,那么持久性的更改将仅潜在地影响这些层。这种分区方式提供了一种去耦技术,从而减少了相关组件上的涟漪效应。我们在第10章的分层体系结构模式中涵盖了这种体系结构样式的更多细节。使用技术分区组织系统当然是合乎逻辑的,但是,就像软件体系结构中的所有事物一样,这需要做出一些权衡。

通过技术分区实施的分离使开发人员能够快速找到代码库的某些类别,因为它是按功能组织的。但是,大多数实际的软件系统都需要跨技术能力的工作流。考虑CatalogCheckout的常见业务工作流程。在技​​术上分层的体系结构中处理CatalogCheckout的代码出现在所有层中,如图8-5所示。

在图8-5中,在技术上分区的体系结构中,CatalogCheckout出现在所有层中。该域遍布整个技术层。将其与域分区相比,域分区使用顶级分区,该顶级分区按域而不是技术功能来组织组件。在图8-5中,设计域划分架构的架构师围绕工作流和/或域构建顶级组件。域分区中的每个组件都可能具有子组件,包括层,但是顶级分区集中于域,这更好地反映了项目中最常发生的更改类型。

这两种风格都不比另一种更正确-请参阅《软件体系结构第一定律》。也就是说,在过去的几年中,我们已经观察到行业朝着整体和分布式(例如,微服务)架构进行域划分的趋势。但是,这是架构师必须做出的第一个决定。

案例:硅谷三明治分区

以我们的示例kata之一为例,“案例研究:硅三明治”。 派生组件时,架构师面临的基本决定之一是顶层分区。 考虑硅三明治的两种不同可能性中的第一种,即域划分,如图8-6所示。

image.png

在图8-6中,架构师围绕领域(工作流)进行了设计,为采购,促销,MakeOrder,ManageInventory,配方,交付和位置创建了离散组件。 在许多这些组件中都包含一个子组件,用于处理所需的各种类型的定制,涵盖了常见和本地的变化。

另一种设计将公用部分和局部部分隔离到各自的分区中,如图8-7所示。 通用和本地代表顶级组件,剩下的购买和交付则用于处理工作流。

哪个更好? 这取决于! 每个分区都有不同的优点和缺点。

image.png

领域分区

域划分的体系结构按工作流和/或域将顶级组件分开。

优点

  • 针对业务功能而不是实现细节进行更紧密的建模
  • 更容易利用逆航道策略围绕领域建立跨职能团队
  • 与模块化单片和微服务架构样式更加紧密地结合
  • 消息流匹配问题域
  • 易于将数据和组件迁移到分布式架构

坏处

  • 自定义代码出现在多个位置

技术分区

技术上划分的体系结构是根据技术功能而不是离散的工作流来分离顶级组件的。这可能表现为受模型-视图-控制器分离或某些其他临时技术分区启发的图层。图8-7根据定制将组件分离。

优点

  • 明确分隔定制代码。
  • 与分层架构模式更紧密地结合。

缺点

  • 更高程度的全局耦合。对“公共”或“本地”组件的更改可能会影响所有其他组件。
  • 开发人员可能必须在公共层和本地层中复制域概念。
  • 通常在数据级别更高的耦合。在这样的系统中,应用程序和数据架构师可能会协作以创建单个数据库,包括定制和域。如果架构师后来想将这种体系结构迁移到分布式系统,那么反过来,将难以解决数据关系。

第二部分介绍了许多其他因素,促使建筑师决定要基于哪种建筑风格进行设计。

开发人员角色

开发人员通常会采用与架构师角色共同设计的组件,然后将它们进一步细分为类,功能或子组件。 通常,类和功能设计是建筑师,技术负责人和开发人员的共同责任,其中绝大部分是开发人员角色。

开发人员永远不要把建筑师设计的组件作为硬道理。 所有软件设计都受益于迭代。 相反,应将初始设计视为初稿,其中的实现将揭示更多细节和改进。

组件识别流程

组件识别最适合作为一个迭代过程,通过反馈产生候选对象并进行细化,如图8-8所示。

该周期描述了通用体系结构的展示周期。某些专业领域可能会在此过程中插入其他步骤或完全更改它。例如,在某些域中,某些代码在此过程中必须经过安全性或审核步骤。以下各节中将显示图8-8中每个步骤的说明。

识别初始组件

在为软件项目提供任何代码之前,架构师必须以某种方式基于他们选择的顶级分区类型来确定要使用哪些顶级组件。除此之外,架构师可以自由地构建所需的任何组件,然后将域功能映射到它们以查看行为应驻留的位置。尽管这听起来有些武断,但如果架构师从头开始设计系统,那么很难从更具体的内容开始。从最初的一组组件中获得良好设计的可能性简直令人难以置信,这就是为什么建筑师必须迭代组件设计以对其进行改进的原因。

将需求分配给组件

架构师确定了初始组件后,下一步便是将需求(或用户案例)与这些组件保持一致,以了解它们的适合程度。这可能需要创建新组件,合并现有组件或将组件分开,因为它们的职责过多。这种映射不一定是精确的-架构师正在尝试寻找一种良好的粗粒度基材,以允许建筑师,技术负责人和/或开发人员进行进一步的设计和改进。

分析角色和职责

在为组件分配故事时,架构师还将查看在需求期间阐明的角色和职责,以确保粒度匹配。考虑应用程序必须支持的角色和行为,可以使架构师调整组件和域的粒度。架构师面临的最大挑战之一是发现组件的正确粒度,这鼓励了此处介绍的迭代方法。

分析架构特征

在将需求分配给组件时,架构师还应该查看先前发现的架构特征,以考虑它们如何影响组件划分和粒度。例如,虽然系统的两个部分可能处理用户输入,但是处理数百个并发用户的部分将需要与仅需要支持少数几个部分的另一部分具有不同的体系结构特征。因此,尽管纯粹的组件设计功能视图可能会产生一个单独的组件来处理用户交互,但分析架构特征将导致细分。

重组组件

反馈对于软件设计至关重要。因此,架构师必须与开发人员不断地迭代其组件设计。设计软件会带来各种意想不到的困难-没有人能预见到在软件项目期间通常会发生的所有未知问题。因此,组件设计的迭代方法至关重要。首先,几乎不可能考虑所有会鼓励重新设计的发现和边缘案例。其次,随着体系结构和开发人员更深入地研究应用程序的构建,他们对行为和角色的位置有了更细致的了解。

组件粒度

为组件找到合适的粒度是架构师最艰巨的任务之一。 组件设计的粒度太细会导致组件之间的交流过多,从而无法获得结果。 太粗的组件会导致内部耦合度过高,从而导致可部署性和可测试性以及与模块化相关的负面副作用的困难。

元件设计

设计组件没有公认的“正确”方法。 而是,存在各种各样的技术,所有这些技术都具有各种折衷。 在所有过程中,架构师都要满足要求,并尝试确定哪些粗粒度的构建基块将构成应用程序。 存在许多不同的技术,所有这些技术都有不同的取舍,并且与团队和组织所使用的软件开发过程相关。 在这里,我们讨论了一些发现组件和陷阱的常规方法。

发现组件

架构师通常与其他角色(例如开发人员,业务分析师和主题专家)合作,根据系统的一般知识以及他们基于技术或领域划分的方式选择分解系统,来创建初始组件设计。 团队目标是一个初始设计,该设计将问题空间划分为粗略的块,并考虑了不同的体系结构特征。

实体陷阱

尽管没有一种确定组件的真正方法,但常见的反模式潜伏着:实体陷阱。 假设某个架构师正在为我们的kata Going,Going,Gone设计组件,最后得到类似于图8-9的设计。

image.png

在图8-9中,架构师基本上采用了需求中标识的每个实体,并基于该实体制作了Manager组件。 这不是架构; 它是框架到数据库的对象关系映射(ORM)。 换句话说,如果系统仅需要简单的数据库CRUD操作(创建,读取,更新,删除),那么架构师可以下载框架以直接从数据库创建用户界面。 存在许多流行的ORM框架来解决这种常见的CRUD行为。

当架构师错误地将数据库关系标识为应用程序中的工作流时,就会出现实体陷阱反模式,这种对应关系在现实世界中很少出现。相反,此反模式通常表明对应用程序的实际工作流程缺乏思考。使用实体陷阱创建的组件也往往过于粗粒度,就源代码的打包和整体结构而言,没有为开发团队提供任何指导。

演员/动作方法

参与者/动作方法是架构师用来将需求映射到组件的一种流行方式。在最初由Rational Unified Process定义的这种方法中,架构师确定了与应用程序一起执行活动的参与者以及这些参与者可能执行的动作。它提供了一种用于发现系统的典型用户以及他们可能会对系统执行哪些操作的技术。

行为者/动作方法与特定的软件开发过程结合在一起变得很流行,尤其是那些更倾向于前期设计的更正式的过程。当需求具有不同的角色和可以执行的各种操作时,它仍然很流行并且效果很好。这种类型的组件分解风格适用于所有类型的系统,无论是整体的还是分布式的。

活动存储

事件风暴作为一种组件发现技术,来自领域驱动设计(DDD),并在微服务中享有盛行,微服务也受到DDD的严重影响。在突发事件中,架构师假设项目将使用消息和/或事件在各个组件之间进行通信。为此,团队尝试根据需求和确定的角色确定系统中发生了哪些事件,并围绕这些事件和消息处理程序构建组件。这在使用事件和消息的微服务之类的分布式体系结构中效果很好,因为它可以帮助架构师定义最终系统中使用的消息。

工作流程方法

事件风暴的替代方法为不使用DDD或消息传递的架构师提供了一种更通用的方法。工作流方法为工作流周围的组件建模,非常类似于事件风暴,但是没有建立基于消息的系统的明确约束。工作流方法识别关键角色,确定这些角色从事的工作流类型,并围绕所识别的活动构建组件。

这些技术没有一个优于其他技术。所有这些都提供了不同的权衡。如果团队使用瀑布式方法或其他较旧的软件开发流程,则他们可能更喜欢Actor / Actions方法,因为它很通用。当使用DDD和相应的体系结构(如微服务)时,事件风暴与软件开发过程完全匹配。

案例研究: Going, Going, Gone: 发现组件

如果团队没有特殊约束,并且正在寻找良好的通用组件分解方法,则Actor / Actions方法可以很好地用作通用解决方案。 这是我们在案例研究中使用的Going,Going,Gone。

在第7章中,我们介绍了Going,Going,Gone(GGG)的体系结构kata,并发现了该系统的体系结构特征。 该系统具有三个明显的角色:投标人,拍卖人和这种建模技术(系统)的内部参与者(经常参与内部活动)。 角色与应用程序交互(由系统在此处表示),该角色标识应用程序何时启动事件,而不是角色之一。 例如,在GGG中,拍卖完成后,系统会触发付款系统处理付款。

我们还可以为这些角色中的每个角色确定一组初始操作:

投标人
查看实时视频流,查看实时出价流,出价

拍卖
将实时出价输入系统,接收在线出价,将商品标记为已售

系统
开始拍卖,付款,跟踪竞标者活动

有了这些动作,我们可以为GGG迭代地构建一组启动器组件。 一种这样的解决方案出现在图8-10中。

image.png

在图8-10中,每个角色和动作都映射到一个组件,而组件又可能需要在信息上进行协作。这些是我们为此解决方案确定的组件:

VideoStreamer
向用户流式传输实时拍卖。

BidStreamer
流式传输出价给用户。 VideoStreamer和BidStreamer都向竞标者提供了拍卖的只读视图。

BidCapture
该组件捕获来自拍卖商和投标人的投标。

BidTracker
跟踪出价并充当记录系统。

拍卖会
开始和停止拍卖。当投标人结束拍卖时,执行付款和解决步骤,包括通知投标人结束。

付款
第三方付款处理器,用于信用卡付款。

参考图8-8中的组件标识流程图,在对组件进行初始标识之后,架构师接下来将分析架构特征,以确定这是否会改变设计。对于该系统,架构师可以肯定地标识出不同的架构特征集。例如,当前的设计具有一个BidCapture组件,以捕获来自投标人和拍卖人的出价,这在功能上是有意义的:从任何人那里获取投标都可以被相同地处理。但是,围绕出价捕获的架构特征又如何呢?拍卖师不需要与可能成千上万的竞标者相同级别的可伸缩性或弹性。同样,架构师必须确保拍卖师的架构特征(如可靠性(连接不会断开)和可用性(系统已启动))可以高于系统的其他部分。例如,如果竞标者无法登录网站或连接中断而对业务不利,那么如果这两种情况发生在拍卖师身上,对拍卖来说都是灾难性的。

因为它们具有不同级别的架构特征,所以架构师决定将Bid Capture组件分为Bid Capture和Auctioneer Capture,以便两个组件中的每个组件都可以支持不同的架构特征。更新后的设计如图8-11所示。

架构师为Auctioneer Capture创建了一个新组件,并将信息链接更新到Bid Streamer(以便在线出价者查看实时出价)和Bid Tracker(后者管理出价流)。请注意,出价跟踪器现在是将两个截然不同的信息流统一在一起的组件:来自拍卖师的单个信息流和来自投标人的多个信息流。

image.png

图8-11中所示的设计不一定是最终设计。 必须发现更多的要求(人们如何注册,围绕付款的管理职能等等)。 但是,此示例提供了一个很好的起点,可以开始对设计进行进一步的迭代。

这是解决GGG问题的一组可能的组件,但不一定正确,也不是唯一的。 很少有软件系统只有开发人员可以实现它们的一种方法。 每个设计都有不同的权衡。 作为建筑师,不要着迷于找到一个真正的设计,因为许多设计就足够了(而且不太可能过度设计)。 相反,尝试客观地评估不同设计决策之间的权衡,并选择权衡最差的决策。

架构Quantum Redux:整体架构与分布式架构之间的选择

回顾“架构量子和粒度”中定义架构量子的讨论,架构量子定义了架构特征的范围。反过来,这又导致建筑师在完成初始组件设计时做出了重要决定:架构是整体的还是分布式的?

整体式体系结构通常以单个可部署单元为特征,包括可在流程中运行的系统的所有功能,通常连接到单个数据库。整体式体系结构的类型包括分层的模块化整体式,将在第10章中进行全面讨论。分布式体系结构则相反—应用程序由运行在各自生态系统中的多个服务组成,并通过网络协议进行通信。分布式体系结构可能具有更细粒度的部署模型,其中每个服务根据开发团队及其优先级可能具有自己的发布节奏和工程实践。

每种体系结构样式都提供了各种权衡,这在第二部分中进行了介绍。但是,基本的决定取决于架构在设计过程中发现多少个量子。如果系统可以用单个量子(换句话说,一组体系结构特征)进行管理,则整体体系结构将提供许多优势。另一方面,如GGG组件分析所示,组件的不同体系结构特征要求分布式体系结构来适应不同的体系结构特征。例如,VideoStreamer和BidStreamer都向竞标者提供拍卖的只读视图。从设计的角度来看,架构师宁愿不处理混合了大规模更新的只读流。除了上述投标人和拍卖人之间的差异外,这些差异还导致建筑师选择分布式架构。

在设计过程的早期确定架构的基本设计特征(整体还是分布式)的能力凸显了使用架构量子作为分析架构特征范围和耦合的一种优势。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容