高级软件工程课程总结

315 阅读41分钟

收获与感想

首先要感谢孟宁老师一个学期下来的辛勤教学,老师独特的教学方式,譬如常常用道家、佛家的理念来阐释软件工程中的思想,让原本枯燥的内容变得生动有趣且易于理解。在学习高级软件工程这门课之前,一直觉得软件工程是一门“文科”课程,只需背背概念,记记模型就能理解,无需深入学习。但通过循序渐进地学习之后,才发现这门课大有学问,并不是想象中的那么枯燥乏味,而是充斥着前人的智慧,处处都有思想的碰撞和启发,我从中学到了不少新知识,例如对象组合和继承,设计模式,软件危机,MVVM模型等,都是一些美妙的思想,让我受益匪浅。

课程内容总结

第一章:码农的自我修养之必备技能

第一章的核心思想是工欲善其事必先利其器,想要成为一个优秀的码农,首先要学会使用工具。

  • 第一部分讲的是打字能力,打字能力对于码农来说是一个再基础不过的技能。

  • 第二部分讲的是VSCODE,说明了VSCODE相比于其他代码编辑器的优秀之处,例如简洁且轻量化,具有进程隔离的插件模型。设计进程隔离的插件模型是出于防止插件干扰到主进程代码的执行,所以将插件放到单独的进程中。可见,一个工具之所以优秀,是因为它本身就蕴含了许多软件工程的科学理念。

  • 第三部分讲的是Git,Git是一个分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。详细地介绍了git add, git commit, git clone, git pull, git checkout, git merge, git push等指令。其中git的设计理念很精妙,譬如采用的line diff技术,即形成增量补丁的技术方法,意味着不必在每次提交时都用新内容去覆盖旧内容,而是通过项目的增量补丁去描述项目的改变。

  • 团队项目中的分叉合并

    1. 克隆代码
    
       git clone 仓库地址
    
    2. 创建分支
    
       git checkout -b mybranch 仓库名
       git branch 查看分支状态
    
    3. 完成某单一功能模块的开发工作
    
       git add filesname
       git commit -m “提交记录”
    
    4. 切换回master分支,将远程的最新仓库版本同步到本地,再合并mybranch到主分支,再推到远程
    
       git checkout master 注意没有-b时是纯切换
       git pull
       git merge --no-ff mybranch
       git push
    
  • git merge --no-ff mybranch,可以保存之前的分支历史。能够更好的查看merge历史,以及branch状态。git merge则不会显示feature,只保留单条分支记录,只有一条单独的时间线。

  • 第四部分讲了vi/vim的使用,vim是一款强大的编辑器,有许多的快捷键,掌握了这些快捷键,编写文档会十分高效。

  • 第五部分讲了正则表达式的内容,用正则表达式可以测试字符串内的模式,替换文本,基于模式匹配从字符串中提取子字符串。总而言之,正则表达式可以按照规则很方便地匹配特定的字符串。

第二章:代码中的软件工程

