心游于艺,道不远人
变继承关系为组合关系 State 模式 继承描述了 is-a 的关系,子类可以继承父类的成员变量和函数,也可以修改父类的成员变量和函数。使用设计模式来实现代码复用,而不是使用继承实现代码复用。
它的核心思想是通过改变对象的内部状态来改变对象的行为,而无需修改对象本身的类结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 interface Role { void doWork () ; } class Engineer implements Role { public void doWork () { System.out.println("写代码" ); } } class Manager implements Role { public void doWork () { System.out.println("开会" ); } } class Employee { private Role role; void setRole (Role role) { this .role = role; } void doWork () { role.doWork(); } } Employee emp = new Employee ();emp.setRole(new Engineer ()); emp.doWork(); emp.setRole(new Manager ()); emp.doWork();
装饰器模式 java 中有一个 Runnable 接口:
1 2 3 interface Runnable { void run () ; }
如何实现 LoggingRunnable,TransactionRunnable?
原始的 CodingTask 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 class CodingTask implements Runnable { @Override public void run () { System.out.println("writing code" ); try { Thread.sleep(3000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } }
一种不太好的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 abstract class LoggingRunnable implements Runnable { protected abstract void doRun () ; @Override public void run () { System.out.println("Task started at " + LocalTime.now()); doRun(); System.out.println("Task done at " + LocalTime.now()); } } abstract class TransactionRunnable implements Runnable { protected abstract void doRun () ; @Override public void run () { System.out.println("begin transaction." ); boolean shouldRollback = false ; try { doRun(); } catch (Exception e) { shouldRollback = true ; throw e; } finally { if (shouldRollback) { System.out.println("rollback" ); } else { System.out.println("commit" ); } } } } class CodingTask extends LoggingRunnable { @Override protected void doRun () { System.out.println("writing code" ); try { Thread.sleep(3000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } } public class Test { public static void main (String[] args) { new CodingTask ().run(); } }
上面的代码虽然实现了功能,但是 CodingTask 的定义却变得不是很明朗:
CodingTask 从字面意思上是任务,原始的 CodingTask 实现了 Runnable 接口含义更明确
引入了另外一个前提,凡是在使用 CodingTask 的地方必须记录日志
如果我们现在不想要 CodingTask 记录日志了,而是当做一个 Transaction 处理,就必须修改代码让 CodingTask 继承 TransactionRunnable 抽象类
如果 CodingTask 要同时完成记录日志和启用事务,由于单继承的局限性,上面的代码无法做到(即使支持多继承,由于 LoggingRunnable 和 TransactionRunnable 都有 doRun,到底调用哪个也是很复杂的)
run 方法比 doRun 具有更好的语义
装饰器模式可以解除上述耦合:
类里面的成员要么是 public 的,要么是 private 的,如果遇到 protected 要反思下是否进行了正确建模。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 class LoggingRunnable implements Runnable { private final Runnable runnable; public LoggingRunnable (Runnable runnable) { this .runnable = runnable; } @Override public void run () { System.out.println("Task started at " + LocalTime.now()); runnable.run(); System.out.println("Task done at " + LocalTime.now()); } } class TransactionRunnable implements Runnable { private final Runnable runnable; public TransactionRunnable (Runnable runnable) { this .runnable = runnable; } @Override public void run () { System.out.println("begin transaction." ); boolean shouldRollback = false ; try { runnable.run(); } catch (Exception e) { shouldRollback = true ; throw e; } finally { if (shouldRollback) { System.out.println("rollback" ); } else { System.out.println("commit" ); } } } } class CodingTask implements Runnable { @Override public void run () { System.out.println("writing code" ); try { Thread.sleep(3000 ); } catch (InterruptedException e) { throw new RuntimeException (e); } } } public class Test { public static void main (String[] args) { CodingTask task = new CodingTask (); task.run(); new LoggingRunnable (task).run(); new LoggingRunnable (new TransactionRunnable (task)).run(); } }
上面的代码,每个类各司其职,但是可以通过嵌套灵活组装。里层的逻辑装饰了外层逻辑。java 流就使用了装饰器模式。
一种快速出错 的常规编程实践提倡一旦出错就立刻抛出异常,使错误的定位更加容易(这和忽略错误并将异常推迟到以后处理的方式截然相反)。
模拟接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 const assert = require ('assert' );class Interface { constructor (name, methods ) { assert (name, '接口名不能为空' ); assert (Array .isArray (methods) && methods.every (m => typeof m === 'string' ), '请检查方法名数组' ); this .name = name; this .methods = methods; } static ensureImplements (obj, ...interfaces ) { for (const inter of interfaces) { const clazz = inter.constructor ; const className = clazz.name ; assert.equal (clazz, Interface , `${className} 必须通过new Interface生成` ); for (const methodName of inter.methods ) { assert.equal (typeof obj[methodName], 'function' , `${className} 没有实现呢${methodName} 方法` ); } } } } const i1 = new Interface ('map' , ['zoomIn' , 'zoomOut' ]);const i2 = new Interface ('show' , ['show' ]);const map = { zoomIn ( ) { console .log ('map zoom in' ); }, zoomOut ( ) { } }; Interface .ensureImplements (map, i1, i2);map.zoomIn ();
作用域、嵌套函数和闭包 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function foo ( ) { var a = 10 ; function bar ( ) { a *= 2 ; return a; } return bar; } const baz = foo (); let ret = baz (); ret = baz (); debugger var blat = foo (); ret = blat (); debugger
上述代码中,返回的对 bar 函数的引用被赋值给变量 baz,bar 函数虽然是在 foo 函数外部,但是依然能够访问变量 a,这是因为 js 中的作用域是词法的。函数是运行在定义它们的作用域中(foo 内部的作用域),而不是运行在调用它们的作用域中。只要 bar 被定义在 foo 中,它就能访问在 foo 中定义的所有变量,即使 foo 的执行已经结束。这是一个闭包的例子,在 foo 返回之后它的作用域就被保存下来,但是只有它返回的那个函数能够访问这个作用域。上面的例子中,baz 和 blat 各有这个作用域及a的一个副本,而且只有它们自己能够对其进行修改。返回一个内嵌函数是创建闭包最常用的手段 。
通过闭包,我们可以实现私有变量:
1 2 3 4 5 6 7 8 9 10 11 12 function Book (newISBN ){ var isbn; this .setISBN = function (newISBN ) { isbn = newISBN; } this .getISBN = function ( ){ return isbn; } this .setISBN (newISBN); } Book .prototype .commonMethod = function ( ){}
上面代码中私有变量通过闭包进行封装可以实现防止无意中修改了变量中的属性。用这种方式创建对象有 2 个弊端:
新的对象的私有属性和方法都需要占用额外的内存(一般的创建对象所有的方法都存储在原型对象中,内存中只存储一份)
不利于派生子类,不能访问父类的私有属性和方法
ES6 提供的 Reflect 可以实现真正的属性私有化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 function priavateProp (obj, filter ){ const handler = { get (obj, prop ) { if (!filter (prop)){ let val = Reflect .get (obj, prop) if (typeof val === 'function' ){ val = val.bind (obj) } return val } }, set (obj, prop, val ) { if (filter (prop)){ throw new Error (`cannot set property ${prop} ` ) } return Reflect .set (obj, prop, val) }, has (obj, prop ) { return filter (prop) ? false : Reflect .has (obj, prop) }, ownKeys (obj ) { return Reflect .ownKeys (obj).filter ( prop => !filter (prop)) } } return new Proxy (obj, handler) } function filter (prop ){ return prop.indexOf ('_' ) === 0 } const o = { _private : 'private property' , name : 'public name' , say ( ){ console .log (this ._private ) } } const p = priavateProp (o, filter)console .log (p) p._private JSON .stringify (p) p.say () console .log ('_private' in p) Object .keys (p) p._private = '000'
继承 克隆函数和原型式继承 1 2 3 4 5 6 const clone = obj => { function F ( ){}; F.prototype = obj; return new F (); }
返回的对象没有属性,但是可以通过原型链访问 obj
的方法和属性。
类似于 Object.create
的早期实现(ES5 之前)。它的核心思想是:基于现有对象创建新对象,并可以扩展或修改其属性和方法 。
原型链继承比类式继承更能节省内存。原型链查找的方式创建的对象使得所有克隆出来的对象共享每个属性和方法的唯一一份实例,只有在直接设置了某个克隆出来的对象的属性和方法的时候,情况才会有所变化。与此同时,在类式继承中创建的每一个对象在内存中都有自己的一份属性(和私有方法)的副本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const Person = { init (name, age ) { this .name = name; this .age = age; }, show ( ) { return `name = ${this .name} ,age = ${this .age} ` ; } }; const s = clone (Person ); s.showAge = function ( ) { return this .age ; }; s.init ('张三' , 14 ); console .log (s.show ()); console .log (s.showAge ());
s 的原型是 Person,所以可以调用 init 和 show
可以动态添加新方法(如 showAge),而不会影响 Person
用这种方法创建的对象往往有较高的内存使用效率,因为它们会共享那些未被改写的属性和方法。那些包含数组和或者对象类型的成员的克隆会有一些麻烦的地方。但是这个问题可以通过一个方法来解决。
多继承与 mixin 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 const Util = { show ( ) { const output = []; for (const key of Object .keys (this )) { output.push (`${key} : ${this [key]} ` ); } return output.join (',' ); } } class Student { constructor (name, age, courses ) { this .name = name; this .age = age; this .courses = courses; } } function mixin (receivingClass, givingClass ) { for (const methodName in givingClass) { if (!receivingClass.prototype [methodName]) { receivingClass.prototype [methodName] = givingClass[methodName]; } } } mixin (Student , Util );const s = new Student ('张三丰' , 83 , ['太极拳' , '乾坤大罗伊' ]);const str = s.show ();
用一些代码来拓展一个类有时候比继承一个类更加适合,这可以轻松解决类中的重复代码问题。
上面的代码可以将工具函数混入到类的原型中实现多继承,注意混入的类不能是 ES6 的 class 而是一个普通对象。因为 ES6 中定义的实例方法是不可枚举的。
单例 js 中的单体是一个用来划分命名空间并将一批相关方法和属性组织在一起的对象,如果它可以被实例化,那么只能被实例化一次。
对象字面量 1 2 3 4 5 6 7 8 const Singleton = { attr1 : true , attr2 : 10 , _privateAttr1 : 'hehe' , method1 ( ) { }, method2 ( ) { }, _privateMethod ( ){ } }
这种方式创建的单体对象在划分命名空间的时候非常有用:代码分模块组织,增强代码的可读性。有了命名空间也可以减少变量的冲突。
上面的代码使用约定的下划线开头表示私有成员,使用闭包可以实现完全私有:
1 2 3 4 5 6 7 8 9 10 11 const Singleton = (function ( ) { const privateAttr = 1 ; const privateMethod = ( ) => 1 ; return { publicAttr : true , publicMethod (args ) { } } })();
上面的这种模式又称为模块模式(module pattern),指的是把一批相关的属性和方法组织成模块并起到划分命名空间的作用。
上面的这种方式实现私有的优势:私有成员不会在单例对象外部被访问,可以自由改变对象的实现细节,而不会殃及无辜。
惰性实例化 生成对资源密集型或者配置开销非常大的单体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const Singleton = (function ( ) { let uniqInstance; function constructor ( ) { const privateAttr = 1 ; const privateMethod = ( ) => 1 ; return { publicAttr : true , publicMethod (args ) { } } } return { getInstance ( ) { if (!uniqInstance) uniqInstance = constructor ( ); return uniqInstance; } }; })();
分支 一种用来把浏览器差异封装到运行期间内动态进行设置的技术。例如 XHR 对象在早期的 IE 中是 ActiveX 对象。如果不采用这种技术,每次调用这个方法的时候浏览器嗅探代码会再次运行,严重缺乏效率。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 const XHRFactory = (function ( ) { const standard = { createXHR ( ) { return new XMLHttpRequest (); } }; const activeXNew = { createXHR ( ) { return new ActiveXObject ('Msxml2.XMLHTTP' ); } }; const activeXOld = { createXHR ( ) { return new ActiveXObject ('Microsoft.XMLHTTP' ); } }; let testObj; try { testObj = standard.createXHR (); return standard; } catch (e) { try { testObj = activeXNew.createXHR (); return activeXNew; } catch (error) { try { testObj = activeXOld.createXHR (); return activeXOld; } catch (error) { throw new Error ('no xhr object found in this enviroment' ); } } } })();
上面的代码之所以有效的原因是:在脚本加载时一次性确定特定浏览器的代码。这样一来初始化完成之后每种浏览器只会执行针对它的 js 代码。在使用分之的时候需要在内存(分支计数需要多创建对象)和时间之间做一个权衡。
工厂模式 简单工厂 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 class Toyota { constructor ( ) { this .name = '丰田' ; } } class Audi { constructor ( ) { this .name = '奥迪' ; } } class BMW { constructor ( ) { this .name = '宝马' ; } } const inters = ['wash' , 'run' , 'repair' ];for (const clazz of [Toyota , Audi , BMW ]) { for (const inter of inters) { clazz.prototype [inter] = function ( ) { console .log (`${this .name} -> ${inter} ` ); } } } const Car = new Intercafe ('Car' , inters);class CarFactory { static makeCar (model ) { let car; switch (model) { case 'toyota' : car = new Toyota (); break ; case 'audi' : car = new Audi (); break ; case 'bmw' : car = new BMW (); break ; } Intercafe .ensureImplements (car, Car ); return car; } } class CarShop { static sellCar (model ) { const car = CarFactory .makeCar (model); car.wash (); return car; } }
上面的例子中将创建对象的工作转交给一个外部对象,因为创建对象的方式可能存在变化。
接口在工厂模式中起着很重要的作用。如果不对对象进行类型检查以确保实现了必须的方法,工厂模式所带来的所处也所剩无几。创建对象并且对它们一视同仁。
工厂方法 工厂方法和简单工厂的区别在于不是使用另外一个来来创建对象,而是使用一个子类。工厂是将一个成员对象推迟到子类中进行的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 const AjaxHandler = new Intercafe ('AjaxHandler' , ['request' , 'createXhr' ]);class SimpleHandler { constructor ( ) { this .createXhr = null ; } request (method, url, callback, postVars ) { const xhr = this .createXhr (); xhr.onreadystatechange = function ( ) { if (xhr.readyState !== 4 ) return ; xhr.status === 200 ? callback.success (xhr.responseText , xhr.responseXML ) : callback.failure (xhr.status ); } xhr.open (method, url, true ); if (method !== 'POST' ) postVars = null ; xhr.send (postVars); } createXhr ( ) { const methods = [ function ( ) { return new XMLHttpRequest (); }, function ( ) { return new ActiveXObject ('Msxml2.XMLHTTP' ); }, function ( ) { return new ActiveXObject ('Microsoft.XMLHTTP' ); } ]; for (const method of methods) { try { method (); } catch (e) { continue ; } this .createXhr = method; return method; } throw new Error ('SimpleHandler:Can not create an xhr object.' ); } } class QueueHandler extends SimpleHandler { constructor ( ) { this .queue = []; this .requestInProcess = false ; this .retryDelay = 5 ; } advanceQueue ( ) { if (this .queue .length === 0 ) { this .requestInProcess = false ; return ; } const req = this .queue .shift (); this .request (req.method , req.url , req.callback , req.postVars , true ); } request (method, url, callback, postVars, override ) { if (this .requestInProcess && !override) { this .queue .push ({ method, url, callback, postVar }); } else { this .requestInProcess = true ; const xhr = this .createXhr (); xhr.onreadystatechange = function ( ) { if (xhr.readyState !== 4 ) return ; if (xhr.status === 200 ) { callback.success (xhr.responseText , xhr.responseXML ) this .advanceQueue (); } else { callback.failure (xhr.status ); setTimeout (() => { this .request (method, url, callback, postVars, true ); }, this .retryDelay * 1000 ); } } xhr.open (method, url, true ); if (method !== 'POST' ) postVars = null ; xhr.send (postVars); } } } class OfflineHandler extends SimpleHandler { constructor ( ) { this .storedRequests = []; } request (method, url, callback, postVars ) { if (XhrManager .isOffline ()) { this .storedRequests .push ({ method, url, callback, postVar }); } else { this .flushStoredRequests (); super .request (method, url, callback, postVars); } } flushStoredRequests ( ) { for (const req of this .storedRequests ) { super .request (req.method , req.url , req.callback , req.postVars ); } } } const XhrManager = { createXhrHandler ( ) { let xhr; if (this .isOffline ()) { xhr = new OfflineHandler (); } else if (this .isHighLatency ()) { xhr = new QueueHandler (); } else { xhr = new SimpleHandler (); } Intercafe .ensureTmplements (xhr, AjaxHandler ); return xhr; }, isOffline ( ) { }, isHighLatency ( ) { } };
上述 createXhr 中 memoize 技术,复杂的设置代码只会在首次调用的时候执行一次,此后就只有针对当前浏览器的代码会被执行。
上面的工厂方法模式可以根据网络条件创建专门的请求对象:OfflineHandler 会在用户离线的时候把请求缓存起来,而 QueueHandler 会在发起新的请求之前确保所有请求都已经成功处理,如果请求失败后还支持指定时间间隔后重试这个请求,直到成功为止。
现在我们只需要使用XhrManager.createXhrHandler
这个工厂方法而不需要实例化特定的类了。
工厂方法模式的好处在于消除对象间的耦合,通过工厂方法而不是 new 关键字可以把所有的实例化代码集中在一个位置,从而可以大大简化更换所用的类或者在运行期间选择类的工作。在派生子类的时候也提供了更大的灵活性 ———— 先创建一个抽象的父类,在子类中创建工厂方法,从而把成员对象的实例化推迟到专门的子类中进行。