软件架构设计的985

Posted 从零开始学架构

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件架构设计的985相关的知识,希望对你有一定的参考价值。

本文目的:许多软件工程师,甚至超过10+年工作经验的工程师,收到软件需求后就开始编码,这样做对吗?不但不对,而且后患无穷。软件工程之所以称为工程,因为它是一个从需求分析,设计,开发到测试验证的反复迭代的可持续交付可靠产品的过程。敏捷开发不只是编码阶段的敏捷,更是设计的敏捷,如何确保系统实现符合需求,利于沟通传达,有良好的扩展性,可维护性等等是整体软件设计的核心。本文将系统讲解需求分析到设计过程中主要的应知应会,即软件架构设计的985,9种设计模式,8种UML设计图,5种架构视图(注,这里介绍的设计模式种类和UML设计图为常用的类别,其它类别可根据需要扩展学习),目标读者包括系统架构师,软件架构师,管理人员,项目经理,各级软件开发工程师,测试工程师,运维工程师等人群。架构师可以了解如何从需求分析到设计落地,工程师可以在编码之前知道模块/类的设计和分布,管理人员或项目经理可以通过例如逻辑视图/开发视图了解项目的进展,汇报进展等。

本文介绍的为通用的设计方法,不区分后台服务端,终端(含嵌入式系统);面向对象,面向服务,面向过程的系统设计皆可参考其中的方法,侧重点可有不同。

范围:本文主要从3个方面来讲解,1. “软件架构4+1视图”章节讲解了如何通过多个视图来描述软件系统和架构,各个视图的主要作用,为了尽可能保持原文意思,较多引用了Kruchten关于4+1视图论文中的描述,同时对重要部分进行了翻译和编辑,删除了现在看来不适合的点;2. “UML”章节描述了在编码前如何使用UML图描述系统,搭建起高阶设计和详细设计的桥梁,着重描述了需求到设计阶段用例的写作;3. “设计模式”章节介绍常用的设计模式,即在解决一类问题时,形成的可复用的解决方案,通过代码的方式希望能够更直观地描述。

文章较长,也可以根据自己的角色着重关注4+1视图中的特定视图(事实上,在描述特定系统时,某个视图有可能会被裁剪),关注用例的编写(Use Case),了解基础的设计模式是如何使用多态在更高的抽象层完成交互,而把实例化延迟到子类的。本文也可在实践中遇到问题时用于方法的查找和参考,也欢迎添加文章末尾的我的微信,给我留言探讨文中未能详尽的内容。

软件架构4+1视图

首先来讲讲架构视图,如何描述系统决定了我们是否清晰地理解了系统。软件架构可以从不同的角度进行考虑,称为不同的视图,Kruchten 1995年提出了软件体系结构的4+1视图模型,提倡软件体系结构的多视图建模方法,其中用例视图位于中心位置(4+1视图中的1)。这些视图包括:逻辑视图,一种静态建模视图;进程视图,一种并发进程或任务视图;开发视图,一种子系统和构件设计视图;物理视图(或部署视图),一种反映物理拓扑结构及连接关系的视图。

系统的关注者有所不同,部分人关注系统功能,部分人关注整体设计,部分人关注局部模块的设计开发等等,使用多个视图可以分别解决架构(architecture)各个利益相关人(stakeholder)的关注点:最终用户、开发人员、系统工程师、项目经理等,并分别处理功能性和非功能性需求。视图是使用以架构为中心、用例(场景)驱动的迭代开发过程设计的。

许多书和文章希望使用一个图试图捕捉系统架构的要点,但是仔细观察这些图表上显示的一组方框和箭头,很明显,他们的作者很难在一个蓝图上表现出比实际表达更多的内容。这些方框代表正在运行的程序吗?还是源代码块?或者物理计算机?或者仅仅是功能的逻辑分组?箭头是否表示编译依赖项?还是控制流?或者数据流?一个架构需要单一的架构风格吗?有时,软件架构会在系统设计阶段过早地对软件进行了不成熟的切分,或者过分强调了软件开发的一个方面:数据工程、运行时的效率、开发策略和团队组织。通常,架构并没有解决其所有“客户”(或“利益相关人”)所关心的问题。因此,使用多个视图来组织对软件架构的描述,每个视图处理一组特定的关注点更为合适。

软件架构涉及软件高层结构的设计和实现,它是将一定数量的架构元素以某种精心选择的形式组合起来的结果,以满足系统的主要功能和性能要求,以及其他一些非功能性需求,如可靠性、可伸缩性、可移植性和可用性。以下公式很好地描述了软件架构:

软件架构 = {元素、组织形式、基本原理/约束}

软件架构涉及抽象、分解和组合、风格和美学。为了描述软件架构,可使用由多个视图组成的模型。为了最终解决大型且具有挑战性的架构,该架构模型由五个主要视图组成(参见图1):

• 逻辑视图,即设计的对象模型和功能分布,

• 进程视图,它描述设计的运行方面,包括进程的实体和交互,并发和同步等,

• 物理视图,描述软件到硬件的映射,并反映其分布,

• 开发视图,描述软件在其开发环境中的静态组织结构。

一个架构的描述,决策可以围绕这四个视图来组织,然后由几个选定的用例或者场景来说明,这些用例或场景将成为第五个视图。实际上,架构部分是从这些场景中演化而来的。

图1 4+1视图模型