本章以一个菜单小程序出发,每学一个新知识,便利用该知识用来完善或者重构菜单小程序。以下为本章主要内容分点。

  • 一个项目代码的风格应当简明、易读、无二义性。譬如写代码要遵守常规语言规范,合理地使用空格、空行、缩进、注释等。且代码应该逻辑清晰,命名合理,让人能轻松地读懂。

  • 编写高质量的代码,可以通过控制结构来简化代码:用if-else语句替代连续的if语句,尽量不用goto语句;通过数据结构简化代码:假如有一个会计相关的账目,一个菜鸟程序员往往将账目的数据全都写入代码中,导致数据和代码产生紧密的耦合,一旦数据发生改变,代码也要做相应的改变。一种更好的方式是用一个数据结构来表示账目,代码只与这个数据结构打交道,数据改变的时候,因为代码和该数据结构之间的接口不变,所以代码不用做修改。

  • 模块化原理的核心是关注点的分离,将软件系统划分成若干个模块,每个模块都尽量专注于一个功能目标,并且相对独立于其他软件模块。这种方式使得系统的变更和维护变得更容易,因为一个模块的变更对其他模块的影响很少,因为模块之间的接口相对稳定。

  • 软件模块化的程度用耦合度和内聚度来衡量,追求高内聚、低耦合。其中耦合度指的是模块之间的依赖程度,即模块之间具有的关系的紧密程度。内聚度指的是软件模块内部各种元素之间互相依赖的紧密程度。

    为何要追求高内聚、低耦合? 答:高内聚和低耦合其实目标一致,都是想让模块之间的关系更加疏松些,否则一个模块的更改,如数据结构的更改,可能会对另一个模块对这个数据结构的访问代码造成破坏。还有很多其他的情况,都会造成软件危机。

  • 微服务概念的提出。微服务是由一系列独立的微服务共同组成软件系统的架构模式,遵循了模块化设计的思想,因为每一个服务之间联系很少,正如上所说,各个微服务之间低耦合,微服务之内高内聚,符合模块化思想。而传统的单体集中式架构相比微服务架构,像一座摩天大楼,所有的功能都聚集在一起,具有更高的耦合度,也就意味着,一旦一个功能部分出错,就极有可能影响到其他部分,从而导致整个系统都出现错误。

  • 回调函数概念的提出。在模块化后的menu菜单程序中,搜索特定节点的代码仍旧写在main函数中,这样一来暴露了底层的数据结构,二来耦合度较高,一旦底层数据结构改变,main函数里的遍历代码也要做修改。故此时回调函数就派上了用场,main函数里创建一个回调函数,再调用底层的函数,并将回调函数作为参数传给底层的函数。底层函数在处理的过程中,会调用传进来的回调函数完成相应的判断或处理。也就是说,对底层模块的操作交给底层模块去做,上层模块只用根据某种条件设计一个回调函数传给底层函数,做到各司其职。

  • 可重入函数与线程安全。可重入函数指的是函数可由多于一个任务并发使用,而不必担心数据出错。设计可重入函数是保证线程安全的方式之一。可重入函数与线程安全本质上是指令乱序执行问题。

