当前位置: 首页 > 科技观察

前端设计模式系列——模板模式

时间:2023-03-13 12:21:23 科技观察

代码写了好几年了,设计模式一直处于忘我的状态。大多数关于设计模式的文章都使用基于类的静态类型语言,例如Java和C++。作为前端开发人员,js是一门基于原型的动态语言。职能已成为一等公民。模式略有不同,甚至简单得不像使用设计模式,有时会引起一些混淆。下面按照“场景”-“设计模式定义”-“代码实现”-“更多场景”-“通用”的顺序进行总结。如有不妥之处,欢迎交流讨论。场景(示例代码来自极客时间课程,ReactHooks核心原理与实战)在平时的开发中肯定遇到过这样的场景:发起异步请求,显示加载状态,获取数据显示在界面上,如果发生错误,则返回错误状态的相关印象。为了操作方便,先写一个mock数据方法:constlist={page:1,per_page:6,total:12,total_pages:2,data:[{id:1,email:"george.bluth@reqres.在",first_name:"windliang",last_name:"windliang",avatar:"https://reqres.in/img/faces/1-image.jpg"},{id:2,email:"janet.weaver@reqres.in",first_name:"Janet",last_name:"Weaver",avatar:"https://reqres.in/img/faces/2-image.jpg"},{id:3,email:"艾玛。wong@reqres.in",first_name:"Emma",last_name:"Wong",avatar:"https://reqres.in/img/faces/3-image.jpg"}]};exportconstgetDataMock=()=>newPromise((resolve,reject)=>{setTimeout(()=>{resolve(list);},2000);});然后是列表组件:从“反应”导入反应;import{getDataMock}from"./mock";exportdefaultfunctionUserList(){//使用三种状态保存用户列表,加载状态和错误状态const[users,setUsers]=React.useState([]);const[加载,设置加载]=React.useState(false);const[error,setError]=React.useState(null);//定义获取用户的回调函数constfetchUsers=async()=>{setLoading(true);尝试{constres=awaitgetDataMock();//请求成功后,将用户数据放入statesetUsers(res.data);}catch(err){//如果请求失败,将错误状态放入状态setError(err);}setLoading(false);};返回({loading?"Loading...":"ShowUsers"}{error&&Failed:{String(error)}

}
    {users&&users.length>0&&users.map((user)=>{return{user.first_name};})}
);}效果如下:其实可能有很多组件需要这个过程,加载->显示数据->加载消失,错误显示,每个组件单独维护这个设置逻辑太麻烦了。这时候就可以使用模板模式了。模板模式看维基百科给出的定义:模板方法是超类中的一个方法,通常是抽象超类,通过一些高层步骤来定义一个操作的骨架。这些步骤本身由与模板方法相同的类中的其他辅助方法实现。”辅助方法可以是抽象方法,在这种情况下,子类需要提供具体的实现,也可以是钩子方法,在超类中具有空主体。子类可以(但不是必须)通过覆盖挂钩方法来自定义操作。模板方法的目的是定义操作的整体结构,同时允许子类细化或重新定义某些步骤。[2]”简单地说,模板模式就是抽象父类提供一个骨架方法,它调用一些抽象方法或空方法,抽象方法/空方法由子类自己实现。看一下UML类图。image-20220210212704745有关烹饪的简单示例,请查看代码示例:abstractclassCook{publicabstractvoidprepareIngredients();公共抽象无效烹饪();publicvoidprepare(){System.out.println("准备干净的锅");}/*一个模板方法:*/publicfinalvoidstartCook(){prepare();准备配料();烹饪();}}classTomatoEggextendsCook{@OverridepublicvoidprepareIngredients(){System.out.println("打鸡蛋,切西红柿");}@Overridepublicvoidcooking(){System.out.println("热油,炒鸡蛋,出锅");System.out.println("少油,炒西红柿,加盐,糖,加鸡蛋炒");System.out.println("出锅");}}classPotatoextendsCook{@OverridepublicvoidprepareIngredients(){System.out.println("土豆片,培根");}@Overridepublicvoidcooking(){System.out.println("热油,炸薯片,出锅");System.out.println("来,炒蒜姜辣椒,炒肉,加土豆炒");System.out.println("加生抽,加盐,加老抽上色");系统.out.println("做饭");}}publicclassMain{publicstaticvoidmain(String[]args){CooktomatoEgg=newTomatoEgg();tomatoEgg.startCook();煮土豆=newPotato();土豆.startCook();System.out.println("开饭啦!");}}/*准备一个干净的锅打散鸡蛋,西红柿切块烧热油,炒鸡蛋,少油,炒西红柿,加盐,糖,加入鸡蛋炒出锅准备干净的锅,切土豆片,油烧热培根,炒土豆片,锅里下油,蒜姜辣椒爆香,下肉翻炒,下土豆,翻炒,加生抽,加盐,加老抽出油panforcoloringLet'seat!*/Cook类提供了骨架方法startCook,写了做饭的主要流程,其他抽象方法prepareIngredients和cooking委托给子类实现自己独特的逻辑我们用js重写一下:constCook=function(){};Cook.prototype.prepare=function(){console.log("准备干净的锅");};Cook.prototype.prepareIngredients=function(){thrownewError("子类必须覆盖prepareIngredients方法");};Cook.prototype.cooking=function(){thrownewError("子类必须覆盖cooking方法");};Cook.prototype.startCook=function(){this.prepare();this.prepareIngredients();this.cooking();};constTomatoEgg=function(){};TomatoEgg.prototype=newCook();TomatoEgg.prototype.prepareIngredients=function(){console.log("混合鸡蛋,切西红柿");};TomatoEgg.prototype.cooking=function(){console.log("热油,炒鸡蛋,出锅");console.log("少油,炒西红柿,加盐,糖,煎鸡蛋");console.log("出锅");};constPotato=function(){};Potato.prototype=newCook();Potato.prototype.prepareIngredients=function(){console.log("切土豆片,bacon");};Potato.prototype.cooking=function(){console.log("热油,炸土豆片,出锅");安慰。log("来,把蒜、姜、花椒炒香,下肉,下土豆");console.log("加入生抽、盐、老抽color");console.log("出锅");};consttomatoEgg=newTomatoEgg();tomatoEgg.startCook();constpotato=newPotato();potato.startCook();console.log("OpenEat!");以上是js以java的形式实现模板方法,js作为一个函数,是一等公民,或许我们可以换个js的模板模式,模板模式就是定义一个方法中的算法骨架,允许子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。原来的定义是通过抽象类继承实现的,但是由于js没有抽象类,所以有点繁琐实现,也许我们可以通过组合,将需要的方法以参数的形式传递给算法骨架。constCook=function({prepareIngredients,cooking}){constprepare=function(){console.log("准备一个干净的锅");};conststartCook=function(){准备();准备配料();烹饪();};return{startCook,};};consttomatoEgg=Cook({prepareIngredients(){console.log("拌鸡蛋,切西红柿");},cooking(){console.log("热油,炒鸡蛋,outof出锅");console.log("少油,炒西红柿,加盐,糖,加鸡蛋炒");console.log("出锅");},});tomatoEgg.startCook();constpotato=Cook({prepareIngredients(){console.log("切土豆片,培根");},cooking(){console.log("热油,炸土豆片,出锅");console.log("来,把蒜、姜、辣椒炒香,把肉炒熟,加土豆");console.log("加生抽,加盐,加老抽上色");console.log("出锅了");},});potato.startCook();console.log("开饭了!");通过组合,代码会变得更加清爽简洁,无需定义TomatoEgg类和Potato类,只需要传递参数即可。但是js只能实现带引号的模板方法。一方面,我们没有通过继承来实现它。另一方面,js不具备抽象类和抽象方法的功能。有些方法如果没有实现,就不能写成代码。找到阶段,直到运行阶段才会收到Error。代码实现回到一开始的异步请求例子,我们可以定义一个requestHook来封装加载处理和数据返回处理的步骤,外界只需要传递request方法即可。import{useState,useCallback}from"react";exportdefault(asyncFunction)=>{//设置异步逻辑相关的三种状态const[data,setData]=useState(null);const[loading,setLoading]=useState(false);const[error,setError]=useState(null);//定义一个回调来执行异步逻辑constexecute=useCallback(()=>{//当请求开始时,设置加载为真并清除现有数据和错误状态setLoading(true);setData(null);setError(null);returnasyncFunction().then((response)=>{//当请求成功时,将数据写入state并设置loading为falsesetData(response);setLoading(false);}).catch((error)=>{//当请求失败时,设置loading为false并设置错误状态setError(error);setLoading(false);});},[asyncFunction]);返回{执行,加载,数据,错误};};上面的Hook可以用于业务调用。importReactfrom"react";importuseAsyncfrom"./useAsync";import{getDataMock}from"./mock";exportdefaultfunctionUserList(){//通过useAsync函数,只需要提供异步逻辑的实现const{execute:fetchUsers,data:users,loading,error}=useAsync(async()=>{constres=awaitgetDataMock();returnres.data;});返回({loading?"Loading...":"ShowUsers"}{error&&Failed:{String(error)}
}
);}完整代码已经放到sandxox上了,有兴趣的同学也可以运行.https://codesandbox.io/s/great-flower-o83v0?file=/src/list.js:0-786更多场景“模板方法”在框架中会更常见,比如我们平时写的vue,它内部定义了每个生命周期的执行顺序,然后给我们打开生命周期的钩子,我们就可以执行自己的操作了。”模板方法》如果说得更广泛一点,ElementUI的对话框也可以作为模板方法。这是一条消息取消确定el-dialog实现了Dialog的基本样式和行为,通过slots提供扩展,让我们实现自己个性化的东西。虽然我们不能在js中真正实现模板模式,但是我们还是实现了模板模式的作用,践行了“开闭原则”:对扩展开放:通过传入不同的参数,可以实现不同的应用需求。Closedformodification:模板方法采用闭包的形式,内部的属性和方法不能被外界修改。模板方法还提高了可重用性。我们可以把公共的部分提取到模板方法中,业务方不需要再去实现。