码农的自我修养之 软件科学基础概论

码农的自我修养之软件科学基础概论

一、软件是什么?

软件的基本构成元素

对象(Object)

  • 一个对象作为某个类的实例,是属性和方法的集合。对象和属性之间有依附关系,属性用来描述对象或存储对象的状态信息,属性也可以是一个对象。对象能够独立存在,对象的创建和销毁显式地或隐式地对应着构造方法(constructor)和析构方法(destructor) 。
  • 面向对象是一种对软件抽象封装的方法
    在这里插入图片描述

函数和变量/常量

  我们看看用函数和变量/常量作为软件基本元素的抽象方法。相对来讲面向对象是一种更高层的软件抽象封装的方法,其中方法大致对应函数,属性大致对应变量/常量。由于函数和变量/常量的相关概念相信您非常熟悉,我们这里仅讨论它们的进程地址空间分布。
在这里插入图片描述
  从图中可以看出从低地址到高地址分别内存区分别为代码段、已初始化数据段、未初始化数据段(BSS)、堆、栈和命令行参数和环境变量。
  面向对象的软件抽象层次更高与进程地址空间对应起来更为复杂,我们以面向过程的编程语言C语言为例简要分析一下函数、常量、全局变量、静态变量和局部变量的存储空间分布。

  • 全局常量(const)、字符串常量、函数以及编译时可决定的某些东西一般存储在代码段(text);
  • 初始化的全局变量、初始化的静态变量(包括全局的和局部的)存储在已初始化数据段;
  • 未初始化的全局变量、未初始化的静态变量(包括全局的和局部的)存储在未初始化数据段(BSS);
  • 动态内存分配(malloc、new等)存储在堆(heap)中;
  • 局部变量(初始化以及未初始化的,但不包含静态变量)、局部常量(const)存储在栈(stack)中;
  • 命令行参数和环境变量存储在与栈底紧挨着的位置。

指令和操作数

  指令(instruction)是由CPU 加载和执行的软件基本单元。一条指令有四个组成部分:标号、指令助记符、操作数、注释,其中标号和注释是辅助性的,不是指令的核心要素。一般指令可以表述为指令码+操作数。
指令码可以是二进制的机器指令编码,也可以是八进制的编码,程序员更喜欢用汇编语言指令助记符,如mov、add 和sub,给出了指令执行操作的线索。
操作数有3 种基本类型:立即数。用数字文本表示的数值;寄存器操作数。使用CPU 内已命名的寄存器;内存操作数。引用内存位置。

0和1是什么?

软件的基本结构

顺序结构

  顺序结构是最简单的程序结构,也是最常用的程序结构,只要按照解决问题的顺序写出相应的语句就行,它的执行顺序是自上而下,依次执行。

分支结构

  分支结构是在顺序结构的基础上,利用影响标志寄存器上标志位的指令和跳转指令组合起来借助于标志寄存器或特定寄存器暂存条件状态实现分支结构。
条件跳转指令的四种类型:

  • 基于特定标志位的值跳转。比如JZ为零跳转、JNO无溢出跳转、JNZ非零跳转、JS有符号跳转、JC进位跳转、JNS无符号跳转、JNC无进位跳转、JP偶校验跳转、JO溢出跳转、JNP奇校验跳转;
  • 基于CMP指令比较两个数是否相等,或一个数与特定寄存器(CX、ECX、RCX)比较是否相等跳转。比如JE相等跳转、JNE不相等跳转、JCXZ当寄存器CX=0跳转、JECXZ当寄存器ECX=0跳转、JRCXZ当寄存器RCX=0跳转(RCX为64位模式下的寄存器);
  • 基于无符号操作数的比较跳转。比如JA大于跳转、JB小于跳转、JNBE不小于或等于跳转(与JA 相同)、JNAE不大于或等于跳转(与JB 相同)、JAE大于或等于跳转、JBE小于或等于跳转、JNB不小于跳转(与JAE 相同)、JNA不大于跳转(与JBE 相同);
  • 基于有符号操作数的比较跳转。比如JG大于跳转、JL小于跳转、JNLE不小于或等于跳转(与JG 相同)、JNGE不大于或等于跳转(与JL 相同)、JGE大于或等于跳转、JLE小于或等于跳转、JNL不小于跳转(与JGE 相同)、JNG不大于跳转(与JLE 相同)。