第三章:从需求分析到软件设计

  • 需求的类型

    1. 功能需求:根据所需的活动描述所需的行为。
    2. 质量需求或非功能需求:描述软件必须具备的一些质量特性。
    3. 设计约束: 设计决策,例如选择平台或接口标准,比如选择阿里云平台还是腾讯云,以及接口的设计标准。
    4. 过程约束: 对可用于构建系统的技术或资源的限制。比如用什么技术开发,何时有云计算服务的资源。
  • 高质量需求:

    1. 需求可测试:能够设置测试标准,比如将需求中的副词和形容词量化,有利于明确测试指标。
    2. 解决冲突:确立需求的优先级,哪些是必须的,哪些是希望的,哪些是可选的。
    3. 需求有特点:具有无二义性,完备性,持久性,正确性。
  • 用例的定义:用例是一个业务过程,经过逻辑整理抽象出来的一个业务过程。用例有三个基本要素:

    • 一个用例应该由业务领域内的某个参与者触发;
    • 用例必须能为特定的参与者完成一个特定的业务任务;
    • 一个用例必须终止于某个参与者,即特定参与者明确地或隐含地得到了业务完成的结果。(第二点的任务可能完成了,但参与者没收到结果,也没意义)
  • 用例满足的四个条件:参考上个部分

    1. 是不是一个业务过程?(系统中某些部分有可能与业务无关,不算用例,比如图书馆系统的登录系统就不算用例,与业务无关)
    2. 是不是由某个参与者触发?
    3. 是不是为特定的参与者完成了一个特定的业务任务?
    4. 是不是显式或隐式地终止于某个参与者,即参与者有没有隐式或显式地得到业务完成的结果?
  • 用例有三个抽象层级:抽象用例,通常用动名词短语表示;高层用例,指明用例在什么时候什么地方开始,以及在什么时候什么地方结束;扩展用例,将参与者与待开发系统完成用例所规定的业务任务的交互过程一步一步详细地描述出来,一般用两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来。对应的两列分别是参与者,系统响应。

  • 用例建模的基本步骤:1.从需求表述中找出用例,往往是动名词短语表示的抽象用例。2.描述用例开始和结束的状态,用TUCBW和TUCEW表示高层用例。3.对用例按照子系统或不同的方面进行分类,描述用例与用例、用例与参与者之间的上下文关系,并画出用例图。4.进一步逐一分析用例与参与者的详细交互过程,完成一个两列的表格将参与者和待开发软件系统之间从用例开始到用例结束的所有交互步骤都列举出来扩展用例。

  • 业务领域建模:是开发团队用于获取业务领域知识的过程。

  • 业务领域建模过程:1.收集应用业务领域的信息,聚集在功能需求层面。(收集纸面上的信息) 2.头脑风暴。列出重要的的应用业务领域概念,给出这些概念的属性,以及概念间的关系。3.给这些应用业务领域概念分类,分别列出哪些是类,哪些是属性,哪些是属性值,以及列出类之间的继承关系、聚合关系、关联关系。4.将结果用UML类图画出来。

  • 头脑风暴是从收集到的纸面信息中按规则抽取识别业务领域相关的概念,需要识别的规则如下:名词,名词短语,Y的X,及物动词(用于表示关系),形容词,数量词,所有关系的表达方法,具有、拥有等。注意这些概念,必须是领域相关的,通用的词汇不算在内。

  • 业务领域概念分类:名词、名词短语可能是类或者属性,形容词一般是属性值。构成关系的表达方法一般是聚合关系。包含关系一般表示关联关系。

  • 关联关系可以引入一个关联类,关联类和数据库里的关系很类似,表达若干个对象之间的关系。

  • MongoDB:是一个通用的、基于文档的、分布式的数据库,为云计算时代的现代应用程序开发者而生。用类似JSON格式的文档存储数据,比传统的关系数据模式具有更强大的数据表达能力。且比关系数据模式数据库更灵活,可以动态地给文档修改属性。

  • MongoDB的一对多关系建模的三种基础方案,分别对应内嵌,子引用,父引用:

    1. 一对很少:可以直接在文档内嵌入“很少”的文档。
    2. 一对很多:此时不适合直接嵌入文档,因为文档很多,势必造成被嵌入的文档变得很大。直接嵌入的文档的ID比较合适。
    3. 一对非常多:此时嵌入ID也不合适,可以在嵌入文档里加入被嵌入文档的ID,检索时根据被嵌入文档ID逐一检索即可。
  • 双向关联:如下图的两个文档是相互关联关系,假如此时person文档要新增一个关联的任务tasks,则该新增tasks里也要关联当前的person文档,则会出现在短暂的时间内会出现不一致的情况,即没法保证操作的原子性,在一些特殊的如银行类的场景内是不适合的,因为查出的数据是错误的。 image.png

  • 反范式化:在以往的关系型数据库里,往往要将数据库化为某某范式,减少冗余。而反范式化则是增加冗余,意在提升查询速度。比如:如果将零件的名字冗余到产品的文档对象中,当想更改某个零件的名字时,就需要同时更新所有包含这个零件的产品对象。在一个读比写频率高的多的系统里,反范式是有使用的意义的。如果你很经常的需要高效的读取冗余的数据,但是几乎不去变更他的话,那么付出更新上的代价还是值得的。更新的频率越高,这种设计方案的带来的好处越少。需要注意的是,一旦你冗余了一个字段,那么对于这个字段的更新将不再是原子的。和上面双向引用的例子一样,如果你在零件对象中更新了零件的名字,那么更新产品对象中保存的名字字段前将会存在短时间的不一致。

  • 统一过程的核心要义:

    1. 用例驱动,以用例建模得到的用例作为驱动开发的目标;
    2. 以架构为中心,保持软件架构稳定,减小软件架构层面的重构造成的混乱;
    3. 增量且迭代,做到增量开发且迭代开发。
  • 敏捷统一过程的四个关键步骤如下:

    1. 确定需求
    2. 通过用例的方式来满足这些需求
    3. 分配这些用例到各增量阶段
    4. 具体完成各增量阶段所计划的任务
  • 敏捷统一过程的增量阶段:在每一次增量阶段的迭代过程中,都要进行从需求分析到软件设计实现的过程,分为五个步骤。

    1. 用例建模
    2. 业务领域建模
    3. 对象交互建模
    4. 形成设计类图
    5. 软件的编码实现和软件应用部署
  • 对象交互建模:对象交互建模和扩展用例具有内在的衔接关系,扩展用例是描述参与者和用例之间一步一步的互动过程,而对象交互建模进一步深入到扩展用例中的关键步骤,通过描述用例内部不同对象之间一步一步的互动过程来清晰地描述业务过程中的关键步骤是如何被执行的。扩展用例需要借助于一个两列的表格来描述;对象交互建模需要借助于剧情描述和剧情描述表。剧情描述是一种不是特别严格的、非形式化的文字描述方式,用来一步一步地描述对象交互的过程;剧情描述表是通过一个五列的表格更严格一些的形式要求进一步将对象交互过程组织起来,这样有利于将对象交互的过程转换成序列图

  • 对象交互建模的基本步骤:

    1. 在扩展用例中右侧一列中找出关键步骤。关键步骤是那些需要在背后进行业务过程处理的步骤,而不是仅仅在表现层与参与者进行用户接口层面交互的琐碎步骤。
    2. 对于每一个关键步骤,从关键步骤在扩展用例两列表格中的左侧作为开始,完成剧情描述,描述一步一步的对象交互过程,直到执行完该关键步骤。
    3. 如果需要的话,将剧情描述进一步转换成剧情描述表。
    4. 将剧情描述或剧情描述表转换成序列图
  • KISS原则:Keep it simple and stupid,保持足够的简洁,将细节留给编码阶段。

  • 剧情描述表有五列,分别是:序号,主体,主体行为,其他对象,主体行为作用的对象。

  • 敏捷统一过程增量阶段的区分(软件开发阶段区分)

    1. 用例建模:参与者->用例->参与者,参与者参与了一个业务过程,比较粗糙。
    2. 业务领域建模:会抽出业务领域的概念,类和属性,以及关联关系等,并进行UML建模
    3. 对象交互建模:根据扩展用例的关键步骤进行剧情分析,写成剧情表,并写成时序图。
    4. 形成设计类图:
      • 找出时序图里用到的所有类并写入DCD(设计类图)
      • 找出属于上述类的方法并写入DCD
      • 从时序图和业务领域模型里找出属性,并写入DCD中
      • 从时序图和业务领域模型里找出关系,并写入DCD中
    5. 软件编码和软件应用部署