将上面的等式独立地应用于每个视图,即对于每个视图,定义要使用的元素集(组件、容器和连接器),通过捕获其有效的形式和模式,并捕获基本原理和约束,可以将架构与需求连接起来。每一个视图都由一个使用自己特定符号的图来描述。对于每个视图,架构师还可以选择特定的架构样式,从而允许多个样式在一个系统中共存。

现在我们将依次查看这五个视图中的每一个,为每个视图提供其用途:解决了哪些关注点,对应的架构图的描述符号,以及我们用来描述和管理它的工具。文中的例子仅是用于介绍和描述,而不是定义这些系统的架构。“4+1”视图模型相当“通用”:可以使用其他符号和工具,也可以使用其他设计方法,尤其是对于和逻辑和进程分解,文中提出的是一些已经成功使用的方法。

逻辑架构,面向对象的分解

逻辑架构主要支持系统应该为其用户提供的功能需求,即系统可以为用户提供哪些服务。系统被分解成一组关键的抽象,主要来源于问题域中,以对象或对象类的形式出现。它们利用抽象、封装和继承的原则。这种分解不仅是为了进行功能分析,还用于识别系统各个部分中通用的机制和设计元素。我们使用Rational/Booch方法通过类图和类模板来表示逻辑架构。类图显示一组类及其逻辑关系:关联(association)、使用(usage)、组合(composition)、继承(inheritance)等等。相关类的集合可以分组到类类别中。类模板关注每个单独的类;它们强调主要的类操作,并标识关键的对象特征。如果定义对象的内部行为很重要,可以使用状态转换图(state transition diagrams)或状态图来完成。公共的机制或服务在类工具中定义(Class Utility)。如果一个应用程序更多是数据驱动的(data-driven),可以使用别的类型的逻辑视图,例如E-R图来表示。

逻辑视图的符号

逻辑视图的符号来自Booch符号。它被大大简化为只考虑架构上重要的项。尤其是,在这个设计层次上,众多的装饰并不是很有用。

软件架构设计的985

图2 逻辑图表示符号

逻辑视图的样式

我们用于逻辑视图的样式是面向对象的样式。逻辑视图设计的主要指导原则是在整个系统中保持一个单一的、一致的对象模型,以避免每个服务器站点或每个处理器的类和机制过早特别化。

逻辑图示例

软件架构设计的985

图3 逻辑图示例

进程架构,进程分解

进程架构考虑了一些非功能性需求,如性能和可用性。它解决了并发性和分布、系统的完整性、容错性,以及逻辑视图中的主要抽象如何与进程架构相适应,控制线程是实际执行的对象的操作。

进程架构可以在多个抽象层次上进行描述,每一个层次处理不同的关注点。在最高层次上,进程架构可以看作是一组独立执行的相互通信的逻辑程序网络(称为“进程”),分布在由局域网或广域网连接的一组硬件资源上。多个逻辑网络可能同时存在,共享相同的物理资源。例如,独立的逻辑网络可用于支持在线运行系统与离线系统的分离,以及支持与软件的仿真或测试版本共存。

进程是组成可执行单元的一组任务。进程表示进程架构可以进行控制的层级(即启动、恢复、重新配置和关闭)。此外,可以复制进程以增加处理负载的能力或提高可用性。

软件被分成一组独立的任务。每个任务是一个单独的控制线程,可以在一个处理节点上单独调度。

然后进一步可以区分:主要任务,即可以唯一定位的架构元素,以及次要任务,次要任务是由于实现原因在本地引入的附加任务(周期性活动、缓冲、超时等)。例如,它们可以实现为轻量级线程。主要任务通过一组定义良好的任务间通信机制进行通信:基于消息的同步和异步通信服务、远程过程调用、事件广播等。次要任务可以通过集合或共享内存进行通信。不能假设主要任务处于同一个进程或处理节点中。

消息流、进程负载可以使用流程图进行分析估计。也可以实现一个“空心”的进程架构,并用虚拟的进程负载来度量它在目标系统上的性能。

进程视图的符号

我们用于流程视图的符号是从Booch最初为Ada任务提出的符号基础上扩展而来的。同样,所使用的符号集中在架构上重要的元素上。

软件架构设计的985

图4 进程图表示符号

进程图示例

软件架构设计的985

图5 进程图示例

所有终端都由单个终端进程处理,该进程由其输入队列中的消息驱动。控制器对象在构成控制器进程的三个任务之一上执行:低循环率任务扫描所有非活动终端(200ms),将任何处于活动状态的终端放入高循环率任务(10ms)的扫描列表中,以检测状态的任何显著变化,并将它们传递给主控制器任务,主控制器任务解释这些更改,并通过消息将它们传递给相应的终端。在这里,控制器进程中的消息传递是通过共享内存完成的。

开发架构,子系统分解

开发架构关注软件开发环境中实际的软件模块的组织。软件被打包成小块程序库或子系统,这些程序库或子系统可以由一个或少数开发人员开发。子系统按层次结构组织,每一层为其上的层提供了一个狭窄且定义良好的接口。

系统的开发架构由模块和子系统图表示,显示了模块“导出”和“导入”关系。完整的开发架构只有在软件的所有元素都被识别之后才能被描述。但是,可以列出控制开发架构的规则:分区、分组、可见性。

