zhangxiang958 / Blog

阿翔的个人技术博客,博文写在 Issues 里,如有收获请 star 鼓励~
https://github.com/zhangxiang958/zhangxiang958.github.io/issues
153 stars 11 forks source link

浅谈设计模式实战 #27

Open zhangxiang958 opened 5 years ago

zhangxiang958 commented 5 years ago

设计模式, 分为三类: 构造型, 结构型, 行为型, 共 23 种. 设计模式不分语言, 只是在构造模式的时候会根据语言的特性不同而不同, 但是它的思想是始终受用的. 设计模式在我们日常写代码的时候常常会遇到, 会帮助我们以更好的方式去组织我们的代码.

闭包与内存管理

其实对于在 Javascript 中,设计模式的运用是少不了函数的封装的,也就是会大幅度使用闭包,但是不管是用还是不用闭包,数据都是存在内存中的,而且闭包保持对变量的引用,加快了函数的执行速度。

单例模式

个人认为在 Javascript 中实现单例模式的方式有很多,其中它的思想就是将代码集中起来,或者构建出一个类,充分体现面对对象的思想。举个例子:

var App = {
    init: function(){

    },
    getStatus: function(){

    },
    ...
    sendRequest: function(){

    }
}

像上面这样就使用了单例模式,这个在日常开发非常常用,在应用初始化的时候,可以直接使用 App.init() 这样一个方法。 其实也可以使用面对对象的方法去写:

function App() {
    this.status = 'start';
}

App.prototype.init = function(){

}

App.prototype.getStatus = function(){

}

App.prototype.sendRequest = function(){

}
....

单例模式的表现形式可以是多样的,但是重要的是在于它将代码集中起来的思想。以及它在各个模块之间的协调通讯上的作用。

策略模式

所谓策略模式,用简单的例子来说就是你去学校可能有多个路线,比如公交车,地铁,开车,骑车等等,但是你的目的地只有一个--学校,那么这个时候,就可以将这些不同方式同一个目的的方法集合起来。

var goToSchool = {
    bus: function(){},
    subway: function(){},
    drive: function(){},
    bike: function(){}
    ....
}

像 Jquery 里面的运动函数,那些 easing 运动函数等等是原本就有的,我们需要增加不同的运动方式的时候,只需要在 $.easing 上面添加新的函数($.easing.move = function(){...})这样在不影响源代码的基础上,我们新增了功能。 在我们日常工作中,也会有使用策略模式的场景,举个例子:当前端与后端对接接口的时候,后端返回来的数据并不是都是可以直接使用的,比如后端返回时间戳,前端需要将时间戳转换为合适的时间格式,或者后端返回一些字符,前端需要将字符进行转码才能显示在页面中,那么这个时候如果每个数据字段在 ajax 中都是用一个函数来进行转化数据,这样的代码组织就会非常混乱,如果我们能够将转码的代码集中起来是非常整洁的。举个例子:

var format = {
    transforTime: function(argv){
        .....
    },
    transforText: function(code){
        ....
    }
}

$.ajax({
    url: ....,
    success: function(data){
        data.time = format.transforTime(data.time);
        data.code = format.transforTime(data.code);
    },
    error: function(){

    }
});

上面的例子可能有点简单, 我们来看一个更有意义的代码:

var validator = {
    //验证类型
    types: {},
    //错误信息
    messages: [],
    //必需的验证类型
    config: {},
    validate: function(data) {

        this.messages = [];

        for(var i in data) {
            if(data.hasOwnPorperty(i)) {

                var type = this.config[i];
                var cheaker = this.types[i];

                if(!type) {
                    continue;
                }
                if(!cheaker) {
                    throw {
                        name: 'error',
                        message: 'no such a cheaker: ' + type
                    }
                }
                if(!cheaker.validate(data[i])) {
                    msg = 'invalid value for' + type;
                    this.meaages.push(msg);
                }
            }
        }
    }
}