循环结构

  循环结构是顺序结构和分支结构的组合起来形成的更为复杂的程序结构,是指在程序中需要反复执行某个功能而设置的一种程序结构,可以看成是一个条件判断语句和一个向前无条件跳转语句的组合。
  C语言中提供四种循环,即goto循环、while循环、do…while循环和for循环。四种循环可以用来处理同一问题,一般情况下它们可以互相代替,但一般不提倡用goto循环,因为强制改变程序的顺序结构经常会给程序的运行带来不可预料的错误,一般我们主要使用while、do-while、for三种循环。

函数调用框架

  函数调用框架是以函数作为基本元素,并借助于堆叠起来的堆栈数据,所形成的一种更为复杂的程序结构。函数内部可以是顺序结构、分支结构和循环结构的组合。堆叠起来的堆栈数据用来记录函数调用框架结构的信息,是函数调用框架的灵魂。

继承和对象组合

  继承和对象组合都是以对象(类)作为软件基本元素构成的程序结构。对象是基于属性(变量或对象)和方法(函数)构建起来的更复杂的软件基本元素,显然它涵盖了前述包括函数调用框架在内所有程序结构,它是一个更高层、更复杂的抽象实体。基于对象(类)之间的继承关系所形成的两个对象各自独立、两个类紧密耦合的程序结构;对象组合则将一个对象作为另一个对象的属性,从而形成两个对象(类)之间有明确的依赖关系,下图可以清晰地对比继承和对象组合的特点。
在这里插入图片描述

软件中的一些特殊机制

  面向对象有三个关键的概念:继承(Inheritance)、对象组合(object composition)和多态(polymorphism),其中多态是一种比较特殊的机制,另外还有回调函数(callback)、闭包(closure)、异步调用和匿名函数也经常用到特殊机制。这几个特殊机制在一些设计模式中比较比较常用,在实际应用中它们又常常交叉综合出现,在理解上带来很多困扰。我们简单做一个介绍。

回调函数

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

多态

  多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。多态是实例化变量可以指向不同的实例对象,这样同一个实例化变量在不同的实例对象上下文环境中执行不同的代码表现出不同的行为状态,而通过实例化变量调用实例对象的方法的那一块代码却是完全相同的,这就顾名思义,同一段代码执行时却表现出不同的行为状态,因而叫多态。简单的说,可以理解为允许将不同的子类类型的对象动态赋值给父类类型的变量,通过父类的变量调用方法在执行时实际执行的可能是不同的子类对象方法,因而表现出不同的执行效果。

闭包

  闭包是变量作用域的一种特殊情形,一般用在将函数作为返回值时,该函数执行所需的上下文环境也作为返回的函数对象的一部分,这样该函数对象就是一个闭包。
  更严谨的定义是,函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在JavaScript中,每当函数被创建,就会在函数生成时生成闭包。

function makeFunc() {
var name = "Mozilla";
function displayName() {
alert(name);
}
return displayName;
}
var myFunc = makeFunc();
myFunc();

异步调用

  Promise对象可以将异步调用以同步调用的流程表达出来,避免了通过嵌套回调函数实现异步调用。ES6原生提供了Promise对象。所谓Promise对象,就是代表了未来某个将要发生的事件,通常是一个异步操作。Promise对象提供了一整套完整的接口,使得可以更加容易地控制异步调用。ES6的Promise对象是一个构造函数,用来生成Promise实例。下面是Promise对象的基本用法。

var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功*/){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) { // resolve(value)
// success
}, function(value) { // reject(error)
// failure
});

匿名函数

  lamda函数是函数式编程中的高阶函数,在我们常见的命令式编程语言中常常以匿名函数的形式出现,比如无参数的代码块{ code },或者箭头函数{ x => code },如下使用Promise对象实现的计时器就用到了箭头函数。

function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
timeout(100).then(() => {
console.log('done');
});

软件的内在特性

  布鲁克斯(Fred Brooks)在其经典著作《人月神话》一书中指出:“开发软件的根本任务是打造构成抽象软件的复杂概念结构,次要任务才是使用代码表达抽象的概念设计并映射成机器指令。”
  因此软件的内在特性更主要体现在复杂的概念结构上,而软件的基本特点是前所未有的复杂度和易变性,为了降低复杂度我们在不同层面大量采用抽象方法建立软件概念模型;为了应对易变性我们努力保持软件设计和实现上的完整性和一致性。

  • 前所未有的复杂度
  • 抽象思维vs. 逻辑思维
  • 唯一不变的就是变化本身
  • 难以达成的概念完整性和一致性

