「整洁架构」实战 MVC 架构重构到整洁架构

软件架构介绍

什么是软件架构

我们先来看看维基百科对 软件架构 的定义, 软件架构是有关软件整体结构与组件的抽象描述,用于指导大型软件系统各个方面的设计。简短却又看不太明白的一个定义,确实很抽象,第一眼看上去相信大多数都会有这么个感觉。自己三五年的工作经验,架构设计对自己来说还不急,自己再多学点XX技术,学的差不多了,再开始学架构设计吧。这种思维模式其实是不对的,往大了说,淘宝的架构需要架构设计,往小了说一个毕业生的毕业设计也需要架构设计,架构的核心不在于自己会多少高大上的技术,架构中用了多少技术,软件架构的终极目标是 用最小的人力成本来满足构建和维护该系统的需求。 所以在平时的开发过程中其实会有很多实践机会来锻炼你的架构设计能力,哪怕是在一次日常的 code review 中。

架构设计的价值

软件系统可以通过行为架构两个纬度来衡量实际价值。正常情况下程序员应该确保自己的系统在这两个维度上的实际价值都能长时间维持在比较好的状态 。但事实上,随着项目不断演进,最初的架构设计往往会为各种“行为”操作作出妥协,最终导致了软件不再“软”,使得系统的价值最终趋近于零。

行为价值
软件系统的行为是其最直观的价值维度。程序员的日常工作就是根据客户提供的需求,编写需求文档,然后按照需求文档将需求转换成可以工作的系统,从而给系统的使用者创造价值。当系统遇到问题时,那么就进行调试,解决问题。这其实就是大多数程序员的全部工作,按照需求文档,编写代码,修复bug... 对于一个普通的程序员来说这又有什么不对呢?一个软件由最开始比较完美的架构设计,慢慢的会为了功能作出各种妥协让步,从一个“软”件,慢慢的变硬,导致最后有了新的变更就变得痛不欲生,只能加班加点。造成这样的原因其实就是因为我们仅仅只是按照需求文档往系统上堆功能,遇到bug了打打补丁,让程序能够正常运行起来就好,最大化眼前的收益,却不知是在一步一步的侵蚀最初的架构。

架构价值
软件系统的第二个价值维度就体现在软件这个词上 : software。 “ware” 的意思是“产品”,而“ soft'’的意思,不言而喻,是指软件的灵活性。软件发明的目的,就是让我们可以以一种灵活的方式 来改变机器的工作行为 。 对机器上那些很难改变的工作行为,比如内存条,CPU,我们通常称之为硬件 ( hardware ) 。为了保证架构的价值,我们就要持续的保证软件系统一直是“软”的状态,也就是说软件应该是对外开放的,易于被修改的,在有需求增加或变更时,对应的软件必须是可以简单而方便的实现。不应该导致代码变更的成本与其实现功能改变不成比例。比如在一个系统中支持了微信支付用了3人天,后续新增了支付宝支付却用了5人天,而且还需要改大量微信支付的代码,这是不能接受的。因为对于客户来说,他们提出的系列的变更需求的范畴都是类似的,因此它们的成本也应该是相同的,但是对于开发者来说,系统持续不断的需求变更导致他们慢慢的忽视了软件架构的设计,从而导致系统变得越来越难维护,最后系统将变得无法修改。

成为一个合格的开发者,思考架构设计的行为价值最基本的,更加重要也必须重视的是在行为过程中必须要要高度警惕架构价值,思考如何才能让软件继续“软”下去。

从代码整洁到模块设计

编写整洁的代码

Bob 大叔的系列著作其实就是在引导一个刚入门的新手该如何慢慢进阶成一名靠谱的程序员。其中《代码整洁之道》教会我们如何写出易读、可扩展 、可维护、可重用的代码,打好基本功。《代码整洁之道:程序员的职业素养》教会我们怎样变成一个有修养的程序员。而《架构整洁之道》则是在技术上的一个进阶,从微观(代码层面)到宏观(架构层面),介绍了软件开发的三种编程范式,设计原则以及提出了整洁架构这一架构模式。
那么第一步,该如何编写整洁的代码呢?《代码整洁之道》引用了一个软件开发中的 5S 原则:

  • 整理:命名的规范
  • 整顿:把你的代码放在它应该在的位置
  • 清楚:整洁的代码
  • 清洁:代码风格、实践手段
  • 身美:不断改进