validator.types.isString = {
    validate: function(data){
        return typeof data === 'String'
    }
}
validator.types.isNumber = {
    validate: function(data){
        return typeof data === 'Number'
    }
}

var data = {
    font: 'test',
    size: 12
}

validator.config = {
    font: 'isString',
    size: 'isNumber'
}

validator.validate(data);

上面的这个是一个经典的例子, 将验证数据的验证器与配置封装到一个, 并且可以随时添加验证规则.

开放封闭

策略模式是符合开放封闭原则的, 在不改动源代码的基础上,通过增加代码,达到增加功能的目的。

迭代器模式

所谓迭代器模式就是使用一个函数去完成一个循环的工作, 并完成一些回调的工作. 像 Javascript 本身就内置了很多迭 代器, forEach, map, every, filter, reduce 等等的. 如果我们不用迭代器, 我们就会使用 for 循环:

for (var i = 0; i < arr.length; i++) {
    //do something
}

但是这样做的弊端在于循环代码与数据紧紧结合在一起, 并且需要知道内部结构才能进行循环, 如果数据从数组变成对象 可能就不适用了, 不利于代码的复用.

Array.prototype.forEach(function(value, index){

});

Array.prototype.every(function(value, index){

});

=>
Array.prototype.forEach = function(func){
    var len = this.length;
    for(var i = 0; i < len; i++) {
        func(this[i], i);
    }
}

像上面这些迭代器就能很好地复用了.适配器用于创建一个复用的代码, 使一些原本不相关不可见的对象协同工作.

单一职责原则

让函数只负责一个功能, 职责分离, 可以提高代码的可重用性.

装饰者模式

在不改变原有函数的基础上,添加新的功能。其实它算是继承的替代方法, 在我们日常的开发中, 对象继承等会用的比较多, 但是又不适宜每个功能都使用继承或者创建一个类, 这样会导致类的泛滥与破坏了封装性, 比如需要使用竹蜻蜓这个功能, 但是我并不需要知道使用者是大雄还是哆啦 A 梦(大雄和哆啦 A 梦可是不同的类),如果使用继承, 传建用竹蜻蜓的大雄或者哆啦 A 梦, 这样又会显得臃肿, 因为竹蜻蜓并不需要知道使用者是谁, 所以使用装饰者模式会让代码更加有复用性.举个一般的例子:

//装饰函数
var a = function(){
    console.log('a');
}

var _a = a;

a = function(){
    _a();
    console.log('decorate');
}

本质思想就是动态地添加功能, 但是不改变原有代码, 像上面的例子一样, a 函数还是可以正常调用, 但是就是添加了装 饰功能.举个日常使用的例子: 为请求添加蒙版

//下面是伪代码, 并不严谨, 意在体现思想.
var _ajax = $.ajax;
$.ajax = (function(){

  var mask = `<div class="loading">loading...</div>`;
  var body = document.body;

  return function(options){
    body.appendChild(mask);

    return _ajax(options).then((res) => {

        body.removeChild(mask);
        return res;
    });
  }

}());

这里就可以改造 ajax 了, ajax 还是像原来一样使用, 但是我们为它添加了添加蒙版的效果.

AOP

AOP 就是面向切面编程, 也就是装饰者模式的改良, 上面的代码我们会发现一个问题, 如果需要改造的代码比较多, 那么有可能创建了很多无谓的全局变量(_ajax), 而且我们仔细想想, 所谓装饰者模式, 其实就是在原函数执行前或者执行后需要添加一些其他的功能吗? 那我们完全可以封装一个便利的函数呀.就直接取名为 before 和 after.

Function.prototype.before = function(func){
    var origin = this;
    return function(){
            func.apply(this, arguments);
            return origin.apply(this, arguments);
    }
};