第四章:软件科学基础概论

继承和对象组合

继承和对象组合都是以对象(类)作为软件基本元素构成的程序结构。对象是基于属性(变量或对象)和方法(函数)构建起来的更复杂的软件基本元素,显然它涵盖了前述包括函数调用框架在内所有程序结构,它是一个更高层、更复杂的抽象实体。

  • 基于对象(类)之间的继承关系所形成的两个对象各自独立、两个类紧密耦合的程序结构。
  • 对象组合则将一个对象作为另一个对象的属性,从而形成两个对象(类)之间有明确的依赖关系。耦合度相比继承更低。
回调函数

回调函数是一个面向过程的概念,是代码执行过程的一种特殊流程。回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,当这个指针调用其所指向的函数时,就称这是回调函数。回调函数不是该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

多态

多态的意思就是多种状态。多态是实例化变量可以指向不同的实例对象,这样同一个实例化变量在不同的实例对象上下文环境中执行不同的代码表现出不同的行为状态,而通过实例化变量调用实例对象的方法的那一块代码却是完全相同的,这就顾名思义,同一段代码执行时却表现出不同的行为状态,因而叫多态。即允许将不同的子类类型的对象动态赋值给父类类型的变量,通过父类的变量调用方法在执行时实际执行的可能是不同的子类对象方法,因而表现出不同的执行效果。应用有:

  • 在Java语言中,接口的有多种不同的实现方式。例如List接口可以实现为LinkedList或者ArrayLIst。
  • 在继承关系中,父类实例化变量可以指向不同的子类对象,用该父类变量可以表现出不同的行为状态。动态连接,即为父类调用子类重写的方法。
闭包

一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。如下图的代码:

function makeFunc() {
    var name = "Mozilla";
    function displayName() {
        alert(name);
    }
    return displayName;
}
​
var myFunc = makeFunc(); //返回一个函数实例的引用,即函数的地址
myFunc(); //执行该返回的函数,成功输出"Mozilla" 

按以前的知识可能会认为,name是函数makeFunc()的一个局部变量,在执行makeFunc()之后就会被垃圾回收掉,但是仍旧能成功输出,原因在于:JavaScript 中的函数会形成了闭包。 闭包是由函数以及声明该函数的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。在本例子中,myFunc 是执行 makeFunc 时创建的 displayName 函数实例的引用。displayName 的实例维持了一个对它的词法环境(变量 name 存在于其中)的引用。因此,当 myFunc 被调用时,变量 name 仍然可用,其值 Mozilla 就被传递到alert中。

异步调用

