1. 可维护性的指标
软件维护的类型:纠错性维护(25%)、适应性维护(21%)、完善性维护(50%)、预防性维护(4%)
可维护性(Maintainability)、可扩展性(Extensibility)、灵活性(Flexibility)、可适应性(Adaptability)、可管理性(Manageability)、支持性(Supportability)这些指的都是可维护性。
评判可维护性的一些方面:
设计结构足够简单;
模块之间松散耦合;
模块内部高度聚合;
不要使用了非常深的继承树,尽量使用delegation替代继承;
代码的圈复杂度不能太高;
不存在重复代码
2. 模块化设计原则
目的:高内聚低耦合;分离关注点 (通过delegation等机制分离功能);信息隐藏 (避免表示泄露、静态工厂方法等等)
评估模块化的五个标准:
可分解性 (Decomposability):让复杂的功能分解成一个个ADT完成
可组合性 (Composability):让一个个ADT组合完成复杂的功能
可理解性 (Understandability):OOP是面向世界上存在的事物编程,所以容易被理解
可持续性 (Continuity):发生变化时使得受影响范围最小
出现异常之后的保护 (Protection):出现异常后使得受影响范围最小
模块化设计的五个原则:
Direct Mapping (直接映射)
Few Interfaces (尽可能少的接口)
Small Interfaces (尽可能小的接口)
Explicit Interfaces (显式接口)
Information Hiding (信息隐藏)
高内聚低耦合
高内聚:模块内部的功能之间的联系要紧密,无关的功能之间要分离成不同的模块
低耦合:模块之间的关系要越松散越好
3. SOLID设计原则
SOLID原则:
(SRP) The Single Responsibility Principle --------- 单一责任原则
(OCP) The Open-Closed Principle ------------------- 开放-封闭原则
(LSP) The Liskov Substitution PrincipleLiskov ---- 替换原则
(DIP) The Dependency Inversion Principle -------- 依赖转置原则
(ISP) The Interface Segregation Principle ---------- 接口隔离原则
单一责任原则(SRP)
一个类,一个责任
尽可能地将功能分割,以达到不应该有多于一个原因让你的ADT发生变化的目的,否则就要拆分开。如果多个责任(功能)放在一个类中就有可能在未来某一个功能发生变化的时候影响到整个类,从而影响到整个程序的很大一部分实现(需要全部重新编译打包部署)。
开放-封闭原则(OCP)
对扩展性的开放,对修改的封闭
在未来对功能进行修改或者扩展的时候,要遵循尽量不去改动已有的代码而是扩展出新的类然后调用新的类的原则。
典型的违反OCP的例子是大量的使用 if-else / switch-case 语句,这给维护造成了极大的麻烦。所以应对的方法是让客户端传入接口的不同子类型,而在ADT中只需要统一的调用接口中共有的方法即可。
替换原则(LSP)
子类型必须能够替换其基类型
代价是失去了子类型扩展出的功能。
依赖转置原则(DIP)
具体的模块应该依赖于抽象的模块,但抽象的模块不应依赖于具体的模块
使用接口隔离应用层和实现层,client面向接口编程。
接口隔离原则(ISP)
大接口分解为多个小的接口
客户端不应依赖于它们不需要的方法,不同的接口向不同的客户端提供服务,客户端只访问自己所需要的接口
5.2 Design Patterns for Maintainability
面向可维护性的设计模式
1. Factory Method 工厂方法
解决的问题:当client不知道要创建哪个具体类的实例,或者不想在client代码中指明要具体创建的实例时,用工厂方法。
定义一个用于创建对象的接口,让其子类来决定实例化哪一个类,从而使一个类的实例化延迟到其子类。
当然也可以通过直接定义静态工厂方法来创建子类实例。
2. Abstract Factory 抽象工厂
解决的问题:当client需要的是多个具有固定搭配的类的组合,如A和B两个系列的类,A1搭配B1,A2搭配B2,于是,就要用抽象工厂对其做一次封装。
定义一个用于创建对象的接口,让其子类来决定实例化哪一组类,从而使类与类之间的搭配对于client来说固定了。
本质上,Abstract Factory是把多类产品的factory method组合在一起
3. Proxy 代理模式
解决的问题:某个对象比较“敏感”/“私密”/“贵重”,不希望被client直接访问到,故设置proxy,在二者之间建立防火墙。
创建一个代理类,它接受client的功能请求,然后把功能请求转发 (delegate) 给实现类,类似于Adapter模式。
Observer 观察者模式
解决的问题:“粉丝”对“偶像”感兴趣,希望随时得知偶像的一举一动。
粉丝到偶像那里注册,偶像一旦有新闻发生,就推送给已注册的粉丝(回调callback粉丝的特定功能)。这是一个双向delegate的关系
. Visitor
解决的问题:对特定类型的object的特定操作(visit),在运行时将二者动态绑定到一起,该操作可以灵活更改,无需更改被visit的类。
为ADT预留一个将来可扩展功能的“接入点”,外部实现的功能代码可以在不改变ADT本身的情况下通过delegation接入ADT
本质上是将数据和作用于数据上的某种/些特定操作分离开来。
6. 共性和差异
Proxy vs. Adaptor
Adapter:目的是消除不兼容,目的是B以客户端期望的统一的方式与A建立起联系。
Proxy:目的是隔离对复杂对象的访问,降低难度/代价,定位在“访问/使用行为”
Visitor vs. Iterator
Iterator:目的是以遍历的方式访问集合数据而无需暴露其内部表示,将“遍历”这项功能delegate到外部的iterator对象。
Visitor: 在特定ADT上执行某种特定操作,但该操作不在ADT内部实现,而是delegate到独立的visitor对象,客户端可灵活扩展/改变visitor的操作算法,而不影响ADT
Strategy vs. visitor
同:二者都是通过delegation建立两个对象的动态联系
Visitor:强调是的外部定义某种对ADT的操作,该操作于ADT自身关系不大(只是访问ADT),故ADT内部只需要开放accept(visitor)即可,client通过它设定visitor操作并在外部调用。
visitor是站在外部client的角度,灵活增加对ADT的各种不同操作(哪怕ADT没实现该操作)。
Strategy:强调是对ADT内部某些要实现的功能的相应算法的灵活替换。这些算法是ADT功能的重要组成部分,只不过是delegate到外部strategy类而已。
strategy是站在内部ADT的角度,灵活变化对其内部功能的不同配置。
设计模式的共性样式1
只使用“继承”,不使用“delegation”
核心思路:OCP/DIP
依赖反转,客户端只依赖“抽象”,不能依赖于“具体”
发生变化时最好是“扩展”而不是“修改”
面向可维护性的构造技术
1. 基于状态的构造
状态模式 State Pattern
核心思想:将程序看作是一个有限状态自动机,侧重于对“状态”及“状态转换”的抽象和编程
不要使用if-else结构在ADT的内部实现状态的转换,而是使用delegation,将状态转换的行为委派到独立的state对象去完成
备忘录模式 Memento Pattern
记住对象的历史状态,以便于“回滚”,恢复到之前的某个状态。设计如下图:
Originator—是需要备忘的类------------------------------------当前的事
Caretaker----添加originator的备忘记录和恢复--------------记事本
Memento-------备忘录,记录originator对象的历史状态-----以前的事
语法成分
可以把一句话转换成为一棵语法解析树,树上的节点分为终止节点(叶节点)和产生式节点(非终止节点)。终止节点无法再往下扩展,产生式节点既可以扩展出终止节点也可以扩展出非终止节。产生式节点中有一个特殊的节点根节点。
操作符
基本操作符:可以使用这些操作符完成所有的语法,只不过繁琐一些
连接 (’ '):x ::= y z ------- x matches y followed by z
重复 (’*’):x ::= y* ------- x matches zero or more y
选择 (’|’):x ::= y | z ---- x matches either y or z
其他操作符:
可选 (’?’):x ::= y? --------------------------- x is a y or is the empty string
至少一次出现 (’+’):x ::= y+ -------------- x is one or more y
集合 (’[…]’):x ::= [a-c] 或 x ::= [abc] — x is a or b or c
取反 (’[^…]’):x ::= [^a-c] ------------------ x does not include from a to c
递归定义
例如解析这个URL:http://didit.csail.mit.edu:4949/ 可以用如下的方式实现:
url ::= 'http://' hostname (':' port)? '/'
hostname ::= word '.' hostname | word '.' word
port ::= [0-9]+
word ::= [a-z]+
1
2
3
4
当然也能用非递归的形式定义hostname,如 hostname ::= (word ‘.’)+ word
解析树
将语法与字符串匹配可以生成一个解析树,该树显示字符串的部分与语法的部分如何对应
正则表达式
正则语法:简化之后可以表达为一个产生式而不包含任何非终止节点。不能有非终止节点。
所以一个正则表达式应该是这样的:markdown ::= ([^]* | '’ [^]* '’ )*
去除引号和空格,从而表达更简洁(更难懂):markdown ::= ([_]*|_[]*)*
为了简化表示,定义了一些简化的表示方式:
. : 表示任意字符
\d:表示任意整数,意义同[0-9]
\s:任何空格字符,包括空格,制表符,换行符
\w:任何单词字符,包括下划线,意义同[a-zA-Z_0-9]
\.,,
,\*,\+,…: 对操作符或特殊字符进行转义,以便从字面上匹配