Function.prototype.after = function(func){
    var origin = this;
    return function(){
        var res = origin.apply(this, arguments);
        func.apply(this, arguments);
        return res;
    }
};

var a = function(){
    console.log('a');
}

a = a.before(function(){
    console.log('ready for a');
});

a = a.after(function(){
    console.log('end a');
});

a();

有什么用处呢? 我们知道产品常常需要一些数据配合研究用户行为, 举个例子他们需要知道点击这个按钮的用户有多少, 但是这个按钮本身肯定还有其他的功能, 如果写在原本功能函数里面肯定会受影响(代码不美观, 耦合严重), 严重的问题在于数据上报一般是在功能开发完成之后加上去的, 种种原因, 如果我们使用 AOP 来改良代码不仅可以轻耦合, 而且美观, 何乐而不为? 另外它还可以动态添加函数参数, 比如 CSRF 攻击函数的 token 参数, 其实这个函数与业务数据毫无关系, 可以动态添加.还有就是与策略模式的结合, 使用上面的策略模式的校验函数, 也就是说在表单提交数据之前, 可以使用 before 来调用校验, 不再需要在业务提交表单的代码中耦合校验, 甚至不会看到校验函数的调用在表单提交代码中.

订阅发布者模式

订阅发布者模式也就叫监听者模式,我们在前端开发中经常会用到,但是我们没有发觉而已。当我们在 DOM 元素上添加 事件:

HTMLDOMElement.on('click', function(){
    console.log("click!");
});

上面的元素当 click 这个事件发生的时候就会执行相对应的回调函数, 在事件还没有发生之前, 浏览器一直监听有没有 相应的事件发生, 如果有则触发函数.对于在日常, 我们除了会使用 DOM 相关事件, 也会使用 DOM 无关事件, 这些事件 是我们自定义的, 我们自己定义这个事件的含义, 让一些模块解耦, 让模块更加清晰.举个简单的例子: 我们在开发中肯定会遇到类似登录这种情况, 但是当登录成功之后, 网页很多地方会相应地去变化, 比如头像改变, 名称 改变, 加载好友列表, 加载用户历史记录等等的行为, 如果这些功能分模块到不同的人去完成,如果我们不进行节解耦,那 么可能登录模块就会像下面这样:

function Login(){

    if(loginSuccess) {

        Avatar.init();
        Name.init();
        FriendList.init();
        HistoryList.init();
    }
}

这样负责登录模块的人员必需知道各个模块的名字与初始化函数的名字, 而且这些初始化的函数名还不能轻易改, 因为会 牵扯到登录模块的修改.同时如果需要添加模块, 那么负责登录这个人员又再一次添加进这个模块的初始化代码, 这样非 常耦合, 如果我们能够使用订阅发布者模式来组织模块之间的通讯的话, 那么就会好很多.

//登录模块只需要在登录成功后发布一个 login 事件
Login.publish('login');

//在其他模块只需要监听这个事件,然后执行相应函数
Avatar.on('login', function(){
    ...doinit
});

Name.on('login', function(){
    ...doinit
});

FriendList.on('login', function(){
    ...doinit
});

HistoryList.on('login', function(){
    ...doinit
});

虽然这样还是有存在一些轻耦合, 各个模块需要知道 login 模块的完成登录事件名是什么, 但是已经比原来的耦合程度 好的多了. 详细可以看我的个人写的一个 demo: Eventemitter

代理模式

和迭代器模式类似, 代理模式也符合单一职责原则.其实代理模式与装饰者模式非常像, 都是通过动态添加职责.但是它们 不同点在于说设计的意图不同, 装饰者模式是预先并不知道本体的功能, 动态地去添加职责, 而代理模式其实本质上做的 和本体的工作是一样的, 也就是代理对象与本体之间的关系是一开始就确定的.代理只不过是添加了一些聪明的机制更好 地完成工作, 所以说撩妹的时候一个聪明的电灯泡太重要了, 而装饰者模式会形成长长的职责链.举个例子: 像图片预加载, 为了用户体验应该在图片还没有加载出来之前替换成一个菊花图, 加载完成再更换成图片:

