《系统架构设计》-03-软件结构体系和架构风格
文章目录
1. 软件结构体系
-
什么是软件结构体系
软件体系结构包括一组软件组件、软件组件的外部可见特性及其相互关系,强调软件设计必须从系统中抽象出某些信息,所以软件体系结构设计本质是一种抽象工作。 -
软件结构体系的作用
随着软件复杂度的提升及软件开发技术的进步,更多的业务系统已不再或不需要关注于算法设计和数据结构,系统的层次化设计理念及高层次的结构化设计成为系统开发成败的决定性因素,即软件体系结构应运而生。
1.1 抽象(Abstract)
1.1.1 抽象的应用
作为系统分析和架构设计的基本手段之一,在面向对象领域、设计模式领域及本节的软件体系结构领域都有广泛应用。
1.1.2 不同层次的抽象
如图,是抽象的层次的发展过程,体现了不同时期使用的抽象方法。
体系结构级别抽象的结果就是软件体系结构
,它是系统总体结构的表现形式,是整个软件抽象的最高层次。
1.2 组件(Component)
1.2.1 定义
是软件体系结构的基本元素
1.2.2 切入点
举例:
- 功能特性:描述组件所实现的整体功能
- 非功能特性:描述组件的执行效率、处理能力、环境假设和整体特性
- 结构特性:描述组件如何与其他部件集成在一起,以构成系统信息
- 家族特性:描述了相同或相关组件之间的关系
在软件开发生命周期中,软件体系结构设计作为一种高层设计位于需求分析和底层设计之间,如图2-2所示。通过开发特定应用领域的体系结构并形成组织级别的体系结构库是一项有价值的组织过程资产建设工作。
CSDN水印打的比较坑,遮盖部分是“可复用设计库”
1.3 组织过程资产(Organizational Process Assets)
1.3.1 定义
是指一个组织中用于影响项目成功的所有资产总和。
如:规章制度、指导方针、规范标准、操作程序、工作流程、行为准则和工具方法等
1.3.2 作用
通过建立体系机构的作用:
- 促进广大开发人员形成习惯性模式
- 促进对设计的重用
- 提供软件可视化视图
- 便于相关关系人理解系统
1.4 体系结构
1.4.1 定义和表现形式
是一个高度抽象的层次,本文归为3种主要的表现形式
风格
(Style):描述某一特定应用领域中系统组织和表现的惯用方式- 模式(Pattern):
- 模型(Model):
1.4.2 作用
- 可以明确如何对软件设计成分进行整理和安排,并且对这些整理和安排加以限制,从而形成一种设计软件的特定套路
- 能够反映领域中众多系统所共有的结构和语义特性,并为我们提供描述系统的术语表和一组指导构建系统的规则
后文将对这三种表现形式进行讲解。
2. 架构风格
描述某一特定应用领域中系统组织和表现的惯用方式
2.1 分布式
现代软件开发的一大现状和问题就是容易形成信息孤岛,分布式作为一种基础架构风格为不同系统之间的交互提供通信范式,从而有效屏蔽底层平台细节。分布式架构风格有以下3种主要的表现形式:
2.1.1 消息传递
通过消息传递的相互作用可以实现分布式。
消息传递系统模型
- 客户端:消息的发送者、生产者
- 服务端:消息的接受者、消费者
- 通道:在消息传递系统中连接消息发送者和接收者的媒介
- 端点:提取专门与消息通道发生直接交互的组件,以解耦客户端和服务端
业务服务整合
通过消息传递可以进行业务服务整合。
下图是基于消息传递的服务整合思路:
其中包含一个独立的业务服务。该业务服务的输入来自于输入通道,而输出则通过输出通道传递到别的消费者。
通常,在由生产者作为起点、消费者作为终点的业务链路中可能存储一个或多个业务服务。这些业务服务都通过消息通道进行数据传输。显然消息通道具备良好的扩展性,一个消息通道的输出可以作为另一个消息通道的输入,多个消息通道即构成一个消息通道链。我们可以把业务服务嵌入到消息通道链的相应节点中,从而构成完整的业务链路。
2.1.2 发布-订阅
发布-订阅风格,通过异步交换事件来提供分布式架构所需实现的交互功能
结构图
下图为该种风格的结构图。
- 事件是整个结构能够运行所依赖的基本数据模型
- 发布者发布事件
- 订阅者关注自身所想关注的事件
- 发布者和订阅者并不需要感知对方的存在,两者之间通过传输事件的基础设施进行完全解耦。
场景示例
场景需求:
- 存在两个独立的系统,分别为系统A和系统B。
- 系统A负责管理用户
- 系统B需要在当系统A中用户被创建和更新时能够获取通知并进行响应。
这是一个分布式的业务场景。我们可以通过远程方法调用等方式进行系统A和系统B之间跨进程的交互,但更好的一种方式是通过触发创建和更新事件并进行解耦。
使用发布-订阅风格
- 先声明事件,系统内部或外部的组件都可以对这些事件中进行订阅。
- 当触发一个事件时,系统会自动调用在这个事件中注册的所有过程。
该图中:
- 声明了用户创建事件(UserCreatedEvent)和用户更新事件(UserUpdatedEvent),
- UserCreatedHandler和UserUpdatedHandler分别是针对这两个事件的处理程序
- 通过事件分离器(EventDispatcher)把所有Event和Handler进行关联,相当于传输事件的基础设施
- Event和Handler提供了高层次的抽象,具体的Event和Handler实现者可以分布在不同的业务系统中从而构成分布式运行环境
2.1.3 Broker
常见的一种架构风格,分布式系统之间通过远程过程调用(Remote Procedure Call,RPC)进行相互作用。
服务端应用组件注册自己到Broker,通过暴露接口的方式允许客户端接入服务。客户端则通过Broker发送请求,Broker转发请求到服务端,调用服务端应用组件并将生成的结果回发给客户端。通过Broker,业务服务之间可以通过发送请求访问远程的服务:
Broker中存在几个核心组件:
- 客户端:
- 服务器端(Server)
- 代理(Proxy)
- 客户端Broker:通过客户端Broker,客户端可以发现远程服务
客户端Broker保证了通讯的透明性,使Client调用远程服务就像调用本地的服务一样。
- 服务端Broker:提供注册服务的接口给Server
调用过程:
- 服务器端Broker需要提供注册服务的接口给Server
- 客户端可以通过客户端Broker发现远程服务,客户端Broker保证了通讯的透明性,使Client调用远程服务就像调用本地的服务一样。
- 通过客户端Broker和服务器端Broker之间的数据发送和接收实现分布式环境下的远程通信。
2.1.4 三种架构对比
消息传递
机制同时面向一对一和一对多,加之技术和时间上的解耦,使得以各种消息中间件为代表的消息传递系统在分布式系统的构建过程中得到广泛应用。
2.2 事件驱动架构(Event Driven Architecture,EDA)
2.2.1 事件处理系统的抽象和设计
设计初衷
由于异步性和并发性,导致:
- 事件到达的时机是系统无法提前确定的
- 一个系统中往往需要同时处理多种事件类型。
因此,对事件的处理,我们同样需要设计一种抽象的框架和流程。
把事件驱动系统按照事件的来源和处理过程分成3个层次:
- 事件源:
套接口(Socket)即是事件产生的源头 - 事件分离器
操作系统级别的select/poll/epoll程序对应事件分离器 - 事件响应程序
业务系统中各种应用程序
通过这种抽象,我们认识到在网络通信模型中,事件源和事件分离器往往并不属于应用程序所能控制的范围之内,应用程序能做的只是对事件的响应。
一个事件在其生命周期中一般会经历发起、接收、分离、分发、处理等阶段。当事件到达事件处理系统,是采用多线程等待事件发生的方式处理事件,亦或采用单线程无限制阻塞单一事件源的方式处理事件,还是两者兼而有之,我们同样面临设计上的需求和选择。一般认为,事件处理系统需要能够处理多个事件源;同时,能够封装事件分离和分发操作,也就是说事件的分离和分发操作应该对应用系统透明;最后,应用程序对事件的处理过程可以串行化以简化开发。在具体介绍事件处理系统的实现模式之前,我们先来看一下IO(Input / Output,输入/输出)操作和事件驱动之间的关系。
2.2.2 IO操作与事件驱动
1)概述
-
内核空间和用户空间
- 内核空间(Kernel Space):内核空间主要存放内核代码和数据,是供系统进程使用的空间。
- 用户空间(User Space):用户空间主要存放的是用户代码和数据,是供用户进程使用的空间。
-
IO操作都两个阶段:
- 将网络收到的数据拷贝到内核空间的临时缓冲区中
- 将内核空间临时缓冲区中的数据拷贝到用户空间缓冲区中。
2)主流的IO操作模式
- 阻塞IO(Blocking IO,BIO)
在默认情况下,所有套接口都是阻塞的,意意味着IO的发起和结束都需等待。
任何一个系统调用都会产生一个由用户态到内核态切换,再从内核态到用户态的切换的过程。而进程上下文切换是通过系统中断程序来实现的,需要保存当前进程的上下文状态。这是一个成本很高的过程。
-
非阻塞IO(Non-blocking IO,NIO)
当我们把套接口设置成非阻塞时,就是由用户进程不停地询问内核某种操作是否准备就绪。这就是我们常说的轮询(Polling)。这同样是一件比较浪费CPU的方式。 -
IO复用
主要依赖于操作系统提供的select和poll机制。这里同样会阻塞进程,但是这里进程是阻塞在select或者poll这两个系统调用上,而不是阻塞在真正的IO操作上。另外还有一点不同于阻塞IO的就是,尽管看起来IO复用阻塞了两次,但是第一次阻塞是在select上时,select可以监控多个套接口上是否已有IO操作准备就绪,而不是像阻塞IO那样,一次只能监控一个套接口。
- 信号驱动IO
过程:
- 通过sigaction系统调用注册一个信号处理程序,主程序可以继续向下执行
- 当所监控的套接口有IO操作准备就绪时,由内核通知触发前面注册的信号处理程序执行
- 将我们所需要的数据从内核空间拷贝到用户空间。
- 异步IO(Asynchronous IO,AIO)
- 和信号驱动IO
- 信号驱动IO:是由内核通知我们何时可以进行IO操作
- 异步IO:是由内核告诉我们IO操作完成了。
- 和信号驱动IO
具体来说就是,信号驱动IO中当内核通知触发信号处理程序时,信号处理程序还需要阻塞在从内核空间缓冲区拷贝数据到用户空间缓冲区这个阶段,而异步IO直接是在第二个阶段完成后内核直接通知可以进行后续操作。
结合图2-8中的各个IO模型效果图,我们发现前4种IO模型的主要区别是在第一阶段,因为它们的第二阶段都是在阻塞等待数据由内核空间拷贝到用户空间;而异步IO很明显与前面4种有所不同,它在第一阶段和第二阶段都不会阻塞。
2.2.3 Reactor模式
1)基本模式
Reactor模式定义事件循环(Event Loop),利用操作系统事件分离器支持单线程
在一系列事件源
上同步
等待事件。体现的是IO复用思想
。
其过程如下图所示:
- 支持多个事件源响应
- 响应的方式是使用一个单线程构建事件循环。这个事件循环是一个死循环,一直阻塞等待事件的发生。
- 当事件发生时,事件循环从操作系统提供的事件分离器中获取事件,并将事件逐个分发对应的事件响应程序
- 事件响应程序对它的事件作出同步处理。
2)组件化的表现形式
如图是Reactor模式的另一种更加组件化的表现形式
图中实现 Client=》Handler=》Client 的请求响应式处理过程:
- Reactor相当于IO事件的派发器(Dispatcher)
- 事件接收器(Acceptor)接受Client连接,绑定该Client请求与对应的Handler,并向Reactor注册此Handler
- Handler进一步的层次划分如read、decode、compute、encode和send等操作。
- 当Handler处理完成时,Acceptor就会唤醒该Handler所绑定的Client
优缺点
- 优点:使用单线程处理多任务能够避免多线程所带来的复杂性
- 缺点:由于Handler的同步处理机制,Reactor模式适合于处理时间短且不阻塞IO的业务操作,有一定局限性。
3)异步Handler
上图中handle对事件处理是同步的,下边我们设计一个异步处理程序。
而,事件串行同步处理且每次分离和分发一个事件,为开发人员提供尽量简单的编程模型。如果希望提升Handler中业务处理的效率,可以优化部分步骤。图2-11中,组成Handler的decode、compute、encode等抽象步骤可以引入线程池(Thread Pool)中工作线程(Working Thread)的方式进行并行化,从而把同步Handler转化成部分异步的Handler。同时,也可以采用多个Acceptor线程构成线程组来避免单个Acceptor可能出现的性能瓶颈。
2.3 系统结构
2.3.1 切入点
划分系统结构的切入点:
- 系统如何与环境交互?
- 系统处理流程如何组织?
- 系统需要支持什么样的变化?
- 系统的生命周期?
围绕这些问题,我们可以抽象出一系列系统结构相关的架构风格。
2.3.2 分层结构
最常见的系统结构风格之一:
- 每一层次之间通过接口与实现的契约方式进行交互
- 可以严格限制跨层调用,也可以支持部分功能的跨层交互以提供分层的灵活性。
下图是典型的三层结构:
2.3.3 交互型结构
概述
- 目的:抽象组件之间的交互关系。
- 常见形式:MVC(Model View Controller)、MVP(Model View Presenter)、MVVM(Model View View Model)等
MVC风格
- View和Model之间存在直接交互
- 通过Controller我们也可以把这层直接交互关系转变成间接交互关系
MVP风格
- 明确地规定Model和View之间不应该存在直接交互
- Presenter中作为一种协调器同时保存着View和Model的引用,确保Model和View之间的数据传递通过Presenter集中进行。
下图是登录场景下的MVP模式的结构示例:
2.4 消息总线
2.4.1 作用
系统组件之间通过消息总线来进行通信,消息是组件间唯一的通信方式。
它负责消息的分派、传递和过滤,并返回处理结果可以支持组件的分布式存储和并发运行。
2.4.2 传递过程
消息总线风格可以视为分布式消息传递风格的一种扩展和延伸。
- 组件挂接在消息总线上,向总线登记自己所感兴趣的消息类型
- 生产者组件发出请求消息
- 总线把请求消息分派到系统中所有对此感兴趣的消费者组件
- 在接收到请求消息后,消费者组件将根据自身状态对其进行响应,并通过总线返回处理结果。
2.4.3 优缺点
- 优点:低耦合、高度扩展性
- 缺点:高复杂度
消息总线对消息具备丰富的预处理功能(包括消息路由、消息转换、和消息过滤),消除了数据传递在时间、空间和技术上的耦合,并提供了高度扩展性,但同时也为系统的设计和实现带来了不可避免的复杂度。
2.5 适配与扩展(管道-过滤器风格)
2.5.1 概述
管道-过滤器风格则是用于解决适配和扩展性问题的代表性架构风格。
- 过滤器(Filter)
功能组件被称为过滤器,负责对数据进行加工处理。
每个过滤器都有一组输入端口和输出端口,从输入端口接收数据,经过内部加工处理之后,传送到输出端口上。
- 管道(Pipe)
数据通过相邻过滤器之间的连接通路。
2.5.2 示例
需求:当输入一串包含大小写的英文字符时,需要把它们全部转换为大写,然后在末尾加一个感叹号。
设计方案:
- 存在通用的Filter和Pipe接口
- Writer Filter作为第一个入口Filter衔接Captial Pipe使数据流转到Captial Filter
- Captial Filter完成了把输入文字转变成大写的过程;
- 大写文字经由Exclamation Pipe到Exclamation Filter并实现在末尾加一个感叹号的效果。
我们可以根据这个风格体系构建出任意多个Filter和Pipe并组装到整个管道-过滤器链中,各个Filter和Pipe相互独立且又共同协作完成复杂业务逻辑。
2.5.3 管道-过滤器 VS 派发风格(Dispatching)
- 管道-过滤器结构是一种组合行为,主功能是以切面(Aspect)的方式实现
Servlet就是典型的管道过滤器结构的应用
- 派发体现的实际上是一种策略行为,主功能以事件实现
广泛应用在各种面向前端交互的事件响应型框架。
posted on 2023-02-20 10:02 运维开发玄德公 阅读(104) 评论(0) 编辑 收藏 举报 来源