Promise对象可以将异步调用以同步调用的流程表达出来,避免了通过嵌套回调函数实现异步调用。所谓Promise对象,就是代表了未来某个将要发生的事件,通常是一个异步操作。异步就是从主线程发起一个子线程来完成任务,当一轮事件循环运行完成之后,回调函数被调用执行。

设计模式

根据模式是主要用于类上还是主要用于对象上来划分的话,可分为类模式和对象模式两种类型的设计模式:

  • 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。比如模板方法模式等属于类模式。
  • 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。由于组合关系或聚合关系比继承关系耦合度低,因此多数设计模式都是对象模式。

根据设计模式可以完成的任务类型来划分的话,可以分为创建型模式、结构型模式和行为型模式 3 种类型的设计模。

  • 单例模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,典型的应用如数据库实例。
  • 原型模式:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。比如在经过一个高昂的数据库操作获得的一个对象,可以对其进行复制,避免下次再进行一次这种高代价操作。
  • 建造者模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。主要应用于复杂对象中的各部分的建造顺序相对固定或者创建复杂对象的算法独立于各组成部分
  • 代理模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。代理模式是不要和陌生人说话原则的体现,典型的应用如外部接口本地化将外部的输入和输出封装成本地接口,有效降低模块与外部模块的耦合度。
  • 适配器模式:将一个类的接口转换成客户希望的那个接口,使得原本由于不兼容而不能一起工作的那些类能够一起工作。继承和对象组合都可以实现适配器模式,但由于组合关系或聚合关系比继承关系耦合度低,所以对象组合方式的适配器模式比较常用。
  • 职责链模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。通过这种方式将多个请求处理者串联为一个链表,去除请求发送者与它们之间的耦合。

设计模式背后的设计原则有:

  • 开闭原则:软件应当对扩展开放,对修改关闭。遵守开闭原则使软件拥有一定的适应性和灵活性的同时具备稳定性和延续性。统一过程以架构为中心增量且迭代的过程和开闭原则具有内在的一致性,它们都追求软件结构上的稳定性。当我们理解了软件结构模型本质上具有不稳定性的时候,一个小小的需求变更便很可能会触动软件结构的灵魂,瞬间让软件结构崩塌,即便通过破坏软件结构的内在逻辑模型打上丑陋的补丁程序,也会使得软件内在结构恶化加速软件的死亡。因此开闭原则在基本需求稳定且被充分理解的前提下才具有一定的价值

  • Liskov替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能。尽管程序员是自己的代码的上帝,但也不能胡来,要做一个遵守自然规律的上帝。且子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。Liskov替换原则如今看来其价值也大大折扣,因为为了降低耦合度我们往往使用对象组合来替代继承关系。反而是Liskov替换原则不推荐的多态成为诸多设计模式的基础。

  • 依赖倒置原则:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。依赖倒置指的是高层对底层、抽象对细节依赖的倒置。

  • 单一职责原则:控制类的粒度大小,提高其内聚度。如果一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多;同时可以提高类的内聚度,符合模块化设计的高内聚低耦合的设计原则。单一职责原则是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,再封装到不同的类或模块中。而发现类的多重职责需要设计人员具有较强的抽象分析设计能力和相关重构经验。

  • 观察者模式:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为,这样所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式。通常与多态相结合,被观察者构建一个容纳Observer接口的链表,观察者们都要实现这个接口,并在主函数里将自己注册到被观察者的通知列表中,当被观察者的某个方法被执行,就会通知列表里的观察者,让观察者们执行相应的动作。

image.png

  • 高温系统例子:其中在初始化观察者类时都将自己注册到被观察者的observer列表中。还体现了依赖倒置原则,即观察者没有依赖观察者的具体类,而是依赖subject接口,被观察者也没有依赖观察者具体类,而是依赖observer接口。 image.png

  • 三层架构:层次化架构是利用面向接口编程的原则将层次化的结构型设计模式作为软件的主体结构。全是call-in调用,如果,类间的关系是一对多,则耦合度会变得很高

  • MVC架构:MVC架构是一种中介者模式架构。中介者模式是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。在MVC架构下,控制器是和视图联合使用的,它捕捉鼠标移动、鼠标点击和键盘输入等事件,将其转化成服务请求,然后再传给模型或者视图。软件的用户是通过控制器来与系统交互的,他通过控制器来操纵模型,从而向模型传递数据,改变模型的状态,并最后导致视图的更新。
  • MVC例子:模型的解析,和视图的更新均在控制器里完成,视图的更新需要自己根据语义来具体实现,而之后的MVVM模型则不需要自己手动实现视图的更新。