软件设计模式初步

设计模式涉及的基本概念

  在面向对象分析中我们涉及到了类、对象、属性以及类与类之间的关系,在此基础上我们进一步理解面向对象设计和实现所涉及的基本术语之间的关系
在这里插入图片描述

什么是设计模式?

  设计模式的本质是面向对象设计原则的实际运用总结出的经验模型。对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解的基础上才能准确理解设计模式。

设计模式的优点

正确使用设计模式具有以下优点。

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

设计模式要解决的问题

  先来看看设计模式要解决的问题。功能分解是处理复杂问题的一种自然的方法,但是需求总是在发生变化,功能分解不能帮助我们为未来可能的变化做准备,也不能帮助我们的代码优雅地演化。结果你想在代码中做一些改变,但又不敢这么做,因为你知道对一个地方代码的修改可能在另一个地方造成破坏。

包容变化

  用模块化来包容变化,使用模块化封装的方法,按照模块化追求的高内聚低耦合目标,借助于抽象思维对模块内部信息的隐藏并使用封装接口对外只暴露必要的可见信息,利用多态、闭包、lamda函数、回调函数等特殊的机制方法,将变化的部分和不变的部分进行适当隔离。这些都是设计模式的拿手好戏。此外在设计模式的基础上,可以更加方便地用增量、迭代和不断重构等开发过程来进一步应对总是变化的需求和软件本质上的模型不稳定特质。

设计模式的分类

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

  • 类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。比如模板方法模式等属于类模式。
  • 对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。由于组合关系或聚合关系比继承关系耦合度低,因此多数设计模式都是对象模式。
    根据设计模式可以完成的任务类型来划分的话,可以分为创建型模式、结构型模式和行为型模式3 种类型的设计模式:
  • 创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。比如单例模式、原型模式、建造者模式等属于创建型模式。
  • 结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,比如代理模式、适配器模式、桥接模式、装饰模式、外观模式、享元模式、组合模式等属于结构型模式。结构型模式分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,所以对象结构型模式比类结构型模式具有更大的灵活性。
  • 行为型模式:用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。比如模板方法模式、策略模式、命令模式、职责链模式、观察者模式等属于行为型模式。行为型模式分为类行为模式和对象行为模式,前者采用继承在类间分配行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,所以对象行为模式比类行为模式具有更大的灵活性。

常用的设计模式

  • 单例(Singleton)模式:某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,典型的应用如数据库实例。
  • 原型(Prototype)模式:将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例,原型模式的应用场景非常多,几乎所有通过复制的方式创建新实例的场景都有原型模式。
  • 建造者(Builder)模式:将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。主要应用于复杂对象中的各部分的建造顺序相对固定或者创建复杂对象的算法独立于各组成部分。
  • 代理(Proxy)模式:为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。代理模式是不要和陌生人说话原则的体现,典型的应用如外部接口本地化将外部的输入和输出封装成本地接口,有效降低模块与外部的耦合度。
    在这里插入图片描述

设计模式背后的设计原则

开闭原则(Open Closed Principle,OCP)

  软件应当对扩展开放,对修改关闭(Software entities should be open for extension,but closed for modification),这就是开闭原则的定义。

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

Liskov替换原则(Liskov Substitution Principle,LSP)

  继承必须确保超类所拥有的性质在子类中仍然成立。

  • Liskov替换原则主要阐述了继承用法的原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说儿子和父母要在DNA基因上一脉相承,尽管程序员是自己的代码的上帝,但也不能胡来,要做一个遵守自然规律的上帝。
  • Liskov替换原则告诉我们,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

依赖倒置原则(Dependence Inversion Principle,DIP)

  高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向接口编程,不要面向实现编程。

  • 由于在软件设计中,细节具有多变性,而抽象层则相对稳定,因此以抽象为基础搭建起来的架构要比以细节为基础搭建起来的架构要稳定得多。这里的抽象指的是接口或者抽象类,而细节是指具体的实现类。
  • 依赖倒置原则在模块化设计中降低模块之间的耦合度和加强模块的抽象封装提高模块的内聚度上具有普遍的指导意义,但是“依赖倒置”这个名称体现的是抽象层次结构上的依赖倒置,并不能直观展现它在模块化设计中的重要价值,我觉得应该给它起个更好的名字,您可以试试重新给它起个名字。

