架构师备战(三)-软件工程总览(软件工程 架构)

软件工程这个篇章非常重要,能够在综合知识里面考到十几分,仅次于架构,在案列和论文里面也会用到。

第一章 软件工程基本概念

1、什么是软件工程?

软件开发的工程性的一些东西,基本思想就是用工程化的思想去做软件。

最早开发软件是没有软件工程的,最早使用计算机做大型的计算,后来计算机软件,成成一步步兴起,原来一个人很短时间能够搞定的事情,现在要许多人一起来完成这个工作。

而软件领域比较虚幻,往往要程序运行之后才能看到最终的效果,这就给软件开发带来了很大的风险。比如软件一开始没有问题,除去成本还能赚钱,但是有问题后修修改改,需要大量的成本,导致最后的亏损。

从而带来一些问题,也就是软件危机。我们希望软件开发能像建房子一样拔地而起,又比较受控,所以提出软件工程。软件工程很多东西跟建造领域的一些东西是有着异曲同工之秒的。

2、软件开发方法

方法论就是你想要做成某些事情,你就需要遵循一些原则,否则就会出现一些问题。

2.1、结构化法

结构化也叫做过程化,就是面向过程的。早期使用C语言开发过程化的东西那个时代,就是结构化时代。

思想原则

  • 用户至上
  • 严格区分工作阶段,每阶段有任务与成果
  • 强调系统开发过程工程化,文档资料标准化
  • 自顶向下,逐步分解

如果用C语言做过开发时,我们从整体看系统比较复杂,如何简化。那就是拆,拆成多个子系统,每个子系统拆模块,模块拆成子模块,最后变成了一个函数,函数输入输出是什么,用什么算法实现等,可能都有要求了,典型的应用就是瀑布模型。对日外包项目就是这种模式,标准化非常高。

结构法化方法思想其实很好,为什么会被淘汰了呢?

因为结构化开发的应变能力比较差,面向过程就是把业务流程固化到一个一个方法中了,是不容易做变更的,而现实中业务流程变化是非常常见的,所以该方法被淘汰了。而面向对象方法就是为了应对需求灵活。

2.2、原型法

一般认为是用在需求分析阶段的,其实就是一个demo. 就是为了避免在需求不明确时,这时候做一个原型来给客户演示。从而避免开发出来的东西跟客户想要的东西不一致.

  • 适用于需求不明确的开发
  • 包括抛弃型原型和进化/演化型原型

2.3、面向对象方法

比如C ,, VB, Java等都是面向对象的语言。

面向对象尝试在计算机里面构建一个与真实事件对应的一个体系。

比如现实中,有一个人,它完成了什么任务,他有什么特点,建模到系统里面。业务流程就可以按照建模进行组装,从而更加的灵活。所以面向对象有以下的一些基本思想原则。

思想原则

  • 更好的复用性
  • 关键在于建立一个全面、合理、统一的模型
  • 分析、设计、实现三个阶段,界限不明确(在做分析阶段的工作时,一般会考虑到设计,或者做了一部分设计,所以界限不明显)

2.4、面向服务的方法

面向服务是在面向对象的基础上,进一步去做标准化这一层级的,服务的粒度会比对象大

  • SO方法分为三个主要的抽象级别:操作、服务、业务流程
    • 操作:函数,方法层级
    • 服务:就是服务
    • 业务流程:由服务协作完成一个业务的处理
  • SOAD(面向服务方法体系)分为三个层次:
    • 基础设计层(底层服务构建)、应用结构层(服务之间的接口和服务级协定)和业务组织层(业务流程建模和服务流程建模编排)
  • 服务建模:分为服务发现、服务规约和服务实现三个阶段

第二章 软件开发模型

  • 瀑布模型: 迭代模型/迭代开发方法
  • 演化模型: 快速开发应用
  • 增量模型: 构建组装模型/基于构建的开发方法
  • 螺旋模型: 统一过程/统一开发方法
  • 原型模型: 敏捷开发方法
  • 喷泉模型: 模型驱动的开发方法
  • V模型: 基于架构的开发方法

1、瀑布模型

瀑布模型阶段

瀑布模型主要分为以下几个阶段

软件计划—>需求分析(产出SRS)—>软件设计—>程序编码—>软件测试—>运行维护

  • 定义阶段: 软件计划,需求分析
  • 开发阶段: 软件设计,程序编码,软件测试
  • 维护阶段: 运行维护

每个阶段该做什么,每个阶段有哪些产出物,都约定好了,就有了一个做事情的主线,这就是模型。

架构师备战(三)-软件工程总览(软件工程 架构)

为什么叫瀑布模型?

由于一步一步的走下来,犹如瀑布的水一样一阶一阶的留下来,所以叫做瀑布模型。

阶段评审

瀑布模型的每一个阶段都有一个评审的过程。如果发现了上一个阶段有问题,就会回退到上一阶段,修改之后在继续往下执行。

使用场景

适用于需求明确的场景,阶段分得很清楚,一步一步的完成。

模型存在问题

因为适用于需求明确场景,就意味着需求不明确的话,那就完蛋了。如果需求错了,设计必然是错的,后面的编码也跟着错了,测试前面阶段测不出来,用户确认测试时才会发现问题。这个代价非常高,用这个方法论开发失败的概率非常高,所以才有其他的方法论来实现。

2、原型模型

原型模型分类

原型包括抛弃型模型,也叫做快速原型模型和演化型模型,也叫做变换模型。

  • 快速原型模型(抛弃型原型)
    • 原型本就只是想获取需求,获取需求后,原型就不具备它的价值,客户以抛弃掉原型,按其他模型来做。
  • 演化模型
    • 把最初的模型一步一步进化,改一点,再改一点,慢慢的进化为最终的系统。

原型模型形态

架构师备战(三)-软件工程总览(软件工程 架构)

3、螺旋模型

螺旋模型的形成

原型作为基础,再跟瀑布结合,形成了螺旋模型。里面用到了一种思想,就是迭代的思想。

迭代思想

一气呵成是瀑布模型的思想。如果一气呵成时极为困难的事情,则可以考虑迭代的思想。

一轮迭代下来是一个雏形没有关系,再逐步的调整精化,多轮迭代,越来越像目标的产出,这就是迭代的思想。

螺旋模型形态

架构师备战(三)-软件工程总览(软件工程 架构)

螺旋模型形态图分析

螺旋模型整个结构像陀螺的螺线一样,一圈一圈的出来。

最中心是基础部分,是一个原型,有了这个原型,我们会用瀑布模型的周期,走一轮,开发出一个版本来。

开发出一个版本之后,这个版本又作为一个原型,作为下一个步骤的起点,一圈一圈下来,就越来越像我们的产品。所以是原型 瀑布形成的螺旋模型。

适用场景

一般适合于大型的系统,有一个显著的特点,引入了风险分析, 是它的一个可圈可点的特点。

4、增量模型

概念与思想

增量模型跟螺旋模型很类似,但是却有些不同。

增量模型的考量是,最开始我们就将整个系统分块了,分为了1,2,3,4,5,6块。我们先做模块一,再做模块二,最后模块六。是按时间一块一块的开发处理出来,然后附属上去的。

增量模型形态

架构师备战(三)-软件工程总览(软件工程 架构)

5、V模型

基本思想

是一个强调测试贯穿于始终的模型,也就是测试尽早做,提前做。因为吃了瀑布模型的亏,一边编码一边测试,不容易产生偏离主线太大的情况。

代码都还没写,如何先做测试呢? 其实主要是先把一系列的测试的规划工作过做在前面。

V模型形态

架构师备战(三)-软件工程总览(软件工程 架构)

6、喷泉模型

只要知道是面向对象的就可以了,它的形态是像喷泉一样,往上面喷下来。

喷泉模型是迭代的,无间隙的。

架构师备战(三)-软件工程总览(软件工程 架构)

7、RAD(快速应用开发)

基本思想

快速应用开发用了两种模型做综合

  • 瀑布模型(SDLC)
  • 基于构建的开发(CBSD)

可以看出,比传统的开发模型要快一些,关键核心点显然不在于瀑布模型,关键点在于基于构建的开发

构建组装模型形态

架构师备战(三)-软件工程总览(软件工程 架构)

RAD快的核心原因是因为构建组装模型。

比如:疫情期间雷神山建立,非常的快捷。是因为基础构建已经生产好了,只是简单的拼装起来就可以使用了,因此快。

再比如,我们将登录模块做成一个标准化的组件,其他系统都使用该模块,那就节省了很多的时间和金钱成本,并且其他系统已经在用了,那么这个组件时非常可靠的。

构件组装模型的特点

  • 快速
  • 省成本
  • 可靠性高

标准化

要将构架标准化,各个系统才能通用。比如CORBA、 COM/DCOM、EJB

缺点

标准化的构建本身开发难度很高,因为要适用于各个系统,所以要求比定制的构建要难很多。投入的成本也比较高。初步建立时,要走的路很长,因为构建很多,并且构建要适用多个场景,难度很大。

8、统一过程

统一过程也叫UP或者RUP。这种开发方法是在基于构建的方法发展而来,也是基于构建化的思想发展而来。

架构师备战(三)-软件工程总览(软件工程 架构)

统一过程的三大特点

  • 用例驱动
    • 在进行软件开发过程中,是用什么驱动力去推动整个过程
    • 用例驱动就是一开始会构建用例,然后一步一步将用例实现出来,测试时也依据用例设计一些测试用例
    • 测试驱动会在开发的一开始,就引入测试相关的东西,整个流程中的每一步衔接都都利用测试去推动流程。
  • 以架构为中心
    • 比如建房子时需要先把框架建立起来,这就是架构的思想。
  • 迭代和增量
    • 整个开发过程不是一次性完成的,而是走多轮迭代,一轮一轮完成的,每次迭代都有新的东西加进来。

统一过程的四个阶段

  • 初始
    • 确定项目的范围和边界:该做什么,不该做什么,要做鉴定
    • 识别系统的关键用例:系统跑起来之后,用例被使用的频率,频率最高的用例就是关键用例,二八原则,百分之二十的用例占用了百分之八十的使用。
    • 展示系统的候选架构
    • 估计项目费用和时间
    • 评估项目风险
  • 细化
    • 分析系统问题领域
    • 建立软件架构基础, 就是完成架构设计
    • 淘汰最高风险元素
  • 构建
    • 开发剩余的构建,能用以前的构建就用以前的,没有就新开发
    • 构建组装与测试,正因为只是构建的组装,所以快
  • 交付
    • 进行β测试
    • 制作发布版本
    • 用户文档定稿
    • 确认新系统
    • 培训、调整产品

9、敏捷方法

相比于以前的的方法而言,敏捷方法是一个相对比较新的方法,在2000年左右才出来的,以前的哪些方法是在七几年,八几年出现的产物,可以说不在同一个时代。在以前的方法很成熟的前提下,来进行的调整。

敏捷方法是一种小步快跑的方式,适合于小型项目开发。

敏捷方法使用了哪些模型

  • 自适应开发
  • 水晶方法
  • 特性驱动开发
  • 极限编程

敏捷方法的基本原则

  • 短平快的回忆
  • 小型版本发布
  • 较少的文档
  • 合作为重
  • 客户直接参与
  • 自动化测试
  • 结对编程
  • 测试驱动开发
  • 持续集成
  • 重构

敏捷方法四大价值观

  • 沟通
    • 敏捷开发砍掉了很多文档,而文档是用来传递信息的,减少了文档,就要强调沟通,比如结对编成,面对面沟通。
  • 简单
    • 简单设计,不过度设计。比如系统考虑长远,要把一些东西做得标准化,但是很多得文档和设计其实没有用上,这就是过度设计。
    • 以简单的思路,让系统下跑起来,后面可以局部重构优化.
  • 反馈
    • 及时跟客户沟通相关的问题
  • 勇气
    • 主要是应对变更的勇气, 因为开发一般排斥变更, 因为一变更就意味着变更, 但是不变更又不可能. 变更在前面爆发出来, 越到后面变更就越小.

敏捷方法就一定正确吗

不一定,因为这些方法都是人提出来的,带有一定的主观性, 所以不一定是最优的一种方法.

敏捷方法五大原则

  • 快速反馈
  • 简单性假设
  • 逐步修改
  • 提倡更改
  • 优质工作

敏捷方法12大最佳实践

  • 计划游戏、结对编成、小型发布、集体代码所有制、隐喻、持续集成
  • 简单设计、每周工作40小时、测试先行、现场客户、重构、编码标准

如果论文考到了敏捷方法的应用,不仅要对价值观进行思想的阐述, 还要讲一讲项目中到底用了一些什么手段,去完成我们的工作.

比如:测试驱动/测试先行等, 比如结对编成(两个程序员结成一对, 一个人写代码, 一个人看着, 隔段时间替换), 国内一般不会这样.

结对编成的好处

  • 两个人会先碰一下思路, 解决方案, , 可以提前发现问题
  • 调试的时候两个人看, 比一个人看更容易发现问题
  • 团队核心人员流动, 新手接替核心业务需要一个过程, 但结对编成可以无缝衔接, 从而减小风险

集体代码所有制度: 所有人都能看到全部代码

持续集成: 一到两周发布一个版本, 持续的集成

每周四十小时: 不加班, 但一般不可能

测试先行: 测试用例的提前设计, 先把测试框架搭建等, 先把测试逻辑写了

敏捷方法特点

  • 极限编程(XP): 费用严格控制
  • 水晶方法(Cockburn): 用最少纪律约束而仍能成功的方法
  • 开放式源码: 程序开发人员在地域上分布很广
  • Scrum: 明确定义了可重复的方法过程, 是用得最多得方法论
  • FDD(驱动开发方法): 有首席程序员和类程序员得概念
  • ASD方法: 核心是猜测 合作与学习

Scrum开发模型

Scrum是目前相对来水用得最广泛的模型方法, 会强调基本2-4周一个冲刺, 所谓的冲刺,就是发布一个可用的版本. 因为人都是有惰性的, 一个时间比较长的项目, 前面一般都会按照任务慢慢来, 但是后面又发现来不及了.所以为了避免这种情况, 让需求更加可用, 所以提出了敏捷方法.

Scrum会先把要开发的功能放在Product Backlog(备忘录). 会从里面取出一小块来, 按照一个项目, 以2-4周为时间限制, 来进行开发, 这样一轮一轮的完成. 从而让每个过程可控.

架构师备战(三)-软件工程总览(软件工程 架构)

10、逆向工程

就是从最终的成品一步一步的反推它的设计和分析. 比如山寨手机, 先买一台手机, 拆了之后看它模仿它的零件. 再比如仿制军事设备.

架构师备战(三)-软件工程总览(软件工程 架构)

  • 实现级: 包括程序的抽象语法树, 符号表, 过程的设计表示. 拿到代码,通过代码构造语法树, 去做一些初步分析.
  • 结构级: 包括反映程序分量之间相互依赖关系的信息, 例如: 调用图, 结构图, 程序和数据结构
  • 功能级: 包括反映程序段功能及程序段之间关系的信息, 例如数据和控制流模型
  • 领域级: 包括反映程序分量或程序诸实体和应用领域概念之间的对应关系的信息, 例如实体关系模型

11、净室软件工程

  • 净室就是无尘室, 洁净室. 也就是一个受控污染级别的环境. 比如芯片制造药串防尘服.
  • 使用盒结构规约(或形式化方法) 进行分析和设计建模, 并且强调将正确性验证, 而不是测试, 作为发现和消除错误的主要机制.
    • 比如使用代码生成器生成代码, 而不用自己写, 从而减小错误产生的概率. 使用类似的约束, 从源头减小出错的概率, 而不是测试出问题, 再去改
  • 使用统计的测试来获取认证被交付的软件的可靠性所必须的出错率信息.

第三章 需求工程概念与分类

1、软件需求概念

软件需求指用户对系统在功能、行为、性能、设计约束方面的期望。

简单来说,就是用户对系统的期望,所以很多的需求来自于用户方。

软件需求指用户解决问题或达到目标所需的条件或能力,是系统或系统的部件需要满足合同、标准、规范或其他正式规定文档所需具有的条件或能力,以及反应这些条件或能力的文档说明。

2、需求工程的分类

  • 需求开发(技术维度)
    • 需求的获取
      • 跟客户做访谈,电话沟通,开会,问卷调查等获取原始需求
    • 需求分析
      • 获取原始需求之后,进行分析整合,因为获取的需求可能存在冲突,因此发现问题并整合需求
    • 需求定义
      • 将分析后的需求,落地为文档的过程,会形成SRS(需求规格说明书)
    • 需求验证
      • 需求获取,分析,定义了,结果客户没有验证,客户说要的不是这个东西。所以需要用户来参与验证。需求验证之后得到需求基线。需求基线就是后面的设计开发就按照验证后的需求作为基线进行设计和开发和需求管理
  • 需求管理(用于支持需求开发,项目管理维度)
    • 变更控制
      • 如果按基线走,表示需求没变更,如果基线变化,表示需求的变化,那就要对基线的变更。
    • 版本控制
      • 基线变更会产生不同的版本,从而进行版本控制
    • 需求跟踪
      • 落实这个基线,就是需求跟踪
    • 需求状态跟踪
      • 需求会分多个状态,后面会讲

3、需求的分类

从技术维度分类

  • 业务需求
    • 高层次的需求,比如老板(决策层)说因为什么样的背景下,要开发一个xxx系统,以完成什么什么样的工作,达到什么样的要求。也就是一些比较泛化的需求。这里决策层需求不管系统哪些人用,因为涉及到哪些人用,是用户需求。
  • 用户需求
    • 最终会去使用系统的那些人员,各方都需要从他们的视角去看系统需要达到什么需求
  • 系统需求:考虑的是把需求转换为计算机化的东西,进一步分为以下几种需求
    • 功能需求
      • 哪些职能要做到
    • 性能需求
      • 广义的性能,安全性,响应时间,吞吐量,比如报表点一下,要多长时间返回结果等。
    • 设计约束
      • 既不是功能需求,也不是性能需求。
      • 比如用客户要求数据库使用mysql, 比如运行在linux服务器上,比如页面需要炫酷一些,比如接口协议使用JSON/XML等都是不属于功能也不属于性能,都归类为设计约束