量化出来,在编写代码的时候虽然没有固定的标准,但有一个大体的规范:

  • 可读性永远是最重要的(想想你被迫看别人代码的样子)
  • 有意义的命名(变量名,方法名,函数名,类名,包名)
  • 只做一件事(降低耦合,提高可读性)
  • 减少依赖关系(提高程序健壮性)
  • 避免不必要的重复代码
  • 避免不必要的注释,尽量对代码段进行自解释
  • 通过所有测试

本 chat 的侧重点会在设计原则和整洁架构上,整洁的代码这块具体的细节就不赘述了,想要了解更多可以看《代码整洁之道》和《重构》。

再识 SOLID 原则

想要构建一个好的软件系统,编写整洁的代码尤为关键,就好比建筑需要好的转头一样。有了结实的转头才能继续修建坚固的墙和各种套间,最终构建成高楼大厦。S.O.L.I.D 原则则是告诉我们如何将数据和函数组合成类,以及如何将这些类连接成程序(类表一组数据和函数的组合),S.O.L.I.D 适用于架构中的中层组件和模块,其目的:

  • 构建出易于拓展的软件,接受变化,使的软件可以很容易的进行变更
  • 抽象化组件的设计,使的软件可以更好的表达自己,便于理解
  • 构建可复用的组件
Single Responsibility Principle 单一职责原则

SRP 原则在日常开发中被提到的应该也是最多的,大多数人对它的理解都是:每个类应该只做一件事,其实这是一个比较模糊的说法。比较准确的含义应该是:

任何一个软件模块应该只对某一类行为者负责

其中:

  • 软件模块指的是一个源代码文件(一组紧密相关的函数和数据结构)
  • 行为者指的是只有一个或多个有共同需求的用户

举个书中的例子,在一个工资管理系统中有一个 Employee

UTOOLS1590585594117.png

这个类中的三个函数很明显违背的 SRP 原则,三个函数也不应该在 Employee 中,计算薪水、计算工时和保存的操作应该分别在财务、人力和 DBA 中去做。那针对这种情况,该怎么做职责分离呢?
最简单粗暴的方式直接把数据和函数分离
UTOOLS1590586003097.png

当然也可以使用 Facade 模式,将具体的实现逻辑隐藏,对外只暴露简单的api
UTOOLS1590586261068.png

Open Closed Principle 开闭原则

OCP 原则最能体现出一个系统拓展性的能力,当新增一个需求时,能不能做到最小化甚至不需要对先有的代码做任何改动,一个良好的软件设计应该是易于拓展,同时抗拒修改。而想要做到这一点,就必须遵守 依赖反转原则,多变的一方应该依赖于稳定的一方,稳定的一方不应该依赖多变的实现。这样就可以在新增功能的同时,减少对内的修改(较稳定端的修改)。

Liskov Substiution Principle 里氏替换原则

LSP 指的是一种可替换性:如果对于每个类型为 S 的对象 o1 都存在一个类型为 T 的对象 o2,能使操作 T 类型的程序 P 在适应 o2 替换 o1 时行为保持不变,我们就可以将 S 称为 T 的子类型。它的主要作用就是用来指导继承关系和接口及其实现的原则。

Interface Segregation Principle 接口隔离原则

客户端不应该被迫依赖于它们不用的接口,如果一个接口包含了过多的方法,而不同的子类只需要实现部分的方法,那么就应该通过分离接口将其拆分,不同的子类实现不同的接口,实现对应的功能。在一般情况下,任何层次的软件设计如果依赖于不需要的东西,都会是有害的。

Dependency Inversion Principle 依赖反转原则

DIP 原则是非常重要的编码思维。从组件的设计到很多架构设计中 DIP 都扮演着非常重要的角色。DIP 指程序要依赖于抽象接口,不要依赖与具体实现,高层模块和底层模块都应该依赖于抽象,也就是所谓的面向接口编程。面向接口编程的好处就是在设计的时候可以忽略具体实现,设计定义抽象逻辑,这样设计出来的组件才会更加稳定(在面向对象语言中,接口相对具体类而言,改动几率小,更加稳定)。所以为了追求架构上的稳定,就必须要多使用稳定的接口,少依赖于多变的具体实现。DIP 的几个原则:

  • 在代码中多使用抽象接口,避免使用多变的具体实现类。对象的创建一般使用工厂模式创建
  • 不要在具体实现类上创建衍生类
  • 不要覆盖(override)包含了具体实现的函数(调用包含了具体实现的函数通常意味着引入了源代码级别的依赖)需要 override 时一般创建抽象函数,子类重写该抽象函数
  • 避免在代码中写入任何与具体实现相关的名字或其它容易变动的事物的名字
UTOOLS1590589064634.png