单一职责原则(Single Responsibility Principle,SRP)

  单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分

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

迪米特法则(Law of Demeter,LoD)

  只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

合成复用原则(Composite Reuse Principle,CRP)

  合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)。它要求在软件复用时,要尽量先使用组合或者聚合关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循Liskov替换原则。
通常类的复用分为继承复用和对象组合复用两种。

常见的软件架构举例

三层架构

  层次化架构是利用面向接口编程的原则将层次化的结构型设计模式作为软件的主体结构。比如三层架构是层次化架构中比较典型的代表,如下图所示我们以层次化架构为例来介绍:
在这里插入图片描述

MVC架构

  MVC即为Model-View-Controller(模型-视图-控制器),MVC是一种设计模式,以MVC设计模式为主体结构实现的基础代码框架一般称为MVC框架,如果MVC设计模式决定了整个软件的架构,不管是直接实现了MVC模式还是以某一种MVC框架为基础,只要软件的整体结构主要表现为MVC模式,我们就称为该软件的架构为MVC架构。

  • Model(模型)代表一个存取数据的对象及其数据模型。
  • View(视图)代表模型包含的数据的表达方式,一般表达为可视化的界面接口。
  • Controller(控制器)作用于模型和视图上,控制数据流向模型对象,并在数据变化时更新视图。控制器可以使视图与模型分离开解耦合。

MVC架构vs. 三层架构

  模型和视图有着业务层面的业务数据紧密耦合关系,控制器的核心工作就是业务逻辑处理,显然MVC架构和三层架构有着某种对应关系,但又不是层次架构的抽象接口依赖关系,因此为了体现它们的区别和联系,我们在MVC的结构示意图中将模型和视图上下垂直对齐表示它们内在的业务层次及业务数据的对应关系,而将控制器放在左侧表示控制器处于优先重要位置,放在模型和视图的中间位置是为了与三层架构对应与业务逻辑层处于相似的层次。

MVVM架构

  MVVM即Model-View-ViewModel,最早由微软提出来,借鉴了桌面应用程序的MVC模式的思想,是一种针对WPF、Silverlight、Windows Phone的设计模式,目前广泛应用于复杂的Javacript前端项目中。
Vue.js框架的MVVM架构:在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。以比较流行的Vue.js框架为例,MVVM架构示意图如下:
在这里插入图片描述
  MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

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

软件架构风格与描述方法

软件架构设计是一项非常有挑战性的工作:

  • 既要考虑满足数量众多的各种系统功能需求,
  • 也需要完成诸如系统的易用性、系统的可维护性等非功能性的设计目标,
  • 还要遵从各种行业标准和政策法规。
  • 不过并不是每一个项目我们都需要从头开始进行完全创新性的设计,更多的是通过研究借鉴优秀的设计方案,来逐步改进我们的设计。换句话说,大多数的设计工作都是通过复用(Reuse)相似项目的解决方案,或者采用一些优秀设计方案的方法,这让看起来非常有挑战性的软件架构设计工作变得有例可循。

构建软件架构模型的基本方法

软件架构风格与策略

管道-过滤器

  管道-过滤器风格的软件架构是面向数据流的软件体系结构,最典型的应用是编译系统。一个普通的编译系统包括词法分析器、语法分析器、语义分析与中间代码生成器、目标代码生成器等一系列对源代码进行处理的过程。就像如图:管道-过滤器风格示意图,对源代码处理的过滤器通过管道连接起来,实现了端到端的从源代码到编译目标的完整系统。
在这里插入图片描述

客户-服务

  • Client/Server(C/S)和Browser/Server(B/S)是我们常用的对软件的网络结构特点的表述方式,但它们背后蕴含着一种普遍存在的软件架构风格,即客户-服务模式的架构风格。
  • 客户-服务模式的架构风格是指客户代码通过请求和应答的方式访问或者调用服务代码。这里的请求和应答可以是函数调用和返回值,也可以是TCP Socket中的send和recv,还可以是HTTP协议中的GET请求和响应。
  • 在客户-服务模式中,客户是主动的,服务是被动的。客户知道它向哪个服务发出请求,而服务却不知道它正在为哪个客户提供服务,甚至不知道正在为多少客户提供服务。
  • 客户-服务模式的架构风格具有典型的模块化特征,降低了系统中客户和服务构件之间耦合度,提高了服务构件的可重用性。