从管理维度分类

  • 基本需求
    • 基本需求就是客户明确要求要实现的需求,即可能是功能上的,也可能是性能上的,也可以是设计约束。所以是不同的维度去思考。
  • 期望需求
    • 也叫隐含需求,客户没说,但是从他的角度来说,这个东西就是我不说,你也应该要知道的需求。这个不好把握,因为需求是隐含方式的。
  • 兴奋需求
    • 就是给系统镀金的需求,用户没有要求要做,也没有期望要做,但是你做了。
    • 做兴奋需求好不好?
      • 当然不好,因为在即要增加开发成本,又要增加风险,并且客户不会给你支付任何报酬。所以我们没有必要去做兴奋需求。
      • 比如:假如你是项目负责人,项目时间维度是六个月,项目奖金是10w, 你手下的一个程序员悄悄的做了镀金需求,导致项目延期,你一毛钱没拿到,还浪费了人力人本。那是不是只有杀了他祭天了啊。

需求的获取方法

  • 收集资料
  • 联合讨论会
  • 用户访谈
  • 书面调查
  • 现场观摩
  • 参与业务实践
  • 阅读历史文档
  • 抽样调查(是节省成本的方式,适用于用户群体多,抽部分样本,以小概全)

架构师备战(三)-软件工程(五) 结构化需求与面向对象需求分析

4、需求开发

4.1、需求定义

前面提到需求工程相关概念时,知道了需求开发分为需求的获取->需求分析->需求定义->需求验证四个阶段。

需求获取主要是通过跟客户访谈,电话沟通,开会,问卷等获取原始需求,然后将原始需求进行分析整合就是需求分析。而将整合后的需求,落地成真正的需求规格说明书,形成SRS时至关重要的一步,也就是需求定义的过程。

需求定义有哪些方法呢?

严格定义法结构化方法,适用于需求明确场景

  • 所有需求都能够被预先定义
  • 开发人员与用户之间能够准确而清晰的交流
  • 采用图形/文字可以充分体现最终系统

原型法适合于需求不明确

  • 并非所有的需求都能在开发前被准确的说明(需求不确定)
  • 项目参加者之间通常都存在交流上的困难(沟通困难)
  • 需要实际的、可供用户参与的系统模型(需要快速构建原型给用户看效果)
  • 有合适的系统开发环境(已经有了一个开发环境的系统,可以直接看)
  • 反复是完全需要和值得提倡的,需求一旦确定,就应该遵从严格的方法(需求会反复调整,调整越多意味着以后出现的问题可能越少,所以提倡反复。但是需求一旦确定,就不能轻易改动)

4.2、需求确认

需求获取,分析,定义了,结果客户没有验证,客户说要的不是这个东西。所以需要用户来参与验证

需求验证之后得到需求基线。需求基线就是后面的设计开发就按照验证后的需求作为基线进行设计和开发和需求管理。

而我们做需求验证的时候,一般需求做需求评审和需求测试。通过之后就需要由客户参与完成需求的验证功能。

需求验证方法

  • 需求评审
    • 比如大家开个会,顺着需求规格说明书(SRS)来当着大家的面过一遍需求,当所有的需求评审完毕之后,客户统一签字了,那就完成了需求的验证。
    • 一般情况就是产品经理,开发负责人,部门负责人(甚至会有更大的领导),还有就是客户方的项目负责人(负责验收项目的相关人员)一起开会,过需求。
    • 一般验证过后的需求,在客户验收系统时会作为一个验收的标准。验收了才会打尾款,所以非常重要.
  • 需求测试
    • 这里的需求测试不是真正的软件测试, 因为系统还没有开发出来
    • 只是通过一些场景化的东西来验证我们预计开发的系统能否合规

需求验证其实在现实中可能会很难做到, 因为现在是一个非常内卷的时代, 软件供应商有点供大于求, 因此甲方的选择就很多, 所以一般都惹不起甲方爸爸. 就有了那句"甲方略我千百遍, 我待甲方如初恋"的说法.

5、需求管理

前面我们知道了, 需求经过验证后会形成需求基线, 需求基线就相当于我们现在一个需求里程碑, 一版需求定稿.

5.1、需求状态

而需求从定义到形成基线,再到最终项目验收我们的需求状态有一个变化, 如下图所示正是需求变化的状态图.

架构师备战(三)-软件工程总览(软件工程 架构)

5.2、需求跟踪

我们的软件需求定稿之后,会作为需求基线,后面的设计与实现都是基于需求基线来实现的. 那么到底需求有没有被实现, 有没有多实现, 能不能满足需求, 这个倒交付项目时才发现不满足交付要求, 那么就是有问题的.

所以我们需要需求跟踪, 知道我们有哪些需求做了, 哪些没做, 哪些多做了等等. 所以我们用需求跟踪矩阵来表示需求是否被完成.

架构师备战(三)-软件工程总览(软件工程 架构)

比如这张表,我们把需求都详细列出来了,哪些完成的就打勾.

比如我们UC-2这个用例本来该有,但是原始需求没有任何一个打勾,表示UC-2可能是不需要实现的,但是我们去实现了,这就属于兴奋需求,镀金需求。

比如我们FR-m这一行所有的用例都没有打勾,那就代表这个需求还没有被实现。

5.3、变更控制

需求变更指的是我们需求基线形成之后,我们要去改变这种计划,那么就需要去走一个变更流程。

注意这里是需求基线形成之后的改变,才会走需求变更流程。如果您的需求还没有形成基线,那么任你如何修改,都是没有关系的。

而需求变更需求走如下流程,状态变化也如下图所示

架构师备战(三)-软件工程总览(软件工程 架构)

主要描述了需求变更的步骤

  • 1、先提交变更申请
    • 一般是书面申请,通常会发公司的企业邮箱留底,告知相关人员。然后会使用公司项目管理平台,提交变更申请。
  • 2、变更评估
    • 不是说申请了,就能够变更,需要对变更进行评估,评估这个变更做了之后与做之前的价值比较,只有变更价值大于不变更价值时,一般才有变更的意义。
  • 3、变更决策
    • 把评估遇到的问题抛出来,然后把甲方,乙方等相关人员一起来进行决策
    • 一般会有一个叫做CCB来决策,也就是变更控制委员会
  • 4、变更实施
    • 决策好之后,就要进行变更的实施了,着手来干这个事情了
  • 5、变更验证
    • 变更的需求实现之后,同样需要用户方来进行验证,不然你变更了,但是客户不承认,那就完蛋了。
  • 6、沟通存档
    • 验证之后对沟通进行存档,从而形成一个依据。

6、结构化需求分析(SA)

结构化需求分析要完成功能模型、数据模型和行为模型的构建。

6.1、功能模型

一般用数据流图进行建模,也就是DFD。

比如我们要开发一个在线教育平台系统,我们把这个系统看作一个整体,去分析哪些人员会用到这个系统。比如有学员,培训部,辅导老师这些人员参与,数据流图就是为了分析这些外部实体与系统之间的关系展现的。

仅仅是这样展示信息量是有限的,一些细节的东西是没有办法搞清楚的,可以对数据流图进行细化。对要完成的系统进行加工。

架构师备战(三)-软件工程总览(软件工程 架构)

数据流图涉及到的东西

  • 数据流(带箭头的数据)
  • 加工(要完成的系统0和下面的1,2,3都是加工,其实就是要完成的功能块)
  • 数据存储(一般对应的是数据表)
  • 外部实体(比如学员)

6.2、数据模型(E-R)

数据模型使用E-R图进行建模,也称为实体-联系模型

架构师备战(三)-软件工程总览(软件工程 架构)

实体与实体之间的联系,比如供应商和工程就存在供应的联系。

6.3、行为模型(STD)

行为模型使用状态转换图进行展现

架构师备战(三)-软件工程总览(软件工程 架构)

比如学员注册之后,没有缴费,就返回到注册环节

如果已缴费,那就开通课程,进入到学习。

学习之后进行测试,如果测试不合格则继续学习,测试合格就退出这个流程。

6.4、数据字典

数据字典,是一个解释性的东西。

比如上面的DFD图中,有一个在线系统注册之后,会将课程安排返回学员。我们知道这个过程,但是不能知道它这里面到底包含了什么,此时我们的数据字典就可以粉墨登场了,数据字典就会解释课程安排里面必须包含哪些东西,可以选择性的包含哪些东西。

配合数据字典,我们就对原始的图,有更加清晰的认识了。同理E-R也可以使用数据字典进行描述.

也就是说, 数据字典是配合各方去进行相应的数据解读的一种工具.

7、面向对象需求分析(OOA)

7.1.概念

  • 对象
    • 对应的是现实中的人, 物这些东西. 一般具有属性,方法,ID(对象的唯一标识)
    • 比如人类, 铁钩, 西瓜, 猫咪 这些东西都是一个对象, 他们都有自己的特点和能做的事, 将这些对象抽象出来就形成了一个类.
  • (实体类, 边界类, 控制类)
    • 类就是对对象进行抽象
    • 实体类
      • 我们需要一个类来保存一些永久性的信息, 用实体类, 因为实体类就是用来存储数据的, 一般对应数据库的表.
    • 边界类
      • 边界类用于封装在用例内/外流动的信息或数据流.边界类位于系统与外界的交界处,包括所有的窗体, 报表, 打印机和扫描仪等硬件的接口,以及与其他系统的接口.
    • 控制类
      • 控制类是用于控制用例工作的类, 一般是由动宾结构的短语(动词 名词 或 名词 动词)转化来的名词.
      • 比如: "身份验证"可以对应于一个控制类,"身份验证器", 它提供了与身份验证相关的所有相关操作.
  • 抽象
    • 就是抽取共性的一个过程
  • 封装
    • 封装就是内部隐藏的一种机制,比如类里面就有这种机制.
    • 比如我们把类的成员变量设置为private 表示外界不能直接访问. 外界要访问,就必须走开发的统一接口, 就能够发现问题和管控, 不容易出现一些不可预见的问题.
  • 继承与泛化
    • 就是父子关系
    • 继承
      • 一个类继承另外一个类, 就拥有了它所有的属性和方法, 继承是一种复用的机制.
    • 泛化
      • 从关系来讲,只有泛化关系, 没有继承. 因为A1和A2继承了父类A的特性, 这个整个东西叫做泛化. 这个看起来是先有A,再有A1和A2. 实际上是先有了A1, A2, 然后把它们的共性抽取出来,这个过程叫做泛化.
  • 多态
    • 多态就是一个接口或者类的不同实现,不同的展现形式. 比如人, 可以分为好人和坏人,就是两种不同的形式,这就是多态.
  • 接口
    • 接口是一种特殊的类, 它只声明的方法, 但不做实现. 通俗的讲, 就类似书的目录, 书的内容就是对目录具体的实现.
  • 消息
    • 消息是对象之间传递信息的一种机制
    • 一般来说是异步
  • 组件
    • 组件也就是构建,颗粒度比类, 比对象都要大一号.
  • 模式与复用
    • 一般我们提模式, 就是为了去复用, 后面设置模式会详解. 设计模式就是解决某种问题的一种常见的解决方案.

第四章 UML统一建模语言

面向对象分析里面有一个非常重要的工具叫UML,UML不仅在工作中非常重要,在考试当中也是非常重要的,即作为上午综合体,又大概率又会出现在下午的案例分析中,作为一个25分的大题。

UML叫做统一建模语言,它主要用于需求分析和软件的设计,来做一些模型的制作。比如我们要开发一个系统,如果我们用纯粹的文字表达和表述,可以想象相关人员理解会多么困难。所以将收集到的相关信息用图形直观的展示出来,UML就是做这么一个事情。从而便于大家的沟通和后续的设计和开发。

1、UML构成

  • 构造块
    • 事物(了解)
      • 结构事物
        • 最静态的部分,包括:类,接口,协作(协作的关系)、用例、活动类、构件和节点
      • 行为事物
        • 代表时间和空间上的动作。包括:消息,动作次序、连接
      • 分组事物
        • 看成是一个盒子,比如:包,构件等概念就是分组事物
      • 注释事物
        • UML模型的解释部分,描述、说明和标注模型的元素。类似我们写代码时的注释
    • 关系(核心)
      • 后面会详解,每个图里面的关系比较多
    • 图(核心)
      • 用例图,时序图的等,后续详解
  • 规则
    • 范围:给一个名字以特定含义的语境
    • 可见性:怎样使用或看见名字
    • 完整性:事物如何正确、一致地相互联系
    • 执行:运行后模拟动态模型的含义是什么
  • 公共机制
    • 规格说明:事物语义的细节描述,它是模型真正的核心
    • 修饰:通过修饰来表示更多的信息
    • 公共分类:类与对象、接口的实现
    • 扩展机制:允许添加新的规则

2、UML图特点

  • 静态图(结构图)
    • 类图:一组类、接口、协作和它们之间的关系
    • 对象图:一组对象及它们之间的关系
    • 构件图:一个封装的类和它的接口
    • 部署图:软硬件之间映射
    • 制品图:系统的物理结构
    • 包图:由模型本身分解而成的组织单元,以及它们之间的依赖关系
    • 组合结构图:多种图的混合使用的一种机制
  • 动态图(行为图)
    • 用例图:系统与外部参与者的交互
    • 顺序图:强调按时间顺序
    • 通信图:也叫做协作图
    • 状态图:状态转换变迁
    • 活动图:类似程序流程图,并行行为
    • 定时图:强调实际时间
    • 交互概览图(多种交互图的组合)

2.1、用例图

用例图的特点

  • 描述一组用例、参与者及它们之间的关系
  • 从用户角度描述系统功能
  • 参与者是外部触发因素(包括用户、组织、外部系统、时间)
  • 用例是功能单元

用例中使用了哪些关系

  • 包含关系
  • 扩展关系
  • 泛化关系

用例建模的流程

  • 识别参与者(必须)
  • 合并需求获得用例(必须)
  • 细化用例描述(必须)
  • 调整用例模型(可选)

用例图

架构师备战(三)-软件工程总览(软件工程 架构)

参与者就是乘客,保安,技术人员。关闭电梯门,打开电梯门等就是用例。

2.2、顺序图

顺序图是一种交互图,强调对象之间消息发送的顺序,同时显示对象之间的交互,也叫做时序图。

架构师备战(三)-软件工程总览(软件工程 架构)

比如登录场景顺序图。

图中的竖着的虚线表示生命线,虚线箭头表示一个返回,实线箭头表示一个调用,最上面的方框表示对象等。用这样一个图来描述登录的流程,并且这个流程是强调先后顺序的。

2.3、通信图

通信图也叫做协作图,跟顺序图有着强相关性。

2.4、状态图

状态图表达的是状态的转换与变迁。

就是一种状态经过什么刺激,转换为另一种状态。应用场景挺多的,比如开发一个系统,有会员机制,积分足够可能就会有会员等级的状态变化。

架构师备战(三)-软件工程总览(软件工程 架构)

2.5、活动图

活动图类似于程序的流程图,但是跟流程图有些区别,它能够去表达一些并行行为

架构师备战(三)-软件工程总览(软件工程 架构)

2.6、定时图

定时图,会强调实际的时间

2.7、UML-4 1视图

UML-4 1视图将会与后面的架构4 1视图会一一对应上

视图往往出现在什么场景:我们看待一个事物,我们觉得它很复杂,难以搞清楚,为了化繁为简,我们会从一个侧面去看,这就是视图。而4 1视图就是分不同角度去看事物。

  • 逻辑视图(logical view)
    • 一般使用类与对象来表示,主要表示系统的功能 针对的人群是系统分析、设计人员
  • 实现视图(implementation view)
    • 一般是呈现了物理代码文件和组件,针对人群的是程序员
  • 进程视图(process view)
    • 一般强调的是并发,跟线程、进程相关,针对的人群一般是系统集成人员
  • 部署视图(deploy view)
    • 强调的是软件到硬件的映射关系,针对的人群是系统和网络工程师
  • 用例视图(use-case view, 4 1的1,它跟其他4个人都有相关性)
    • 强调的是需求分析的模型, 针对的人群是最终的用户

UML作为一种工具,虽然是从需求分析阶段去用它,但事实上,它在整个开发各个阶段都会要用到,分析阶段用了设计阶段用,设计阶段用了实现阶段也会用到前期设计好的这些图。

3、OOA阶段两个重要模型

需求分析中有两个重要模型必须要建立,一个是用例模型,一个是分析模型。

也就是说,我们要去做UML分析阶段的建模工作,主要就是建这两种模型,其他的模型不是必须的,但可以辅助性的使用。用例模型对应的是用例图,是必须的。分析模型对应的是类图,也是必须的。

用例模型的构建分四个步骤,每个步骤都会各司其职,首先识别参与者,再合并需求并获得用例,然后细化用例模型,最后调整用例模型。

分析模型的构建也分四个步骤,主要完成先定义类,再定义类之间的关系,然后为类添加职责,从而为类建立交互图。

  • 用例模型(分为四个步骤)
    • 1、识别参与者
    • 2、合并需求获得用例
    • 3、细化用例描述
      • 用例名称
      • 简要说明
      • 事件流
      • 非功能需求
      • 前置条件
      • 后置条件
      • 扩展点
      • 优先级
    • 4、调整用例模型
      • 包含关系
      • 扩展关系
      • 泛化关系
  • 分析模型(分为四个步骤)
    • 1、定义概念类
    • 2、识别类之间的关系
      • 依赖关系
      • 关联关系
      • 聚合关系
      • 组合关系
      • 泛化关系
      • 实现关系
    • 3、为类添加职责
    • 4、建立交互图

4、用例图详解(重要)

4.1、用例图的定义

用例图是描述一组用例、参与者及它们之间的关系