中间的曲线代表了软件架构中的抽象层与具体实现层的边界。所有跨越这个边界源代码级别的依赖都应该是单向的,即具体的实现层依赖于抽象层。

初识整洁架构

六边形架构(Hexagonal Architecture)、DCI(Data, Context, Interactive)架构,BCE(Boundary,Controller,Entity)架构

六边形架构

六边形架构是 Alistair Cockburn 在2005年提出,为了解决传统的分层架构带来的问题,六边形架构也算是一种分层架构,只不过是从内到外的分层,而不是上下分层。传统的三层架构(表示层,业务逻辑层,数据访问层)存在的一些问题:

  • 程序的核心逻辑可能会散落在不同的层里面,导致后续如果需要替换某一层难度会非常大
  • 对核心逻辑的测试非常困难
  • 核心逻辑会依赖到第三方的具体依赖,导致业务和具体的技术强绑定,变动困难
UTOOLS1590820385894.png

六边形架构又称为端口-适配器,六边形架构将系统分为内部和外部,内部代表了应用的核心业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。六边形架构的几个特点:

  • 关注点分离:软件的价值在与它的业务价值,所以重心放在业务逻辑上,将业务逻辑和外部的驱动(具体技术)分离开来,业务和技术是无关的,这样对于业务逻辑而言会更加稳定,也更容易测试
  • 外部可替换:一个端口对应多个适配器,它体现了对外部的抽象。内部不关心外部如何使用端口,从一开始就要假定外部使用者是可替换。比如对于数据持久化,对于业务逻辑来说是不需要知道自己用的是什么,只需要提供端口,然后会有对应适配器进行实现提供持久化功能
  • 依赖倒置:依赖倒置是六边形架构的基础,为了保证内部业务逻辑的稳定性,就必须做到不让内部依赖与外部组件,只能外部依赖与内部,这样外部才是可被替换的,实现这一点就需要使用依赖倒置,由驱动者适配器将被驱动者适配器注入到应用内部,端口的定义在应用内部,而具体的技术实现是由适配器完成
DCI 架构

DCI 是对象的 Data 数据对象使用的 Context 场景对象的 Interaction 交互行为三者简称, DCI 的重点是在不同的场景下的交互行为,是面向对象中状态和行为的一种范式设计。传统的 MVC 架构虽然结构简单清晰,但是对于业务逻辑的肢解防止随着项目的复杂,会越来约混乱,跟踪代码时会非常困难。DCI 最突出的亮点就是 基于用例驱动设计,这样更加符合用户的心智模型,代码即需求,基于用例的代码编写对与程序员后续理解起来也更方便。

整洁架构解析

整洁架构是将六边形架构、DCI架构和BCE架构的设计理念做了一个综合。这些架构都有一个共同的设计目标:按照不同的关注点对软件进行分割,至少有一层是包含软件的核心逻辑。具备的特点:

  • 独立于框架:整个系统的架构不会依赖于具体的架构,架构可以被当作工具来使用,但是不会让系统来是适应架构
  • 可被测试:系统的业务逻辑可以脱离UI、数据库、Web等其它外部元素独立测试
  • 独立于UI
  • 独立于数据库
  • 独立与其它任何第三方依赖
UTOOLS1590824388487.png
依赖规则

图中的同心圆代表软件系统中的不同层次,越靠近中心,其所在的软件层次就越高。外层圆代表的是机制,内层圆代表的是策略。源码中的依赖关系必须指向同心圆的内层,即底层机制指向高层策略。内层圆中的代码不能涉及外层圆中的代码,尤其是内层圆中的代码不应该引用外层圆代码中的变量,方法等。

整洁架构的几个概念

业务实体
业务实体这一层中封装的是整个系统的关键业务逻辑, 一个业务实体既可以是 一个带有方法的对象,也可以是一组数据结构和函数的集合。无论如何,只要它能 被系统中的其他不同应用复用就可以。

用例
软件的用例层中通常包含的是特定应用场景下的业务逻辑,这里面封装并实现了整个系统的所有用例 。 这些用例引导了数据在业务实体之间的流入/流出,并指挥着业务实体利用 其中的关键业务逻辑来实现用例的设计目标。用例这一层相对于 业务实体 来说是外层,所以对于这一层发生的变化也不应该影响到内层,所以 业务实体层 也不应该依赖用例层。同时 用例层 相对于外部框架/基础设施来说是内层,所以用例层也不应该直接依赖于它们。