P2P

  P2P(peer-to-peer)架构是客户-服务模式的一种特殊情形,P2P架构中每一个构件既是客户端又是服务端,即每一个构件都有一种接口,该接口不仅定义了构件提供的服务,同时也指定了向同类构件发送的服务请求。这样众多构件一起形成了一种对等的网络结构,如图:P2P网络结构示意图。
在这里插入图片描述
P2P架构典型的应用有文件共享网络、比特币网络等。

发布-订阅

  在发布-订阅架构中,有两类构件:发布者和订阅者。如果订阅者订阅了某一事件,则该事件一旦发生,发布者就会发布通知给该订阅者。观察者模式体现了发布-订阅架构的基本结构。
  在实际应用中往往会需要不同的订阅组,以及不同的发布者。由于订阅者数量庞大往往在消息推送时采用消息队列的方式延时推送。如图:包含消息队列的发布-订阅架构示意图。
在这里插入图片描述

CRUD

  CRUD 是创建(Create)、读取(Read)、更新(Update)和删除(Delete)四种数据库持久化信息的基本操作的助记符,表示对于存储的信息可以进行这四种持久化操作。CRUD也代表了一种围绕中心化管理系统关键数据的软件架构风格。一般常见的各类信息系统,比如ERP、CRM、医院信息化平台等都可以用CRUD架构来概括。
  如图:医院信息基础平台示意图中心位置为运营数据、临床数据、基础数据等,各种应用系统围绕了中心数据做CRUD持久化操作,即呈现出典型的CRUD架构风格。
在这里插入图片描述

层次化

  较为复杂的系统中的软件单元,仅仅从平面展开的角度进行模块化分解是不够的,还需要从垂直纵深的角度将软件单元按层次化组织,每一层为它的上一层提供服务,同时又作为下一层的客户。
在这里插入图片描述
  通信网络中的OSI(Open System Interconnection)参考模型是典型的层次化架构风格,如图:OSI参考模型示意图。在OSI参考模型中每一层都将相邻底层提供的通信服务抽象化,隐藏它的实现细节,同时为相邻的上一层提供服务。

软件架构的描述方法

  软件架构模型是通过一组关键视图来描述的,同一个软件架构,由于选取的视角(Perspective)和抽象层次不同可以得到不同的视图,这样一组关键视图搭配起来可以完整地描述一个逻辑自洽的软件架构模型。一般来说,我们常用的几种视图有分解视图、依赖视图、泛化视图、执行视图、实现视图、部署视图和工作任务分配视图。

分解视图Decomposition View

  分解是构建软件架构模型的关键步骤,分解视图也是描述软件架构模型的关键视图,一般分解视图呈现为较为明晰的分解结构(breakdown structure)特点。分解视图用软件模块勾划出系统结构,往往会通过不同抽象层级的软件模块形成层次化的结构。由于前述分解方法中已经明确呈现出了分解视图的特征,我们这里简要了解一下分解视图中常见的软件模块术语。

  • 子系统(Subsystem),一个系统可能有一些列子系统组成;
  • 包(Package),子系统又由包组成;
  • 类(Class),包又由类组成;
  • 组件(Component),一般用来表示一个运行时的单元;
  • 库(Library)是具有明确定义的接口的共享软件代码的集合,可以是代码库,也可以是由代码库编译打包后的静态库,还可以构建成动态链接库;
  • 软件模块(Module)用来指软件代码的结构化单元,模块化(modular)是在软件架构中各部分都被明确定义的接口所描述时使用,也就是可以明确无误地指定各部分的外部可见行为。
  • 软件单元(Software unit)是在不明确该部分的类型时使用。

依赖视图Dependencies View

  依赖视图展现了软件模块之间的依赖关系。比如一个软件模块A调用了另一个软件模块B,那么我们说软件模块A直接依赖软件模块B。如果一个软件模块依赖另一个软件模块产生的数据,那么这两个软件模块也具有一定的依赖关系。
  依赖视图在项目计划中有比较典型的应用。比如它能帮助我们找到没有依赖关系的软件模块或子系统,以便独立开发和测试,同时进一步根据依赖关系确定开发和测试软件模块的先后次序。
  依赖视图在项目的变更和维护中也很有价值。比如它能有效帮助我们理清一个软件模块的变更对其他软件模块带来影响范围。