4.2、用例图的基本思想

  • 1、从用户角度描述系统的功能
    • 比如我们要去进行系统的开发,要去获取对应的需求,是不是你也会去找到,系统到底有哪些人会去用它。然会去与用它的人沟通,看一下他们希望用到一些什么系统的功能。
  • 2、参与者是外部触发因素
    • 常见参与者:系统用户,老师,学生都属于参与者
    • 特殊的参与者:组织、外部系统,时间
      • 外部B系统通过某种机制调用A系统, B对于A来说就是一个参与者
      • 时间,温度,传感器等都可以别鉴定为参与者,比如时间,我每个月,会有每个月的一号统计上一个月的报表,这个是时间就是参与者
  • 3、用例是功能单元

4.3、用例图的设计思想

用例图本质上来说,是一种比较简单的图,它并不复杂,设计这种图的目的就是让大家从枯燥文字信息里面获取信息的格局发生变化才提出的,所以它本质上就很直观,其实也很实用。

4.4、用例建模的流程

  • 识别参与者(必须)
  • 合并需求获得用例(必须)
  • 细化用例描述(必须)
  • 调整用例模型(可选)

一个图书管理员相关的用例图如下

架构师备战(三)-软件工程总览(软件工程 架构)

用例图是怎么绘制出来的呢,一般是这样的情况,比如我们前期已经调研出来了一些情况,可能你都已经采访了图书管理员,比如你问他平时都会用到哪些功能呢,他巴拉巴拉给你说了一大堆,你就从这一段文字当中,提取有效的信息。

4.4.1、识别参与者(必须)

图书管理员提取出来,成为参与者。这就是识别参与者

4.4.2、合并需求获得用例(必须)

然后图书馆管理员的描述了解到他要用到新增书籍,查询数据,登记外借信息,查询外借信息等,这就是所谓的合并需求获取用例。本质上就是把文字转化为图形的一个过程。

4.4.3、细化用例描述(必须)

这个用例描述非常重要,不可或缺,包含如下内容:

  • 用例名称
  • 简要说明
  • 事件流
    • 主事件流
    • 备选事件流
  • 非功能需求
  • 前置条件
  • 后置条件
  • 扩展点
  • 优先级

我们就看这张图,能够看出来登记外界信息到底要登记什么吗,显然是不知道的,这时候就需要细化用例描述了。

架构师备战(三)-软件工程总览(软件工程 架构)

有了这个细化用例描述,就很清楚这个用例是干什么用的了。

4.4.4、调整用例模型(可选)

也叫做优化用例模型,主要是根据三大关系,包含关系(早期也叫使用关系)、扩展关系、泛化关系。

4.4.5、包含、扩展、泛化

包含关系:其中这个提取出来的公共用例称为抽象关系,而把原始用例称为基本用例或基础用例关系。当可以从两个或两个以上的用例中提取公共行为时,应该使用包含关系来表示它们。

扩展关系:如果一个用例明显的混合了两种或两种以上的不同场景,根据情况可能发生多种分支,则可以将这个用例分为一个基本用例和一个或多个扩展关系,这样描述可能更加清晰。

架构师备战(三)-软件工程总览(软件工程 架构)

泛化关系:当多个用例共同有一种类似的结构和行为时,可以将他们的共性抽象成父用例,其他的用例作为泛化关系的子用例。在用例的泛化关系中,子用例是父用例的一种特殊形式,子用例集成了父用例所有结构、行为和关系。

前面这张图不好展示泛化关系,下面这张图将展示这三种给关系。

架构师备战(三)-软件工程总览(软件工程 架构)

5、类图与对象图详解(重要)

5.1、类图与对象图的概念

类图(class diagram)描述一组类、接口、协作和它们之间的关系

对象图(object diagram)描述一组对象及它们之间的关系、对象图描述了在类图中所建立的事物实例的静态快照

5.2、类图与对象图的区别

类图和对象图基本上是一样的,只是对象图一般会在类前面有个冒号,或者变量名:类名, 具体的属性可能会具有具体的值。

类图如下,是抽象出来的概念

架构师备战(三)-软件工程总览(软件工程 架构)

对象图如下,抽象的一种具体实现,对象具有具体的变量,对象的属性具有具体的值。但是图的关系是一样的。

架构师备战(三)-软件工程总览(软件工程 架构)

类图:分三层,第一层表示类名,第二层表示类的属性,第三层表示类具有的方法

对象图:也分三层,第一层格式“对象名:类名”,表示是类的实例化。第二层表示属性,属性可以被赋值。第三层表示方法。当然,层数是可以省略或者不写的。

5.3、类与类之间的关系

类与类之间的关系

  • 1: 表示一个集合中的一个对象与另一个集合中的1个对象
  • 0..*:表示一个集合中的一个对象对应另一个集合中0个或多个对象(可以不对应)
  • 1..*:表示一个集合中的一个对象对应另一个集合中1个或多个对象(至少对应一个)
  • *: 表示一个集合中的一个对象对应另一个集合中的多个对象

我们使用一个简单的类图来说明类与类之间的关系。

架构师备战(三)-软件工程总览(软件工程 架构)

类和类之间可能存在1对0到多的这种关系,比如上面的一个书籍列表,可以对应0到多本书籍,也就是可以包含多本书籍,也可以不包含。所以书籍列表和书籍之间是1:0..*的关系。

如何来判断这种1对0..*多关系呢,其实可以通过E-R图来进行判断。类图里面也是同样的逻辑。只是这个类图会比E-R更加细致一点而已。

5.4、类图与对象图关系说明

5.4.1、依赖关系

概念:一个事物发生变化影响另一个事物,它们之间的关系就叫做依赖关系

使用正向虚线实心箭头表示。

架构师备战(三)-软件工程总览(软件工程 架构)

上图就是表示A调用了B的方法,当B类发生变化时,A也要跟着发生变化,这就是依赖关系。

5.4.2、泛化关系

概念:泛化关系表示的是特殊和一般的关系,也是父子关系,特殊指的是子类,一般指的的父类。子类继承了父类的特点,所以子类的特殊,父类是一般。

使用正向实线空心箭头表示。

架构师备战(三)-软件工程总览(软件工程 架构)

动物是一般的类,狗子是一种特殊的动物,除了会动物的喝水技能,还会自己的特殊技能汪汪叫。

5.4.3、关联关系

概念:描述了一组链,链是对象之间的连接

聚合关系:整体与部分生命周期不同,使用正向实线空心菱形头表示

组合关系:整体与部分生命周期相同,使用正向实线实心菱形头表示

整体是带有菱形的那一端

架构师备战(三)-软件工程总览(软件工程 架构)

聚合和组合的共性就是都表示整体和部分的关系。比如上面的书籍列表和书籍就是整体和部分的关系

聚合和组合的不同在于生命周期是否与整体相同,相同则是组合关系,不同则是聚合关系。

比如:车子和轮胎的关系就是聚合关系,车子坏了,轮胎是新的,轮胎可以拆下来放在其他车子上。也就意味着车子的消亡,并不会影响轮子的消亡。也就是轮子(部分)的生命周期和车子(整体)的生命周期是不同的。

比如:公司和部门的关系就是组合关系,公司倒闭了,公司的部门一定也会随之消亡。不存在公司都不存在了,公司的某个部门还存在。整体和部分的生命周期是一样的,这就是组合关系。

组合和聚合的应用场景

组合和聚合的应用场景是不同的,可能导致的情景也是不一样的。主要应用在内存回收场景。

比如我们在游戏场景,我们要去捡一个装备,装备没捡到我们掉线了,服务器不能回收装备,导致内存居高不下。但是重启后又好了,因为内存回收了。这就是聚合场景导致的问题。但是组合场景又不适用,因为装备你不捡,其他人还可能去捡。

5.4.4、实现关系

概念:表示接口与类之间的关系,,接口只定义类的方法,而接口的实现类就是对接口的实现

使用正向虚线空心箭头表示

架构师备战(三)-软件工程总览(软件工程 架构)

6、顺序图详解

顺序图(sequence diagram, 顺序图),顺序图是一种交互图(interaction diagram),它强调的是对象之间消息发送的顺序,同时显示对象之间的交互

下面以一个简单的ATM机读卡的顺序图来说明

架构师备战(三)-软件工程总览(软件工程 架构)

最上面的每一个框的东西都是一个对象,是可以使用对象图的模式表示的,也就是变量名:类名或者:类名,也可以像这样直接写一个具体的对象名称.

每个对象沿着一条虚线下来, 表示对象的生命线, 也就是对象从诞生到消亡的过程.

而上面的正向实线箭头的查卡表示一个消息, 这个消息讲述的是用户发送了一个查卡的一个消息给ATM读卡器,ATM读卡器接收了一个消息之后, 就执行了一个读取卡的操作,然后再从上到下按照顺序的执行完流程.

这种图在软件设计师里面考得比较多. 架构师考试里面相对较少.

7、活动图详解

活动图(activity diagram),将进程或其他计算结构展示为计算机内部一步步得控制流和数据流。活动图专注于系统得动态视图。它对系统的功能建模和业务流程建模特别重要,并强调对象间的控制流程。

下面使用一张用户下单活动图来做简单分析

架构师备战(三)-软件工程总览(软件工程 架构)

活动图类似于结构化里面的流程图,就是开始之后先做什么,再做什么,再做什么,一步一步的下来。

但是,活动图跟流程图有所区别,流程图不能表示并发操作,但是流程图可以

可以看到,我们生成送活动,和用户选择支付方式到收款,最后都到发货这个节点是可以并行而操作的。因此它表现出了并发的形式。

活动图跟状态图很类似,但是考试的时候,一般更喜欢考状态图

7.1、泳道式的活动图

活动图还有一种变种,就是泳倒式活动图。有的时候我们不仅仅需要了解一个活动中干了什么,但是我们没法知道谁该干哪些工作,所以泳道图就是为了让你知道,哪些活动该哪些人或者系统去做。

以上图改造成为一个泳道活动图。涉及到的对象有客户,系统,供应商。

架构师备战(三)-软件工程总览(软件工程 架构)

上面一个个对象,就形如一个个泳道,在自己泳道里卖弄的流程就归属于自己,这样就能够明确哪些任务是由谁去做了。

8、状态图详解

状态图(state diagram), 描述一个状态机,它由状态,转移,事件和活动组成。状态图给出了对象的动态视图。

它对于接口、类、协作的行为建模微微重要,而且强调事件导致的对象行为,这非常有助于对反应式系统建模。

下面使用烧水壶烧开水开关由关到开并且把水烧开的过程总状态变化为例来说明。

架构师备战(三)-软件工程总览(软件工程 架构)

我们在很多系统中,都不可避免的会涉及到一些同一事物,但是因为某种因素的环境刺激,它会在几种状态进行切换的情况。

比如:会员卡,会员登记升级。再比如住酒店,酒店订房间也是一种,房间是空闲则可以预定,预定后可以取消,但是不能被别人预定。都是状态的转换。

再比如:开水壶的工作过状态图,它有两种状态,一种是关闭,一种是打开。关闭状态下,会判断有没有水,但是开关被打开了,此时会不会水壶被烧坏了,这并不科学,所以没水是打开了开关,会跳回到关闭状态。有水时打开开关,将会进行烧水,水开了,如果继续烧,可能会烧坏水壶,所以并不科学,因此水开了,开关自动跳回关闭。

水壶烧水

  • 源状态:Off
  • 目标状态:On
  • 触发状态转换的事件:ternOn 就是打开开关
  • 监护条件:有水,满足条件才会进行状态变化,否则状态不会变更
  • 动作:满足条件之后,会有一个动作,也就是烧水动作,
  • 转换:烧水这个动作是为了将源状态转换为目标状态

9、通信图(协作图)详解

通信图(communication diagram),通信图也是一种交互图,它强调手法消息的对象或参与者的结构组织。

顺序图和通信图表达了类似的基本概念,但它们所强调的概念不同,顺序图强调的是时序,通讯图强调的是对象之间的组织结构(关系)

架构师备战(三)-软件工程总览(软件工程 架构)

通信图的表现形式跟顺序图大同小异,它会把大致的流程表示出来,仅此而已,并不会强调顺序。

只是会把所有出现的对象之间的关系梳理清楚,仅此而已,节点里面一般是对象,正向箭头一般表示是消息。逆向箭头或者逆向虚线箭头表示的是一种回调,当然也是一种消息。

10、定时图详解

定时图强调了实际的时间,顺序图也强调时间顺序,但是只强调第一步做什么,第二步做什么。但是有时候我们的业务需要实现几点几分做什么,几点几分又做什么。

定时图在架构考试中很少考到,我们简单了解即可。

下面我们用定时图表示病人打了麻醉剂之后在麻醉和清醒之间的定时图

架构师备战(三)-软件工程总览(软件工程 架构)

这个图表示前五分钟处于麻醉中,后面五分钟就属于清醒中了,时间刻度表示得非常得清楚。

第五章 系统设计

1、系统设计分类

我们知道需求规格说明书(SRS)落地之后, 就要开始着手系统设计了,看一下这个系统该如何来设计,并且如何实现。学习系统设计之前,需要先了解系统设计有哪些分类。