var image = (function(){
    var image = document.createElement('img');
    document.body.appendChild(image);

    return function(src){
        image.src = src;
    }
}());

var proxyImage = (function(){
    var img = new Image();

    img.onload = function(){
        image(this.src);
    }

    return function(src){
        image('loading.gif');
        img.src = src;
    }
}());

这里的代理函数为原本加载函数添加了一个 loading 效果, 增强了功能, 万一我们以后不需要 loading 效果了, 只需要切换使用本体函数 image 就可以了, proxyImage 不影响原本的函数. 其实代理模式的本质还是说职责单一原则, 与职责分离, 让单个函数负责单一的功能, 像缓存代理, 也就是将一些运行过的结果缓存起来,比如加减乘除这样, 使用一个 proxy 函数代理起来, 将结果缓存在一个对象里面, 但是里面的 add, minus 等等的函数还都只是负责数与数之间的运算而已.

外观模式

我们很常用,对于一个模块,我们通常使用一个 init 函数,但是其实 init 函数里面可以包含了很多函数的初始化操作这样可以屏蔽子摸块的复杂性, 抽象一个高层接口.

var APP = {
    init(){
        getConfig();
        initEvent();
        render();
    },
    getConfig(){
        ...
    },
    initEvent(){
        ....
    },
    render(){
        ...
    }
}

App.init();

职责链模式

日常开发我们肯定会遇到很多需要判断的场景, 比如输入一个数字, 在 0 - 100 的区间, 会有不同的意义, 最直观的例子就是在学生分数 90 - 100 是优秀, 在 80 - 89 是良好, 在 70 - 79 是中等, 在 60 - 69 是及格, 在 0 - 60 是不及格, 有可能区间会更细, 但是这里如果不断使用 if...else 来写不仅会很 low, 更重要的是这样的代码逻辑会很复杂很难让后来人看懂, 更加难去维护.

function checkMark(num) {
    if(num >= 90 && num <= 100) {
        ...
    } else if(num >= 80 && num <= 89) {
        ...
    } else if(num >= 70 && num <= 79) {
        ...
    } else if(num >= 60 && num <= 69){
        ...
    } else {
        ....
    }
}

会不会觉得上面的代码似曾相识? 虽然你可以使用一些函数比如 lowerThan90 这样去拆分上面的链条, 但是并不灵活, 万一一天修改规则为 80 以上是优秀, 那么就很难修改, 说明代码耦合得比较严重:

function checkMark(num) {
    if(num >= 90 && num <= 100) {
        ...
    } else {
        lowerThan90(num);
    }
}

function lowerThan90(num) {
    ....
}

其实职责链的模式和很多东西都非常像, 和 nodejs 的中间层, promise 模式其实都有点思想上的雷同.

function Chain(func){
    this.func = func;
    this.next = null;
}

Chain.prototype.setNext = function(fn){
    return this.next = fn;
}

Chain.prototype.pass = function(){
    var res = this.func.apply(this, arguments);
    if(res === 'next') {
        return this.next && this.next.apply(this.next, arguments);
    }

    return res;
}

上面是一个简单的例子, 但是我还是觉得如果用装饰者模式会更简洁.

//下面为伪代码:
Function.prototype.next = function(func){
    var origin = this;
    return function(){
        var res = origin.apply(this, arguments);

        if(res === 'next') {
            return func.apply(this, arguments);
        }

        return res;
    }
}

var res = check.next(lowerThan90).next(lowerTahn80)...;

优点在于可控, 可以将某个节点跳过不做, 这是 if...else 没办法做到的, 另外就是将发散型的函数变成了链式的模式,更加清晰, 缺点在于链条过长性能会比较差, 所以使用职责链链条不要过长.