泛化视图Generalization View

  泛化视图展现了软件模块之间的一般化或具体化的关系,典型的例子就是面向对象分析和设计方法中类之间的继承关系。值得注意的是,采用对象组合替代继承关系,并不会改变类之间的泛化特征。因此泛化是指软件模块之间的一般化或具体化的关系,不能局限于继承概念的应用。
  泛化视图有助于描述软件的抽象层次,从而便于软件的扩展和维护。比如通过对象组合或继承很容易形成新的软件模块与原有的软件架构兼容。

执行视图Execution View

  执行视图展示了系统运行时的时序结构特点,比如流程图、时序图等。执行视图中的每一个执行实体,一般称为组件(Component),都是不同于其他组件的执行实体。如果有相同或相似的执行实体那么就把它们合并成一个。
  执行实体可以最终分解到软件的基本元素和软件的基本结构,因而与软件代码具有比较直接的映射关系。在设计与实现过程中,我们一般将执行视图转换为伪代码之后,再进一步转换为实现代码。

实现视图Implementation View

  实现视图是描述软件架构与源文件之间的映射关系。比如软件架构的静态结构以包图或设计类图的方式来描述,但是这些包和类都是在哪些目录的哪些源文件中具体实现的呢?一般我们通过目录和源文件的命名来对应软件架构中的包、类等静态结构单元,这样典型的实现视图就可以由软件项目的源文件目录树来呈现。
  实现视图有助于码农在海量源代码文件中找到具体的某个软件单元的实现。实现视图与软件架构的静态结构之间映射关系越是对应的一致性高,越有利于软件的维护,因此实现视图是一种非常关键的架构视图。

部署视图Deployment View

  部署视图是将执行实体和计算机资源建立映射关系。这里的执行实体的粒度要与所部署的计算机资源相匹配,比如以进程作为执行实体那么对应的计算机资源就是主机,这时应该描述进程对应主机所组成的网络拓扑结构,这样可以清晰地呈现进程间的网络通信和部署环境的网络结构特点。当然也可以用细粒度的执行实体对应处理器、存储器等。
  部署视图有助于设计人员分析一个设计的质量属性,比如软件处理网络高并发的能力、软件对处理器的计算需求等。

工作任务分配视图Work-assignment View

  工作分配视图将系统分解成可独立完成的工作任务,以便分配给各项目团队和成员。工作分配视图有利于跟踪不同项目团队和成员的工作任务的进度,也有利于在个项目团队和成员之间合理地分配和调整项目资源,甚至在项目计划阶段工作分配视图对于进度规划、项目评估和经费预算都能起到有益的作用。
  每个视图都是从不同的角度对软件架构进行描述和建模,比如从功能的角度、从代码结构的角度、从运行时结构的角度、从目录文件的角度,或者从项目团队组织结构的角度。
  软件架构代表了软件系统的整体设计结构,它应该是所有这些视图的集合。但我们不会将不同角度的这些视图整合起来,因为不便于阅读和更新。不过我们会有意识地将不同角度的视图之间的映射关系和重叠部分了然于胸,从而深刻理解软件架构内在的一致性和完整性,这就是系统概念原型。

什么是高质量软件?

软件质量的三种视角

软件质量的含义

  • 生产商:产品符合标准规范
  • 消费者:产品适于使用且带来益处
  • 按照我们一般的常识理解,质量是指和其他竞争者相比产品或服务有更高的标准,也就是所谓,不怕不识货就怕货比货,质量是在比较中衡量的。
  • 按照字典的解释,质量是指较好的一类或优秀的等级。

IEEE的软件质量定义

  • IEEE将软件质量定义为,一个系统、组件或过程符合指定要求的程度,或者满足客户或用户期望的程度。
  • 软件质量是许多质量属性的综合体现,各种质量属性反映了软件质量的方方面面。人们通过改善软件的各种质量属性,从而提高软件的整体质量。

几种重要的软件质量属性

不同的视角下的质量

  • 从用户的角度看,高质量就是恰好满足或超出了用户的预期目标。
  • 从工业生产的角度看,高质量就是符合标准规范的程度。
  • 从产品的角度看,高质量意味着产品具有良好的产品内在特性。
  • 从市场价值的角度看,高质量意味着客户愿意为此付费的数量。