image.png

  • MVVM模型:MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

    • 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的"View"上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
    • 可重用性。可以把一些视图逻辑放在一个ViewModel里面,让很多View重用这段视图逻辑。
    • 独立开发。开发人员可以专注于业务逻辑和数据的开发,设计人员可以专注于页面设计。

    与MVC模型不同在于,MVVM模型无需自己编写控制器里修改视图的代码,可以实现自动更新,即更改模型里的值,视图就会自动更新。

  • MVVM中的“智能更新”,以Vue.js为例。

    被观察者会执行的操作:

image.png 刚开始,先遍历data,用Object.defineProperty给被观察的对象增添getter和setter。被观察对象的get函数出现两种情况时会被调用:

1.  观察者初始化时,会调用被观察者的get函数,再调用dep.addSub将自己加入到被观察对象的观察者列表中。
2.  当获取被观察对象的值时。

被观察对象的set函数在如下情况会被调用:

1.  更改值时,即调用模型里对象的get函数时,会先更新值,再调用notify通知观察者列表里的所有观察者model里的数据已经更新,并调用观察者的update函数,完成页面更新。

观察者会执行的操作:

image.png

起初解析DOM,获取每一个{{}}里的成员,初始化为watcher。初始化的过程中,将更新视图的回调函数传给watcher。将该表达式对应的模型 data里的属性的get赋值给自己的get函数,并执行一次初始更新操作this.update()。该更新操作是将watcher自己添加到对应被观察者的观察者列表之中。

当模型发生改变时,观察者的update也会被调用,先获取更新的值,再将这个值传给回调函数,更新界面。即观察者的get绑定的是被观察者的get函数,所以要获取模型的值,直接调用观察者本身的get函数即可,会返回模型里的值,并利用这个值去更新视图。

  • 软件架构风格与策略

    • 管道过滤器,如编译系统,一环接一环地处理数据流。
    • C/S,B/S架构
    • P2P架构
    • 发布订阅架构
    • CRUD,分别是创建、读取、更新、删除四种数据库持久化信息的基本操作助记符。
    • 层次化架构,例如OSI模型。
  • 软件架构的描述方法

    • 分解视图,用软件模块勾画出系统结构,往往会通过不同抽象层级的软件模块形成层次化的结构。层次包括:子系统->包->类->组件->库->软件模块,软件单元等。
    • 依赖视图,体现了软件模块之间的依赖关系,比如软件模块A调用了另一个软件模块B,那么软件模块A直接依赖软件模块B。如果一个软件模块依赖另一个软件模块产生的数据,那么这两个软件模块也具有一定的依赖关系。
    • 泛化视图,展现了软件模块之间的一般化或具体化的关系。有助于描述软件的抽象层次,从而便于软件的扩展和维护。比如通过对象组合或继承很容易形成新的软件模块与原有的软件架构兼容。
    • 执行视图,展示了系统运行时的时序结构特点,比如流程图、时序图等。执行视图中的每一个执行实体,一般称为组件(Component),都是不同于其他组件的执行实体。如果有相同或相似的执行实体那么就把它们合并成一个。
    • 实现视图,描述软件架构与源文件之间的映射关系,比如软件架构的静态结构以包图或设计类图的方式来描述,一般通过目录和源文件的命名来对应软件架构中的包、类等静态结构单元,这样典型的实现视图就可以由软件项目的源文件目录树来呈现。
    • 部署视图,将执行实体和计算机资源建立映射关系。这里的执行实体的粒度要与所部署的计算机资源相匹配,比如以进程作为执行实体那么对应的计算机资源就是主机,这时应该描述进程对应主机所组成的网络拓扑结构,这样可以清晰地呈现进程间的网络通信和部署环境的网络结构特点。
    • 工作分配视图,•系统分解成可独立完成的工作任务,以便分配给各项目团队和成员。工作分配视图有利于跟踪不同项目团队和成员的工作任务的进度,也有利于在个项目团队和成员之间合理地分配和调整项目资源,甚至在项目计划阶段工作分配视图对于进度规划、项目评估和经费预算都能起到有益的作用。
  • 视图辨析

    • 管道过滤器:执行视图(有执行流程),分解试图(有软件模块的层次化)
    • P2P:部署视图(因为资源和服务都分布在不同的端上)
    • 发布订阅:分解视图
    • CRUD:分解视图,依赖视图(边缘的应用依赖中心提供的服务)
    • OSI:分解视图,依赖视图(下层给上层提供服务)
    • 给出源代码目录:实现视图

