卓越工程布道:掌握条件判断的模式
一、背景(前断言)
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. 性能上的保障@timeout(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 startassert(!isNaN(a)); // 断言a不是NaNassert(!isNaN(b)); // 断言b不是NaN// P end --- 以上是divide逻辑成立的必要条件// C startconst result = expensiveHighPrecisionDivide(a, b);// C end// 后断言 Q start --- 以下是divide逻辑成立并结束的必要条件assert(!isNaN(result))// Q endreturn result;}
function divide (a, b) {// 前断言 P startassert(!isNaN(a)); // 断言a不是NaNassert(!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;}// 防御性编程的一段 endconst result = expensiveHighPrecisionDivide(a, b);// C end// 后断言 Q start --- 以下是divide逻辑成立并结束的必要条件assert(!isNaN(result))// Q endreturn 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 startassert(!isNaN(a)); // 断言a不是NaNassert(!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 endreturn result;}
function divide (a, b) {// P startif (isNaN(a)) return; // 断言a不是NaNif (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 endreturn result;}
看起来与一般前端代码无异了,但如果混淆断言和卫述,把卫述放到断言前:
function divide (a, b) {// 卫述 start --- 下面有一些卫述,表达了一些场景if (b === 0) {return Infinity;}if (b === 1) {return a;}if (a === b) {return 1;}// 卫述 end// P startif (isNaN(a)) return; // 断言a不是NaNif (isNaN(b)) return; // 断言b不是NaN// P end --- 以上是divide逻辑成立的必要条件// C startconst result = expensiveHighPrecisionDivide(a, b);// C end// Q start -- 以下是后断言,是divide逻辑成立的必要条件if (isNaN(result)) return;// Q endreturn result;}
虽然这种代码正常执行,甚至与原代码等价,但从维护角度上,如果开发者重构涉及到18行一定会提心吊胆:是什么导致了a、b还有isNaN的情况?这明显不是divide的场景!我需要向上排查。
3.2极限编程下的模式应用
; 生成一系列keyREAD X,Y,A,B,C,DDATA 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,YSPRITE B,X,YSPRITE 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 codespeed = 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……