产品视角下的软件质量

  用户看到的产品质量和开发者看到的产品质量是不同的,我们将用户看到的产品质量称为外部质量,比如正确的功能、发生故障的数量等;开发者看到的产品质量称为内部质量。比如代码缺陷等。将外部质量和内部质量之间的依赖关系总结出了很多成熟的质量模型,比较常见的质量模型有Jim McCall 软件质量模型(1977 年)、Barry W. Boehm 软件质量模型(1978 年)、FURPS/FURPS+ 软件质量模型、R. Geoff Dromey 软件质量模型、ISO/IEC 9126 软件质量模型(1993 年)和ISO/IEC 25010 软件质量模型(2011 年)等,其中以Jim McCall 软件质量模型最具基础性和奠基性。

过程视角下的软件质量

特定类型的缺陷在哪里常常出现?
如何尽早发现缺陷?
如何构建容错机制健全质量保证体系?
如何组织安排过程活动可以改善软件质量?

过程改进模型

  过程质量不像产品质量强调结果性评价,过程质量更关注持续不断的过程改进,因此过程质量模型一般称为过程改进模型,常见的有CMM/CMMI、ISO 9000和Software Process Improvement and Capability dEtermination (SPICE)等。其中以CMM/CMMI最为著名。

  • CMM/CMMI的全称为能力成熟度模型(Capability Maturity Model)或能力成熟度模型集成(Capability Maturity Model Integration),CMM/CMMI是美国卡内基-梅隆大学研制的一种用于评价软件生产能力并帮助其改善软件质量的方法,也就是评估软件能力与成熟度的一套标准,它侧重于软件开发过程的管理及工程能力的提高与评估,是国际软件业的质量管理标准。CMMI共有5个级别,代表软件团队能力成熟度的5个等级,数字越大,成熟度越高,高成熟度等级表示有比较强的软件综合开发能力。
    在这里插入图片描述

价值视角下的软件质量

  • 软件的价值与软件的技术同样重要,在某种程度上决定着软件技术因素的取舍。价值视角下的软件质量表现为软件产品投入运营后的效果,一般在商业环境下软件投入使用后的效果是通过投资回报ROI(return on investment)来量化评价的。投资回报在不同情形下可能被表述为降低成本、预期收益或提高生产力等。
  • 对于一个已经投入运营的软件项目来讲,我们可以通过前期投入、运营成本、运营收入、毛利率、税后纯利润以及它们的增长率等来衡量和预测项目的价值,这都有比较成熟的估值方法来衡量软件项目的价值。

几种重要的软件质量属性

易于修改维护(Modifiability)

  包容变化是软件设计质量的关键需求。如何才能包容变化?理想的情况是设计和代码能够应对变化而本身不需要修改和维护,但现实是软件难以避免需要修改和维护,那么设计和代码易于修改和维护就能有效地包容变化。那什么样的设计和代码易于修改和维护的呢?那就回到了我们从代码编写、需求分析和系统设计等贯穿始终的一个主题——高内聚低耦合的模块化设计。

  • 高内聚低耦合:软件模块的高内聚可以将变化的需求局限于单个软件模块内部,从而易于修改维护;软件模块的高内聚可以将变化的需求局限于单个软件模块内部,从而易于修改维护;
  • 高内聚和通用性:需求变更(变化)直接影响的软件模块会带来模块职责(功能内聚)发生改变,因此直接影响的软件模块需要根据需要进行修改;需求变更(变化)间接影响的软件模块不会造成模块职责(功能内聚)的改变,因此间接影响的软件模块仅需要做适应性的修改,一般是软件模块接口及实现的修订。最小化受变化直接影响的软件模块数量的策略,需要在模块化设计过程中预测预期的需求变更(变化),从而确定最有可能更改的设计决策,并将每个决策封装在独立软件模块中。在模块化设计中保持软件模块的高内聚,使得变化带来的修改和维护仅限于分配相应职责的少数软件模块。软件模块越通用就越有可能适应变化,即通过修改软件模块接口的输入而无需修改软件模块本身。
  • 低耦合和接口:最小化受变化间接影响的软件模块数量的策略侧重于减少依赖关系,即降低耦合度会减小一个软件模块的更改所波及到的其他软件模块的数量。如果软件模块仅通过接口与其他软件模块交互,则更改的影响不会超出一个软件模块的边界,除非软件模块之间的接口也发生了改变。即便需要接口也发生改变,我们可以为修改的软件模块增加新的接口,而不去修改软件模块任何现有接口,从而减小所波及到的其他软件模块的数量。

良好的性能表现(Performance)