第五章:软件危机和软件过程

随着大容量、高速度计算机的出现,使计算机的应用范围迅速扩大,因此软件开发需求和软件的规模都急剧增长。高级语言开始出现、操作系统迅猛发展、大量数据处理导致数据库管理系统的诞生等,导致软件开发的基础环境发生了重大变化,从与计算机硬件基础环境直接打交道,变为在一个更高抽象层级之上编写软件。软件系统的规模越来越大,复杂程度越来越高,软件可靠性问题也越来越突出。原来的个人设计、个人使用的方式不再能满足要求,迫切需要改变软件生产方式,提高软件开发效率,软件危机开始爆发 。人们一直寻找解决软件危机的方法,诞生了包括结构化程序设计、专家系统、面向对象的分析和设计方法等,当人们意识到大型软件中打造抽象软件的复杂概念结构的根本困难是缺乏有效的管理,而非技术本身时,于是针对软件开发活动及软件本身的管理,对软件生命周期建模,形成了众多有效管理软件开发过程的理论和方法,经过研究和实践逐渐演化成如今的CMM/CMMI、敏捷方法、DevOps等软件过程模型

银弹

银弹指的是可以解决复杂问题的简单、奇妙方案,在此指解决软件危机的方案。

软件的生命周期

一般来讲,我们将软件的生命周期划分为:分析、设计、实现、交付和维护这么五个阶段。

  • 分析阶段的任务是需求分析和定义
  • 设计阶段分为软件架构设计和软件详细设计
  • 实现阶段分为编码和测试
  • 交付阶段主要是部署、交付测试和用户培训等
  • 维护阶段一般是软件生命周期中持续时间最长的一个阶段,而且在维护阶段很可能会形成单独的项目,从而经历分析、设计、实现、交付几个阶段,最终又合并进维护阶段。
描述性的过程和说明性的过程
  • 描述性的过程试图客观陈述在软件开发过程中实际发生什么
  • 说明性的过程试图主观陈述在软件开发过程中应该会发生什么。显然说明性的过程是抽象的过程模型,有利于整个软件项目团队对软件开发过程形成一致的理解,能够在与实际软件开发过程比较时找出项目过程中的问题。
瀑布模型

开发过程是通过设计一系列阶段顺序展开的,从系统需求分析开始直到产品发布和维护,项目开发进程从一个阶段“流动”到下一个阶段。

原型化的瀑布模型

原型就是根据需要完成的软件的一部分,完成哪一部分是根据开发原型的目标确定,比较常见的有用户接口原型和软件架构原型。

  • 在需求分析阶段,原型可能是软件的用户接口部分,比如用户交互界面,用户接口原型可以有效地整理需求,在需求分析的过程提供直观的反馈形式,有利于确认需求是否被准确理解。
  • 在设计阶段,原型可能是软件架构的关键部分,比如采用某种设计模型解决某个问题,软件架构原型可以有效地评估软件设计方案是否能够有效解决特定问题,有利于验证技术方案的可行性,为大规模投入开发提供技术储备和经验积累。
V模型
  • V模型也是在瀑布模型基础上发展出来的,我们发现单元测试、集成测试和系统测试是为了在不同层面验证设计,而交付测试则是确认需求是否得到满足。瀑布模型中前后两端的过程活动具有内在的紧密联系,如果将模块化设计的思想拿到软件开发过程活动的组织中来,可以发现通过将瀑布模型前后两端的过程活动结合起来,可以提高过程活动的内聚度,从而改善软件开发效率。V模型是开始一个特定过程活动和评估该特定过程的活动成对出现,运用了模块化的思想
  • 注意:V模型的开发和测试是串行的而不是并行的,成对出现指的是测试各个阶段会和开发的各个阶段对标,测试更完善。比如单元测试验收的对象是详细测试说明书,集成测试验收的对象是概要设计说明书,系统测试验证的是需求说明书。
螺旋模型

螺旋模型最大的特点在于引入了其他模型不具备的风险管理,使软件在无法排除重大风险时有机会停止,以减小损失。螺旋模型会沿着螺线进行若干次迭代,每一次迭代过程分被为四个主要阶段:

  • 计划
  • 确定目标,选定实施方案,弄清项目开发限制条件
  • 评估所选方案和风险
  • 实施开发和测试