大体来说,开发架构考虑到内部的需求,这些需求与开发难易度,软件管理,可重用性或通用性,以及工具集或开发语言带来的限制相关。开发视图是需求分配、工作分配到团队(甚至是团队组织)的基础,用于成本评估和计划,监控项目进度,评估软件重用、可移植性和安全性。它是建立产品线的基础。

开发图符号

软件架构设计的985

图6 开发图表示符号

开发视图的样式

我们建议对开发视图采用分层样式,定义大约4到6层的子系统。每一层都有明确的责任。设计规则是某一子系统只能依赖于同一层或下一层的子系统,以尽量减少模块间非常复杂的依赖网络的发展,允许简单的逐层发布策略。

软件架构设计的985

图7 开发视图样式

开发架构示例

上图显示了加拿大休斯飞机公司开发的空中交通控制系统产品线的五个层次的开发组织。这是与之前所示的逻辑架构相对应的开发架构。

第1层和第2层构成了一个独立于域的分布式基础设施,在整个产品线中很常见,并使其不受硬件平台、操作系统或现成产品(如数据库管理系统)的影响。在这个基础设施中,第3层添加了一个ATC框架,以形成一个特定于领域的软件体系结构。使用这个框架,在第4层构建了一个功能面板。第5层非常依赖于客户和产品,包含了大部分用户界面和与外部系统的接口。大约72个子系统分布在5个层中,每个层包含10到50个模块,并且可以在其他图上表示。

物理架构,将软件映射到硬件

物理架构主要考虑系统的非功能性需求,如可用性、可靠性(容错性)、性能(吞吐量)和可伸缩性。该软件在计算机网络或处理节点(简称节点)上执行。识别出的各种元素-网络、流程、任务和对象需要映射到各个节点上。我们预计将使用几种不同的物理配置:一些用于开发和测试,另一些用于为不同服务器站点或不同客户部署系统。因此,软件到节点的映射需要高度灵活,并且对源代码本身的影响最小。

物理图符号

物理蓝图在大型系统中可能会变得非常混乱,因此它们有几种形式,有或者没有对应到进程视图的映射关系。 

软件架构设计的985

图8 物理视图表示符号

图9显示了大型PABX的一种可能的硬件配置,而图10显示了进程架构在两种不同物理架构上的映射,分别对应于小型和大型PABX。C、 F和K是三种不同容量的计算机,支持三种不同的可执行程序。

软件架构设计的985

图9 物理视图示例

软件架构设计的985

图10 物理视图示例

场景,把所有元素放在一起

这四个视图中的元素通过使用一小部分重要场景(Rubin和Goldberg所描述的更一般的用例实例-use cases)可以无缝地协同工作,我们为这些用例描述了相应的脚本(即对象之间和进程之间的交互序列)。从某种意义上说,这些场景是对最重要需求的抽象。它们的设计使用对象场景图和对象交互图来表示。

此视图与其他视图(因此称为“+1”)是多余的,但它有两个主要用途:

• 作为在架构设计期间发现架构元素的驱动因素,

• 在该架构(architecture)设计完成后,作为验证和说明的角色,在纸上和作为架构原型测试的起点。

场景符号

符号与组件的逻辑视图非常相似,但是使用进程视图的连接器来进行对象之间的交互。请注意,对象实例用实线表示。

场景示例

图11示出了小型PABX的场景片段。相应的交互脚本是:

1.  手机的控制器检测并验证从挂接到摘机的转换,并发送消息唤醒相应的终端对象。

2.  终端分配一些资源,并告诉控制器发出一些拨号音。

3.  控制器接收数字并将其传送到终端。

4.  终端使用编号计划来分析数字流。

5.  输入有效的数字序列后,终端将打开一个对话。

软件架构设计的985

图11 场景视图示例

视图之间的对应关系

各种视图不是完全正交或独立的。一个视图中的元素按照一定的设计规则和启发式方法连接到其他视图中的元素。

从逻辑到进程视图

我们确定了逻辑架构中类的几个重要特征:

• 自治性(Autonomy):对象是主动的、被动的、受保护的吗?

- 主动的对象主动调用其他对象的操作或其自己的操作,并完全控制其他对象对其自身操作的调用

- 被动对象从不自发地调用任何操作,也无法控制其他对象对其自身操作的调用

- 受保护对象从不自发地调用任何操作,而是对其操作的调用有一定的仲裁权。

• 持久性:对象是暂时的、永久的吗?它们是进程或处理器的故障吗?

• 从属关系:一个对象的存在或持久性是否依赖于另一个对象?

• 分布:对象的状态或操作是否可从物理架构(architecture)中的多个节点、进程架构中的多个进程访问?

在架构的逻辑视图中,我们认为每个对象都是活动的,并且可能是“并发的”,也就是说,与其他对象“并行”的行为,而我们不再关注实现这种效果所需的精确并发程度。因此,逻辑架构只考虑需求的功能方面。

然而,当我们定义进程架构时,用它自己的控制线程(例如,它自己的Unix进程)实现每个对象在当前的技术状态下并不太实用,因为这会带来巨大的开销。此外,如果对象是并发的,则必须有某种形式的仲裁来调用它们的操作。

另一方面,由于以下几个原因,需要多个控制线程:

• 对某些类别的外部刺激做出快速反应,包括与时间相关的事件

• 利用节点中的多个CPU或分布式系统中的多个节点

• 为了提高CPU利用率,在某些控制线程挂起等待其他活动完成(例如访问某个外部设备或访问其他活动对象)时将CPU分配给其他活动