系统设计的分类如下

  • 界面设计
  • 结构化设计
  • 面向对象设计(最重要

1.1、界面设计

界面设计也叫做人机界面设计,属于系统与用户交互的纽带。而人机界面设计在架构师考试中相对来说考得比较浅,我们了解一下人机界面设计得一些理念即可。

  • 置于用户控制之下
    • 以不强迫用户进入不必要的或不希望的动作的方式来定义交互方式
      • 简单来说,就是用户点了什么,预期的结果就是什么
      • 有些网站有那种您需要点多次才能进去的情况,第一次始终进的是广告,第二次才能看到对应的资源。这就违反了这个原则,从而让用户强制进广告让网站盈利。
    • 提供灵活的交互
    • 允许用户交互可以被中断或者撤销
      • 不然某个操作非常耗时,不小心点错了,但是不能中断,那就是用户体验不好。
    • 当技能级别增加时可以使交互流水化并允许定制交互
    • 使用户隔离内部技术细节
      • 用户不需要了解你用了哪些技术,关注的只是能不能快速使用,所以要对用户隔离技术细节,直接上手操作即可。
    • 设计应允许用户和出现在屏幕上的对象直接交互
  • 减少用户得记忆负担
    • 减少对短期记忆的要求
      • 不可能使用你的系统,还得先看一下帮助文档,那就是用户体验不好
    • 建立有意义的缺省
      • 某些东西不需要用户填,设置默认值可以让用户少填一点
    • 定义直觉性的捷径
      • 定义某些公用的符号 比如放大镜图标就是放大的意思,×符号就是关闭的意思等。
    • 以不断进展的方式揭示信息
  • 保持界面的一致性
    • 允许用户将当前任务放入有意义的语境
    • 在应用系列内保持一致性
    • 如过去的交互模型已建立起了用户期望,除非有迫不得已的理由,不哟啊改变它。

人机界面上设计考察的深度不是很深,了解到这里就可以了。

1.2、结构化设计

结构化设计分为概要设计和详细设计(一般已经被面向对象设计取代了)。

概要设计:对应的是模块的划分,模块接口的设计。对应后面的集成测试(把各个模块集合起来测接口)。

详细设计:是基于概要设计已经把很多东西分成小的块,也就是分成了对应的函数了,对函数内部进行具体流程的设计或算法的设计就叫做详细设计。一般针对的是模块内。

详细设计的基本思想

  • 抽象化
  • 自顶向下,逐步求精
    • 因为是结构化的,也就是面向过程,所以是自顶向下设计
  • 信息隐蔽
    • 同样是需要去调用接口,而不是直接操作实现类
  • 模块独立(高内聚、低耦合)

高内聚(内聚程度从上到下逐渐降低)

  • 功能内聚(内聚程度最高)
    • 完成一个单一功能,各个部分协同工作,缺一不可。对应单一职责原则,就是一个类实现的功能月单一越好,否则会指责过重,啥都要它做。
  • 顺序内聚
    • 处理袁术相关,而且必须顺序执行
  • 通信内聚
    • 所有处理元素集中在一个数据结构的区域上
  • 过程内聚
    • 处理元素相关,而且必须按也顶的次序执行
  • 瞬时内聚(时间内聚)
    • 所包含的任务必须在同一时间间隔内执行
  • 逻辑内聚
    • 完成逻辑上相关的一组任务
  • 偶然内聚(巧合内聚)
    • 完成一组没有关系或松散关系的任务

低耦合(耦合程度从上到下逐渐升高)

  • 非直接耦合
    • 两个模块直接没有直接关系,它们之间的联系完全是通过主模块的控制和调用来实现的
  • 数据耦合
    • 一组模块借助参数表传递简单数据
  • 标记耦合
    • 一组模块通过参数传递记录信息(数据结构)
  • 控制耦合
    • 模块之间传递的信息中包含了控制模块内部逻辑的信息
  • 外部耦合
    • 一组模块都访问同一全局变量,而且不是通过参数传递该全局变量的信息
  • 公共耦合
    • 多个模块都访问一个公共数据环境
  • 内部耦合
    • 一个哦快直接访问宁一个模块的内部数据,一个模块不通过正常入口转到另一个模块内部。两个模块有一部分代码重叠,一个模块多个入口。

高内聚,低耦合,意思就是内聚程度越高越好,耦合程度越低越好。

结构化设计的基本原则

  • 保持模块的大小适中
    • 这个适中的尺度不好确认,也就是说模块的职责不能过重
  • 尽可能减少调用的深度
    • 调用深度过深会导致栈内存占用较多,一般不超过七层,递归除外
  • 多扇入,少扇出
    • 模块的独立性很好,多扇入表示调用该模块的应用比较多。少扇出表示本模块调用其他模块较少,意味着该模块功能比较独立,不依赖其他模块,依赖多了就是高耦合了
  • 单入口,单出口
    • 不容易出错,入口出口都单一
  • 模块的作用域应该在模块之内
    • 尽量使用局部变量,作用域就可控。全局变量可能其他模块也会修改
  • 功能应该是可以预测的

结构化设计这些知识了解就行,不用深究。

2、面向对象设计(重要)

2.1、设计原则

  • 单一职责原则
    • 设计目的单一的类
    • 也就是设计类时,它只做自己该做的时,不要在类里面设计太多的东西,从而达到高内聚,低耦合
  • 开放-封闭原则
    • 对扩展开发,对修改关闭
    • 比如我们新增或修改一个功能时,我们最好新增一个类来操作,把逻辑都写在新的类里面,然后再原来要修改的地方,直接调用这个类的方法。这样就较少了代码的修改,从而减少了出现错误的风险。
    • 再比如:我们设计的时候用接口设计,接口分别有多找那个实现,我们要新增一种实现,就新增一个接口实现类就可以了,然后使用SPI切换一个接口的具体实现。这就是对扩展开放,对修改关闭(尽量的减少直接修改代码)
  • 里氏(Liskov)替换原则
    • 子类可以替换父类
    • 子类继承了父类的所有特性,父类出现的地方可以使用子类替换它。
  • 依赖倒置原则
    • 要依赖于抽象,而不是具体实现。针对接口编成,不要针对实现编程。
    • 为什么针对接口编程就变成了依赖倒置原则呢
      • 因为原本是依赖实现类,现在依赖的是实现类的接口。以前是A->B,A->C, B->D, C->E这种上层调用下层,如果下层的方法改变了,那么上层得跟着改变,特麻烦。面向对象就在上层定义接口,让下层去实现上层的接口。就变成了下层依赖于上层的接口,这样看依赖关系就倒置了。
  • 接口隔离原则
    • 使用多个专用接口比使用单一的总接口要好
  • 组合重用原则
    • 要尽量使用组合,而不是集成关系达到重用目的。
    • 继承是一种紧耦合,而我们不希望紧耦合。不然会导致父类修改了什么,所有的子类都需要改。
  • 迪米特(Demter)原则(最少知识原则)
    • 一个对象应当对其他对象尽可能少的了解
    • 说白了,就是使用private这类关键字,将自己对象的属性封装起来,避免被其他类非法修改。只能通过统一的setter或者getter来进行操作。

2.2、设计模式的概念

什么是模式?

模式通俗来讲就是套路,比如武侠小说跳悬崖之后必有奇遇,这就是一套套路,也是一种模式。

很多时候我们会有一些模式化的东西,却解决一些现有的问题,是一种很不错的策略,这就是设计模式。

设计模式的概念

  • 架构模式(高层次)
    • 软件设计中的高层决策,例如C/S结构就属于架构哦是,架构模式反映了开发软件系统过程中所作的基本设计决策。
  • 设计模式(中层次)
    • 主要关注软件系统的设计,与具体的实现语言无关
  • 惯用法(低层次)
    • 最低层次的模式,关注软件系统的设计与实现,实现时通过某种特定的程序设计语言来描述构建与构建之间的关系。每种编程语言都有它自己特定的模式,即语言的惯用法。例如引用-计数就是C 语言中的一种惯用法。

惯用法和设计模式最大的区别就是是否跟计算机语言相关。

3、设计模式

我们传统的23种设置模式如下

  • 创建型模式:用于创建对象
    • 工厂方法(Factory Method) 模式
    • 抽象工厂(Abstract Factory) 模式
    • 原型(Protptype) 模式
    • 单例(Singleton) 模式
    • 构建器模式
  • 结构型模式:建立更大的结构
    • 适配器(Adapter)模式
    • 桥接(Bridge)模式
    • 组合(Composite)模式
    • 装饰(Decorator)模式
    • 外观(Facade)模式
    • 享元(Flyweight)模式
    • 代理(Proxy)模式
  • 行为型模式:交互及职责分配
    • 责任链(Chain of Responsibility)模式
    • 命令(Command)模式
    • 解释器(Interpreter)模式
    • 迭代器(Iterator)模式
    • 中介者(Mediator)模式
    • 备忘录(Memento)模式
    • 观察者(Observer)模式
    • 状态(State)模式
    • 策略(Strategy)模式
    • 模板方法(Temmlate)模式
    • 访问者(Visotor)模式

本篇文章主要说明创建型模式,它的类图,设计思想以及Java代码的实现。

3.1、工厂方法模式

简要说明

定义一个创建对象的接口,但是子类决定需要实例化哪一个类。工厂方法使得子类实例化的过程推迟。

速记关键字

动态生产对象

通俗的讲,就是我们创建一个工厂类,我们要创建什么对象,传一个参数给工厂,工厂就负责创建一个对象返回给我。

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

Java代码实现

/** * 产品接口 */public interface IProduct { /** * 生产产品的方法 */ void product();}public class Product1 implements IProduct{ @Override public void product() { System.out.println("生产产品1:手机"); }}public class Product2 implements IProduct{ @Override public void product() { System.out.println("生产产品2:电脑"); }}public class Product3 implements IProduct{ @Override public void product() { System.out.println("生产产品3:平板"); }}/** * 工厂方法类 */public class Creator { /** * 根据产品类型生产产品 该类一般是静态类 * @param productType 产品类型 * @return 产品 */ public static IProduct factory(Integer productType){ if (productType == 1){ return new Product1(); } if (productType == 2){ return new Product2(); } if (productType == 3){ return new Product3(); } throw new RuntimeException("请传入正确的产品类型"); }}/** * 客户端 */public class Client { public static void main(String[] args) { // 创建产品1 IProduct product1 = Creator.factory(1); // 创建产品2 IProduct product2 = Creator.factory(2); // 创建产品3 IProduct product3 = Creator.factory(3); // 打印一下,看一下是不是对应的产品 product1.product(); product2.product(); product3.product(); }}

注意:这里的工厂方法,也就是Creator的factory方法,一般是静态的,因为它为了方便不用再去实例化工厂了,因为所有的事情都交给这一个工厂做了

输出结果

架构师备战(三)-软件工程总览(软件工程 架构)

3.2、抽象工厂模式

由上面的工厂方法知道,我们的工厂方法讲所有的事情都交给了工厂。但是我们如果要针对手机我们要生产一系列的产品,比如华为手机,苹果手机。此时工厂方法就不能够满足了。我们需要一个华为手机工厂,苹果手机工厂。这里抽象工厂模式应运而生。它就是为了生成这样的系列产品的工厂模式。

简要说明

提供一个接口,可以创建一系列相关或者相互依赖的对象,而无需指定它们的具体的类。

速记关键字

生产成系列对象

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

Java代码实现

/** * 抽象产品A, 手机工厂,用于生产手机 */public interface AbstractProductA { void product();}public class ConcreteProductA1 implements AbstractProductA { @Override public void product() { System.out.println("生产产品A:华为手机"); }}public class ConcreteProductA2 implements AbstractProductA { @Override public void product() { System.out.println("生产产品A:苹果手机"); }}/** * 抽象产品B, 笔记本电脑工厂,用于生产笔记本 */public interface AbstractProductB { void product();}public class ConcreteProductB1 implements AbstractProductB { @Override public void product() { System.out.println("生产产品B:华为笔记本"); }}public class ConcreteProductB2 implements AbstractProductB { @Override public void product() { System.out.println("生产产品B:苹果笔记本"); }}/** * 抽象工厂 可以是接口 也可以是抽象类 总的定义需要生产哪些系列产品 */public interface AbstractFactory { // 创建产品A(返回的对象是抽象产品A) AbstractProductA createProductA(Integer type); // 创建产品B(返回的对象是抽象产品B) AbstractProductB createProductB(Integer type);}/** * 具体工厂A,实现了抽象工厂,但是只实现抽象产品A的具体对象创建 */public class ConcreteFactoryA implements AbstractFactory{ @Override public AbstractProductA createProductA(Integer type) { if (type == 1){ return new ConcreteProductA1(); } if (type == 2){ return new ConcreteProductA2(); } throw new RuntimeException("请传入正确的产品A类型"); } @Override public AbstractProductB createProductB(Integer type) { return null; }}/** * 具体工厂B,实现了抽象工厂,但是只实现抽象产品B的具体对象创建 */public class ConcreteFactoryB implements AbstractFactory{ @Override public AbstractProductA createProductA(Integer type) { return null; } @Override public AbstractProductB createProductB(Integer type) { if (type == 1){ return new ConcreteProductB1(); } if (type == 2){ return new ConcreteProductB2(); } throw new RuntimeException("请传入正确的产品B类型"); }}/** * 客户端 */public class Client { public static void main(String[] args) { // 先实例化某个具体的工厂 AbstractFactory factoryA = new ConcreteFactoryA(); AbstractFactory factoryB = new ConcreteFactoryB(); // 比如我要通过两个工厂分别生产一个华为手机和一个华为笔记本 AbstractProductA productA = factoryA.createProductA(1); AbstractProductB productB = factoryB.createProductB(1); // 输出 productA.product(); productB.product(); }}

运行结果

架构师备战(三)-软件工程总览(软件工程 架构)

由类图和代码可以清晰的看出来,我们的抽象工厂定义了要创建哪些系列的产品,这些产品交给哪些具体的工厂去实现。而每个具体的工厂都会根据产品的类型参数,去创建不同的产品。这样我们新增一个具体的产品非常的容易,我们增加一个具体的工厂也改动不大,这就是设计模式的魅力所在。

3.3、原型模式

简要说明

用原型实例指定创建对象的类型,并且通过拷贝这个原型来创建新的对象

速记关键字

克隆对象

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

浅克隆与深克隆

浅克隆表示只克隆对象的直接属性,如果对象里面包含另一个对象,不会连这个对象下面的属性也克隆,而是设置的这个对象的引用。 深克隆表示除了克隆对象的直接属性,如果对象里面包含另一个对象,也会连这个对象下面的属性也克隆,而不是设置的这个对象的引用。

Java代码实现

@Datapublic class User implements Cloneable{ private String name; private Integer age; // 地址对象,浅克隆之后的这个对象不会克隆,使用的是原来的引用,深克隆则会克隆 private Address address; // 浅克隆,实现Cloneable接口即可 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } // 深克隆,在实现浅克隆的基础上,将Address手动克隆一遍,Address本身也需要实现Cloneable接口 protected Object deepClone() throws CloneNotSupportedException { User clone = (User)super.clone(); // 把里里面是对象的属性,执行一遍克隆即可 Address address = (Address)clone.getAddress().clone(); clone.setAddress(address); return clone; }}@Datapublic class Address implements Cloneable{ private String province; private String city; private String area; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }}/** * 客户端实现 */public class Client { public static void main(String[] args) throws CloneNotSupportedException { // 创建对象 Address address = new Address(); address.setProvince("重庆"); address.setProvince("重庆市"); address.setCity("渝中区"); User user = new User(); user.setName("IT动力"); user.setAge(18); user.setAddress(address); // 对象浅克隆, 浅克隆之后的属性里面的对象属性是不变的,因为存储的是引用 User clone = (User)user.clone(); System.out.println("浅克隆不会克隆address属性,结果应该为true: " (clone.getAddress() == user.getAddress())); // 对象深克隆 User clone2 = (User)user.deepClone(); System.out.println("深克隆会克隆address属性, 结果应该为false: " (clone2.getAddress() == user.getAddress())); }}

输出结果

架构师备战(三)-软件工程总览(软件工程 架构)

从上面看到,深克隆的实现我们手动去做了克隆Address对象的操作,但是在实际代码中,我们显然不能够去这样做。而实现的方式就是每个对象都实现序列化接口。深克隆时会序列化成流,然后再从流里读取出来进行反序列化。这样就能实现深克隆。

JSON序列化代码实现

@Datapublic class Address implements Serializable { private String province; private String city; private String area;}@Datapublic class User implements Serializable { private String name; private Integer age; private Address address; protected Object deepClone() throws CloneNotSupportedException { // 这里直接引入fastjson做测试 return JSON.parseObject(JSON.toJSONString(this), User.class); }}/** * 客户端实现 */public class Client { public static void main(String[] args) throws CloneNotSupportedException { // 创建对象 com.dp.prototype.Address address = new Address(); address.setProvince("重庆"); address.setProvince("重庆市"); address.setCity("渝中区"); User user = new com.dp.prototype.User(); user.setName("IT动力"); user.setAge(18); user.setAddress(address); // 对象深克隆 User clone2 = (User) user.deepClone(); System.out.println("深克隆会克隆address属性, 结果应该为false: " (clone2.getAddress() == user.getAddress())); }}

输出结果

架构师备战(三)-软件工程总览(软件工程 架构)

3.4、单例模式

简要说明

保证一个类只有一个实例,并提供一个访问它的全局访问点

速记关键字

单实例

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

单例模式,无外乎就是如何创建一个单实例,并且提供一个唯一访问入口。但是单例如何实例化又分为了饿汉式单例模式和懒汉式单例模式。

饿汉式单例模式

饿汉式单例模式,就是我们创建私有的一个静态变量,直接对进行实例化,并且提供一个私有的构造函数,防止对象被新建。最后提供一个公开的获取实例的方法,直接返回声明的对象。

它是从出生就做好了实例化,后面也不需要再去实例化了,所以它是天生线程安全的。

懒汉式单例模式

饿汉式是一开始就把对象创建好了,懒汉式一开始比较懒,它不创建,在方法被调用时才会创建,所以叫懒汉式。但是这样创建在多线程可能会有问题,因此一般需要加锁。

加锁得方式有整个方法加锁,还有一种叫做双重检测锁得单例模式,能够应对高并发。

Java代码实现

/** * 饿汉式单例模式 */public class HungrySingleton { // 私有得静态得实例变量,饿汉式直接初始化对象 private static final HungrySingleton instants = new HungrySingleton(); // 私有构造函数,防止其他类通过反射实例化该类 private HungrySingleton(){ System.out.println("我是饿汉式单例的私有构造方法,我被调用了"); } // 静态的public方法,供外界网文的唯一入口 public static HungrySingleton getInstance(){ // 直接返回上面对应的对象 return instants; }}/** * 懒汉式单例模式 */public class LazySingleton { // 私有得静态得实例变量,饿汉式直接初始化对象 private static LazySingleton instants; // 私有构造函数,防止其他类通过反射实例化该类 private LazySingleton(){ System.out.println("我是懒汉式单例的私有构造方法,我被调用了"); } // 静态的public方法,供外界网文的唯一入口, 注意懒汉式在这里需要加锁,不然会存在线程安全问题 public static synchronized LazySingleton getInstance(){ // 为空 则创建并且赋值给全局变量 if (instants == null){ instants = new LazySingleton(); } // 返回上面对应的对象 return instants; }}/** * 懒汉式单例模式2 双重检测锁(推荐) */public class LazySingleton2 { // 私有得静态得实例变量,饿汉式直接初始化对象 注意,这里需要将全局变量加上volatile关键字,从而禁止重排序 private static volatile LazySingleton2 instants; // 私有构造函数,防止其他类通过反射实例化该类 private LazySingleton2(){ System.out.println("我是懒汉式单例2的私有构造方法,我被调用了"); } // 静态的public方法,供外界网文的唯一入口 public static LazySingleton2 getInstance(){ // 第一个if判断是否为空,不为空直接返回,避免synchronized同步代码块的执行,多线程场景下频繁加锁会影响性能 if(instants == null){ // 为空的情况才加锁 synchronized (LazySingleton2.class){ // 第二个if判断是否为空,当a线程优先获得锁,执行到此处,b线程没竞争到锁会被阻塞在外面,a线程判断实例是否为空,为空则new实例, // a线程释放锁之后,b线程拿到锁进来后判断instance是否为null,此时不为null,则释放锁往下 if(instants == null){ instants = new LazySingleton2(); } } } return instants; }}

单例模式的代码实现方式有这么多种,那么工作中会使用哪种呢。如果需要考虑并发问题,就需要使用双重检测锁,这样性能会高一些。

为什么不使用饿汉式呢,因为它一开始就把对象初始化了,也就是内存从一开始就占用了。如果存在胆量的单例,但是却没有使用,那就是对内存的一种极大的浪费。而懒汉式则只有在真正需要时才会去创建对象。

3.5、构造器模式

简要说明

讲一个复杂类的表示与其构造相分离,使得形同的构建过程能够得出不同的表示。

速记关键字

复杂对现象构造

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

java代码实现

/** * 房子类 包含了房子的长宽高 */@Datapublic class Room { private String length; private String width; private String height; @Override public String toString() { return "Room{length='" length ", width='" width ", height='" height '}'; }}/** * 构建器接口 */public interface Builder { // 构建房子的长度 void buildLength(); // 构建房子的宽度 void buildWidth(); // 构建房子的高度 void buildHeight(); // 构建房子 Room buildRoom();}/* * 大房子的具体构建者 */public class LargeRoomBuilder implements Builder { private final Room room = new Room(); @Override public void buildLength() { room.setLength("100米"); } @Override public void buildWidth() { room.setWidth("120米"); } @Override public void buildHeight() { room.setHeight("5米"); } @Override public Room buildRoom() { return room; }}/* * 小房子的具体构建者 */public class SmallRoomBuilder implements Builder { private final Room room = new Room(); @Override public void buildLength() { room.setLength("30米"); } @Override public void buildWidth() { room.setWidth("40米"); } @Override public void buildHeight() { room.setHeight("3米"); } @Override public Room buildRoom() { return room; }}/** * 指挥者,引入指挥者的目的:指定构造流程、方式 */public class Director { // 引入构建器 private final Builder builder; // 构造注入 public Director(Builder builder) { this.builder = builder; } // 构建房间方法 public Room buildRoom() { // 构建房间,分别构建长宽高,然后就能能够构建一个房间 builder.buildLength(); builder.buildWidth(); builder.buildHeight(); return builder.buildRoom(); }}/** * 客户端 */public class Client { public static void main(String[] args) { // 指定构造者,交由指挥者构建大房子 Room largeRoom = new Director(new LargeRoomBuilder()).buildRoom(); System.out.println(largeRoom); // 指定构造者,交由指挥者构建小房子 Room smallRoom = new Director(new SmallRoomBuilder()).buildRoom(); System.out.println(smallRoom); }}

输出打印

架构师备战(三)-软件工程总览(软件工程 架构)

3.6、责任链模式

简要说明

通过多个对象处理的请求,减少请求的发送者与接收者之间的耦合。将接受对象链接起来,在链中传递请求,直到有一个对象处理这个请求。

速记关键字

传递职责

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

由类图可以比较容易的看出来,其实就是自己关联自己,形成了一个链,并且自己有不同的实现类,实现类就是在链路上的一环又一环。比如请假需要经过层层职级不一样的人进行审批。

Java代码实现

/** * 请假流程抽象类 */public abstract class LeaveProcess { // 请假流程组合了自己, 也就需要指定下一个处理者 protected LeaveProcess nextHandler; // 处理人名称 protected final String name; public LeaveProcess(String name) { this.name = name; } // 处理请求 public abstract void handleRequest(Request request); // 设置下一个处理者 public void setNextHandler(LeaveProcess nextHandler) { this.nextHandler = nextHandler; }}/** * 员工类 */public class Employee extends LeaveProcess{ public Employee(String currHandler) { super(currHandler); } @Override public void handleRequest(Request request) { System.out.println("员工:" name "发起了请求ID=" request.getRequestId() "的请假流程"); if (nextHandler != null){ nextHandler.handleRequest(request); } }}/** * 业务经理处理请求 */public class Business extends LeaveProcess{ public Business(String currHandler) { super(currHandler); } @Override public void handleRequest(Request request) { System.out.println("业务经理:" name "处理了请求ID=" request.getRequestId() "的请假流程"); if (nextHandler != null){ nextHandler.handleRequest(request); } }}/** * 老板处理请求 */public class Boss extends LeaveProcess{ public Boss(String currHandler) { super(currHandler); } @Override public void handleRequest(Request request) { System.out.println("老板:" name "处理了请求ID=" request.getRequestId() "的请假流程"); if (nextHandler != null){ nextHandler.handleRequest(request); } }}/** * HR处理请求 */public class Hr extends LeaveProcess{ public Hr(String currHandler) { super(currHandler); } @Override public void handleRequest(Request request) { System.out.println("Hr:" name "处理了请求ID=" request.getRequestId() "的请假流程"); if (nextHandler != null){ nextHandler.handleRequest(request); } }}/** * 测试类 */public class Client { public static void main(String[] args) { // 创建流程节点相关人员 LeaveProcess employee = new Employee("小明"); LeaveProcess business = new Business("大明"); LeaveProcess boss = new Boss("大明明"); LeaveProcess hr = new Hr("小绿"); // 设置下一个节点处理人 employee.setNextHandler(business); business.setNextHandler(boss); boss.setNextHandler(hr); // 创建请求,发起流程 Request request = new Request("1"); employee.handleRequest(request); }}

实现输出

架构师备战(三)-软件工程总览(软件工程 架构)

3.7、命令模式

简要说明

将一个请求封装为一个对象,从而可用不同的请求对客户进行参数化,将请求排队或记录请求日志,支持可撤销操作。

速记关键字

日志记录,可撤销

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

类图角色说明

  1. Invoker:调用者角色,它会去执行或者撤销命令等操作
  2. Command: 命令角色,可以是接口,也可以是抽象类,包含了有哪些命令
  3. Receiver: 命令接受者角色,知道如何实施和执行一个请求相关的操作
  4. ConcreteCommand: 将一个接受者对象与一个动作绑定,调用接受者相应的操作,实现execute

这几个角色之间的关系如类图所示,我们的调用者使用组合关系,引入了命令接口或者父类。而我们具体的命令则继承或者实现命令接口或者抽象类。

而我们具体的命令实现,则关联了接收者接口,接受这接口实现类实现去做具体命令要做的事情。

Java代码实现

/** * 定义接收者接口,该接口将会被具体的命令通过组合关系来进行调用 */public interface Receiver { // 命令具体实施方法 void doSomething();}/** * 打开命令接收者,就是需要具体去打开电视机这个实施过程 */public class OpenCommandReceiver implements Receiver{ @Override public void doSomething() { System.out.println("成功的打开了电视机"); }}/** * 关闭命令接收者,就是需要具体去打开电视机这个实施过程 */public class CloseCommandReceiver implements Receiver{ @Override public void doSomething() { System.out.println("成功的关闭了电视机"); }}/** * 命令接口 主要定义命令的执行操作 */public interface Command { // 命令执行 void exec();}/** * 打开命令实现 */public class OpenCommand implements Command { // 这里组合具体的命令接收者 private final Receiver receiver; // 使用构造函数注入 public OpenCommand(Receiver receiver) { this.receiver = receiver; } @Override public void exec() { // 执行命令 receiver.doSomething(); }}/** * 关闭命令实现 */public class CloseCommand implements Command { // 这里组合具体的命令接收者 private final Receiver receiver; // 使用构造函数注入 public CloseCommand(Receiver receiver) { this.receiver = receiver; } @Override public void exec() { // 执行命令 receiver.doSomething(); }}/** * 命令调用者 ,组合了命令接口,调用最终的命令 */public class Invoker { // 组合命令接口 private final Command command; public Invoker(Command command) { this.command = command; } // 对外提供统一的命令调用入口 public void action(){ // 它真正去调用命令接口 command.exec(); }}/** * 测试 */public class Client { public static void main(String[] args) { // 创建打开和关闭的接收者对象 Receiver openCommandReceiver = new OpenCommandReceiver(); Receiver closeCommandReceiver = new CloseCommandReceiver(); // 创建命令打开和关闭操作 Command openCommand = new OpenCommand(openCommandReceiver); Command closeCommand = new CloseCommand(closeCommandReceiver); // 使用调用者调用命令 Invoker openInvoker = new Invoker(openCommand); Invoker closeInvoker = new Invoker(closeCommand); // 执行打开和关闭操作,因为每一个命令都是独立的对象,我们一个打开命令和一个关闭命令就形成了一组操作,也就任务打开是可以撤销的,撤销就是执行关闭命令 openInvoker.action(); closeInvoker.action(); }}

测试结果

架构师备战(三)-软件工程总览(软件工程 架构)

3.8、解释器模式

简要说明

给定一种语言,定义它的文法表示,并定义一个解释器,该解释器用来根据文法表示来解释语句中的句子。

速记关键字

虚拟机机制,表达式解析,语法解析,词法解析等

这种设计模式主要应用于虚拟机底层,用于解释语法和词法是否符合要求。

假设现在我们定义了一个计算表达式,我们如何校验该表达式是否正确呢,又如何去执行出结果呢。

其实有一个叫做抽象语法树的概念,是源代码语法结构的一种抽象表示。它以树状的形式表示编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

用树来表示符合文法规则的句子(从下往上,从左往右看)

架构师备战(三)-软件工程总览(软件工程 架构)

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

解释器模式角色说明

  • 抽象表达式(Abstract Expression)
    • 定义解释器接口,约定解释器的解释操作,主要包含解释方法interpret()
  • 终结符表达式
    • 抽象表达式的子类,用来实现文法中终结符相关的操作,文法中的每一个终结符都有一个具体的终结表达式与之对应
  • 非终结符表达式
    • 也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应一个非终结符表达式
  • 上下文环境
    • 通常包括各个解释器需要的公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
  • 客户端
    • 将要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,

Java代码实现

/** * 抽象表达式 用于与定义解释器方法 */public abstract class AbstractExpression { // 解释器方法,参数是上下文,上下文定义了解释器需要的功能功能,所有每一个解释器方法,都可以操作上下文 public abstract int interpret(Context context);}/** * 上下文类 */public class Context { // 定义上下文变量操作集合 private Map<Variable, Integer> map = new HashMap<>(); /** * 根据变量,获取对应变量的值(从上下文集合中) * @param variable 变量对象 * @return 变量值 */ public Integer getValue(Variable variable){ return map.get(variable); } /** * 向上下文集合中赛值 * @param key 键 * @param value 值 */ public void put(Variable key, Integer value) { map.put(key, value); }}/** * 我们的表达式分为了变量和操作符号,我们将变量封装起来,操作符和变量将会形成一颗语法树,正如前面我们看到的抽象语法树一样 */public class Variable extends AbstractExpression { // 变量名称 private final String varName; public Variable(String varName) { this.varName = varName; } @Override public int interpret(Context context) { // 解释器,将会从上下文中根据本变量对象,去获取变量对应的值 return context.getValue(this); } @Override public String toString() { return varName; }}/** * 定义加法表达式,属于非终结符表达式。因为a b的加号是需要进行计算的 * 加法分为左边和右边,所以需要组合左右两边两个变量 */public class PlusExpression extends AbstractExpression{ // 号左边的表达式 private final AbstractExpression left; // 号右边的表达式 private final AbstractExpression right; public PlusExpression(AbstractExpression left, AbstractExpression right) { this.left = left; this.right = right; } @Override public int interpret(Context context) { // 左右两边分别解释,然后相加 return left.interpret(context) right.interpret(context); } @Override public String toString() { return "(" left.toString() " " right.toString() ")"; }}/** * 定义加减法表达式,属于非终结符表达式。因为a-b的减号是需要进行计算的 * 减法分为左边和右边,所以需要组合左右两边两个变量 */public class MinusExpression extends AbstractExpression{ //-号左边的表达式 private final AbstractExpression left; //-号右边的表达式 private final AbstractExpression right; public MinusExpression(AbstractExpression left, AbstractExpression right) { this.left = left; this.right = right; } @Override public int interpret(Context context) { // 左右两边分别解释,然后相加 return left.interpret(context) - right.interpret(context); } @Override public String toString() { return "(" left.toString() " - " right.toString() ")"; }}/** * 终结符表达式角色 */public class ResultExpression extends AbstractExpression { // 我们的结果是一个整型 private final Integer value; public ResultExpression(Integer value) { this.value = value; } @Override public int interpret(Context context) { // 已经是结果了,不需要再处理了,直接返回即可 return value; } @Override public String toString() { return String.valueOf(value) ; }}/** * 客户端类 */public class Client { public static void main(String[] args) { // 创建上下文 Context context = new Context(); // 创建多个变量 Variable a = new Variable("a"); Variable b = new Variable("b"); Variable c = new Variable("c"); Variable d = new Variable("d"); // 将变量及对应的值存储到环境对象中 context.put(a, 1); context.put(b, 2); context.put(c, 3); context.put(d, 4); // 获取抽象语法树 a b - c d。通过多个非终结符解释器一次次的解释 AbstractExpression expression = new PlusExpression(new MinusExpression(new PlusExpression(a, b), c), d); // 进行计算 int result = expression.interpret(context); // 通过终结符解释器解释(一般可以省略) ResultExpression resultExpression = new ResultExpression(result); System.out.println("表达式为:" expression.toString() ",结果为:" resultExpression.toString()); }}

前面我们记录了创建型设计模式,知道了通过各种模式去创建和管理我们的对象。但是除了对象的创建,我们还有一些结构型的模式。

3.9、适配器模式(Adapter)

简要说明

将一个类的接口转换为用户希望得到的另一个接口。它使原本不相同的接口得以协同工作。

速记关键字

转换接口

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

角色说明

  • 目标角色(Target)
    • 客户要使用的目标接口(新系统的接口)
  • 源角色(Adaptee)
    • 需要被适配的源接口(我们不能改动的接口,需要适配器去做适配的那个源头接口)
  • 适配器角色(Adapter)
    • 核心是实现Target接口, 组合Adaptee接口(当然也能继承,但继承会增加耦合性,推荐组合方式)

/** * 两孔插座实现类,Target目标接口 */public interface TwoPinSocket { // chargeWithTwoPin()方法,主要打印接口是几孔的 void chargeWithTwoPin(); // voltage(),主要打印电压信息 Integer voltage();}/** * 三孔插座接口,需要被适配器适配的接口 */public interface ThreePinSocket { // chargeWithThreePin()方法,主要打印接口是几个孔的 void chargeWithThreePin(); // voltage(),主要打印电压信息 Integer voltage();}// 为了方便看到效果,我们额外对Target接口和适配器接口做一个实现,打印数据帮助理解/** * 两孔插座实现类 */public class TwoPinSocketImpl implements TwoPinSocket{ public void chargeWithTwoPin() { System.out.println("欢迎使用中国标准的两孔的插座"); } public Integer voltage() { System.out.println("我是两孔插座,需要的电压是220伏特"); return 220; }}/** * 三孔插座实现类 */public class ThreePinSocketImpl implements ThreePinSocket{ public void chargeWithThreePin() { System.out.println("欢迎使用美国标准的三孔的插座"); } public Integer voltage() { System.out.println("我是三孔插座,我需要的电压是110伏特"); return 110; }}/** * 适配器类实现 * 实现target(目标)接口,组合adaptee(适配源)接口 */public class Adapter implements TwoPinSocket{ // 这个就是适配源,要适配的类的接口,一般使用构造注入 private final ThreePinSocket threePinSocket; // 构造注入 public Adapter(ThreePinSocket threePinSocket) { this.threePinSocket = threePinSocket; } public void chargeWithTwoPin() { System.out.println("我是适配器,我要将两孔插座适配三孔"); threePinSocket.chargeWithThreePin(); } public Integer voltage() { final Integer voltage = threePinSocket.voltage(); System.out.println("我是适配器适配电压方法,将适配源的110V电压提升到220V,以适配两孔插座所需要的的电压"); // 适配器把电压从 110V 升到 220V, 就能够使用 return voltage * 2; }}/** * 客户端类,使用target(目标接口)实现功能 */public class Client { private final TwoPinSocket twoPinSocket; public Client(TwoPinSocket twoPinSocket) { this.twoPinSocket = twoPinSocket; } public void chargeRequest() { System.out.println("我是华为手机,正在以" twoPinSocket.voltage() "伏特电压进行充电中n"); }}/** * 测试类 */public class Main { public static void main(String[] args) { // 先使用两孔充电.不需要做适配 final TwoPinSocket twoPinSocket = new TwoPinSocketImpl(); final Client client = new Client(twoPinSocket); client.chargeRequest(); // 坐飞机去美国了,美国旅馆只有三孔 怎么办 final ThreePinSocket threePinSocket = new ThreePinSocketImpl(); // 创建一个适配器, 要适配的是三孔插座 final Adapter adapter = new Adapter(threePinSocket); // 此时我们创建客户端时,传入的接口不再是目标接口,而是目标接口的实现类,也就是我们的适配器 Client client1 = new Client(adapter); // 对于客户端来说,接口不变,只是适配器去做了处理 client1.chargeRequest(); }}

输出结果

架构师备战(三)-软件工程总览(软件工程 架构)

适配器模式主要是因为已有的类或者接口已经不能改变了,我们新需要的目标接口又不足以支持业务,因此需要我们去建立一个适配器,将它们协同起来工作。

就比如我们的数据源,定义的JDBC连接协议这些已经定了,mysql,oracle等数据库厂商就需要去实现JDBC协议,并对该协议进行适配,从而连接对应的数据库。

3.10、桥接模式

简要说明

将类的抽象部分和它的实现部分分离开来,使它们可以独立的变化。

速记关键字

继承树拆分

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

举个例子,现在要创建一个Web应用框架,此框架能创建不同的应用,比如博客,新闻网站和网上商城等。并可以为每一个Web应用创建不同的主题样式,如浅色或深色。

架构师备战(三)-软件工程总览(软件工程 架构)

可以看到,我们的维度其实就两个维度,一个是应用维度,一个是主题样式维度。它们原本是一颗继承树样式。因为所有的都是应用。不管是深色还是浅色的博客或者商城。如果都继承Web应用类,那么增加一个网站,将会产生两个子类,这样会造成子类泛滥成灾。而使用桥接之后,我们将Web网站的主题样式抽象出来,并且在Web网站的上层依赖(组合)主题样式,这样仿佛架起了一座桥来解决问题,因此叫做桥接模式。

Java代码实现

/** * 网站应用抽象类 */public abstract class AbstractWebSite { // 使用组合关系 网站主题颜色 protected final TopicColor topicColor; public AbstractWebSite(TopicColor topicColor) { this.topicColor = topicColor; } // 网站类型 public abstract void type();}// 博客网站实现类public class BlogWebSite extends AbstractWebSite{ public BlogWebSite(TopicColor topicColor) { super(topicColor); } @Override public void type() { topicColor.color(); System.out.print("博客"); }}// 商城网站实现类public class ShopWebSite extends AbstractWebSite{ public ShopWebSite(TopicColor topicColor) { super(topicColor); } @Override public void type() { topicColor.color(); System.out.print("商城"); }}/** * 主题颜色接口 */public interface TopicColor { void color();}/** * 主题颜色实现 */public class LightColor implements TopicColor{ @Override public void color() { System.out.print("浅色"); }}/** * 主题颜色实现 */public class DarkColor implements TopicColor{ @Override public void color() { System.out.print("深色"); }}// 测试类public class Client { public static void main(String[] args) { // 创建两种主题颜色 TopicColor darkColor = new DarkColor(); TopicColor lightColor = new LightColor(); // 渲染网站 AbstractWebSite blogWebSite = new BlogWebSite(darkColor); AbstractWebSite shopWebSite = new ShopWebSite(lightColor); // 打印结果 blogWebSite.type(); shopWebSite.type(); }}

结果打印了深色博客,浅色博客。如果需要渲染其他网站,同样的逻辑即可。大大的较少了子类的创建。

3.11、装饰器模式

简要说明

动态的给一个对象添加一些额外的职责,它提供了子类扩展功能的一个灵活的替代,比派生一个子类更加灵活。

速记关键字

动态附加职责

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

由类图和上面的描可以知道,装饰器实现了主体的接口,并且组合了主体的接口,从而实现自己给自己添加额外职责的目的。

Java代码实现

/** * 饮料类,主体的抽象 */public abstract class Drink { // 总价 private BigDecimal price; // 每一个额外职责需要花费多少钱 abstract BigDecimal cost(); public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; }}/** * 水,饮料的主体,也就是本质 */public class Water extends Drink{ public Water() { // 假设一杯水的初始价值是3元 super.setPrice(new BigDecimal(3)); } public BigDecimal cost() { // 花费的钱就是父类已经经过添加调料的价格 return super.getPrice(); }}/** * 装饰器 抽象类 */public abstract class DecoratorAdditive extends Drink{ // 不仅仅实现了drink接口,并且把drink接口作为依赖进行组合进来,这里的依赖主要是为了方便添加额外的职责。 // 因为我们装饰器实现了Drink接口,相当于装饰器的实现类也实现了该接口,就可以为做添加自己的操作了 private final Drink drink; public DecoratorAdditive(Drink drink) { this.drink = drink; } public BigDecimal cost() { // 左边表示饮料添加调料之前的价格,add后面表示调料的价格,最终的价格就是原来价格 刚刚添加的调料的价格 return super.getPrice().add(drink.cost()); }}/** * 装饰器实现类 加冰 */public class Ice extends DecoratorAdditive{ public Ice(Drink drink) { super(drink); // 价冰 一块钱 调用父类设置自己的价格 super.setPrice(new BigDecimal(1)); }}/** * 装饰器实现类 加西瓜汁 */public class WatermelonJuice extends DecoratorAdditive{ public WatermelonJuice(Drink drink) { super(drink); // 冰的西瓜汁2块 调用父类设置自己的价格 super.setPrice(new BigDecimal(2)); }}/** * 测试类 */public class Client { public static void main(String[] args) { // 我要了一杯水 final Drink water = new Water(); System.out.println("我要了一杯水,花费了:" water.cost()); // 喝得不够凉爽,给我加一点冰 final Ice ice = new Ice(water); System.out.println("我给水加了冰块,花费了:" ice.cost()); final WatermelonJuice juice = new WatermelonJuice(ice); System.out.println("我给水加了冰块之后又加了点西瓜汁,花费了:" juice.cost()); }}

输出结果

上面一杯水花费了3元,一杯加冰的水3 1=4元。一杯西瓜汁冰水就是2 1 3=6元

装饰器与桥接模式的区别

装饰器模式是指在不改变现有对象结构的情况下,动态的给该对象添加一些职责(这个职责是额外的,也就是说不添加也可以)。

装饰器模式跟桥接模式虽然看上去很类似,结果也很类似。但是他们的基本思想是完全不一样的。

装饰器模式有一个主体,装饰的目的是给主体添加额外的职责,但是这个职责可以不添加,也可以添加多个。

比如我们以水为主体,不添加职责它还是水,还是可以喝。再比如我们给他加一点冰,变成了冰水,在添加一点西瓜汁,那就是西瓜汁冰水。它的本质没有被改变。

桥接模式,是两个不同的维度,并且存在了父子关系。比如网站的主题颜色,也是网站构建的一部分,是将这部分抽象出来了,以组合的方式供给主题进行选择性调用。

3.12、组合模式

简要说明

将对象组合成树形结构以表示“整体-部分”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

速记关键字

树形目录结构

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

由类图其实可以看出,组合模式就是将具有父子关系的结构,组装形成一棵树,并且根据规范,树干节点和叶子节点均需要实现统一接口或者继承同一个抽象类。只是各自实现树干和叶子节点的特有功能。下面我们以菜单目录和菜单为例,使用组合模式组装菜单。

Java代码实现

/** * 抽象菜单类 */ @Datapublic abstract class AbstractMenu{ // 菜单名称或目录名称 protected String name; // 菜单路由 protected String route; // 菜单目录图标 protected String icon; public AbstractMenu(String name) { this.name = name; } public AbstractMenu(String name, String route) { this.name = name; this.route = route; } public AbstractMenu(String name, String route, String icon) { this.name = name; this.route = route; this.icon = icon; } // 菜单目录下可以添加菜单 public void add(AbstractMenu menu){ // 抛出不支持操作异常 throw new UnsupportedOperationException(); } // 菜单目录下可以删除菜单 public void remove(AbstractMenu menu){ // 抛出不支持操作异常 throw new UnsupportedOperationException(); } // 获取菜单信息 public abstract AbstractMenu getMenu();}/** * 菜单目录 */public class MenuDir extends AbstractMenu implements Serializable { private List<AbstractMenu> children; public MenuDir(String name) { super(name); children = new ArrayList<AbstractMenu>(); } public MenuDir(String name, String route, String icon) { super(name, route, icon); children = new ArrayList<AbstractMenu>(); } @Override public void add(AbstractMenu menuDir) { // 添加菜单子目录 children.add(menuDir); } @Override public void remove(AbstractMenu menuDir) { // 删除菜单子目录 children.remove(menuDir); } // 获取菜单信息 public AbstractMenu getMenu() { // 当前菜单信息 final MenuDir menuDir = new MenuDir(name, route, icon); // 子菜单信息 final List<AbstractMenu> children = this.getChildren(); menuDir.setChildren(children); return menuDir; } public List<AbstractMenu> getChildren() { return children; } public void setChildren(List<AbstractMenu> children) { this.children = children; }}/** * 菜单信息,也就是叶子节点 */public class Menu extends AbstractMenu implements Serializable { public Menu(String name) { super(name); } public Menu(String name, String route) { super(name, route); } public Menu(String name, String route, String icon) { super(name, route, icon); } public AbstractMenu getMenu() { return new Menu(getName(), getRoute(), getIcon()); }}/** * 测试类 使用组合模式构建菜单 */public class Client2 { public static void main(String[] args) { // 构建菜单结构 // 创建一级菜单目录 AbstractMenu sysMgr = new MenuDir("系统管理", "/system", null); // 创建二级菜单目录 AbstractMenu usrMgr = new MenuDir("用户管理", "/system/user", null); // 用户管理下面分为 系统用户,会员用户分别做管理,它们都是叶子节点 Menu employer = new Menu("员工管理", "/system/user/employer", "员工管理图标"); Menu members = new Menu("会员管理", "/system/user/members", "会员管理图标"); // 构建关系 usrMgr.add(employer); usrMgr.add(members); sysMgr.add(usrMgr); final AbstractMenu menu = sysMgr.getMenu(); System.out.println(menu); }}

如果通过断点,我们可以非常清晰的看清楚整棵树的结果,非常的清晰的表示了父子结构,展示了整体和部分的逻辑。

3.13、享元模式

简要说明

提供了大量细粒度对象共享的有效方法

速记关键字

汉字编码,对象共享,对象缓存

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

如类图所述,享元模式其实在我们工作中非常常用,虽然您可能不知道这时享元模式,但是它就是。

它广泛运用在池技术里面,比如JDK的String或者Integer的常量池.它们缓存了一部分非常常用的数字或者字符,把它们放在池中,一般先从池中获取,不存在就新建一个对象。新建对象后将新建的对象回填到池中。从而避免大量的对象创建并减少内存的消耗。

这个跟我们的缓存简直是一模一样,我们缓存是热数据,而享元模式缓存的就是热对象。一样的道理。

Java代码实现

/** * 享元模式接口 */public interface IFlyWeight { void test();}/** * 享元模式接口 */@Datapublic class FlyWeightImpl implements IFlyWeight{ private String from; private String to; public FlyWeightImpl(String from, String to) { this.from = from; this.to = to; } public void test() { System.out.println("测试从" from "到" to); }}/** * 享元工厂,也就是在这里 我们如果每次都创建对象,那么创建的对象就会很多。如果存在大量的公共信息,那么可有将公共信息抽象出来,然后共享起来 */public class FlyWeightFactory { private static final Map<String ,IFlyWeight> poll = new ConcurrentHashMap<String, IFlyWeight>(); public static IFlyWeight getInfo(String from, String to){ String key = from "->" to; if (poll.containsKey(key)){ System.out.println("使用缓存:" key); return poll.get(key); } System.out.println("首次查询,创建对象:" key); IFlyWeight ticket = new FlyWeightImpl(from, to); poll.put(key,ticket); return ticket; }}/** * 测试类 */public class Client { public static void main(String[] args) { // 其实享元模式就是将一些共享度比较高(不经常变化,但是创建次数很多的对象)放在资源池(一般是Map<对象唯一标识,对象>或者List<对象>格式)里面。 // 如果我们资源池子里面没有对象,我们才去创建,创建之后把把对象放入资源池。下次进行时直接从资源池获取,不再重复创建对象。从而减少资源的开销。 // 一般在JDK常量池,数据库的连接池,我们自己写的代码做旁路缓存模式(先查redis, redis存在,直接返回,不存在,写入redis)是一样道理 final IFlyWeight info1 = FlyWeightFactory.getInfo("重庆", "北京"); final IFlyWeight info2 = FlyWeightFactory.getInfo("成都", "北京"); final IFlyWeight info3 = FlyWeightFactory.getInfo("上海", "北京"); final IFlyWeight info4 = FlyWeightFactory.getInfo("北京", "深圳"); final IFlyWeight info5 = FlyWeightFactory.getInfo("重庆", "北京"); info1.test(); info2.test(); info3.test(); info4.test(); info5.test(); }}

说白了,享元模式就是给热对象做了一层缓存而已。因为热对象会频繁被创建,但是这个对象本身又不怎么改变,因此缓存起来将会减少内存的开销和效率的提升。

3.14、外观模式

简要说明

定义一个高层接口,为子系统中的一组接口提供一个一致的外观,从而简化了该子系统的使用。

速记关键字

对外统一接口

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

由类图可以看出,其实我们就是将几个类的方法调用放在同一个类中,提供一个统一的调用入口。就跟以前的hao123导航一样,就是一个简单的整合,怕大家不好找,放在一个统一的地方,方便调用而已。可以说是一个接口的门户。

Java代码实现

/** * 画形状接口 */public interface Shape { void draw();}public class Rectangle implements Shape{ public void draw() { System.out.println("画长方形"); }}public class Square implements Shape { public void draw() { System.out.println("画正方形"); }}public class Circle implements Shape{ public void draw() { System.out.println("画圆"); }}// 定义一个统一的类,来画圆形,正方形,长方形public class ShapeMaker { private final Shape circle; private final Shape rectangle; private final Shape square; public ShapeMaker() { circle = new Circle(); rectangle = new Rectangle(); square = new Square(); } /** * 下面定义一堆方法,具体应该调用什么方法,由这个门面来决定 */ public void drawCircle(){ circle.draw(); } public void drawRectangle(){ rectangle.draw(); } public void drawSquare(){ square.draw(); }}// 测试类public class Client { public static void main(String[] args) { // 创建统一外观 final ShapeMaker shapeMaker = new ShapeMaker(); // 使用统一外观进行调用方法 shapeMaker.drawCircle(); shapeMaker.drawRectangle(); shapeMaker.drawSquare(); }}

外观模式非常简单,就不在赘述

3.15、代理模式

简要说明

为其他对象提供一种代理以控制这个对象的访问

速记关键字

快捷方式,代理公司

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

代理模式类图也很简单,就是代理类需要实现源对象的接口,并且组合源对象,然后调用源对象方法。但是在调用源对象前后能做一些不可描述的事情,因此代理类的权限非常之大,它甚至可以不去调用源对象的方法。

代理模式分为静态代理和动态代理。静态代理就是需要一个实实在在的代理类对象存在,这会导致会产生大量的代理对象。于是,动态代理应运而生。动态代理就是不需要你生成代理类了,由代码帮你生成一个。

JDK动态代理就是针对该类是实现了接口的,就可以进行代理,Java自带的功能。

Cglib代理则不需要实现接口,但是需要引入cglib的jar包,但是我们一般的业务实现都是有接口的,两种方式都可以使用,根据情况而定。

Java代码实现

// 静态代理实现/** * 源接口 */public interface IUserDao { Object getUserInfo();}/** * 原接口实现类 */public class UserDaoImpl implements IUserDao{ public Object getUserInfo() { Map<String, Object> user = new HashMap<String, Object>(); user.put("userId", 1); user.put("userName", "zhangsan"); System.out.println("userId:" user.get("userId")); System.out.println("userName:" user.get("userName")); return user; }}/** * 静态代理类 */public class UserStaticProxy implements IUserDao{ private final IUserDao userDao; public UserStaticProxy(IUserDao userDao) { this.userDao = userDao; } public Object getUserInfo() { System.out.println("我是代理类,我在执行目标方法之前干了点事情"); final Object userInfo = userDao.getUserInfo(); System.out.println("我是代理类,我在执行目标方法之后又干了点事情"); return userInfo; }}// 静态代理测试public class Client { public static void main(String[] args) { final UserStaticProxy userStaticProxy = new UserStaticProxy(new UserDaoImpl()); final Object userInfo = userStaticProxy.getUserInfo(); }}/** * JDK动态代理类 需要实现InvocationHandler接口 */public class JdkProxy implements InvocationHandler{ private Object target; // 使用实现InvocationHandler方式 public Object getInstance(Object object){ this.target = object; // 创建一个代理类,传入类信息,类的接口信息,已经本类的信息 return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this); } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("JDK动态代理开始"); // 使用反射执行对应的方法 final Object invoke = method.invoke(target, args); System.out.println("JDK动态代理结束"); return invoke; }}/** * JDK动态代理类 使用静态方法 */public class JdkProxyUtil { private static final JdkProxy proxy = new JdkProxy(); public static Object getProxy(Object object){ return proxy.getInstance(object); }}/** * 这是一个没有接口实现的类 */public class UserService { public Object getUserInfo(){ Map<String, Object> user = new HashMap<String, Object>(); user.put("userId", 2); user.put("userName", "没有实现接口的用户"); System.out.println("userId:" user.get("userId")); System.out.println("userName:" user.get("userName")); return user; }}/** * 测试类 */public class Client { public static void main(String[] args) { // 有接口的实现 final IUserDao userDao = new UserDaoImpl(); final IUserDao userInfo = (IUserDao)JdkProxyUtil.getProxy(userDao); userInfo.getUserInfo(); // jdk获取没有接口的实现类的代理对象, 此时我们将会惨遭报错 final UserService userService = new UserService(); final UserService userServiceProxy = (UserService) JdkProxyUtil.getProxy(userService); userServiceProxy.getUserInfo(); }}/** * cglib动态代理类 需要实现MethodInterceptor接口 */public class CglibProxy implements MethodInterceptor { // 同理 维护一个对象 private Object target; // 返回一个代理对象: 是 target 对象的代理对象 public Object getProxyInstance(Object target) { this.target = target; //1. 创建一个工具类 Enhancer enhancer = new Enhancer(); //2. 设置父类 enhancer.setSuperclass(target.getClass()); //3. 设置回调函数 enhancer.setCallback(this); //4. 创建子类对象,即代理对象 return enhancer.create(); } public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { // TODO Auto-generated method stub System.out.println("Cglib代理模式开始"); Object returnVal = method.invoke(target, args); System.out.println("Cglib代理模式结束"); return returnVal; }}/** * Cglib代理工具类 */public class CglibProxyUtil { private static final CglibProxy proxy = new CglibProxy(); public static Object getProxy(Object object){ return proxy.getProxyInstance(object); }}/** * 测试类 */public class Client { public static void main(String[] args) { // Cglib获取有接口的实现类的代理对象 final IUserDao userDao = new UserDaoImpl(); final IUserDao userInfo = (IUserDao) CglibProxyUtil.getProxy(userDao); userInfo.getUserInfo(); // Cglib获取没有接口的实现类的代理对象 final UserService userService = new UserService(); final UserService userServiceProxy = (UserService) CglibProxyUtil.getProxy(userService); userServiceProxy.getUserInfo(); }}

代理模式也很简单,就是我们需要实现对应的接口,写一下代理前后的逻辑即可。最后附上cglib的gav坐标

<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.6</version></dependency>

3.16、迭代器模式

简要说明

提供一种方法来顺序访问一个聚合对象中的各个元素,而不是暴露该对象的内部状态

速记关键字

数据集,迭代,循环

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

其实迭代器模式在我们的不同语言中,均对其实现了,就是我们的各种集合,List,Set等都是迭代器模式的实现。

就是把一个集合中的元素一个一个的遍历迭代出来。

*Java代码实现

/** * 定义迭代器接口 */public interface Iterator<T> { // 下一个元素 T next(); // 是否有下一个元素 boolean hasNext(); // 第一个元素 boolean first(); // 最后一个元素 boolean last();}/** * 定义抽象聚合对象接口, 这里会组合迭代器接口 */public interface Iterable<T> { // 聚合迭代器 Iterator<T> iterator();}/** * 定义集合操作的基本方法,这里主要做新增和删除和统计元素个数 * @param <T> */public interface List<T> { // 添加元素 T add(T item); // 删除元素 T remove(int index); // 获取数组集合大小 int size();}/** * 定义具体聚合对象,迭代器在内部实现 它就相当于arrayList的实现, 所以再实现自己的List接口 * @param <T> */public class Aggregate<T> implements Iterable<T>, List<T>{ // 最大容量 private final int maxCapacity; // 集合元素 private final Object[] elements; // 元素个数 private int size; public Aggregate(int capacity) { this.maxCapacity = capacity; // 初始化指定容量 elements = new Object[capacity]; } @Override public Iterator<T> iterator() { // 使用自己实现的迭代器 return new IteratorImpl<>(); } @Override public T add(T item) { // 超过容器大小了,则抛出异常,这里不做演示扩容处理 if (size > maxCapacity - 1) { throw new IllegalStateException("Capacity overflow!"); } // 当前元素设置为新增元素,并且远哥个数 1 elements[size ] = item; return item; } @Override public T remove(int index) { // 检查数组下标是否越界 if (index < 0 || index > size - 1) { throw new ArrayIndexOutOfBoundsException(); } // 没越界,取出对应位置元素 T removed = (T) elements[index]; // 进行数组元素移动,将删除元素右边的都往左移动一个元素位置,从而把空间位置释放出来,因为数组是一片连续的地址空间,所以需要挪移到到一起 if (index < size - 1) { // 这里直接将数组当前位置 1后的全部元素拷贝到,从当前位置作为起点,拷贝起点 元素数量个位置结束 System.arraycopy(elements, index 1, elements, index, size - index - 1); } // 将最后一个元素设置为null, 并对site--,就相当于删除了元素了 elements[--size] = null; return removed; } @Override public int size() { return size; } /** * 具体的迭代器实现 * @param <T> */ public class IteratorImpl<T> implements Iterator<T>{ // 定义一个游标,主要表示操作数组元素的下标索引 private int cursor; @Override public T next() { // 如果没有下一个元素,则抛出没有找到元素异常 if (!hasNext()){ throw new NoSuchElementException(); } // 取数组当前下标元素,并且下标向后移动一位 return (T)elements[cursor ]; } @Override public boolean hasNext() { // 当超出数组长度时,表示已经没有下一个元素了 return cursor != size; } @Override public boolean first() { // 第一个元素 return cursor == 1; } @Override public boolean last() { return cursor == size; } }}/** * 测试迭代器 */public class Client { public static void main(String[] args) { int size = 10; // 创建一个聚合对象,并初始化数据 Aggregate<Integer> aggregate = new Aggregate<>(size); for (int i = 0; i < size; i ) { aggregate.add(i); } // 移除第一个元素和最后一个元素 aggregate.remove(size - 1); aggregate.remove(0); System.out.println("size: " aggregate.size()); // 使用迭代器进行迭代,不过现在一般都使用for循环 Iterator<Integer> iterator = aggregate.iterator(); while (iterator.hasNext()) { Integer item = iterator.next(); System.out.println( ("当前元素: " item) ",有下一个: " iterator.hasNext() ",是第一个:" iterator.first() ",是最后一个:" iterator.last()); } // 已经移除了最后一个元素,再移除这个位置的元素,将会报错 System.out.println("======================================="); System.out.println("测试素组下标越界,上面删除了两个,还剩八个,这个获取第九个元素,会报错"); aggregate.remove(size - 1); }}

结果输出

架构师备战(三)-软件工程总览(软件工程 架构)

3.17、中介者模式

简要说明

用一个中介对象来封装一系列的对象交互,它使各对象不需要显式的相互调用,从而达到低耦合,还可以独立的改变对象间的交互。

速记关键字

不直接引用

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

其实从类图我们可以看出,同事类组合了中介类,而我们的中介类有一个注册方法,可以将同事信息注册给中介进行管理。因此同事们的通信就可以交给中介了。

Java代码实现

/** * 中介者接口 */public interface Mediator { // 中介注册同事 void register(Colleague colleague); // 给同事发送信息 void send(String msg, Colleague colleague);}/** * 同事抽象类 */public abstract class Colleague { // 组合中介,用于实现群聊的功能 protected final Mediator mediator; // 同事名称 protected final String name; public Colleague(String name, Mediator mediator) { this.name = name; this.mediator = mediator; } // 接受消息(由子类实现) abstract void receiveMsg(String msg); // 发送消息(由子类实现) abstract void sendMsg(String msg);}/** * 具体同事类 */public class ConcreteColleague extends Colleague { public ConcreteColleague(String name, Mediator mediator) { super(name, mediator); } @Override void receiveMsg(String msg) { // 中介者调用 System.out.println(name "收到了消息: " msg); } @Override void sendMsg(String msg) { //通过中介者发送 System.out.println(name "发送了消息: " msg); mediator.send(msg, this); }}/** * 具体的中介者 */public class ConcreteMediator implements Mediator { // 存储同事列表,把同事信息管理起来 private final List<Colleague> colleagues = new ArrayList<>(); @Override public void register(Colleague colleague) { colleagues.add(colleague); } @Override public void send(String msg, Colleague colleague) { // 遍历所有同事,如果不是自己,那么就发送消息 for (Colleague coll : colleagues) { if (!coll.equals(colleague)) { // 给不是自己的发送消息,就是不是自己的同事都需要接受消息 coll.receiveMsg(msg); } } }}/** * 测试类 */public class Client { public static void main(String[] args) { // 创建中介者 Mediator mediator = new ConcreteMediator(); // 创建三个同事,并且指定中介者 Colleague colleague1 = new ConcreteColleague("小明", mediator); Colleague colleague2 = new ConcreteColleague("小丽", mediator); Colleague colleague3 = new ConcreteColleague("小鑫", mediator); // 把同事注册到中介者类 mediator.register(colleague1); mediator.register(colleague2); mediator.register(colleague3); // 小明发送消息,小丽和小鑫通过中介收到信息 colleague1.sendMsg("大家好,我是老实的小明,很高兴认识大家"); // 小丽发送消息,小明和小鑫通过中介收到消息 System.out.println("============================================"); colleague2.sendMsg("你们好,我是漂亮的小丽,很高兴认识大家"); }}

结果输出

架构师备战(三)-软件工程总览(软件工程 架构)

3.18、备忘录模式

简要说明

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,从而可以在以后将对象恢复到原先保存的状态。

速记关键字

游戏存档

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

角色说明

  • Originator:对象,也就是需要保存状态的对象
  • Memento:备忘录对象,负责保存好记录,即Originator内部状态
  • Caretaker:守护者对象,负责保存多个备忘录对象,一般使用集合管理,从而提升效率(如果需要保存多个不同状态的值,一般使用HashMap)

Java代码实现

/** * 需要保存状态的对象Originator */public class Originator { // 对象的状态字段 private String state; public String getState() { return state; } public void setState(String state) { this.state = state; } // 编写一个存档方法,并返回一个存档对象 public Memento saveMemento(){ // 创建一个新的存档 return new Memento(state); } // 通过备忘录对象,恢复对象状态 public void resumeState(Memento memento){ // 其实就是将当前对象的状态设置为从备忘录对象中的状态 this.state = memento.getState(); }}/** * 备忘录对象,负责保存好记录,即Originator内部状态 */public class Memento { // 保存的状态 private final String state; // 构造注入存档的状态 public Memento(String state) { this.state = state; } // 获取状态 public String getState() { return state; }}/** * 守护者对象,负责保存多个备忘录对象,一般使用集合管理,从而提升效率(如果需要保存多个不同状态的值,一般使用HashMap) */public class Caretaker { // list用于存储很多备忘录对象 private final List<Memento> mementoList = new ArrayList<>(); /** * 加入备忘录对象到集合 * @param memento 备忘路对象 */ public void add(Memento memento){ mementoList.add(memento); } /** * 根据索引下标获取备忘录对象 * @param index 索引 */ public Memento get(Integer index){ return mementoList.get(index); }}/** * 测试 */public class Client { public static void main(String[] args) { // 创建对象 Originator originator = new Originator(); // 创建守护者, 其实这里的守护者可以对比享元模式中的享元工厂,东西很类似 Caretaker caretaker = new Caretaker(); originator.setState("状态#1 攻击力1000"); // 使用守护者保存存档数据 Memento memento = originator.saveMemento(); caretaker.add(memento); originator.setState("状态#2 攻击力2000"); // 使用守护者保存存档数据 Memento memento2 = originator.saveMemento(); caretaker.add(memento2); originator.setState("状态#3 攻击力1500"); // 使用守护者保存存档数据 Memento memento3 = originator.saveMemento(); caretaker.add(memento3); // 我们希望恢复到攻击力巅峰的状态,也就是状态2 System.out.println("当前状态:" originator.getState()); originator.resumeState(memento2); System.out.println("恢复存档的状态:" originator.getState()); }}

实现输出

架构师备战(三)-软件工程总览(软件工程 架构)

其实备忘录模式跟享元模式很像,都是缓存数据,如果备忘录模式是存档多个状态,使用HashMap的话,那就更加接近了。

但是我们的备忘录模式,是将一个状态存储为一个备忘录对象,至于会不会恢复到当前状态,不一定。一般没有减少内存的场景,主要用于存档数据。

而享元模式主要是存储共享数据,让已经创建好的不变的对象减少重复创建过程,从而减少内存的消耗。

3.19、观察者模式

简要说明

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新

速记关键字

联动,广播消息

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

基于上面的类图,我们来实现一个监听器。类图中的Subject对应我们的被观察对象接口(IObservable),Observer对应我们的监听器接口(IListener)

我们实现被观察对象出现变化,然后通知所有的监听器实现类, 此接口我们参考JDK的观察者实现

Java代码实现

/** * 可观察目标接口,一般需要实现观察者的注册,移除,和通知等接口 */public interface IObservable { // 注册观察者 void registerObserver(IListener listener); // 移除观察者 void deleteObserver(IListener listener); // 通知所有观察者 void notifyObservers(); // 通知指定观察者或者监听器 void notifyObservers(IListener listener); // 删除所有观察者 void deleteObservers();}/** * 观察者接口,也就是监听器接口,需要实现哪些方法,一般是 */public interface IListener { // 假设这里要做一个update操作, 通知的时候,可以把被观察的对象传递过来,当然也可以传递其他参数 // 传入了被观察对象之后,我们可以在不同的观察者的实现类里面对不同的观察目标做不同的处理 void update(IObservable observable);}/** * 观察目标1 */public class DemoObservale1 implements IObservable{ // 监听器列表 private final List<IListener> listeners = new ArrayList<IListener>(); public void registerObserver(IListener listener) { listeners.add(listener); } public void deleteObserver(IListener listener) { listeners.remove(listener); } public void notifyObservers() { for (IListener listener : listeners) { // 通知单个listener notifyObservers(listener); } } public void notifyObservers(IListener listener) { // 通知指定的观察者,并将自己传递给观察者 listener.update(this); } public void deleteObservers() { for (IListener listener : listeners) { // 删除单个listener deleteObserver(listener); } }}/** * 观察目标2 */public class DemoObservale2 implements IObservable{ // 监听器列表 private final List<IListener> listeners = new ArrayList<IListener>(); public void registerObserver(IListener listener) { listeners.add(listener); } public void deleteObserver(IListener listener) { listeners.remove(listener); } public void notifyObservers() { for (IListener listener : listeners) { // 通知单个listener notifyObservers(listener); } } public void notifyObservers(IListener listener) { // 通知指定的观察者,并将自己传递给观察者 listener.update(this); } public void deleteObservers() { for (IListener listener : listeners) { // 删除单个listener deleteObserver(listener); } }}/** * 监听器1 */public class DemoListener1 implements IListener{ public void update(IObservable observable) { if (observable instanceof DemoObservale1){ System.out.println("监听器1:接收到了来自监听目标(Observale1)信息"); }else if (observable instanceof DemoObservale2){ System.out.println("监听器1:接收到了来自监听目标(Observale2)信息"); } }}/** * 监听器2 */public class DemoListener2 implements IListener{ public void update(IObservable observable) { if (observable instanceof DemoObservale1){ System.out.println("监听器2:接收到了来自监听目标(Observale1)信息"); }else if (observable instanceof DemoObservale2){ System.out.println("监听器2:接收到了来自监听目标(Observale2)信息"); } }}/** * 测试 */public class Client { public static void main(String[] args) { // 创建两个监听目标 final IObservable observale1 = new DemoObservale1(); final IObservable observale2 = new DemoObservale2(); // 创建两个监听器 final IListener listener1 = new DemoListener1(); final IListener listener2 = new DemoListener2(); // 分别让监听器1,2去监听目标1,2 observale1.registerObserver(listener1); observale1.registerObserver(listener2); observale2.registerObserver(listener1); observale2.registerObserver(listener2); // 通知全部 observale1.notifyObservers(); observale2.notifyObservers(); // 通知单个监听器看 System.out.println("===================只通知监听器1===================="); observale1.notifyObservers(listener1); observale2.notifyObservers(listener1); }}

测试结果

架构师备战(三)-软件工程总览(软件工程 架构)

观察者模式其实就是被观察对象看里面包含了注册和删除监听的方法,所以会对所有的监听器进行管理。

然后当触发某个动作时,调用了通知所有监听器(观察者)的方法,从而达到广播消息和状态联动的效果。

3.20、策略模式

简要说明

定义一系列算法,把它们一个个封装起来,并且使它们之间可以相互替换,从而让算法可以根据使用它的用户而改变。

速记关键字

多方案切换

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

其实策略模式最经典的例子就是,商品每日打折方法不同的例子。比如商品今天搞活动,打八折,明天又搞活动,买一送一等。假如每天一个策略,我们计算价格时就不好计算。

所以我们将一个个的策略封装成一个算法,打折时只需要选择对应的策略即可。

再比如我们工作中,我们假设要提供TCP的接口,我们使用netty启动一个socket服务,我们要接受客户端的流式数据,假设有些客户需要的是XML数据,有些客户需要的是JSON数据,有些甚至还需要一些健康检查的特定格式数据。

我们就可以通过策略模式,去判断流属于哪种格式,不同格式调用不同的策略进行请求数据解析。

netty服务使用策略模式支持XML/JSON的高性能TCP服务,是我很久之前工作中写下的一个小模块,在gitee上已经开源了,有兴趣的朋友请移步https://gitee.com/hdl_1107/hdl-parent.git进行源码查看。

下面我们使用策略模式模拟商品打折,我们就是根据不同的原价和对应的策略算法,计算出打折之后的价格。

Java代码实现

/** * 策略接口,也就是算法接口 */public interface IStrategy { // 打折算法接口,根据原价计算打折之后的价格 BigDecimal discount(BigDecimal originPrice);}/** * 打折扣类 */public class DiscountStrategy implements IStrategy{ // 折扣 0-1之间 private final BigDecimal discount; public DiscountStrategy(BigDecimal discount) { this.discount = discount; } @Override public BigDecimal discount(BigDecimal originPrice) { // 校验原价, 比0小,返回0 if (originPrice.compareTo(BigDecimal.ZERO) < 0){ return BigDecimal.ZERO; } if (discount.compareTo(BigDecimal.ZERO) < 0 || discount.compareTo(BigDecimal.ONE) > 0){ throw new RuntimeException("折扣范围只能在0-1之间"); } // 打几折就是乘以0.多少 保留两位小数,并向上四舍五入 return originPrice.multiply(discount).setScale(2, RoundingMode.HALF_UP); }}/** * 满减类 */public class FullReductionStrategy implements IStrategy { // 满减 需要两个参数 满多少减多少 private final BigDecimal fullPrice; private final BigDecimal reducePrice; public FullReductionStrategy(BigDecimal fullPrice, BigDecimal reducePrice) { this.fullPrice = fullPrice; this.reducePrice = reducePrice; } @Override public BigDecimal discount(BigDecimal originPrice) { // 校验原价, 比0小,返回0 if (originPrice.compareTo(BigDecimal.ZERO) < 0){ return BigDecimal.ZERO; } // 校验满减价格,减的价格不能大于满的价格 if (reducePrice.compareTo(fullPrice) > 0){ throw new RuntimeException("满减中减的价格不能高于满的价格"); } // 如果原价小于满减价格,不做满减优惠,返回原价 if (originPrice.compareTo(fullPrice) < 0){ return originPrice; } // 大于价格,做满减 return originPrice.subtract(reducePrice).setScale(2, RoundingMode.HALF_UP); }}/** * 策略上下文类 我这里实现了策略接口,当然也可以不实现,直接使用一个方法去调用策略对应的方法是一样的效果 */public class Context implements IStrategy { private static final Map<String, IStrategy> strategyMap = new HashMap<>(); static { // 九折 strategyMap.put("discount-90", new DiscountStrategy(new BigDecimal("0.90"))); // 八五折 strategyMap.put("discount-85", new DiscountStrategy(new BigDecimal("0.85"))); // 满一百减二十 strategyMap.put("full-reduce-100-20", new FullReductionStrategy(new BigDecimal(100), new BigDecimal(20))); } // 折扣或者满减类型 private final String type; public Context(String type) { this.type = type; } @Override public BigDecimal discount(BigDecimal originPrice) { // 根据类型获取策略 IStrategy strategy = strategyMap.get(type); if (strategy == null){ throw new RuntimeException("请检查您的策略"); } return strategy.discount(originPrice); }}/** * 测试类 */public class Client { public static void main(String[] args) { // 两个折扣,一个满减 Context context1 = new Context("discount-90"); Context context2 = new Context("discount-85"); Context context3 = new Context("full-reduce-100-20"); BigDecimal discount = context1.discount(new BigDecimal(100)); System.out.println("一百元打九折:" discount.longValue()); BigDecimal discount2 = context2.discount(new BigDecimal(100)); System.out.println("一百元打八五折:" discount2.longValue()); // 满一百,减二十 BigDecimal discount3 = context3.discount(new BigDecimal(100)); System.out.println("满一百减二十:" discount3.longValue()); }}

结果输出

架构师备战(三)-软件工程总览(软件工程 架构)

其实策略模式在我们工作中用得还是比较多的,因为我们经常就会有那种根据不同类型,具有不同实现或者计算的业务场景。并且策略模式 工厂模式可以减少那种大批量的if else if语句。您可以自己去尝试一下。

3.21、模板方法模式

简要说明

模板方法定义了一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变算法结构即可重新定义算法的某些特定步骤。

速记关键字

框架

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

其实模板方法模式,在我们工作中用得非常之多,特别是您在写一些框架性的东西,一些抽象化的组件,公共的模块时,尤其会用到这一块。

而我在工作中一般是这样使用模板方法的

  • 定义一个接口,将我们要要实现的目标接口抽象在接口中
  • 定义一个抽象类,然后实现该接口,并且实现它的方法。且把主要逻辑写在该方法中(模板方法,因为有些没有实现,只能称之为模板),将不能实现的使用抽象方法标识,供子类实现。将一些抽象类中可以共用并且重写的的方法都定义为protected,以便子类能够重写或直接调用。
  • 编写抽象类的多个子类,对父类没有实现的方法进行特定的实现。如果需要,还可以重写父类的一些protected方法

Java代码实现

public interface ILoginService { // 登录 String login(LoginParam param);}/** * 登录参数 */@Datapublic class LoginParam implements Serializable { // 密码登录参数 private String username; private String pwd; // 短信和短信验证码登录参数 private String mobile; private String smsCode;}/** * 抽象类,模板方法 */public abstract class AbstractLoginService implements ILoginService{ @Override public String login(LoginParam param) { before(); // 根据登录方式,返回不同结果 String loginType = getLoginType(); // 用户名密码登录 if ("pwd".equals(loginType)){ System.out.println("账号密码登录:" param.getUsername() "," param.getPwd()); after(); return "1"; } // 手机号验证码登录 else if ("sms".equals(loginType)){ System.out.println("手机号验证码登录:" param.getMobile() "," param.getSmsCode()); after(); return "2"; } System.err.println("登录类型不正确,请检查"); after(); return null; } // 获取登录类型,吗、交给子类实现 abstract String getLoginType(); // 定义钩子函数before,可以供子类实现 protected void before(){ } // 定义钩子函数after,可以供子类实现 protected void after(){ }}/** * 用户名,密码登录 */public class UpLogin extends AbstractLoginService{ @Override String getLoginType() { return "pwd"; } @Override protected void before() { // 重写钩子函数 System.out.println("我是子类UpLogin, 我在主方法之前被执行"); } @Override protected void after() { super.after(); // 重写钩子函数 System.out.println("我是子类UpLogin, 我在主方法之前后被执行"); }}/** * 手机号验证码登录 */public class SmsLogin extends AbstractLoginService{ @Override String getLoginType() { return "sms"; }}/** * 测试类 */public class Client { public static void main(String[] args) { // 用户名密码登录 LoginParam loginParam = new LoginParam(); loginParam.setUsername("小明"); loginParam.setPwd("12345678"); ILoginService loginService = new UpLogin(); loginService.login(loginParam); // 短信验证码登录 System.out.println("=========================="); LoginParam smsLoginParam = new LoginParam(); smsLoginParam.setMobile("18888888888"); smsLoginParam.setSmsCode("666666"); ILoginService smsLogin = new SmsLogin(); smsLogin.login(smsLoginParam); }}

测试结果

架构师备战(三)-软件工程总览(软件工程 架构)

3.22、状态模式

简要说明

允许一个对象在其内部改变时改变它的行为

速记关键字

状态变成类

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

状态模式主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。比如订单从待付款到待收货的咋黄台发生变化,执行的逻辑是不一样的。

所以我们将状态抽象为一个接口或者抽象类,对不同状态进行封装成单独的实体,用于实现各种状态处理的逻辑。

再设计一个上下文类,它组合了状态接口,用于发送请求。针对不同的状态提供不同的处理方法即可。

Java代码实现

/** * 状态接口 提供处理状态的方法 */public interface IState { // 处理状态,交给实现类实现 void handleState();}/** * 未付款状态 */public class UnpaidState implements IState{ @Override public void handleState() { System.out.println("下单成功,订单状态为待付款"); }}/** * 已付款状态 */public class PaidState implements IState{ @Override public void handleState() { System.out.println("支付成功,订单状态为已付款"); }}/** * 已取消状态 */public class CancelState implements IState{ @Override public void handleState() { System.out.println("订单取消支付,订单状态为已取消"); }}/** * 订单状态上下文类 */public class Context { // 组合订单状态 private final IState state; public Context(IState state) { this.state = state; } // 提供处理订单方法 public void handleOrderByState(){ state.handleState(); }}/** * 测试类 */public class Client { public static void main(String[] args) { // 创建上下文并创建未支付状态 Context context = new Context(new UnpaidState()); context.handleOrderByState(); // 创建上下文并创建已支付状态 Context context2 = new Context(new PaidState()); context2.handleOrderByState(); }}

结果输出

架构师备战(三)-软件工程总览(软件工程 架构)

其实我们可以看出来,状态模式和策略模式非常像,都有一个Context类,都有一个接口或抽象类被Context组合。而后抽象类或接口有自己的不同实现。

它们确实很像,但是它们确实有区别,因为状态模式围绕着状态的变化,它的子类之间的状态是可以进行转换的,比如订单状态由未付款变为已付款。但是策略模式则不会,只会二者取其一,进行一种策略操作。

3.23、访问者模式

简要说明

表示一个作用域某对象结构中的个元素的操作,使得在不改变各元素的前提下定义作用域这些元素的新操作。

速记关键字

数据与操作分离

类图如下

架构师备战(三)-软件工程总览(软件工程 架构)

角色说明

  • Visitor(抽象访问者):为每种具体的被访问者(ConcreteElement)声明一个访问操作
  • ConcreteVisitor(具体访问者):实现对被访问者(ConcreteElement)的具体访问操作,所以需要组合多个元素,也就是组合一组元素集合
  • Element(抽象被访问者):通常有一个Accept方法,用来接收/引用一个抽象访问者对象(基本原理)
  • ConcreteElement(具体被访问者对象):实现Accept抽象方法,通过传入的具体访问者参数、调用具体访问者对该对象的访问操作方法实现访问逻辑
  • Clent、ObjectStructure(客户端访问过程测试环境):该过程中,被访问者通常为一个集合对象,通过对集合的遍历完成访问者对每一个被访问元素的访问操作;

Java代码实现

/** * 定义被访问接口 */public interface Person { // 提供一个方法,让访问者可以访问 void accept(Action action);}/** * 访问者,这里提供了多个访问方法,从而获取多个不同的访问结果,它们的参数分别对应具体的被访问元素 */public interface Action { // 得到男性 的测评 void getManResult(Man man); // 得到女的 测评 void getWomanResult(Woman woman);}/** * 被访问者元素男人实现,传入自己给访问者访问 */public class Man implements Person{ @Override public void accept(Action action) { action.getManResult(this); }}/** * 被访问者元素女人实现,传入自己给访问者访问 */public class Woman implements Person{ @Override public void accept(Action action) { action.getWomanResult(this); }}/** * 访问者实现类 对不同的被访问元素做不同的访问 */class Success implements Action { @Override public void getManResult(Man man) { System.out.println("男人给的评价: 歌手很表演很nice"); } @Override public void getWomanResult(Woman woman) { System.out.println("女人给的评价: 歌手很表演很nice"); }}class Normal implements Action { @Override public void getManResult(Man man) { System.out.println("男人给的评价是: 歌手很表演比较普通"); } @Override public void getWomanResult(Woman woman) { System.out.println("女人给的评价是: 歌手很表演比较普通"); }}public class Fail implements Action { @Override public void getManResult(Man man) { System.out.println("男人给的评价: 歌手很表演有点糟糕"); } @Override public void getWomanResult(Woman woman) { System.out.println("女人给的评价: 歌手很表演有点糟糕"); }}/** * 数据结构,管理很多人(Man , Woman) */class ObjectStructure { //维护了一个集合 private List<Person> persons = new LinkedList<>(); //添加 public void add(Person p) { persons.add(p); } //删除 public void delete(Person p) { persons.remove(p); } // 显示测评情况(便利) public void show(Action action) { for (Person p : persons) { p.accept(action); } }}/** * 测试类 */public class Client { public static void main(String[] args) { // 使用数据结构来创建 ObjectStructure os = new ObjectStructure(); // 添加我们我们的访问者 os.add(new Man()); os.add(new Woman()); // 创建成功的被访问者 Success success = new Success(); // 通过数据结果遍历访问者,然后进行访问成功的数据 os.show(success); System.out.println("========================"); // 创建失败的被访问者 Fail fail = new Fail(); // 通过数据结果遍历访问者,然后进行访问失败的数据 os.show(fail); System.out.println("========================"); // 创建中肯的的被访问者 Normal normal = new Normal(); os.show(normal); }}

其实访问者模式和观察者模式的思想也非常类似,代码实现也很类似。都会提供一个管理被访问者/观察者集合,提供新增和删除方法,并且提供一个遍历集合的方法,并通知所有元素或者指定元素的方法。

它们只是应用场景不一样,其实类图都很类似。

结果输出

架构师备战(三)-软件工程总览(软件工程 架构)

第六章 软件测试与维护

1、软件测试方法

1.1、测试基本思想

  • 尽早、不断的进行测试
    • 在V模型其实已经凸显出这种思想了
  • 程序员避免测试自己设计的程序
    • 因为测试自己设计的程序,其实是不容易发现问题的,因为人从本质上都不愿意找自己的茬。而且由于你的思维惯性的影响,你必然认为这种做法往往是对的你才这么去做,所以有些问题不容易被发现,所以交叉检查效果会好很多。
  • 既要选择有效合理的数据,也要选择无效不合理的数据
    • 有效合理:输入数据符合要求,比如选择题选A,B,C,D答案就是有效合理的
    • 无效不合理:输入数据不符合要求:比如选择题不选A,B,C,D,你填写”除了这几个答案外的
  • 修改后应该进行回归测试
    • 因为修改一个Bug,很可能引入新的bug,然后需要重新测试之前的功能,就叫做回归测试
  • 尚未发现的错误数量与该程序已发现的错误数成正比
    • 比如模块A有2Bug,B有5个Bug, 可能B的质量要差一些,需要重点测试

1.2、测试类型

1.2.1、静态测试

静态测试是纯手工,不依赖计算机,并且不让计算机去运行它。比如写了一段代码,我们在脑海中凭空运行。这就是静态测试。

  • 桌前检查
  • 代码审查
  • 代码走查

说白了,就是自己检查代码,相互下、交叉检查代码以及尝试运行代码,来进行人工性的检查

1.2.2、动态测试

是计算机运行来看结果,比如我们写好了一段代码,我们执行程序来看结果,只要依赖了计算机就是动态测试,不能认为自动化测试才是动态测试。

  • 黑盒测试
    • 不知道内部结构,所以只能根据功能来进行测试
  • 白盒测试
    • 能够看到内部结构,能够根据内部结构来设计测试用例
  • 灰盒测试(白 黑)

1.3、测试用例设计

1.3.1、黑盒测试法

  • 等价类划分
    • 我们选择测试用例时,应该选不同的测试用例来测试。
    • 比如测试不同积分,灯影不同的Vip用户。1000是普通会员,2000就是银卡会员。
    • 0-1000选一个,1000-2000选一个,2000-3000选一个,从而达到全面测试的目的
  • 边界值分析
    • 比如银卡是1000-2000之间,那么最容易出错的 就是1000和2000这个边界值。那么我们一般测试就用9999,1000,1001,1999,2000,2001.这样就可以测试出边界值是否正确。
  • 错误推测
    • 一般依据经验来进行推测,也就是我做了多年代码工作,你去检测其他人的代码,你随便挑选几个,一测试就有问题,因为你这都是你曾经踩过的坑。这些地方就是容易出错
  • 因果图
    • 知道是黑盒测试方法即可

1.3.2、白盒测试法

  • 基本路径测试
  • 循环覆盖测试
  • 逻辑覆盖测试

覆盖方法

了解即可

  • 语句覆盖(最弱)
  • 判定覆盖
  • 条件覆盖
  • 条件判定覆盖
  • 修正的调价你判断覆盖
  • 条件组合覆盖
  • 点覆盖
  • 边覆盖
  • 路径覆盖(最强) 路径是指程序的路径

1.4、测试阶段

1.4.1、冒烟测试

  • 单元测试
    • 模块测试,模块功能,性能,接口等
  • 集成测试
    • 测模块间的接口,分为一次性组装测试和增量式组装测试
    • 一次性组装测试
      • 一次性把所有的模块组装起来进行测试,一次性组装测试会快一些
    • 增量式组装测试
      • 我先组装两个模块,两个协作搞定之后,再加一个进来。增量测试会更加彻底。增量分为自顶向下和自底向上和混合式。
      • 自顶向下:先测顶层模块,再联合下面的模块。如果下面的模块没有集成,此时需要自己人为写桩模块。
      • 自底向上:先测地下的模块,底下的模块要有人调用,此时需要自己写一些上面的模块,叫做驱动模块,来调用我们的测试模块。
      • 混合式
  • 确认测试
    • 验证软件与需求的一致性,分为内部确认测试,Alpha测试 ,Beta测试和验收测试
    • 内部确认测试
    • Alpha测试
      • 针对产品的测试,在开发环境用户进行测试。要请求用户到开发环境来进行测试。
    • Beta测试
      • 针对产品的测试,比如某某软件Beta版本,如QQ,这就是在给腾讯做免费的Beta测试。在用户自己的环境,由用户测试
    • 验收测试
  • 系统测试
    • 真是环境下,验证完整的软件配置项能否和系统正确连接
    • 恢复测试
    • 安全性测试
    • 压力测试
    • 性能测试(每一种测试测面的维度不一样)
      • 负载测试:测不同负载下的性能,比如1000并发下响应时间是怎样的,2000并发下响应时间是怎样的
      • 强度测试:强调的是把系统资源降到最低,看它能不能正常运行
      • 容量测试:更多的考虑的是并发数,同时并发多少。
    • 可靠性测试
    • 可用性测试
    • 可维护性测试
    • 安装测试
  • 回归测试
    • 测试软件变更之后,变更部分的正确性对变更需求的符合性

1.4.2、面向对象测试

  • 算法层(对应单元测试):包括等价类划分测试,组合功能测试(基于判定表的测试),递归函数测试和多态消息测试
  • 类层(对应模块测试):包括不变式边界测试,模态类测试和非模态类测试
  • 模板层/类树层(对应集成测试):包括堕胎服务测试和展平测试
  • 系统层(对应系统测试

2、软件调试

2.1、软件调试方法

  • 蛮力法:主要思想是”通过计算机找错“,低效,耗时。比如debug,单步运行,比较耗时
  • 回溯法(反向找错):从出错处人工沿控制流程往回追踪,直至发现出错的根源。复杂程序由于回溯路径多,难以实施。就是自己从报错哪个地方,往回看代码,看哪儿会出错。
  • 原因排除法(正向找错):主要思想是演绎和归纳,用二分法实现。

2.2、软件调试与测试的区别

  • 测试的目的是找出存在的错误,调试的目的是定位错误并修改程序以修正错误
  • 测试和调试在目标、方法和思路上都有所不同
  • 测试从一个已知的调价开始,使用预先定义的过程(测试用例),有预知的结果;调试从一个未知的条件开始,结束的过程不可预计。
  • 测试过程可以预先设计,进度可以事先确定;调试不能描述过程或持续时间

简单来理解就是测试是测试人员写测试用例,进行黑盒白盒等测试,调试就是开发人员使用Debug找错误并修改的过程。

3、系统运行与软件维护

3.1、系统转换计划

遗留系统的演化策略

架构师备战(三)-软件工程总览(软件工程 架构)

时至今日,你想去开发一个系统,想完全不涉及到已有的系统,基本是不可能的事情。但是对于已有系统我们有一个策略。

比如我们是淘汰掉已有系统,还是继承已有系统,或者集成已有系统,或者改造遗留的系统呢,都是不同的策略。

技术水平(技术维度)

比如你开发是用的Java语言还是python语言,你使用的框架是spring家族还是flask等,就是技术维度。

新的稳定的技术的技术水平就比老的技术水平(技术维度)高一些。

业务价值

主要是看您现在的系统的业务流程是否能符合现有的业务流程需要,就是能否体现它的业务价值。

淘汰策略

遗留系统的技术含量较低,且具有较低的业务价值。

对遗留系统的完全淘汰是企业资源的根本浪费,系统分析师应该善于”变废为宝“,通过对了、遗留系统功能的理解和借鉴,可以帮助新系统的设计,降低新系统开发的风险。

集成策略

遗留系统的技术含量高,但业务价值较低,可能只完成了某个部门或子公司的业务管理。

这种系统在各自的局部领域里工作良好,但是对于整个企业来说,存在多个这样的系统,不同的系统基于不同的平台,不同的数据模型,形成了一个个信息孤岛,对这种遗留问题的烟花策略为集成。

继承策略

遗留系统的技术含量较低,已经满足企业运作的功能或性能要求,但具有较高的商业价值,目前企业的业务尚紧密依赖该系统。

对这种遗留系统的演化策略为继承。在开发新系统时,需要完全兼容遗留系统的功能模型和数据模型.为了保证业务的连续性,新老系统必须并行运行一段时间,再逐渐切换到新系统上运行.

改造策略

遗留系统具有较高的业务价值,基本上能够满足企业业务运转和决策支持的需要.这种胸痛可能建成的时间还很短,对这种遗留系统的演化策略为改造.

改造包括系统功能的增强和数据模型的改造两个方面.

系统功能的增强指的是再原有系统的基础上增加新的应用要求,对遗留系统本身不做改变.

数据模型的改造指的是将遗留系统的旧的数据模型向新的数据模型的转化.

新旧系统转换策略

架构师备战(三)-软件工程总览(软件工程 架构)

直接转换策略

风险高,老系统停了,新系统刚刚跑起来,可能会出现问题,如果出问题了又得该,所以风险大,因此也诞生了并行转换策略.

并行策转换策略

一部分使用新系统,一部分使用旧系统,等新系统没有问题,再全部切换过去. 这个跟我们的金丝雀发布思想一致.

分段转换策略

其实就是上卖弄两种的这种策略, 比如我们有多个子系统,我一个一个的部署,没问题之后再部署其他子系统,然后逐一部署. 或者先部署一个区域,没问题了再逐个部署其他区域.

数据转换与迁移

也就是老数据库的数据提取到新的数据库里面该怎么做.一般都是先做数据抽取,然后做数据转换,然后做数据装载,也就是数据入库.

迁移方式

  • 使用工具迁移(导入导出等)
  • 采用手工录入
  • 系统切换后通过新系统生成(啥也不做,业务在新系统业务处理时新增)

3.2、软件维护

  • 正确性维护
    • 指改正在系统开发阶段已发生而系统测试阶段尚未发现的错误.
  • 适应性维护
    • 指使应用软件信息技术变化和管理需求变化而进行的修改.
    • 企业的外部市场环境和管理需求的不断变化也使得各级管理人员不断提出新的信息需求
  • 完善性维护
    • 扩充功能和改善性能而进行的修改.对已有的软件系统增加一些在系统分析和设计阶段没有规定的功能和性能特征
  • 预防性维护
    • 为了改进应用软件的可靠性和可维护性,为了适应未来的软硬件环境变化,应主动增加预防性的新的功能,以使用系统适应各类变化而不被淘汰.

总结

软件工程的知识主要就是这些,其中软件工程中软件开发模型和UML图和设计模式是重点中的重点,需要好好掌握,其他的简单的理解即可, 学无止境,继续加油!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

(0)
上一篇 2023年4月26日 上午9:54
下一篇 2023年4月26日 上午10:10

相关推荐