接口适配器
软件的接口适配器层中通常是一组数据转换器,它们负责将数据从对用例和业 务实体而言最方便操作的格式,转化成外部系统(譬如数据库以及 Web)最方便操作的格式。例如,这一层中应该包含整个GUI MVC框架。展示器、视图、控制器都属于 适配器,对于业务的处理,需要从控制器传递给用例层,用例层作为业务逻辑处理的入口,最终返回结果为控制器。

这一层也会负责将数据格式从用例和实体层最方便的格式转换为所使用的持久化框架最方便的格式,也就是说在这一层会将业务实体的数据格式转换为持久化数据库所需要的 PO 格式,这个转换是在 接口适配器层,这样未来一旦换了持久化框架,那么也不会影响到内层逻辑。

同时,这一层也会负责将来自外部的数据格式转换为系统内部业务实体所需要的格式。比如在一个微服务架构的系统中,在A服务中调用B服务,那么这个过程就在 接口适配器层 中,并且会将返回的数据格式转换成当前服务业务实体需要的格式。

跨越边界

图中每一个同心圆都表示一个边界。源代码级别的依赖关系一定是外层依赖与内层。层次越往内,那么其抽象和策略的层次越高,最内层的圆中包含的是最通用的,最高层的策略,最外层的圆包含的是最具体的实现细节。那么出现内层一定会调用外层这种情况,比如用例层需要调用网关适配器,进行数据的持久化,按照整洁架构的原则是不允许这样操作,这个时候就需要采用依赖反转原则来解决这种相反性。在用例层定义需要持久化的策略,一般是一个接口,在网关适配器中实现这个接口调用具体的持久化框架进行功能实现,这样就可以避免用例层直接依赖适配器层。

插件化思维

当我们希望设计出来的系统的具备更好的可拓展性,可维护性,那么就必须在设计的时候具备插件化思维,抽象化使用者的场景,将这些场景进行收敛,封装成抽象的高层策略暴露为使用者。一般情况会使用 约定/注入 方式。比如 Spring 框架给使用者提供各种前置/后置处理器策略,允许使用者通过自定义处理器实现,然后注入到 Spring 中,Spring 根据对应的策略处理用户注入的事件。那么对软件系统来说,用例层,业务实体层封装的就是核心逻辑和高层策略,这些策略就可能包含了对数据持久化的策略,对第三方依赖调用时的策略,或者是提供策略,暴露给调用者,那么调用者编写对应符合该策略的功能代码,那么就可以使用我们提供的内置功能,比如SonarQube允许开发者上传自己的代码质量测试实现。

延迟抉择思维

在最初的架构设计中完成了高层策略的设计后,那么对于细节的技术实现该怎么选择呢?比如数据持久化,数据展示等。延迟抉择可以帮到我们,在一个敏捷开发团队中都会采用迭代思想来交付项目,按照精益思想,我们应该在每个迭代中减少浪费,最大化交付价值。那么什么叫做浪费呢?在当前迭代和迭代目标不相关的都算是浪费,如果团队做了一件事,而这件事在下个迭代才产生价值,那么这件事也算是浪费。那么这也是 延迟抉择 想要表达的,如果一件事没有到必须要做决定,必须要做的时候,那么就先搁置它,直到必须要做的时候,因为到了必须要处理的时候,对这件事的理解程度才会达到最大化。那么可以这么做的前提就是必须要做好高层策略的设计,比如数据持久化,在一开始业务可能只是需要保存简单的数据,那么这个时候是不是真的需要一个数据库呢?一个简单文档存储在当前阶段是不是更能带来价值呢?

实战 :MVC 架构重构整洁架构

项目背景介绍

前面唠叨了这么多理论知识,接下来会演示一个项目实战,一个最常见的 mvc 架构的代码,我们该怎么把它重构成 整洁架构。这个系统是一个比较简单的微服务系统,虽然麻雀虽小,但五脏俱全。(为了快速演示整个重构的套路,所以代码中没有涉及到测试相关的代码,一般情况下,在重构前一定是需要有测试来保证整个重构过程的。)


UTOOLS1590843356943.png

整个系统包含两种角色,学生和老师

  • 学生:有两个功能。1. 可以发表笔记;2.删除笔记(如果该笔记是优秀笔记,那么也会把这个状态记录删除)
  • 老师:有两个功能。1. 将学生的笔记标记为优秀笔记,标记成功会请求 通知服务,发送标记成功的通知;2. 将学生的笔记取消优秀状态,然后请求 通知服务,发送取消成功通知

MVC 版代码和遗留问题介绍

