泡茶和泡咖啡,整个过程都是类似的。我们可以抽离其中的概念:泡东西=>煮水->收集浸泡材料->泡->等。
玩一个抽象概念的文字游戏:我去停宝马,我去停奥迪,我去停旋风冲锋,我去停三轮=>你都可以说:我去停车了。
再比如,我有女朋友了,我有男朋友了,我new了一个Object->我有对象了。
这很明显就是继承,万物皆对象的角度来说,如果对象的定义足够广泛,女朋友什么的都可以包含在其中。而JavaScript真正用到继承的地方其实不多。如果你想让一个对象拥有一个对象的属性,常用的方法就是mixin。
const mixin=(obj1,obj2)=>{ Object.keys(obj2).forEach((key)=>{ obj1[key]=obj2[key]; }); return obj1;}
这样obj1就拥有了obj2的属性。但对于继承类来说,mixin后this的指针可能是不正确的。
你也可以通过prototype来实现继承:
class A{ getB:new B();}
而模板方法就是基于继承的一种设计模式。它至少由两个类组成,一个是抽象的父类,一个是负责业务场景的具体类。在父类中通常包括一些子类用到的公共方法,也包括执行顺序算法。
假如抽象父类有n个子类,各自都有自己的执行逻辑,那么可以把相同的行为进行提炼封装到父类中。不同的地方留待子类来实现,就是所谓的"模板方法模式"。
不举那些比较虚的例子,直接拿工作中的实践来说,一个管理系统的页面,大致都有这些要素:
请求数据->加载列表->渲染条件查询控件->渲染操作按钮
但是业务实在是太多了。基本上所有页面都不会是那么标准,而是它的变种。以两个页面比如教师管理和学生管理来说,二者的的流程可能是这样的:
教师管理:请求教师数据->加载列表->渲染查询控件(工号)->(不渲染操作按钮)学生管理:请求学生数据->加载列表->渲染查询控件(学号)->渲染增删改查按钮
我们思考这里面的流程,流程是相似的。因此可以实现一个Admin抽象类:
class Admin{ constructor(info){ this.info=info } // 获取api前缀 getApi(operate){ return `${this.info.api}/${operate}`; }
// 查 async getData(params){ let api=this.getApi('list') return await http.post(api,params); } // 增 async addData(params){ let api=this.getApi('add') return await http.post(api,params); } // 改 async updateData(params){ let api=this.getApi('update') return await http.post(api,params); } // 删 async deletData(params){ let api=this.getApi('delete') return await http.post(api,params); }
// 加载数据 loadData(data){ var list=''; data.list.forEach((x)=>{ list+=`<li>${x.name}-${x.phoneNumber}</li>` }) document.querySelectorAll('#root').innerHTML=`<h1>${this.info.title}</h2>`; document.querySelectorAll('#list').innerHTML=list; }
// 操作按钮绑事件 btnBindEvent(params){ let operates=['add','delete','update','get']; operates.forEach((x)=>{ document.querySelector(`#${x}`).addEventListener('click',async (e)=>{ const data=await this[`${x}Data`](params); this.loadData(data); }) }) } // 获取查询条件 getPrams(){ //... 子类自己实现 }
// 初始渲染 async render(data){ const data=await this.getData(); const params=this.getParams(); this.loadData(data); this.btnBindEvent(params); }}
class Person{
}
Person.prototype=new Admin({ title:'人员管理', api:'/person'});// 复写缺失的方法Person.prototype.getParams=function(){ // ...}
// Person 可以修改loadData等等的方法。
这样你new一个Admin类,做好配置,一整套页面模板就出来了。
模板方法模式是一种严重依赖抽象类的设计模式。
抽象方法被声明在抽象类中,抽象方法并没有具体的实现过程,是一些“哑”方法,或者叫做插槽方法。比如Admin类中的getParams、loadData都被声明为抽象方法。当子类继承了这个抽象类时,必须重写父类的抽象方法。如果Person没有相应的方法,那么100%会得不到预期的结果。
除了抽象方法之外,如果每个子类中都有一些同样的具体实现方法,那这些方法也可以选择放在抽象类中,这可以节省代码以达到复用的效果,这些方法叫作具体方法。当代码需要改变时,我们只需要改动抽象类里的具体方法就可以了。
当我们在JavaScript中使用原型继承来模拟传统的类式继承时,并没有编译器帮助我们进行任何形式的检查,我们也没有办法保证子类会重写父类中的“抽象方法”。
一个解决方法是"内置"一套解决方案。比如getParam可以检索指定数据模型的值(在react或vue中是检索当前指定节点的状态,在jQuery中则是指定区域下的表单域),但是内置的成本比较大,你得跟后端,跟自己做很多很多的约定。写出的程序刻板而缺乏灵活性。在变通之下也缺乏良好的应对。
另一个方案,就是抛异常。
getPrams(){ throw new Error('找不到实现方法') }
从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师定好了框架的骨架,程序员继承框架的结构之后,负责往里面填空。笔者工作场景就是基于这种模板模式。
往小了说,我们可以问一个问题:
如何设计一个ui组件?
答案很简单:
指定容器->请求数据->绘制界面->通知渲染完毕。
你把上述过程封装起来就可以了。
在react中的componentDidAmount等生命周期其实就是模板方法的实现。
在react或Vue中,会详尽地设计了多个生命周期钩子,其实就是模板方法的实现。
设想我们的Admin类已经适用了大多数场景,但业务的内容是无穷无尽的。比方说我有的页面需要做前端权限拦截。如果不符合某种条件就不会渲染这个页面,直接不渲染或跳转别的界面。
class Admin{ //... // 默认 beforeRender(){ return true; }
// 初始渲染 async render(){ const before= this.beforeRender() if(before){ const data=await this.getData(); const params=this.getParams(); this.loadData(data); this.btnBindEvent(params); }else{ alert('无权限查看!') } }}Person.prototype=new Admin({ title:'人员管理', api:'/person'});
Person.prototype.beforeRender=function(condiction){ // ...CONDICTION}
正常情况下,你不写beforeRender也没事。假如要控制访问权限,就可以复写beforeRender方法。
“不要给我们打电话,我们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞原则。在好莱坞,把简历递交给演艺公司后就只有回家等待。由演艺公司对整个娱乐项的完全控制,演员只能被动式的接受公司的差使,在需要的环节中,完成自己的演出。
梦想进入好莱坞的人们,你们不要找好莱坞。那怎么办呢?答案是,让好莱坞来找你!
这就是非常著名的好莱坞原则。
模板方法模式充分的体现了“好莱坞”原则。IOC是Inversion of Control的简称,IOC的原理就是基于好莱坞原则,所有的组件都是被动的(Passive),所有的组件初始化和调用都由容器负责。
所有的framework都是遵循好莱坞原则设计的,否则就不叫framework。framework使用IoC的目的:
在这一原则的指导下,我们允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式去使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员一样,都是“别调用我们,我们会调用你”。
模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而是改为父类通知子类,哪些方法应该在什么时候被调用。作为子类,只负责提供一些设计上的细节。