一、背景(前断言)
1.1本文的由来
本文是普适性的经验分享,并非按规范局限在 JavaScript 前端视角 做出的总结,除JavaScript外至少深入结合了以下非纯粹OOP领域语言的经验:
ActionScript 3.0:这是ECMAScript 4 规范的实现,与ES6+、TypeScript相比ECMAScript 4 更加严格而且更早,业界应用广泛且积累厚重,有重要参考意义;
PHP:非严格OOP但大规模应用于Web的语言,在前后端分离历史阶段具有重要意义,甚至是前端熟知的JSX的发展也离不开XHP这一PHP非官方分支;
C / C++:早期状态和消息管理、绘图方案不健全时,有大量的表驱动、策略模式应用,卫述、断言是常态;
Basic:结构化编程,逻辑组织不当、难以形成大规模应用是其特点,但极限编程也有模式应用;
1.2设计模式消歧义
二、理论支撑(内功心法)
2.1霍尔逻辑 (Hoare logic)
2.2断言 (Assertion)、不变式 (invariant) 与卫 (Guard)
2.3契约式编程 (Design by Contract)
契约式编程对代码编写过程非常严格,就像在按契约进行编码,例如对方法(Method)的契约式编程一般包含以下部分:
// 7. 性能上的保障
300, "Execute timeout.") (
// 4. 可接受和不可接受的值或类型,5. 返回的值或类型,以及它们的含义
void assignNormalize(uint64 addr, float value)
throws GpuException, AssertException {
// 1. 先验条件(P / 前断言)
assert(loc != null, "Not a valid addr");
assert(value != null, "Not a valid value");
// 3. 不变式
invariant(value != 0, "Cannot divide by zero.")
// 6. 副作用(使用dispose模式管理)
const disposed = using(Gpu, (gpu, mem) => {
// 1. 先验条件(P / 前断言)
assert(mem.writable(addr, UINT64_LEN));
const result = gpu.normalize(mem.get(addr), value);
mem.assign(addr, result);
// 2. 后验条件(P / 后断言)
assert(result == mem.read(addr, UINT64_LEN));
})
// 2. 后验条件(Q / 后断言)
assert(disposed, "Dispose failed.");
}
void cosumer(uint64 baseLoc) {
// 1. 先验条件(P / 前断言)
assert(baseLoc > 0x100000000);
try {
assignNormalize(baseLoc + 0x1FFFF, 0x10);
}
// 4. 这里编译时报错,在assignNormalize是未提及此类异常
catch(MathException e) {
// ...
}
catch(GpuException e) {
// ...
}
}
2.4防御性编程 (Defensive Design)
先以一例契约式编程来举例:
function divide(a, b) {
// 前断言 P start
assert(!isNaN(a)); // 断言a不是NaN
assert(!isNaN(b)); // 断言b不是NaN
// P end --- 以上是divide逻辑成立的必要条件
// C start
const result = expensiveHighPrecisionDivide(a, b);
// C end
// 后断言 Q start --- 以下是divide逻辑成立并结束的必要条件
assert(!isNaN(result))
// Q end
return result;
}
function divide (a, b) {
// 前断言 P start
assert(!isNaN(a)); // 断言a不是NaN
assert(!isNaN(b)); // 断言b不是NaN
// P end --- 以上是divide逻辑成立的必要条件
// C start
// 防御性编程的一段 start,消除墨菲定律效力或者尽早退出
if (b === 0) {
return Infinity;
}
if (b === 1) {
return a;
}
if (a === b) {
return 1;
}
// 防御性编程的一段 end
const result = expensiveHighPrecisionDivide(a, b);
// C end
// 后断言 Q start --- 以下是divide逻辑成立并结束的必要条件
assert(!isNaN(result))
// Q end
return result;
}
if (a?.b?.c) {
result = a?.b; // 上下确定了a.b.c一定存在,这里滥用了Optional Chaining
}
2.5小结
function sample() {
/* 前断言 P {
这里是方法能够运行的前提
} */
/* 防御 {
消除异常、性能、安全等问题
} */
/* 逻辑正文 C */
}
三、设计模式(外功招式)
3.1卫述 (Guard Clause) / 保镖模式 (Bouncer Pattern)
if (guard) return; // 卫述
提前退出特性可以删除嵌套,使得代码更扁平。举例,使用卫述来简化深层嵌套:
// ❌ 改进前
function getPayAmount() {
let result;
if (isDead){
result = deadAmount();
} else {
if (isSeparated){
result = separatedAmount();
} else {
if (isRetired){
result = retiredAmount();
} else{
result = normalPayAmount();
}
}
}
return result;
}
// ✅ 改进后
function getPayAmount() {
if (isDead){
return deadAmount();
}
if (isSeparated){
return separatedAmount();
}
if (isRetired){
return retiredAmount();
}
return normalPayAmount();
}
3.1.2卫述与断言的区别
卫述与断言能力近似,甚至在大部分场景中的编码形式相同。
function divide (a, b) {
// P start
assert(!isNaN(a)); // 断言a不是NaN
assert(!isNaN(b)); // 断言b不是NaN
// P end --- 以上是divide逻辑成立的必要条件
// C start
// 下面有一些卫述,表达了一些场景
if (b === 0) {
return Infinity;
}
if (b === 1) {
return a;
}
if (a === b) {
return 1;
}
const result = expensiveHighPrecisionDivide(a, b);
// C end
// Q start -- 以下是后断言,是divide逻辑成立的必要条件
assert(!isNaN(result));
// Q end
return result;
}
function divide (a, b) {
// P start
if (isNaN(a)) return; // 断言a不是NaN
if (isNaN(b)) return; // 断言b不是NaN
// P end --- 以上是divide逻辑成立的必要条件
// C start
// 下面有一些卫述,表达了一些场景
if (b === 0) {
return Infinity;
}
if (b === 1) {
return a;
}
if (a === b) {
return 1;
}
const result = expensiveHighPrecisionDivide(a, b);
// C end
// Q start -- 以下是后断言,是divide逻辑成立的必要条件
if (isNaN(result)) return;
// Q end
return result;
}
看起来与一般前端代码无异了,但如果混淆断言和卫述,把卫述放到断言前:
function divide (a, b) {
// 卫述 start --- 下面有一些卫述,表达了一些场景
if (b === 0) {
return Infinity;
}
if (b === 1) {
return a;
}
if (a === b) {
return 1;
}
// 卫述 end
// P start
if (isNaN(a)) return; // 断言a不是NaN
if (isNaN(b)) return; // 断言b不是NaN
// P end --- 以上是divide逻辑成立的必要条件
// C start
const result = expensiveHighPrecisionDivide(a, b);
// C end
// Q start -- 以下是后断言,是divide逻辑成立的必要条件
if (isNaN(result)) return;
// Q end
return result;
}
虽然这种代码正常执行,甚至与原代码等价,但从维护角度上,如果开发者重构涉及到18行一定会提心吊胆:是什么导致了a、b还有isNaN的情况?这明显不是divide的场景!我需要向上排查。
3.2极限编程下的模式应用
生成一系列key
READ X,Y,A,B,C,D
DATA 120,140,0,1,2,3
; 将key映射到value上
DEF SPRITE A,(0,1,0,1,0)=CHR$(1)+CHR$(0)+CHR$(3)+CHR#(2)
DEF SPRITE B,(0,1,0,0,0)=CHR$(0)+CHR$(1)+CHR$(2)+CHR#(3)
DEF SPRITE C,(0,1,0,1,0)=CHR$(5)+CHR$(4)+CHR$(7)+CHR#(6)
DEF SPRITE D,(0,1,0,0,0)=CHR$(4)+CHR$(5)+CHR$(6)+CHR#(7)
...
; 函数调用
SPRITE A,X,Y
SPRITE B,X,Y
SPRITE C,X,Y
...
3.3表驱动 (Table-Driven Method)
const CITY_TABLE = {
"Paris": {
"country": "France",
"population": 2140526,
"area": 105.4,
"famousAttractions": ["Eiffel Tower", "Louvre Museum", "Notre-Dame Cathedral"],
"timezone": "Europe/Paris"
},
"Reykjavik": {
"country": "Iceland",
"population": 123300,
"area": 273,
"famousAttractions": ["Blue Lagoon", "Hallgrimskirkja", "Golden Circle"],
"timezone": "Atlantic/Reykjavik"
},
"Marrakech": {
"country": "Morocco",
"population": 928850,
"area": 230,
"famousAttractions": ["Jardin Majorelle", "Bahia Palace", "Koutoubia Mosque"],
"timezone": "Africa/Casablanca"
}
};
const COUNTRY_TABLE = {
"France": {
"population": 67221998,
"area": 551695,
"officialLanguage": ["French"],
"capital": "Paris"
},
"Iceland": {
"population": 356991,
"area": 103000,
"officialLanguage": ["Icelandic"],
"capital": "Reykjavik"
},
"Morocco": {
"population": 36910560,
"area": 446550,
"officialLanguage": ["Arabic", "Berber"],
"capital": "Rabat"
}
}
const cityInfo = CITY_INDEXED_TABLE[city];
const countryInfo = COUNTRY_TABLE[cityInfo.country];
如果给一张表,甚至不用学习数字电路都能理解里面的逻辑,这张表有专有名词叫Functional Model:
3.3.2表驱动评估表
3.4策略模式 (Strategy Pattern)
class Bird {
// ...
double getSpeed() {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
throw new RuntimeException("Should be unreachable");
}
}
abstract class Bird {
// ...
abstract double getSpeed();
}
class European extends Bird {
double getSpeed() {
return getBaseSpeed();
}
}
class African extends Bird {
double getSpeed() {
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
}
}
class NorwegianBlue extends Bird {
double getSpeed() {
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}
// Somewhere in client code
speed = bird.getSpeed();
// 改写前
function getSpeed(type): number {
switch (type) {
case EUROPEAN:
return getBaseSpeed();
case AFRICAN:
return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
case NORWEGIAN_BLUE:
return (isNailed) ? 0 : getBaseSpeed(voltage);
}
}
// 以策略模式改写后
const SPEED_METHODS = {
EUROPEAN: () => getBaseSpeed(),
AFRICAN: () => getBaseSpeed() - getLoadFactor() * numberOfCoconuts,
NORWEGIAN_BLUE: () => (isNailed) ? 0 : getBaseSpeed(voltage),
};
function getSpeed(type) {
return SPEED_METHODS[type]?.();
}
3.4.1策略模式评估表
3.5责任链模式 (Chain of Responsibility Pattern)
很遗憾的是,网上大量责任链模式的案例都生搬了OOP实践,在函数为第一公民的JavaScript的现实开发中很难被采用,因此建议不要在前端日常业务开发中使用OOP的责任链模式实践,这会带来理解和应用的负担。
职责分离:将原本混杂在一起的职责边界划分清楚,形成多个的具备单一职责的函数;
构造责任链:将这些单一职责函数按顺序排列到数组中,形成责任链;
执行责任链:编写一个操纵函数,遍历责任链的各个函数并执行,设定终止执行的条件。
// 场景1,大量任务先后执行
if (a()) return;
if (b()) return;
if (c()) return;
if (d()) return;
// ...
const processors = [a,b,c,d];
const manipulator = processors => {
for (const processor of processors) {
if (processor()) {
return;
}
}
}
// 场景2,大量任务嵌套执行
if (a()) {
if (b()) {
if (c()) {
if (d()) {
// ...
}
}
}
}
const processors = [a,b,c,d];
const manipulator = processors => {
for (const processor of processors) {
if (!processor()) {
return;
}
}
}
可见,manipulator的实现决定了责任链执行的顺序。
3.5.1用责任链重构逻辑
const exponents = ["e", "E"];
const numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const operators = ['-', '+'];
const chars = [ ...numbers, ...exponents, ...operators, '.' ];
const isNumber = function (s) {
// 前断言
if (typeof s !== "string") {
return;
}
// 防御
s = s.trim();
if (s.length === 0) {
return false;
}
let hasPoint = false;
let hasExponent = false;
let hasNumber = false;
let numberAfterExponent = true;
for (let i = 0; i < s.length; i++) {
// 不变式,断言每个s[i]都在指定集合内
if (!chars.includes(s[i])) {
return false;
}
// 卫述开始
if (s[i] === "." && (hasExponent || hasPoint)) {
return false;
}
if (exponents.includes(s[i]) && (hasExponent || !hasNumber)) {
return false;
}
if (operators.includes(s[i]) && !e.includes(s[i - 1]) && i !== 0) {
return false;
}
if (s[i] === ".") {
hasPoint = s[i] === ".";
}
// 卫述结束
if (numbers.includes(s[i])) {
hasNumber = true;
numberAfterExponent = true;
}
if (exponents.includes(s[i])) {
numberAfterExponent = false;
hasExponent = true;
}
}
return hasNumber && numberAfterExponent;
};
这个函数已经使用卫述优化,但仍然难以维护,原因是每一个卫都有一定的关联性,后续对卫的维护是需要分析所有的逻辑,因此感觉整体复杂。
// 用责任链模式优化
const exponents = ["e", "E"];
const numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const operators = ['-', '+'];
const chars = [ ...numbers, ...exponents, ...operators, '.' ];
// Step 1. 职责分离
interface IProcessor {
ctx => boolean;
}
const checkNumeric: IProcessor = (ctx) => {
if (numbers.includes(ctx.s[i])) {
ctx.hasNumber = true;
ctx.numberAfterExponent = true;
}
}
const checkDecimal: IProcessor = (ctx) => {
if (ctx.s[i] !== ".") {
return;
}
if (ctx.hasExponent || ctx.hasPoint) {
return false;
}
ctx.hasPoint = true;
}
const checkExponent: IProcessor = (ctx) => {
if (!exponentSign.includes(ctx.s[i])) {
return;
}
if (ctx.hasExponent || !ctx.hasNumber) {
return false;
}
ctx.numberAfterExponent = false;
ctx.hasExponent = true;
}
const checkOperator: IProcessor = (ctx) => {
// 不变式,断言每个s[i]都在指定集合内
if (!operators.includes(ctx.s[i])) {
return;
}
if (ctx.i !== 0 && !exponents.includes(ctx.s.charAt(i - 1))) {
return false;
}
}
// Step 2. 构造责任链
const processors: IProcessor[] = [
checkNumeric,
checkDecimal,
checkExponent,
checkOperator,
];
// Step 3. 执行责任链
const manipulator = ctx => {
for (const processor of processors) {
if (processor(ctx) === false) {
return false;
}
}
return true;
}
const isNumber = s => {
// 前断言
if (typeof s !== "string") {
return;
}
// 防御
s = s.trim();
if (s.length === 0) {
return false;
}
const ctx = {
s: s.trim(),
hasPoint: false,
hasExponent: false,
hasNumber: false,
numberAfterExponent: true,
};
for (ctx.i = 0; ctx.i < s.length; ctx.i++) {
// 不变式,断言每个s[i]都在指定集合内
if (!chars.includes(ctx.s[i])) {
return false;
}
// 消费责任链函数
if (!manipulator(ctx)) {
return false;
}
}
return ctx.hasNumber && ctx.numberAfterExponent;
}
3.5.2写卫述不行吗?
// Step 2. 构造并执行责任链
const processors: IProcessor[] = [
checkNumeric,
checkDecimal,
checkExponent,
checkOperator,
];
// Step 3. 执行责任链
const manipulator = ctx => {
for (const processor of processors) {
if (processor(ctx) !== false) {
return;
}
}
}
// Step 2+3. 构造并执行责任链
const manipulator = ctx => {
if (checkNumeric(ctx) === false) {
return false;
}
if (checkDecimal(ctx) === false) {
return false;
}
if (checkExponent(ctx) === false) {
return false;
}
if (checkOperator(ctx) === false) {
return false;
}
}
3.5.3责任链的典型形态
3.5.3.1Pipeline形态
const ROUTES = {
line1: [a, b, c],
line2: [a, b, d, e],
line3: [d, e, f, g],
};
const manipulator = (line) => {
for (const processor of ROUTES[line]) {
processor()
}
}
3.5.3.3Middleware形态
3.5.4责任链模式评估表
建议按以下评估表确认是否可以使用责任链模式:
3.6小结
写 if / else 前,判断是断言还是卫述; 坚持卫述表达,尽早退出; 数据、逻辑混合?走表驱动方法; 判断、执行逻辑混合?走策略模式; 逻辑像任务流水线?走责任链模式。
四、写在后面(后断言)
本文给出的是与条件判断相关的基础知识,如果想了解的知识不在里面,有几种可能性:
读者已经融会贯通这些与条件判断相关的理论和模式,并有自己的实践理论方法;
读者可能直接跳过理论,直接到设计模式看代码去了,对理论支撑部分理解不够,主观臆断了。
还有很多设计模式经典但没讲,理由可能是:
贵精不贵多:本文的模式非常基础、非常重要,若能把文中模式熟练应用,别的模式甚至不用也无所谓;
普适性不高:GoF中提的那么多模式,也不是每种在业务开发场景中都有用武之地;
普适性太高:一些经典模式已经形成专用的框架或库了,大家都在用,对此也没有问题,学习使用这些框架或库即可掌握,如:Redux、RXJS、EventEmitter……