目录
1.实现细节
1.1.数据库只是实现细节
1.2.Web是实现细节
1.3.应用程序框架是实现细节
1.4.案例分析:视频销售网站
1.5.拾遗
1.5.1.按层封装
1.5.2.按功能封装
1.5.3.端口和适配器
1.5.4.按组件封装
1.5.5.组织形式和封装的区别
2.总结
1.实现细节
1.1.数据库只是实现细节
为了应对磁盘访问速度带来的限制,业界逐渐发展出了两种截然不同的系统:文件系统与关系型数据库系统。数据库终究只是在硬盘与内存之间相互传输数据的一种手段而已。
当问题涉及数据存储时,这方面的操作通常是被封装起来,隔离在业务逻辑之外的。也就是说,我们确实需要从数据存储中快速地存取数据,但这终究只是一个底层实现问题。我们完全可以在数据访问这一较低的层面上解决这个问题,而不需要让它与系统架构相关联。
1.2.Web是实现细节
GUI只是一个实现细节。而Web则是GUI的一种,所以也是一个实现细节。作为一名软件架构师,我们需要将这类细节与核心业务逻辑隔离开来。
或者说:Web只是一种I/O设备。早在20世纪60年代,我们就已经了解编写设备无关应用程序的重要性。
1.3.应用程序框架是实现细节
不要让框架污染我们的核心代码,应该依据依赖关系原则,将它们当作核心代码的插件来管理。eg:千万别在业务对象里到处写@autowired注解。业务对象应该对Spring完全不知情才对。
当然,有一些框架是避免不了使用的。比如你在用Java,那么标准类库也是不太可能避免使用的。
当我们面临框架选择时,尽量不要草率地做出决定。在全身心投入之前,应该首先看看是否可以部分地采用以增加了解。另外,请尽可能长时间地将框架留在架构边界之外,越久越好。因为谁知道呢,也许你可以不用买奶牛也能喝到牛奶。
1.4.案例分析:视频销售网站
以线上收费视频网站为例,计划向个人或者企业提供一批收费的线上教学视频。个人用户通常既是购买者又是观看者。而企业用户则不同,他们购买视频通常是用来给其他人观看的。视频作者需要负责上传视频文件、写简介、提供习题等。管理员需要负责增加新的视频播放列表,往视频播放列表里添加和删除视频,并且为各种许可类型设置价格。
系统架构设计中的第一步,是识别系统中的各种角色和用例。
典型用例分析
初步组件架构图
双实线代表了系统架构边界,可以看到这里将系统划分成视图、展示器、交互器、控制器和工具类这几个组件。可以很简单地将它们交付为5个.jar文件;也可以将视图和展示器放在一个.jar文件中,而将其他所有的组件合并为另一个.jar文件。动态、灵活地根据系统变更来调整部署方式。
控制流是从右向左的。输入发生在控制器端,然后输入的数据经交互器处理后交由展示器格式化出结果,最后由视图来展示这个结果。请注意,图中的箭头并不是一直从右向左的。事实上大部分的箭头都是从左向右的。这是因为该架构设计要遵守依赖关系原则。所有跨越边界的依赖关系都应该是同一个方向,而且都指向包含更高级策略的组件。
另外,还应该注意一下图中的“使用”关系(开放箭头),它和控制流方向是一致的;而“继承”关系(闭合箭头)则与之相反,它反映的是我们对开闭原则的应用,通过调整依赖关系,可以保证底层细节的变更不会影响到高层策略组件。
存在两个维度上的隔离。第一个是根据单一职责原则对所使用的系统的各个角色进行了隔离,第二个则是对依赖关系原则的应用。这两个维度的隔离都是为了将不同变更原因和不同变更速率的组件分隔开来。
1.5.拾遗
假设正在构建一个在线书店,这个例子的任务是实现一个客户查看订单状态的用例。
1.5.1.按层封装
“按层封装”即传统的水平分层架构,是最简单的。在这种常见的分层架构中,Web代码分为一层,业务逻辑分为一层,持久化是另外一层。在Java中,分层的概念通常是用包来表示的。存在问题是:很快就会发现将代码分为三大块并不够,需要进一步进行模块化。
Java类说明:
OrdersController:Web控制器,类似Spring MVC控制器,负责处理Web请求。
OrderService:定义订单相关业务逻辑的接口。
OrderServiceImpl:Order服务的具体实现。
OrdersRepository:定义如何访问订单持久信息的接口。
JdbcOrderRepository:持久信息访问接口的实现。
1.5.2.按功能封装
按功能封装,即垂直切分,根据相关的功能、业务概念或者聚合根来切分。在常见的实现中,所有的类型都会放在一个相同的包中,以业务概念来命名。相比之前,它们都被放到了同一个Java包中,比较容易找到“查看订单”相关代码。
1.5.3.端口和适配器
我们可以创造出一个业务领域代码与具体实现细节(数据库、框架等)隔离的架构。内部区域包含了所有的领域概念,而外部区域则包含了与外界交互的部分(例如UI、数据库、第三方集成等)。这里主要的规则是,只有外部代码能依赖内部代码,反之则不能。
1.5.4.按组件封装
在按层封装结构中,OrdersController可以在某些情况下绕过了OrderService类,直接调用OrdersRepository,绕过业务逻辑层是不合理的,尤其是在业务逻辑层要控制权限的情况下。
一个架构设计原则——内容是“Web控制器永远不应该直接访问数据层”,如何执行?采用静态分析工具约束不大持久,作者个人更倾向选择能够让编译器执法的做法。
“按组件封装”是指将一个粗粒度组件相关的所有类放入一个Java包中。这就像是以一种面向服务的视角来构建软件系统,与微服务架构类似,在这里,“按组件封装”将UI与粗粒度组件分离。这种方式将“业务逻辑”与“持久化代码”合并在一起,称为“组件”。组件是部署单元。组件是系统中能够部署的最小单位,对应在Java里就是jar文件。
“按组件封装”的好处是,如果我们需要编写和订单有关的代码,只有一个位置需要修改——OrdersComponet。在这个组件中,仍然应该关注重点隔离原则,但这是组件内部问题,使用者不需要关心。
1.5.5.组织形式和封装的区别
我经常遇到的一个问题是,Java中public访问控制修饰符的滥用。从另外一个角度来看,如果我们将Java程序中的所有类型都设置为public,那么包就仅仅是一种组织形式了(类似文件夹一样的分组方式),而不是一种封装方式。
以“按层封装”为例,OrderService与OrderRepository需要public修饰符,因为包外的类需要依赖它们。然而,具体实现类(OrderServiceImpl和JdbcOrdersRepository)则可以设置更细致的访问权限(包范围内的protected)。不需要有人依赖它们,它们是具体的实现细节。
以“组件封装”为例,OrdersComponet接口有来自Controller的依赖关系,但是其他类都可以设置为包protected。Public类型越少,潜在的依赖关系就越少。现在包外代码就不能再直接使用OrdersRepository接口或者其对应的实现,我们就可以利用编译器来维护架构设计原则了。
最好能利用编译器来维护所选的系统架构设计风格,小心防范来自其他地方的耦合模式,例如数据结构。所有的实现细节都是关键的!
带有访问修饰符的类型被虚化了
2.总结
数据库只是实现细节,Web是实现细节,应用程序框架是实现细节,应与业务逻辑解耦。
在视频销售网站示例中,系统架构设计中的第一步,是识别系统中的各种角色和用例。将系统划分成视图、展示器、交互器、控制器和工具类这几个组件。控制流是从右向左的,通过调整依赖关系,可以保证底层细节的变更不会影响到高层策略组件。
在拾遗一章,作者分别使用按层封装、按功能封装、端口和适配器和按组件封装四种方式划分了组件。由于Java中public访问控制修饰符的滥用,作者提出:最好能利用编译器来维护所选的系统架构设计风格,小心防范来自其他地方的耦合模式。