这是一篇总结,总结了考试可能涉及的知识点,主要是一些八股
代码中的软件工程
代码风格的原则:简明、易读、无二义性
三重境界:规范整洁、逻辑清晰、优雅
编写高质量代码的基本方法
通过控制结构简化代码
通过数据结构简化代码
一定要有错误处理
性能优先策略背后隐藏的代价:将更多精力放在提高工作效率、质量保证、代码的可读性、可扩展性等方面
拒绝修修补补要不断重构代码
模块化软件设计
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。
关注点分离:分而治之
使用耦合度(Coupling)和内聚度(Cohesion)来衡量软件模块化的程度
-
耦合度是指软件模块之间的依赖程度,一般可以分为紧密耦合(Tightly Coupled)、松散耦合(Loosely Coupled)和无耦合(Uncoupled)。
-
内聚度是指一个软件模块内部各种元素之间互相依赖的紧密程度。
-
理想的内聚是功能内聚,也就是一个软件模块只做一件事,只完成一个主要功能点或者一个软件特性(Feather)。
基本写法:将数据结构和它的操作与菜单业务处理进行分离处理
软件设计中的一些基本方法
KISS(Keep It Simple & Stupid)原则
-
一行代码只做一件事
-
一个块代码只做一件事
-
一个函数只做一件事
-
一个软件模块只做一件事
使用本地化外部接口来提高代码的适应能力
先写伪代码的代码结构更好一些
因为伪代码不需要考虑异常处理等一些编程细节,最大限度地保留了设计上的框架结构,使得设计上的逻辑结构在伪代码上体现出来。从伪代码到实现代码的过程就是反复重构的过程,这样避免了顺序翻译转换所造成的结构性损失
可重用软件设计
消费者重用是指软件开发者在项目中重用已有的一些软件模块代码,以加快项目工作进度。
生产者重用是指在延长生产者责任制度下,生产者或者代表生产者的组织通过修理、翻新和再利用等方式,将废弃的产品或零件重新投入市场的活动。
接口
接口就是互相联系的双方共同遵守的一种协议规范,在我们软件系统内部一般的接口方式是通过定义一组API函数来约定软件模块之间的沟通方式。
在面向过程的编程中,接口一般定义了数据结构及操作这些数据结构的函数;而在面向对象的编程中,接口是对象对外开放(public)的一组属性和方法的集合。
接口规格包含五个基本要素:
-
接口的目的;
-
接口使用前所需要满足的条件,一般称为前置条件或假定条件;
-
使用接口的双方遵守的协议规范;
-
接口使用之后的效果,一般称为后置条件;
-
接口所隐含的质量属性。
微服务
-
由一系列独立的微服务共同组成软件系统的一种架构模式
-
每个微服务可以有一个自己独立的运行环境和软件堆栈
-
系统中的各微服务是分布式管理的,各微服务之间非常强调隔离性,互相之间无耦合或者极为松散的耦合
传统单体集中式架构是适应大型机、小型机单体服务器环境的软件架构模式
微服务架构则是为了适应PC服务器的大规模集群及基于虚拟化技术和分布式计算的云计算环境的架构模式。
RESTful API
-
GET用来获取资源;
-
POST用来新建资源(也可以用于更新资源);
-
PUT用来更新资源;
-
DELETE用来删除资源。
耦合
公共耦合
-
当软件模块之间共享数据区或变量名的软件模块之间即是公共耦合,显然两个软件模块之间的接口定义不是通过显式的调用方式,而是隐式的共享了共享了数据区或变量名。
数据耦合
-
在软件模块之间仅通过显式的调用传递基本数据类型即为数据耦合。
标记耦合
-
在软件模块之间仅通过显式的调用传递复杂的数据结构(结构化数据)即为标记耦合,这时数据的结构成为调用双方软件模块隐含的规格约定,因此耦合度要比数据耦合高。但相比公共耦合没有经过显式的调用传递数据的方式耦合度要低。
通用接口定义的基本方法
-
参数化上下文:通过参数来传递上下文的信息,而不是隐含依赖上下文环境
-
移除前置条件:3个数求和,3个数就是前置条件,移除前置条件变为任意数求和(输入:数组、len(数组))
-
简化后置条件:如果编程语言支持直接获得数组的个数,或者通过分析数组数据智能得出数组的个数,我们可以进一步移除前置条件len与numbers数组长度之间的约束关系
可重入函数与线程安全
指令乱序
程序的代码执行顺序有可能被编译器或CPU根据某种策略打乱指令执行顺序,目的是提升程序的执行性能,让程序的执行尽可能并行。
指令乱序问题需要分为三个层次
-
第1层是多线程编程中的业务逻辑层面的函数可重入性和线程安全问题
-
第2层是编译器编译优化造成的指令乱序
-
第3层是CPU乱序执行指令的问题。
线程安全
线程(thread)是操作系统能够进行运算调度的最小单位
如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的
可重入函数
可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误。
可重入函数可以在任意时刻被中断,稍后再继续运行,不会丢失数据
可重入函数要么使用局部变量,要么在使用全局变量时保护自己的数据。
可重入函数的基本要求
-
不为连续的调用持有静态数据;
-
不返回指向静态数据的指针;
-
所有数据都由函数的调用者提供;
-
使用局部变量,或者通过制作全局数据的局部变量拷贝来保护全局数据;
-
使用静态数据或全局变量时做周密的并行时序分析,通过临界区互斥避免临界区冲突;
-
绝不调用任何不可重入函数。
可重入的函数不一定是线程安全的
不可重入的函数一定不是线程安全的
为了提高流水线的运行效率,CPU会对无依赖的前后指令做适当的乱序和调整,对控制依赖的指令做分支预测,这些都会导致指令乱序执行。
CPU只要将结果顺序地提交到ISA寄存器,就可以保证顺序一致性(Sequential consistency)。
编译器指令乱序
编译器进行指令重排优化只能保证是单线程安全
编译器屏障
主要用途就是告诉编译器不要优化重排指令顺序
软件科学
软件的基本构成元素
-
对象(Object):一个对象作为某个类的实例,是属性和方法的集合
-
函数和变量/常量
-
指令和操作数:由 CPU 加载和执行的软件基本单元
-
0和1是什么?指令是由0和1构成的特定结构的信息,0和1就是软件的基本信息元素
软件的基本结构
-
顺序结构
-
分支结构 if-else, switch, goto
-
循环结构 while, do-while, for
-
函数调用框架:函数调用框架是以函数作为基本元素,并借助于堆叠起来的堆栈数据,所形成的一种更为复杂的程序结构
-
继承和对象组合
-
-
基于对象(类)之间的继承关系所形成的两个对象各自独立、两个类紧密耦合的程序结构
-
对象组合则将一个对象作为另一个对象的属性,从而形成两个对象(类)之间有明确的依赖关系
-
继承可以重用代码,但是破坏了代码的封装特性,增加了父类与子类之间的代码模块耦合,因此我们需要避免使用继承,在大多数情况下,应该使用对象组合替代继承来实现相同的目标
软件中的一些特殊机制
回调函数
回调函数是一个面向过程的概念,是代码执行过程的一种特殊流程。回调函数就是一个通过函数指针调用的函数。把函数的指针(地址)作为参数传递给另一个函数,当这个指针调用其所指向的函数时,就称这是回调函数。
多态
在面向对象语言中,接口的多种不同的实现方式即为多态。
可以理解为允许将不同的子类类型的对象动态赋值给父类类型的变量,通过父类的变量调用方法在执行时实际执行的可能是不同的子类对象方法,因而表现出不同的执行效果。
闭包
闭包是变量作用域的一种特殊情形,一般用在将函数作为返回值时,该函数执行所需的上下文环境也作为返回的函数对象的一部分,这样该函数对象就是一个闭包。
异步调用
所谓Promise对象,就是代表了未来某个将要发生的事件,通常是一个异步操作。Promise对象提供了一整套完整的接口,使得可以更加容易地控制异步调用。
Promise对象可以将异步调用以同步调用的流程表达出来,避免了通过嵌套回调函数实现异步调用
匿名函数
lamda函数是函数式编程中的高阶函数,在我们常见的命令式编程语言中常常以匿名函数的形式出现
软件的内在特性
前所未有的复杂度
软件系统的功能扩展和维护必须是不同软件部件的添加,因而软件系统的复杂度是非常陡峭的非线性增长
抽象思维 vs. 逻辑思维
人们试图将抽象对象作为软件的基本元素来构建复杂软件的逻辑模型,但没有银弹
复杂性是软件的本质属性
唯一不变的就是变化本身
Lehman将系统分成三种类型:S系统、P系统和E系统。E系统很好地解释了软件易变性的本质原因
人类为了探索大自然的奥秘而进行的科研活动大多属于S系统或P系统,常见的绝大多数软件都属于E系统
软件是嵌入到现实业务环境中的,现实业务环境不断变化,我们对业务的理解也在不断深入,我们对业务的抽象需要调整的更加合理,需求规格、软件系统及其管理的数据都在不断变化
难以达成的概念完整性和一致性
软件的复杂性和软件易变性这两个本质属性结合起来,使得我们在抽象的基础上建立逻辑模型的努力,永远达不到终点,难以达成软件概念上的完整性和一致性
因为当我们付出极大的努力为复杂的软件建立起概念模型时,与此同时需求或环境已经发生了些许变化,或者发现建立概念模型的抽象有些许偏差
软件设计模式初步
设计模式涉及的基本概念
封装的威力集中体现在对象组合对继承的替代作用上。
设计模式的本质是面向对象设计原则的实际运用总结出的经验模型。
设计模式的优点
-
可以提高程序员的思维能力、编程能力和设计能力。
-
使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
-
使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。
设计模式要解决的问题
功能分解是处理复杂问题的一种自然的方法,但是需求总是在发生变化,功能分解不能帮助我们为未来可能的变化做准备,也不能帮助我们的代码优雅地演化
用模块化来包容变化,使用模块化封装的方法,按照模块化追求的高内聚低耦合目标,借助于抽象思维对模块内部信息的隐藏并使用封装接口对外只暴露必要的可见信息,利用多态、闭包、lamda函数、回调函数等特殊的机制方法,将变化的部分和不变的部分进行适当隔离
此外在设计模式的基础上,可以更加方便地用增量、迭代和不断重构等开发过程来进一步应对总是变化的需求和软件本质上的模型不稳定特质
设计模式一般由四个部分组成
-
该设计模式的名称;
-
该设计模式的目的,即该设计模式要解决什么样的问题;
-
该设计模式的解决方案;
-
该设计模式的解决方案有哪些约束和限制条件。
设计模式的分类
根据模式是主要用于类上还是主要用于对象上来划分的话
-
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。比如模板方法模式等属于类模式。
-
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。由于组合关系或聚合关系比继承关系耦合度低,因此多数设计模式都是对象模式。
根据设计模式可以完成的任务类型来划分的话
-
创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。比如单例模式、原型模式、建造者模式等属于创建型模式。
-
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构,比如代理模式、适配器模式、桥接模式、装饰模式、外观模式、享元模式、组合模式等属于结构型模式。结构型模式分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。由于组合关系或聚合关系比继承关系耦合度低,所以对象结构型模式比类结构型模式具有更大的灵活性。
-
行为型模式:用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。比如模板方法模式、策略模式、命令模式、职责链模式、观察者模式等属于行为型模式。行为型模式分为类行为模式和对象行为模式,前者采用继承在类间分配行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,所以对象行为模式比类行为模式具有更大的灵活性。
常用的设计模式
太多了,挑了几个
-
职责链(Chain of Responsibility)模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。通过这种方式将多个请求处理者串联为一个链表,去除请求发送者与它们之间的耦合。
-
中介者(Mediator)模式:定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
设计模式背后的设计原则
开闭原则
软件应当对扩展开放,对修改关闭 遵守开闭原则使软件拥有一定的适应性和灵活性的同时具备稳定性和延续性 开闭原则在基本需求稳定且被充分理解的前提下才具有一定的价值
Liskov替换原则
继承必须确保超类所拥有的性质在子类中仍然成立 Liskov替换原则主要阐述了继承用法的原则,通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能 Liskov替换原则如今看来其价值也大大折扣,因为为了降低耦合度我们往往使用对象组合来替代继承关系
依赖倒置原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象 依赖倒置原则在模块化设计中降低模块之间的耦合度和加强模块的抽象封装提高模块的内聚度上具有普遍的指导意义
单一职责原则
单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分 单一职责原则的核心就是控制类的粒度大小、提高其内聚度 如果遵循单一职责原则可以降低类的复杂度,因为一个类只负责一项职责,其逻辑肯定要比负责多项职责简单得多;同时可以提高类的内聚度,符合模块化设计的高内聚低耦合的设计原则
迪米特法则
如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。 其目的是降低类之间的耦合度,提高模块的相对独立性
合成复用原则
它要求在软件复用时,要尽量先使用组合或者聚合关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循Liskov替换原则。
复习
高质量需求的特点
Making Requirements Testable(需求可测试) 帮助使需求可测试的三种方法
-
为每个副词和形容词指定定量描述
-
将代词替换为实体的具体名称
-
确保每个名词都定义在需求文档中的恰好一个地方
Resolving Conflicts(解决冲突) 不同的利益相关者有不同的要求 需要对需求进行优先排序 优先级可能会将需求分为三类
-
essential:绝对必须满足
-
esirable:非常可取,但没有必要
-
optional:可能,但可以消除
Characteristics of Requirements(需求的特点)
-
Correct 正确性
-
Consistent 一致性
-
Unambigious 无二义性
-
Complete 完整性
-
Feasible 可行性
-
Relevant 无与主要目标不相关的需求
-
Testable 可测试
-
Traceable 可追踪
有哪些需求类型
-
功能需求:根据所需的活动描述所需的行为
-
质量需求或非功能需求:描述软件必须具备的一些质量特性
-
设计约束: 设计决策,例如选择平台或接口组件
-
过程约束: 对可用于构建系统的技术或资源的限制
需求分析的两类基本方法
-
原型化方法(Prototyping) 原型化方法可以很好地整理出用户接口方式(UI,User Interface),比如界面布局和交互操作过程。
-
建模的方法 (Modeling) 建模的方法可以快速给出有关事件发生顺序或活动同步约束的问题,能够在逻辑上形成模型来整顿繁杂的需求细节。
用例满足的四个必要条件
-
必要条件一 :它是不是一个业务过程?
-
必要条件二:它是不是由某个参与者触发开始?
-
必要条件三:它是不是显式地或隐式地终止于某个参与者?
-
必要条件四:它是不是为某个参与者完成了有用的业务工作?
统一过程的核心要义是什么
统一过程的核心要义是用例驱动(Use case driven)、以架构为中心(Architecturecentric)、增量且迭代(Incremental and Iterative)的过程。
模块化的基本原理
模块化(Modularity)是在软件系统设计时保持系统内各部分相对独立,以便每一个部分可以被独立地进行设计和开发。这个做法背后的基本原理是关注点的分离。 关注点分离的思想背后的根源是由于人脑处理复杂问题时容易出错,把复杂问题分解成一个个简单问题,从而减少出错的情形。
本地化外部接口是什么含义
本地化外部接口是指使用代理模式来降低模块与外部的耦合度。也就是说,客户端通过代理间接地访问目标对象,而不是直接调用目标对象的接口。这样可以保护目标对象的内部实现,也可以增加或修改一些功能。
接口的5个基本要素
-
接口的目的;
-
接口使用前所需要满足的条件,一般称为前置条件或假定条件;
-
使用接口的双方遵守的协议规范;
-
接口使用之后的效果,一般称为后置条件;
-
接口所隐含的质量属性。
通用接口定义的基本方法
-
参数化上下文:通过参数来传递上下文的信息,而不是隐含依赖上下文环境
-
移除前置条件:3个数求和,3个数就是前置条件,移除前置条件(3个数求和的约束)变为任意数求和(输入:数组、len(数组))
-
简化后置条件:如果编程语言支持直接获得数组的个数,或者通过分析数组数据智能得出数组的个数,我们可以进一步移除前置条件len与numbers数组长度之间的约束关系
依赖倒置原则的含义及其应用价值
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象 依赖倒置原则在模块化设计中降低模块之间的耦合度和加强模块的抽象封装提高模块的内聚度上具有普遍的指导意义
MVC架构为什么更灵活以及MVVM架构为什么更智能
MVC 架构更灵活是因为它可以适应不同类型和规模的应用程序,只需要简单地定义好模型、视图和控制器之间的职责和关系。MVVM 架构更智能是因为它可以实现高度解耦和自动同步的用户界面交互,只需要定义好数据绑定和命令绑定即可。
执行视图、依赖视图、分解视图
依赖视图展现了软件模块之间的依赖关系。 依赖视图在项目计划中有比较典型的应用。比如它能帮助我们找到没有依赖关系的软件模块或子系统,以便独立开发和测试,同时进一步根据依赖关系确定开发和测试软件模块的先后次序。依赖视图在项目的变更和维护中也很有价值。比如它能有效帮助我们理清一个软件模块的变更对其他软件模块带来影响范围。
执行视图展示了系统运行时的时序结构特点,比如流程图、时序图等。 执行视图中的每一个执行实体,一般称为组件(Component),都是不同于其他组件的执行实体。 执行实体可以最终分解到软件的基本元素和软件的基本结构,因而与软件代码具有比较直接的映射关系
分解视图用软件模块勾划出系统结构,往往会通过不同抽象层级的软件模块形成层次化的结构。一般分解视图呈现为较为明晰的分解结构特点。
没有银弹的含义
在10年内无法找到解决软件危机的杀手锏(银弹) Brooks认为软件工程专家们所找到的各种方法都是舍本逐末,它们解决不了软件中的根本困难,即软件概念结构的复杂性,无法达成软件概念的完整性和一致性,自然无法从根本上解决软件危机带来的困境。
简述v模型
V模型也是在瀑布模型基础上发展出来的,我们发现单元测试、集成测试和系统测试是为了在不同层面验证设计,而交付测试则是确认需求是否得到满足。也就是瀑布模型中前后两端的过程活动具有内在的紧密联系,如果将模块化设计的思想拿到软件开发过程活动的组织中来,可以发现通过将瀑布模型前后两端的过程活动结合起来,可以提高过程活动的内聚度,从而改善软件开发效率。这就是V模型。 V模型中开始一个特定过程活动和评估该特定过程的过程活动成对出现,从而便于软件开发过程的组织和管理。
简述敏捷宣言的核心思想
-
个体和互动 高于 流程和工具
-
工作的软件 高于 详尽的文档
-
客户合作 高于 合同谈判
-
响应变化 高于 遵循计划
职责链(Chain of Responsib4ility)模式
为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。通过这种方式将多个请求处理者串联为一个链表,去除请求发送者与它们之间的耦合。
观察者(Observer)模式
指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为,这样所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式。
学号:156
参考资料《代码中的软件工程》代码中的软件工程: 《代码中的软件工程》一书的配套ppt和源代码