• 确定活动的优先级(并有可能提高响应能力)

• 支持系统可扩展性(通过其他进程分担负载)

• 在软件的不同区域之间分离关注点

• 实现更高的系统可用性(使用备份过程)

我们同时使用两种策略来确定“正确”的并发量并定义所需的进程集。考虑到一系列潜在的物理目标体系结构,我们可以:

• 由内而外:

从逻辑体系结构开始:定义多路复用单个控制线程的代理任务

跨一个类的多个活动对象;其持久性或生存期从属于活动对象的对象也在同一个代理上执行;需要以互斥方式执行的几个类或只需要少量处理的类共享一个代理。这个集群继续进行,直到我们将进程减少到一个合理的小数目,仍然允许分配和使用物理资源。

•从外到内:

从物理架构开始:识别对系统的外部刺激(请求),定义处理刺激的客户端进程和只提供服务而不启动服务的服务器进程;使用问题的数据完整性和序列化约束来定义正确的服务器集,并将对象分配给客户端和服务器代理;确定必须分发哪些对象。结果是类(及其对象)映射到进程架构的一组任务和流程上。通常,一个活动类有一个代理任务,也有一些变化:一个给定类有几个代理来增加吞吐量,或者几个类映射到一个代理上,因为它们的操作很少被调用或需保证顺序执行。

请注意,这不是一个线性的、确定性的产生一个最佳的进程架构的过程;它需要几次迭代才能得到一个可接受的折衷方案。

图12显示了一些假设的空中交通控制系统中的一小部分类是如何映射到进程上的。

软件架构设计的985

图12 逻辑视图到进程视图的映射举例

飞行类映射到一组飞行代理上:有很多航班要处理,外部刺激率高,响应时间很关键,负载必须分散在多个cpu上。此外,航班处理的持久性和分布方面被推迟到航班服务器上,出于可用性的原因,它会被复制。

飞行剖面或许可始终从属于飞行,尽管有复杂的类,但它们共享飞行类的过程。航班被分配到其他几个进程,特别是用于显示和外部接口。

扇区划分类,由于其完整性限制,为分配管制员对航班的管辖权而建立了空域划分,只能由单个代理处理,但可以与航班共享服务器进程:更新很少。

位置空域和其他静态航空信息是受保护的对象,在几个类之间共享,很少更新;它们被映射到自己的服务器上,并分发给其他进程。

从逻辑视图到开发视图

类通常被实现为模块,例如Ada包可见部分的类型。大类被分解成多个包。紧密相关的类的集合-类类别-被分组到子系统中。定义子系统时必须考虑其他约束,例如团队组织、预期代码量(每个子系统通常为5K到20K SLOC)、预期重用和通用性程度、严格的分层原则(可见性问题)、发布策略和配置管理。因此,我们通常会得到一个与逻辑视图没有一一对应关系的视图。

逻辑视图和开发视图非常接近,但处理的关注点非常不同。我们发现,项目越大,这些视图之间的距离就越大。同样,对于进程视图和物理视图:项目越大,视图之间的距离就越大。例如,如果我们比较图3b和图6,则不存在类类别到层的一对一映射。如果我们采用“外部接口网关”类别,它的实现是跨多个层实现的:通信协议在第1层或以下的子系统中,一般网关机制在第2层的子系统中,实际的特定网关在第5层子系统中。

从进程视图到物理视图

进程和进程组被映射到可用的物理硬件上,以各种配置进行测试或部署。

这些场景主要与逻辑视图(使用哪些类)以及对象之间的交互涉及多个控制线程时的流程视图相关。

裁剪模型

并非所有的软件架构都需要完整的“4+1”视图。在架构描述中可以省略无用的视图,如只有一个处理器的物理视图,以及只有进程或程序的进程视图。对于非常小的系统,甚至有可能逻辑视图和开发视图非常相似,不需要单独的描述。这些场景在所有情况下都是有用的。

迭代过程

