基于装饰器——我劝你不要在业务代码上装逼!!!
装饰器模式的定义
在传统的面向对象语言中,给对象添加功能常使用继承的方式,但继承的方式并不灵活,会带来一些许多问题,如:超类和子类存在强耦合性,也就是说当改变超类时,子类也需要改变。 而装饰器模式的出现改变的这种方式,装饰器模式可在不改变现有对象解构的基础上,动态地为对象添加功能
。
传统的 JavaScript 装饰器
var plane = {
fire : function ( ) {
console. log ( "普通子弹" ) ;
} ,
} ;
var missleDecorator = function ( ) {
console. log ( "发射导弹" ) ;
} ;
var atomDecorator = function ( ) {
console. log ( "发射原子弹" ) ;
} ;
var fire1 = plane. fire;
plane. fire = function ( ) {
fire1 ( ) ;
missleDecorator ( ) ;
} ;
var fire2 = plane. fire;
plane. fire = function ( ) {
fire2 ( ) ;
atomDecorator ( ) ;
} ;
plane. fire ( ) ;
装饰函数
在 JavaScript 中,几乎一切都是对象,其中函数也被成为对象,在平时的开发中,我们都在和函数打交道。在给对象扩展属性和方法时,很难在不改动原功能函数的情况下,给函数添加一些额外的功能,最直接的粗暴方式就是直接改写函数,但这是最差的方式,这违反了开放——封闭原则。 如下:
function a ( ) {
console. log ( 1 ) ;
}
function a ( ) {
console. log ( 1 ) ;
console. log ( 2 ) ;
}
很多时候,我们都不想去触碰之前的一些代码,但需要添加功能,所以如果需要在不改变原功能函数的情况下,给函数添加功能。可使用以下方式: 要想完美的给函数添加功能,可使用 AOP 来装饰函数
。
AOP:一种编程规范,通过将关注点从主业务逻辑中剥离出来并单独处理,以此来提高代码的可读性和重用性。 如下:
Function . prototype. before = function ( beforeFn ) {
var _self = this ;
return function ( ) {
beforeFn . apply ( this , arguments) ;
return _self . apply ( this , arguments) ;
} ;
} ;
Function . prototype. after = function ( afterFn ) {
var _self = this ;
return function ( ) {
var ret = _self . apply ( this , arguments) ;
afterFn . apply ( this , arguments) ;
return ret;
}
}
Function . prototype. before = function ( beforeFn ) {
var _self = this ;
return function ( ) {
beforeFn . apply ( this , arguments) ;
return _self . apply ( this , arguments) ;
} ;
} ;
Function . prototype. after = function ( afterFn ) {
var _self = this ;
return function ( ) {
var ret = _self . apply ( this , arguments) ;
afterFn . apply ( this , arguments) ;
return ret;
}
}
var o1 = function ( ) {
console. log ( '1' ) ;
}
var o2 = function ( ) {
console. log ( '2' ) ;
}
var o3 = function ( ) {
console. log ( '3' ) ;
}
var desctor = o1. after ( o2) ;
desctor = desctor. after ( o3) ;
desctor ( ) ;
AOP的应用
1.数据上报
在程序开发中,当业务代码开发完后,在结尾时需要加很多的日志上报的代码,普遍我们会去改已经之前封装好的功能函数。其实这并不是一个好的方式,那如何在不直接修改之前函数的基础上添加日志上报功能呢? 如下:
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" />
< title> AOP日志上报</ title>
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" />
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" />
< script src = " https://unpkg.com/vue@3.2.20/dist/vue.global.js" > </ script>
</ head>
< body>
< div id = " app" >
< button class = " btn" @click = " handler" > Button</ button>
< p id = " tt" > {{message}}</ p>
</ div>
</ body>
</ html>
< script type = " text/javascript" >
const { reactive, ref, createApp } = Vue;
const app = createApp ( {
setup ( ) {
const message = ref ( "未点击" ) ;
const count = ref ( 0 ) ;
Function . prototype. before = function ( beforeFn ) {
var _self = this ;
return function ( ) {
beforeFn . apply ( this , arguments) ;
return _self . apply ( this , arguments) ;
} ;
} ;
Function . prototype. after = function ( afterFn ) {
var _self = this ;
return function ( ) {
var ret = _self . apply ( this , arguments) ;
afterFn . apply ( this , arguments) ;
return ret;
} ;
} ;
function handler ( ) {
message. value = ` 已点击 ${ ++ count. value} ` ;
}
handler = handler. after ( log) ;
function log ( ) {
message. value = message. value + "-----> log reported" ;
console. log ( "log report" ) ;
}
return {
message,
handler,
} ;
} ,
} ) ;
app. mount ( "#app" ) ;
</ script>
2.动态参数
在日常开发中,我们需要向后台接口发送请求来获取信息,例如传参如下。业务在后续时需要添加新参数,每个接口需要把 token 值也一并传过去, 普遍我们会去改封装的请求方法,把 token 参数添加进去。但我们直接修改封装好的请求方法不是好的行为,那我们可使用上面说过的 AOP 方式来改进。
{
name : 'xxxx' ,
password : 'xxxx' ,
}
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" />
< title> AOP动态参数</ title>
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" />
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" />
< script src = " https://unpkg.com/vue@3.2.20/dist/vue.global.js" > </ script>
</ head>
< body>
< div id = " app" > {{message}}</ div>
</ body>
</ html>
< script type = " text/javascript" >
const { reactive, ref, createApp } = Vue;
const app = createApp ( {
setup ( ) {
const message = ref ( "empty params" ) ;
Function . prototype. before = function ( beforeFn ) {
var _self = this ;
return function ( ) {
beforeFn . apply ( this , arguments) ;
return _self . apply ( this , arguments) ;
} ;
} ;
Function . prototype. after = function ( afterFn ) {
var _self = this ;
return function ( ) {
var ret = _self . apply ( this , arguments) ;
afterFn . apply ( this , arguments) ;
return ret;
} ;
} ;
function ajax ( type, url, params ) {
message. value = ` ${ type} ----> ${ url} -----> ${ JSON . stringify ( params) } ` ;
}
function getToken ( ) {
return 'token' ;
}
ajax = ajax. before ( function ( type, url, params ) {
params. token = getToken ( ) ;
} )
ajax ( 'get' , 'https://www.baidu.com/userinfo' , { name : 'se' , password : 'xsdsd' } ) ;
return {
message,
} ;
} ,
} ) ;
app. mount ( "#app" ) ;
</ script>
3.表单校验
在日常开发中,我们经常要去做校验表单数据,通常的方式是在功能函数中进行判断处理或将判断逻辑提取为一个函数的方式。但这种方式其实是与功能性函数相混合,且校验逻辑与功能性函数有耦合关系。那我们可使用 AOP 方式来改进。 如下:
<! DOCTYPE html >
< html lang = " en" >
< head>
< meta charset = " UTF-8" />
< title> AOP表单验证</ title>
< meta http-equiv = " X-UA-Compatible" content = " IE=edge" />
< meta name = " viewport" content = " width=device-width, initial-scale=1.0" />
< script src = " https://unpkg.com/vue@3.2.20/dist/vue.global.js" > </ script>
</ head>
< body>
< div id = " app" >
< label>
姓名:
< input
type = " text"
v-model = " data.name"
placeholder = " 请输入姓名"
/>
</ label>
< label>
密码:
< input
type = " text"
v-model = " data.pass"
placeholder = " 请输入密码"
/>
</ label>
< p v-if = " data.name || data.pass" > {{data.name + '/' + data.pass}} ----after-----> {{data.message}}</ p>
< hr>
< button @click = " submitBtn" > submit</ button>
</ div>
</ body>
</ html>
< script type = " text/javascript" >
const { reactive, ref, createApp, watchEffect } = Vue;
const app = createApp ( {
setup ( ) {
const data = reactive ( {
name : "" ,
pass : "" ,
message : "" ,
} ) ;
Function . prototype. before = function ( beforeFn ) {
var _self = this ;
return function ( ) {
if ( beforeFn . apply ( this , arguments) === false ) return ;
return _self . apply ( this , arguments) ;
} ;
} ;
function valid ( ) {
if ( ! data. name || ! data. pass) {
alert ( "用户名或密码不能为空" ) ;
return false ;
}
}
function formSubmit ( ) {
console. log ( "data ------>" , data) ;
data. message = ` ${ data. name} ------- ${ data. pass} ` ;
}
formSubmit = formSubmit. before ( valid) ;
function submitBtn ( ) {
formSubmit ( ) ;
}
return {
data,
submitBtn,
} ;
} ,
} ) ;
app. mount ( "#app" ) ;
</ script>
装饰器模式的优缺点
优点:
扩展性强:装饰器模式允许在不修改现有代码的情况下,动态地添加新功能或修改现有功能。通过使用装饰器,可以在运行时按需组合和堆叠装饰器对象,实现各种组合方式,从而实现更多的功能扩展。 遵循开闭原则:装饰器模式通过添加装饰器类来扩展功能,而不是修改现有的代码。这样可以保持原有代码的稳定性,符合开闭原则,即对扩展开放,对修改关闭。 分离关注点:装饰器模式将功能的扩展和核心功能分离开来,每个装饰器类只关注单一的额外功能。这样可以使代码更加清晰、可读性更高,并且容易维护和测试。 缺点:
增加复杂性:使用装饰器模式会增加额外的类和对象,引入了更多的复杂性和层次结构。这可能使代码变得更加复杂,理解和调试起来可能更加困难。 潜在的性能影响:由于装饰器模式涉及多个对象的组合和堆叠,可能会引入额外的运行时开销,对性能产生一定的影响。尤其是当装饰器链较长时,可能会导致性能下降。
装饰器模式的适用场景
动态地扩展对象功能:当需要在运行时动态地为对象添加额外的功能或责任时,装饰器模式是一个很好的选择 遵循开闭原则:如果你希望在不修改现有代码的情况下扩展功能,而且要保持代码的稳定性,装饰器模式是一个合适的解决方案。 分离关注点:当你希望将不同的功能分离开来,使每个功能都有自己独立的装饰器类时,装饰器模式是有用的。每个装饰器只关注单一的额外功能,这样可以使代码更加清晰、可读性更高,并且容易维护和测试。 多层次的功能组合:如果你需要实现多个功能的组合,而且每个功能都可以灵活选择是否添加,装饰器模式可以很好地满足这个需求。通过堆叠多个装饰器对象,可以按照特定的顺序组合功能,实现各种组合方式。 继承关系的替代方案:当你面临类似于创建大量子类的情况时,装饰器模式可以作为继承关系的替代方案。通过使用装饰器模式,可以避免创建过多的子类,而是通过组合不同的装饰器来实现不同的功能组合。
Tip: 文章部分内容参考于曾探
大佬的《JavaScript 设计模式与开发实践》。文章仅做个人学习总结和知识汇总