良好的性能表现主要体现在系统的速度和容量上。比较典型的几个性能指标有:

  • 响应时间(Response time),就是软件响应请求的速度有多快。在响应时间的分析中,在底层受到操作系统根据优先级、FIFO或时间片的进程调度策略的影响;在上层受到网络延时、业务处理、数据库访问等应用处理过程的影响。为了追求响应时间性能的提高,需要根据软件类型的不同,比如实时系统、Web服务等,在不同的层次上分析其中影响响应时间的关键因素并加以改进。
  • 吞吐量(Throughput),是单位时间内能处理的请求数量
  • 负载(Load),用系统能同时支持的用户数量来衡量负载能力,一般是在响应时间和吞吐量受到影响之前的负载能力。
    提高性能的策略:
  • 提高资源的利用率
  • 更有效地管理资源分配
  • 先到先得:按收到请求的顺序处理
  • 显式优先级:按其分配优先级的顺序处理请求
  • 最早截止时间优先:按收到请求的截止时间的顺序处理请求
  • 减少对资源的需求

安全性(Security)

与安全性特别相关的两个关键软件架构特征:系统的免疫力(Immunity)和系统的自我恢复能力(Resilience)。

  • 系统的免疫力(Immunity)是挫败未遂攻击的能力。一般在软件设计中要确保所有安全需求都包含在设计中得到考虑,并尽可能减少可利用的安全漏洞。
  • 系统的自我恢复能力(Resilience)是指可以快速轻松地从攻击中恢复的能力。在软件设计中要包含异常检测机制,一旦发现攻击导致的异常能启动自我恢复程序。

可靠性(Reliability)

  如果软件系统在假定的条件下能正确执行所要求的功能,则软件系统是可靠的(reliable)。可靠性与软件内部是否有缺陷(Fault)密切相关,而软件内部的缺陷产生的前因后果大致如下图所示:
在这里插入图片描述
  与故障(Failure)相比,缺陷是人为错误(Human Error)的结果,而故障是可观察到的偏离要求的行为表现,是缺陷在某种条件下造成的系统失效。
  可以通过防止或容忍缺陷的存在使软件更加可靠,一般通过缺陷检测和故障恢复两类方法来提高软件的可靠性。缺陷检测主要包括被动缺陷检测、主动缺陷检测和异常处理等,故障恢复的方法则是根据系统设计中创建的应急方案执行撤销、回退、备份、服务降级、修复或报告等。

  • 被动缺陷检测,等待执行期间发生故障。
  • 主动缺陷检测:定期检查症状或尝试预测故障何时发生。

健壮性(Robustness)

  如果软件能够很好地适应环境或具有故障恢复等机制,则软件是健壮的(robust)。健壮性是比可靠性更高的软件质量要求。可靠的软件只说明它在假定的条件下能够正确地执行,也就是输入和环境是符合要求的,而健壮的软件在不正确的输入或异常环境条件下还能正确执行。也就是说,可靠性和软件内部是否有缺陷有关,而健壮性与软件容忍错误或外部环境异常时的表现有关。
  在软件设计和编码过程中提高健壮性的一个重要策略是“防人之心不可无,害人之心不可有”。“防人之心不可无”要求我们对所有的外部输入、返回值和其他上下文环境条件进行检测,只允许软件或软件模块在正确的输入、返回值和特定的环境条件下才会继续执行;“害人之心不可有”则要求我们在调用访问外部软件或软件模块时,也要确保自己提供的输入和其他上下文环境条件是符合规格要求的。
  这种相互怀疑的策略,通过假定外部软件有缺陷或者有恶意攻击的可能,可以有效提高软件整体的健壮性。
  异常处理和故障恢复的基本方法在可靠性和健壮性上是通用的,都是根据系统设计中创建的应急方案执行撤销、回退、备份、服务降级、修复或报告等。

易用性(Usability)

  易用性反映了用户操作软件的容易程度。易用性更多地体现在软件的人性化交互设计中,其中做到交互逻辑与业务逻辑自然统一,符合用户操作习惯和思维习惯非常关键。除此之外,软件架构上需要支持易用性设计,比如支持多种语言扩展、不同操作方式的自定义、撤销/重做、自动保存和错误恢复等。

商业目标(Business goals)

  商业目标是客户希望软件系统表现出的某些质量属性,其中最普遍的目标是在软件质量有保障的情况下将开发成本降到最低、完成时间尽量提前,简而言之就是又好又快又便宜。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值