Witt等人提出设计或架构常常分为4个阶段:草图绘制、组织、细化和优化,细分为约12个步骤,且可能需要一些回溯。我们认为这种方法对于一个雄心勃勃、无先例的项目来说过于“线性化”。在4个阶段结束时,对架构的验证知之甚少。我们提倡更迭代的开发,对架构进行原型化(prototyped),测试(tested),测量(measured),分析(analyzed),然后在随后的迭代中改进。除了可以降低与架构相关的风险外,这种方法对项目还有其他好处:团队建设、培训、熟悉架构、工具的获取、过程和工具的运行等等(我们在这里说的是一个进化原型,它慢慢成长为系统,以及这种迭代方法还允许对需求进行细化、成熟和更好的理解。

场景驱动的方法

系统最关键的功能是通过场景(或用例)的形式捕获的。我们所说的“关键”是指:最重要的功能,系统存在的理由,或使用频率最高的功能,或存在一些必须减轻的重大技术风险的功能。

开始:

• 基于风险和关键程度,为某个迭代选择了少量的场景。场景可以被合成以用于抽象许多的用户需求。

• 对场景进行“脚本化”,以便识别主要抽象(类、机制、过程、子系统),分解成成对的序列(对象、操作)。

• 将发现的架构元素在4个蓝图中列出:逻辑、进程、开发和物理。

• 然后编码实现、测试、测量该架构,并且通过分析可能会检测到一些缺陷或潜在的增强。

• 吸取经验教训(lessons learned)。

循环:

下一次迭代可以从以下步骤开始:

• 重新评估风险,

• 扩展要考虑的场景

• 选择一些能够降低风险或扩大架构覆盖范围的附加场景,然后:

• 尝试在初步架构中编写这些场景

• 发现为适应这些场景而需要发生的其他架构元素或有时重大的架构更改

• 更新4个主要蓝图:逻辑、进程、开发、物理

• 根据变化修改现有方案

• 升级实现(架构原型),以支持新的扩展场景集。

• 测试。如果可能的话,在真实的目标环境下测量负载。

• 然后审查所有五个蓝图,以检测简化、重用和通用性的可能性。

• 更新设计指南和基本原理。

• 吸取经验教训。

结束循环

最初的架构原型会演变为真实的系统。希望经过2到3次迭代后,架构本身变得稳定:没有发现新的主要的抽象,没有新的子系统或过程,没有新的接口。故事的其余部分是在软件设计领域内,顺便说一句,开发可以继续使用非常相似的方法和过程。

这些迭代的持续时间有很大的不同:随着要实施的项目的规模、参与的人员的数量以及他们对领域和方法的熟悉程度,以及这个开发组织的系统“前所未有”的程度。因此,对于小型项目(例如,10 KSLOC),迭代的持续时间可能是2-3周,对于大型指挥和控制系统(例如700 KSLOC),迭代的持续时间可能长达6-9个月。

架构设计文档

架构设计过程中生成的文档包含在两个文档中:

• 一个软件架构(Architecture)文档,其组织近似遵循“4+1”视图(参见下面的典型大纲)

• 软件设计指南,其中包括(除其他外)最重要的设计决策,必须遵守这些决策,以维护系统的架构完整性。

软件架构文档大纲:

标题页

更改历史记录

目录

图片一览表

1.     范围

2.     参考

3.     软件架构

4.     架构目标和约束

5.     逻辑架构

6.     进程架构

7.     开发架构

8.     物理架构

9.     场景

10.  大小和性能

11.  质量

附录

A、 缩略语

B、 定义

C、 设计原则


结论

这种“4+1”视图模型已经成功地应用于多个大型项目中,这些项目在术语上有或者没有局部的定制和调整。它实际上允许不同的系统关注者(stakeholder)找到他们想要了解的软件架构。系统工程师从物理视图,然后是过程视图来处理它。最终用户、客户、数据专家。项目经理、软件配置人员从开发的角度来看。


UML

UML(Unifided Modeling Lanauage)的目的是为系统架构师、软件工程师和软件开发人员提供工具用于软件系统的分析,设计和实现,以及用于商业和类似流程的建模。UML(uml1)的最初版本起源于三种领先的面向对象方法(Booch、OMT和OOSE),并结合了建模语言设计、面向对象编程和架构描述语言的最佳实践。相对于uml1,UML的最新修订版(UML2.0)显著地增强了它的抽象语法规则和语义定义更为精确,更模块化的语言结构,并且提高了用于大规模系统建模的能力。UML的主要目标之一是通过使用对象可视建模工具来提高行业的状态互操作性。然而,为了能够在工具之间进行有意义的模型信息交换,需要首先在语义和语法表达上达成一致。UML满足以下要求:

1. 基于MOF的通用元模型的定义,指定了UML的抽象语法。这个抽象语法定义了一组UML建模概念、它们的属性和它们之间的关系,以及用于组合这些概念以构建部分或完整UML模型的规则。

2. 每个UML建模概念语义的详细解释。语义定义UML概念如何被计算机实现,定义的表达方式与具体的技术无关。

3. 用于表示单个UML建模概念的可读的符号元素规范,以及针对系统建模的不同方面,将它们组合起来成为不同的图类型(diagram types)的规则。

UML表示法常用的图有以下几种:

1. 用例图

一个参与者(actor)发起一个用例(use case)。用例定义了参与者与系统之间的一组交互序列。在用例图中,参与者用一个人形图标表示,系统则用一个方框表示,一个用例表示为方框中的一个椭圆。通信关联(communication association)将参与者与他们参与的用例进行连接。用例之间的关系通过包含(include)关系和扩展(extend)关系进行定义。

软件架构设计的985

图13 用例图

用例是描述系统行为的方法,更多的时候使用文档来描述,以下是一个常用的模板,可供参考。

内容项
描述
用例名称
每个用例都给予一个名字。
概述
用例的简短描述,一般是一两句话。
依赖 为可选项,描述该用例是否依赖其它用例,即它是否包含或扩展另一个用例。
参与者
总是有一个主要参与者来启动用例,也可以有次要参与者参与到用例中。
前置条件 从该用例的角度在用例开始时必须为真的一个或多个条件。
主序列描述 用例的主体是对该用例主序列的叙述性描述,这是参与者和系统之间最经常的交互序列。该描述的形式是参与者的输入,接着是系统的响应。
可替换序列描述 主序列的可替换分支的叙述性描述。主序列可能有多个可替换分支。
非功能性需求

非功能性需求的叙述性描述,例如性能和安全性需求。

例如:

安全需求:系统应加密PIN码

性能需求:系统应在5秒内响应参与者的输入

后置条件
该用例终点处(从该用例的角度看)总是为真的条件,如果遵循了主序列。
未解决的问题 在开发期间,有关用例的问题被记录下来,用于和用户进行讨论。

举例:

用例名称:下单请求

概述:客户下单从在线购物系统中购买商品。客户的信用卡要验证有足够信用为所要购买的目录商品付款。

参与者:客户

前置条件:客户已选择一个或多个目录商品

主序列:

  1. 客户提出订单请求和客户账户ID来为购买付款。

  2. 系统检索客户账户信息,包括该客户的信用卡详细信息。

  3. 系统针对购买价格检查客户的信用卡,并创建信用卡购买授权号码(如果检索通过)。

  4. 系统创建发货单,包含订单明细,客户ID和信用卡授权号码。

  5. 系统确认批准购买,并向客户显示订单信息。

可替换序列:

第2步:如果客户没有账号,则系统为其创建一个账号

第3步:如果客户的信用卡请求被拒绝,则系统提示客户输入不同的信用卡号码。客户可以输入一个不同的信用卡号码或取消订单。

后置条件:系统为客户创建了发货单。


2. 类图

类图中类用方框描绘,类之间的静态(永久)关系被描绘成连接方框之间的连线。UML表示法支持以下三种类之间的主要关系类型:关联(association),整体部分关系(whole/part relationship)和泛化/特化(generalization/specificaiton relationship)关系。

软件架构设计的985

图14 类图

3. 对象图

对象图与类图相似,区别是对象名称需要带有下划线,可以在对象名和类名之间使用冒号分隔来完整地描绘一个对象,例如anObject:Class。

4. 通信图

通信图和顺序图是UML的两种主要类型的交互图。他们用来描述对象间是如何进行交互的。在这些交互图中,对象用长方形方框表示,对象的名字不需要使用下划线标绘。通信图(communication diagram),它展示了合作对象间如何通过发送与接收消息进行动态的交互。通信图描绘了交互对象的组织结构。连接方框的线代表了对象间的交互。与这些线相邻的带有标签的箭头表示了对象间消息传递的名字和方向。同时,对象间传递消息的顺序被进行了编号。

软件架构设计的985

图15 通信图

5. 顺序图

顺序图将对象交互通过时间序列的方式进行描绘。顺序图具有两个维度,其中参与交互的对象被描绘在水平方向,而垂直方向代表时间纬度。从每个对象框出发都有一条被称为生命线(lifeline)的垂直虚线。每条生命线可以选择性地具有一个使用双实线表示的激活杆,用来表示对象的执行时间。

软件架构设计的985

图16 顺序图

6. 状态机图

在状态机图中圆角框表示状态,连接圆角框的弧线表示转换,状态图的初始状态用一个始于小黑圆圈的弧线表示。终结状态是可选的,它被描绘为嵌套在大白圈中的小黑圆圈。在表示状态转换的弧线上,使用事件(Event)进行标记。事件引起了状态的转换,当事件发生时,为了发生转换,可选的布尔条件(condition)必须为真。可选的动作作为转换的结果被执行。一个状态可具有以下任意的动作:进入动作(entry action,在进入状态的时候执行),退出动作(exit action,在退出状态的时候执行)。

软件架构设计的985

图17 状态机图

7. 活动图

活动图描述活动的顺序,展现活动从一个活动到另一个活动的流程,类似流程图,活动图显示了活动序列,决策节点和循环,甚至还有并发活动。

软件架构设计的985

图18 活动图

8. 部署图

部署图以物理节点和节点间物理连接的方式展示了一个系统的物理配置。一个节点使用一个立方体表示,连接则用这些立方体之间的连线表示。本质上,部署图是以系统节点为关注点的一种类图。

图19 部署图

设计模式

什么是模式?模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心,能够一次一次地使用该方案而不必做重复劳动。一般而言,模式包括四个基本要素:
1. 模式名(pattern name):模式的名称
2. 问题(problem):描述了应该在何时使用模式
3. 解决方案(solution):描述了设计的组成部分,他们之间的相互关系及各自的职责和协作方式。解决方案并不描述一个特定而具体的设计或实现,而是提供设计问题的抽象描述和怎样用一个具有一般意义的元素组合(类或对象组合)来解决这个问题。
4. 效果(consequence)描述了模式应用的效果及使用模式应权衡的问题。
设计模式是对用来在特定场景下解决一般设计问题的类和相互通信的对象的描述。面向对象设计和设计模式在实现中一个重要的原则是利用多态的特性(继承或接口),通过基类或接口来操作子类,这是各类模式在实现时的核心。
常用的设计模式包括:

1. SINGLETON模式:Singleton是一个很简单的模式,使用该模式的类只有一个实例,这一类实例往往在程序启动时被创建出来,在程序结束时才被删除。有时,这种对象是应用程序的基础对象,通过这些基础对象可以得到系统中的许多其它对象。

public class Singleton{  private static Singleton theInstance = null;  private Singleton() {}    public static Singleton Instance()  {    if(theInstance == null)      theInstance = new Singleton();    return theInstance;  }}

例如应用访问单个Flash硬件的基础服务,可以采用Singleton模式。

2. TEMPLATE模式:它展示了面向对象编程中诸多经典重用形式中的一种。其中通用算法被放置在基类中,并且通过继承在不同的具体上下文中实现该通用算法。但是这项技术是有代价的。继承是一种非常强的关系。派生类不可避免地要和它们的基类绑定在一起。例如,其它类型的排序算法确实也需要IntBubbleSorter中的outOfOrder和swap方法。然而,却没有办法在其他排序算法中重用outOfOrder和swap。由于继承了BubbleSorter,就注定要把IntBubbleSorter永远地和BubbleSorter绑定在一起。不过Strategy模式提供了另外一种可选方案。

public abstract class BubbleSorter{ private int operations = 0; protected int length = 0;
protected int doSort()  {   operations = 0;   if(length <= 1)   return operations;    for(int nextToLast = length-2;nextToLast >= 0; nextToLast--)     for(int index = 0;index <=nextToLast;index++)     {        if(outOfOrder(index))         swap(index);        operations++;     }    return operations;  }    protected abstract void swap(int index);  protected abstract boolean outOfOrder(int index);}
public class IntBubbleSorter extends BubbleSorter{ private int[] array = null; public int sort(int[] theArray) { array = theArray; length = array.length; return doSort(); }
  protected void swap(int index)  {    int temp = array[index];    array[index] = array[index+1];    array[index+1] = temp;  }    protected boolean outOfOrder(int index)  {    return (array[index] > array[index+1]);  }}


3. STRATEGY模式:相对于Template模式,Strategy模式使用了一种非常不同的方法来倒置通用算法和具体实现之间的依赖关系。不是将通用的应用算法放进一个抽象基类中,而是将它放进一个名为BubbleSorter的具体类中。我们把通用算法必须要调用的抽象方法定义在一个名为SortHandle的接口中。从这个接口派生出IntSortHandle,并把它传给BubbleSorter。之后,BubbleSorter就可以把具体工作委托给这个接口去完成。

好处是IntSortHandle类对BubbleSorter类一无所知。它不依赖于冒泡排序的任何实现方式。由于swap和outOfOrder方法的实现直接依赖于冒泡排序算法,所以Template模式部分地违反了DIP原则。而Strategy模式不包含这样的依赖,可以在BubbleSorter之外的其它Sorter实现中使用IntSortHandle。

Strategy模式比Template模式多提供了一个额外的好处,尽管Template模式允许一个通用算法操纵多个可能的具体实现,但是由于Strategy模式完全遵循DIP原则(依赖倒置原则),从而允许每个具体实现都可以被多个不同的通用算法操纵。

public class BubbleSorter{ private int operations = 0; private int length = 0; private SortHandle itsSortHandle = null;
public BubbleSorter(SortHandle handle) { itsSortHandle = handle; }
  public int sort(Object array)  {   itsSortHandle.setArray(array);   length = itsSortHandle.length();   operations = 0;   if(length <= 1)   return operations;        for(int nextToLast = length-2; nextToLast >= 0; nextToLast--)      for(int index = 0; index <= nextToLast; index++)      {       if(itsSortHandle.outOfOrder(index))       itsSortHandle.swap(index);        operations++;      }    return operations;  }}
public interface SortHandle{ public void swap(int index); public boolean outOfOrder(int index); public int length();  public void setArray(Object array);}
public class IntSortHandle implements SortHandle{ private int[] array = null;
public void swap(int index) { int temp = array[index];    array[index] = array[index+1];    array[index+1] = temp; }
public void setArray(Object array) { this.array = (int[])array; }
  public int length()  {   return array.length;  }    public boolean outOfOrder(int index)  {   return (array[index] > array[index+1]);  }}

4. FACTORY模式:依赖倒置原则告诉我们应该优先依赖于抽象类,而避免依赖于具体类。当这些具体类不稳定时,更应该如此。Factory模式允许我们只依赖于抽象接口就能创建出具体对象的实例。具体的对象由其对应的工厂类对象创建,且子类(工厂子类)决定实例化哪个类,工厂方法使一个类的实例化延迟到其子类。在准备生产某个类对象的时候,先创建其对应的子类工厂。

public interface ShapeFactory{  public Shape make(String shapeName) throws Exceptions;}
public class ShapeFactoryImplementation implements ShapeFactory{ public Shape make(String shapeName) throws Exception { if(shapeName.equals("Circle")) return new Circle();    else if(shapeName.equals("Square"))      return new Square();    else      throw new Exception("ShapeFactory cannot create:" + shapeName); }}


5. COMPOSITE模式:该模式适用于有层次结构的,组合的问题。它适合将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。

public interface Shape{ public void draw();}
public class CompositeShape implements Shape{ private Vector itsShapes = new Vector();
public void add(Shape s) { itsShapes.add(s); }
public void draw() {    for(int i = 0; i < itsShapes.size(); i++)    {      Shape shape = (Shape) itsShapes.elementAt(i);      shape.draw();    } }}


6. OBSERVER模式:观察者模式定义了对象间的一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这个模式也叫发布订阅者模式(publish-subscribe)。

public interface Observer{ public void update();}
public class Subject{  private Vector itsObservers = new Vector();    protected void notifyObserver()  {   Iterator i = itsObservers.iterator();   while(i.hasNext())   {   Observer observer = (Observer)i.next();      observer.update();   }  }    public void registerObserver(Observer observer)  {   itsObservers.add(observer);  }}
public interface TimeSource{  public int getHours();  public int getMinutes();  public int getSeconds();}
public class MockTimeSource extends Subject implements TimeSource{ private int itsHours;  private int itsMinutes;  private int itsSeconds;    public void setTime(int hours, int minutes, int seconds)  {   itsHours = hours;    itsMinutes = minutes;    itsSeconds = seconds;    notifyObservers();  }    public int getHours()  {   return itsHours;  }    public int getMinutes()  {    return itsMinutes;  }    public int getSeconds() { return itsSeconds;  }}
public class MockTimeSink implements Observer{ private int itsHours;  private int itsHours; private int itsHours;
  public MockTimeSink(TimeSource source)  {    itsSource = source;  }   public int getHours() { return itsHours; }
public int getMinutes() { return itsMinutes; }
public int getSeconds() { return itsSeconds; }
  public void update()  {   itsHours = itsSource.getHours();    itsMinutes = itsSource.getMinutes();    itsSeconds = itsSource.getSeconds();  }}


7. ADAPTER模式:将类的接口转换成客户端期待的另一个接口,从而使原本因为接口不匹配而不能一起工作的两个类能够在一起工作,不改变原有类的接口,这种适用于类内部功能可复用,但接口不匹配。

8. DECORATOR模式:装饰模式将类的接口转换成客户端期待的另一个接口,从而使原本因为接口不匹配而不能一起工作的两个类能够在一起工作,改变原有类的接口。例如,如果需要在使用Modem拨号功能前,需要将音量设置为某个值(10),可以不改变Modem接口,通过创建一个名为LoadDialModem的类,它派生自Modem,并且委托给一个它包含的Modem示例。它捕获对dial函数的调用并在委托前把音量调高。以下代码示例中,LoadDialModem包含了一个成员变量itsModem。

public interface Modem{ public void dial(String pno); public void setSpeakerVolumn(int volume);  public String getPhoneNumber();  public int getSpeakerVolume();}
public class HayesModem implements Modem{ public void dial(String pno) { itsPhoneNumber = pno; }
public void setSpeakerVolume(int volume) { itsSpeakerVolume = volume; }
public String getPhoneNumber() { return itsPhoneNumber; }
public int getSpeakerVolume() { return itsSpeakerVolume; }
private String itsPhoneNumber; private int itsSpeakerVolume;}
public class LoudDialModem implements Modem{  public LoudDialModem(Modem m)  {   itsModem = m;  }    public void dial(String pno)  {   itsModem.setSpeakerVolume(10);   itsModem.dial(pno);  }    public void setSpeakerVolume(int volume)  {   itsModem.setSpeakerVolume(volume);  }    public String getPhoneNumber()  {   return itsModem.getPhoneNumber();  }    public int getSpeakerVolume()  {   return itsModem.getSpeakerVolume()  }    private Modem itsModem;}
9. STATE模式: 状态机模式是最重要的设计模式之一,在程序中往往需要处理有限状态机(FSM),状态机是一组状态集合,系统因为收到某些事件(Event),触发在不同状态间切换,例如表示TCP协议栈的连接状态。有不同的方法来实现FSM,第一个也是最直接的策略是使用嵌套switch/case语句,但仅仅适用于简单的状态机,对于大型FSM,具有大量状态和事件的状态机,使用switch/case语句是一项非常困难并且容易出错的工作。以下代码示例使用状态机模式实现了一个地铁十字转门的状态机。
public interface TurnstileController{ public void lock(); public void unlock(); public void thankyou(); public void alarm();}
public interface TurnstileState{  void coin(Turnstile t);  void pass(Turnstile t);}
class LockedTurnstileState implements TurnstileState{ public void coin(Turnstile t) { t.setUnlocked(); t.unlock(); } public void pass(Turnstile t) { t.alarm(); }}
class UnlockedTurnstileState implements TurnstileState{ public void coin(Turnstile t) {    t.thankyou(); } public void pass(Turnstile t) { t.setLocked(); t.lock(); }}
public class Turnstile{  private static TurnstileState lockedState = new LockedTurnstileState();  private static TurnstileState unlockedState = new UnlockedTurnstileState();    private TurnstileController turnstileController;  private TurnstileState state = lockedState;    public Turnstile(TurnstileController action)  {    turnstileController = action;  }    public void coin()  {    state.coin(this);  }    public void pass()  {   state.pass(this);  }    public void setLocked()  {   state = lockedState;  }    public void setUnlocked()  {   state = unlockedState;  }    public boolean isLocked()  {    return state == lockedState;  }    public boolean isUnlocked()  {   return state == unLockedState;  }    void thankyou()  {   turnstileController.thankyou();  }    void alarm()  {   turnstileController.alarm();  }    void lock()  {   turnstileController.lock();  }    void unlock()  {   turnstileController.unlock();  }}





参考:
[1] 《Architectual Blueprints-The "4+1"View Model of Software Architecture》,Philippe Kruchten
[2] 《OMG Unified Modeling Language》 Version 2.5.1
[3] 《软件建模与设计-UML,用例,模式和软件体系结构》Hassan Gomaa
[4] 《敏捷软件开发-原则,模式与实践》Robert C. Martin
[5] 《设计模式-可复用面向对象软件的基础》Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides

以上是关于软件架构设计的985的主要内容,如果未能解决你的问题,请参考以下文章

985高校的高材生只会写代码片段,丢人吗?

985大学的高材生只会写代码片段,丢人吗?

985大学的高材生只会写代码片段,丢人吗?

985大学的高材生只会写代码片段,丢人吗?

论如何设计一款端对端加密通讯软件

SOA架构与微服务的区别异同