CMM/CMMI

为了应对软件危机保证软件产品的质量,80年代中期,美国联邦政府提出对软件承包商的软件开发能力进行评估的要求。CMM就是软件能力成熟模型。CMMI共有五个级别:

  • CMMI一级,初始级。在初始级水平上,软件组织对项目的目标与要做的努力很清晰,项目的目标可以实现。但是由于任务的完成带有很大的偶然性,软件组织无法保证在实施同类项目时仍然能够完成任务。项目实施能否成功主要取决于实施人员。
  • CMMI二级,管理级。在管理级水平上,所有第一级的要求都已经达到,另外,软件组织在项目实施上能够遵守既定的计划与流程,有资源准备,权责到人,对项目相关的实施人员进行了相应的培训,对整个流程进行监测与控制,并联合上级单位对项目与流程进行审查。二级水平的软件组织对项目有一系列管理程序,避免了软件组织完成任务的偶然性,保证了软件组织实施项目的成功率。
  • CMMl三级,已定义级。在已定义级水平上,所有第二级的要求都已经达到,另外,软件组织能够根据自身的特殊情况及自己的标准流程,将这套管理体系与流程予以制度化。这样,软件组织不仅能够在同类项目上成功,也可以在其他项目上成功。科学管理成为软件组织的一种文化,成为软件组织的财富。
  • CMMI四级,量化管理级。在量化管理级水平上,所有第三级的要求都已经达到,另外,软件组织的项目管理实现了数字化。通过数字化技术来实现流程的稳定性,实现管理的精度,降低项目实施在质量上的波动。
  • CMMI五级,持续优化级。在持续优化级水平上,所有第四级的要求都已经达到,另外,软件组织能够充分利用信息资料,对软件组织在项目实施的过程中可能出现的问题予以预防。能够主动地改善流程,运用新技术,实现流程的优化
敏捷方法

增量开发加上迭代开发,才算真正的敏捷开发。代开发其实就是"重复开发",所谓"增量开发",指的是软件的每个版本,都会新增一个用户可以感知的完整功能。敏捷开发的好处有:

  • 早期交付,能快速形成一个软件版本
  • 降低风险,及时了解市场需求,降低产品不适用的风险。通过原型产品,可以了解市场的接受程度。

敏捷宣言如下:

  • 个体和互动 高于 流程和工具
  • 工作的软件 高于 详尽的文档
  • 客户合作 高于 合同谈判
  • 响应变化 高于 遵循计划
DevOps

DevOps(Development和Operations的组合)是一组过程、方法与系统的统称,用于促进软件开发、技术运营和质量保障(QA)部门之间的沟通、协作与整合。它的出现是由于软件行业日益清晰地认识到:为了按时交付软件产品和服务,开发和运营工作必须紧密合作。传统的软件组织将开发、IT运营和质量保障设为各自分离的部门。DevOps是一套针对这几个部门间沟通与协作问题的流程、方法和系统。Dev的工作是添加新特性,而Ops的工作是保持系统运行的稳定和快速;而Dev在添加新特性时所带来的代码变化,会导致系统运行不稳定和变慢,从而引发Dev与Ops的冲突。然而从全局来看,Dev和Ops的目标是一致的,即都是让业务所要求的那些变化能随时上线可用。当Dev团队已经实践了敏捷,而Ops团队还是传统运维的工作方式时,导致产品部署的时间间隔过长使得一个开发团队的敏捷工作变成了一直试图避免的瀑布生命周期。这时无论开发团队有多么敏捷,从总体上改变企业业务缓慢和迟钝的表现都是极其困难的。因此可以将DevOps看成是敏捷方法从技术开发领域扩展到业务运维领域,也就是实现业务上全周期的敏捷性,在业务运营者作出决策、开发者进行响应和IT运维上线部署之间能够紧密互动和快速反馈,从而形成与业务需求始终努力保持一致的持续改进过程。DevOps使得敏捷方法的优势可以体现在整个企业业务组织机构层面。通过实现反应灵敏且稳定部署持续交付的业务运维,使其能够与开发过程的创新保持同步,DevOps可以做到整个业务实现过程的敏捷性

参考资料

代码中的软件工程 gitee.com/mengning997…