mvc 版的代码可以查看github。当前版本中也还有很多的坏味道和遗留的一些问题,在重构的时候我们会一起重构;

  • 代码中充斥着大量的 map
  • 业务逻辑和具体的技术实现强绑定在一起,互相依赖
  • 没有 domain model,直接把 PO 当 domain model 用
  • API 请求和返回没有对应的 model

重构手法

重构之后的代码查看 clean-arch分支,里面包含了每一步重构的commit,可以 checkout 到对应的 commit 进行查看

UTOOLS1590849608132.png

1. 分层

1.1 创建 adapter 和 domain
分别创建 adapter/inbound/rest/resourcesadapter/outbound/gatewayadapter/outbound/persistencedomain

1.2 移动 controller
controller 内的类移动到 adapter/inbound/rest/resources 下并按照领域分包

1.3 移动 repositories,feign 和 services

  • controller 中的请求入参封装为对应的 xxRequest 对象。
  • controller 中的返回值数据封装为对应的 xxResponse 对象。
  • repositories/ 放到 adapter/outbound/persistence/
  • feign/ 放到 adapter/outbound/gateway/
  • services/ 下的内放到 domain/
2. 拆解 PO 和 domain model

models/ 放入到 domain 下放入对应的领域包下,然后拷贝一份到 adapter/outbound/persistence/下改名为 XXPO,接着把 domain 下的对象把和持久化相关的配置全部删除。

3. 使用 DIP 移除对持久层的直接依赖

目前 domain service 中对数据的操作还是直接调用的 jpaRepository,需要使用 DIP 进行依赖反转。

  • adapter/outbound/persistence/ 下的 repository 更名为 repositoryVendor
  • domain/xx/ 下创建对应的接口 xxRepository,将之前对直接调用 xxRepository中的方法定义在该接口中
  • adapter/outbound/persistence/xx/ 下创建实现类 xxRepositoryImpl 实现xxRepository对应的功能
  • domain/xxService 中使用 DI 注入 xxRepository 解除对 jpaRepository 的直接依赖
4. 使用 DIP 移除 domain 对 gateway 的依赖

使用 DIP 原则进行依赖倒置,解除 domain 层对 gateway 层的直接依赖(Spring feign)

5. 抽取 usecase
  • 将 domain service 中的业务逻辑按需求用例梳理,放入到用例层 application/usecases/,一般情况下会将每个业务实体拆解成 editquery两类用例,分别对外提供编辑和查询的入口。原 controller 中对 domain service 的调用改为依赖对应的 xxUseCase
  • domain/notification 相关代码放入到 application/gateway/notification 下,通知的 context 在当前服务不属于核心领域,相对于笔记服务是第三方服务
6. 移除代码中存在内层依赖外层的情况

外层可以依赖与内层,内层不能依赖与外层。比如在 application 层依赖了 adapter 中的 xxRequest。出现这种数据对象的依赖问题,可以把外层的对象里面的数据当作基本类型的参数传递给内层,如果参数过多,也可以在内层创建对应的 xxDto,在外层构造好这个 xxDto 当作参数传递给内层。

重构经验总结

严格模式

如果团队内的编码水平存在明显的差距。那么建议严格按照实例的架构模式,也就是每个请求必须由 controller 进来调用 usecase,然后 usecase 在调用对应的 domain service,即使 usecase 或 domain service 可能只是一层简单的代理,那么也要按照这种模式写,虽然比较啰嗦,但是长期来看,可以很好的解决代码混乱的问题。

宽松模式

为了避免 usecase 或 domain service 只是做了一层代理,在宽松模式下,我们可以根据当下的情况进行抉择是否需要这一层,是否可以直接跳过这一层

  • controller 中只存在最简单的 CURD 时,那么 controller 完全可以直接注入对应的 repository
  • 当出现跨上下文即有一个场景需要调用到多个 domain service 时,那么就需要创建对应的 usecase,在 usecase 中完成跨上下文的操作
  • usecase 和 domain service 中出现的数据对象一般被命名为 xxDto,如果这个 xxDto 只在 usecase 内用到,那么应该放到 usecase 这一层
  • 同层之间可以互相依赖
  • 不应该存在循环依赖
跨层数据的转换

基于内层不能依赖与外层的数据的原则。如果外层想要传递一个外层的数据对象到内层,那么一般情况会将外层的这个数据对象拆解,把里面的变量当作参数传递给内层。如果传递的变量个数比较多,那么就可以在内层创建一个对应的 xxDto,现在外层构造这个 xxDto,然后将这个 xxDto 传递给内层。

参考文献

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

推荐阅读更多精彩内容