设计模式总结2

4.行为型设计模式

行为型设计模式用于不同对象之间职责划分或算法抽象,行为型设计模式不仅仅涉及类和对象,还设计类或对象之间的交流模式并加以实现。

4.1 模板方法模式

模板方法模式:父类中定义一组算法骨架,而将一些实现步骤延迟到子类中,使得子类可以不改变父类的算法结构的同时可重新定义算法中某些实现步骤

1.需求分析

项目经理:小白,咱们页面中的提示框样式能否统一,我发现每个页面的提示框真是千奇百怪,根本不一致,你能不能把他们归一化。

小白:小白听呆了,可是我要去一个一个页面修改么?

项目经理:当然不是,我的意思是让你写一个弹出框插件,将这些类弹出框封装好,然后再各个页面中重新调用就可以了,日后有什么需求直接修改这个插件就行了。

小白:那这个插件中我要写多少种弹出框呀

小明:不用写那么多,你试一试模板方法模式吧。模板方法模式就是将多个模型抽象化归一,从中抽象提取出来一个最基本的模板,当然这个模板可作为实体对象也可以作为抽象对象,这要看你需求了,然后其他模板只需要继承这个模板方法,也可以拓展某些方法

小白:不太理解,能举个例子吗?

小明:生活中用蛋糕模具做蛋糕,做出的蛋糕是外形相同的,因为他们用的同一个模具,这是最基本的一个蛋糕。当然我们看到商店里面的蛋糕五花八门,有的是奶油,有的是巧克力,有的是果汁,这些都是对蛋糕的二次加工,也就是顾客对蛋糕的不同的需求,所以说我们需求方案中提到的基本提示框应该就是我们抽象出来的,因为它是最简单的一个提示框,其他提示框都比这个提示框要多一些功能,也就是说我们要对这个添加’佐料“让其满足用户的需求。比如标题提示框多了一个标题组件,取消按钮提示框多了一个取消按钮组件,但是首先要实现最基础的提示框。

2.创建基本提示框

小白:你这么说我有点明白了,我首先要做的就是创建一个基本提示框基类,然后其他提示框类只需要在继承基础上,拓展自己所需即可了吧,这样日后需求再变动我们修改基类,那么所有提示框类实现的样式都会统一变化了。

那我先实现基本提示框吧,他有一个提示内容,一个关闭按钮和确定按钮,如下代码

//模板类,基础提示框data渲染数据
var Alert=function(data) {
    //没有数据则返回,防止后面程序执行
    if(!data) {
        return;
    }
    //设置内容
    this.content=data.content;
    //创建提示框面板
    this.panel=document.createElement('div');
    //创建提示内容组件
    this.contentNode=document.createElement('p');
    //创建确定按钮组件
    this.confirmBtn=document.createElement('span');
    //创建关闭按钮组件
    this.closeBtn=document.createElement('b');
    //为提示框面板添加类
    this.panel.className='alert';
    //为关闭按钮添加类
    this.closeBtn.className='a-close';
    //为确定按钮添加类
    this.confirmBtn.className='a-confirm';
    //为确定按钮添加文案
    this.confirmBtn.innerHTML=data.confirm|| '确认';
    //为提示内容添加文案
    this.contentNode.innerHTML=this.content;
    //点击确定按钮执行方法,如果data中有success方法则为success方法,否则为空函数
    this.success=data.success||function(){};
    //点击关闭按钮执行方法
    this.fail=data.fail|| function() {};
}

既然这个基本提示框是可创建的,那么它也应该有一些基本方法,比如应该有init方法来组装提示框,bindEvent方法来绑定点击确定或者关闭按钮事件等等。如下代码:

//提示框原型方法
Alert.prototype = {
    //创建方法
    init: function () {
        //生成提示框
        this.panel.appendChild(this.closeBtn);
        this.panel.appendChild(this.contentNode);
        this.panel.appendChild(this.confirmBtn);
        //插入页面中
        document.body.appendChild(this.panel);
        //绑定事件
        this.bindEvent();
        //显示提示框
        this.show();
    },
    bindEvent: function () {
        var me = this;
        //关闭按钮点击事件
        this.closeBtn.onclick = function () {
            //执行关闭取消方法
            me.fail();
            //隐藏弹层
            me.hide();
        }
        //确定按钮点击事件
        this.confirmBtn.onclick = function () {
            //执行关闭确认方法
            me.success();
            //隐藏弹层
            me.hide();
        }
    },
    //隐藏弹层
    hide: function () {
        this.panel.style.display = 'none';
    },
    //显示弹层
    show: function () {
        this.panel.style.display = 'block';
    }
}

3.根据模板创建类

有了这个提示框 基类,再想拓展其他类型弹层就容易多了,比如右侧按钮提示框。

//右侧按钮提示框
var RightAlert=function(data) {
    //继承基本提示框构造函数
    Alert.call(this,data);
    //为确认按钮添加right类设置位置居右
    this.confirmBtn.className=this.confirmBtn.className+" right";
}
//继承基本提示框方法
RightAlert.prototype=new Alert();
console.log(RightAlert);

创建一个提示框,如下代码

new RightAlert({
    content: '提示内容',
    success: function () {
        console.log('ok');
    },
    fail: function () {
        console.log('cancel')
    }
}).init();

其他类型的就直接继承这个基本提示框,然后再增加自己需要的

4.模板方法模式总结

模板方法的核心在于对方法的重用,它将核心方法封装在基类中,让子类继承基类的方法,实现基类方法的共享,达到方法共用。当然这种设计模式也将基类控制子类必须遵守某些法则。这是一种行为的约束。当然为了让行为的约束更可靠,基类中封装的方法通常是不变的算法,或者具有稳定的调用方式。

子类继承父类的方法亦是可以扩展的,这就要求对基类继承的方法进行重写。为了更好的实践,我们通常要控制这种拓展,这样才能让基类对子类有更稳健的束缚力。然而子类对自身私有行为的拓展还是很有必要的。

4.2 状态模式

状态模式:当一个对象的内部发生改变时,会导致其行为的改变,这看起来像是改变了对象。

1.需求分析

月底公司要开展月末最美图片评选活动,要开展一个投票征集活动,让网友投票选出我们本月最美的图片。根据网友的投票,每张图片有以下几种结果....

小白:实现这样的需求我要做多少分支判断呀,如果我将所有图片的结果展示用一个函数封装,内部也不免有多个分支判断,如下代码:

function showResult(result) {
    if (result == 0) {
        //处理结果0
    } else if (result == 1) {
        //处理结果1
    } else if (result == 2) {
        //处理结果2
    } else if (result == 3) {
        //处理结果3
    }
}

小白去找小明:如果项目经理哪天心血来潮,想增删结果,我的工作岂不是要悲剧了。小明,有什么办法可以减少代码中的条件判断语句么?并且使每种判断情况独立存在,这样更方便管理。

小明:对于这类分支条件内部独立结果的管理,我想状态模式应该会很适合,每一种条件作为对象内部的一种状态,面对不同判断结果,它其实就是选择对象内的一种状态

小白:结果,对象内部的状态,你把我说蒙了,举例说一下吧

小明:对于我们这个简单的例子,我们可以将不同的判断结果封装在状态对象内,然后该状态对象返回一个可被调用的接口方法,用于调用状态对象内部某种方法,如下代码:

//投票结果状态对象
var ResultState = function () {
    //判断结果保存在内部状态中
    var States = {
        //每种状态作为一种独立方法保存
        state0: function () {
            //处理结果0
            console.log('这是第一种情况')
        },
        state1: function () {
            //处理结果1
            console.log('这是第二种情况')
        },
        state2: function () {
            //处理结果2
            console.log('这是第三种情况')
        },
        state3: function () {
            //处理结果3
            console.log('这是第四种情况')
        },
    }

    //获取某一种状态并执行其对应的方法
    function show(result) {
        state['state' + result] && state['state' + result]();
    }
    return {
        //返回调用状态方法接口
        show: show
    }
}();

如果我们想调用第三种结果,按照下面的方式来实现

ResultState.show(3)
//这是第四种情况

上面的简单案例显示了状态模式的基本雏形,对于状态模式,主要目的就是将条件判断的不同结果转化为状态对象的内部状态,既然是状态对象的内部状态,所以一般作为状态对象内部的私有变量,然后提供一个能够调用状态对象内部状态的接口方法对象。这样当我们需要增加、修改、调用、删除某种状态方法时就会很容易,也方便了我们对状态对象内部状态的管理。

2.实现过程

上面听起来很不错,如何实现呢?举个例子,还记得超级玛丽游戏 吗?玛丽要吃蘑菇,那么它就会跳起来,顶出蘑菇;玛丽想到悬崖的另一边,它就要跳起来;玛丽想避免被前面的乌龟咬到,它就要开枪将其打掉;前方飞过炮弹,玛丽要躲避。

跳跃,开枪,蹲下,奔跑,这些都是一个一个状态,如果我们用if或者switch条件判断语句写的代码一听就不靠谱,而且也是很多维护的。因为增加或者删除一个状态需要改动的地方太多了。此时使用状态模式就再好不过了。对于玛丽,有的时候它需要跳跃开枪,有的时候它需要蹲下开枪,有的时候适合奔跑开枪 ,如果这些组合状态用if或者swtich判断去实现,无形中增加的成本是无法想象的,举例如下:

//单条件判断,每增加一个动作就增加一个判断
var lastAction='';
function changeMarry(action) {
    if(action=='jump') {
        //跳跃动作
    }else if(action=='move') {
       //移动动作 
    }else {
        //默认情况
    }
    lastAction=action;
}

//复合条件判断对于条件判断的开销是翻倍的
var lastAction1='';
var lastAction2='';
function changeMarryComp(action1,action2) {
    if(action1=='shoot') {
        //射击
    }else if(action1=='jump'){
        //跳跃
    }else if(action1=='move' && action2=='shoot') {
        //移动中射击        
    }else if(action1=='jump' && action2=='shoot'){
        //跳跃中射击
    }
    //保留上个动作
    lastAction1=action1;
    lastAction2=action2;
}

3.状态的优化

如上代码,即使判断语句实现了我们的需求,其代码结构、代码的可读性以及代码的可维护性都是很糟糕的,这样日后对新动作的添加或者原有动作修改的成本都是很大的,甚至会影响到其他动作。为了解决这一类问题我们可以使用状态模式。具体的思路是这样的:首先创建一个状态对象,内部保存状态变量,然后内部封装好每种动作对应的状态,最后状态对象返回一个接口对象,它可以对内部的状态修改或者调用。如下代码

//创建超级玛丽状态类
var MarryState = function () {
    //内部状态私有变量
    var _currentState = {},
        //动作与状态方法映射
        states = {
            jump: function () {
                console.log('jump')
            },
            move: function () {
                console.log('move');
            },
            shoot: function () {
                console.log('shoot');
            },
            squat: function () {
                console.log('squat');
            }
        };
    //动作控制类
    var Action = {
        //改变状态方法
        changeState: function () {
            //组合动作通过传递多个参数实现
            var arg = arguments;
            //重置内部状态
            _currentState = {};
            //如果有动作则添加动作
            if (arg.length) {
                //遍历动作
                for (var i = 0, len = arg.length; i < len; i++) {
                    //向内部状态中添加动作
                    _currentState[arg[i]] = true;
                }
            }
            //返回动作控制类
            return this;
        },
        //执行动作
        goes: function () {
            console.log('触发一次动作');
            //遍历内部状态保存的动作
            for (var i in _currentState) {
                //如果该动作存在就执行
                states[i] && states[i]();
            }
            return this;
        }
    }

    //返回接口方法change、goes
    return {
        change: Action.changeState,
        goes: Action.goes
    }
}

如果我们的超级玛丽顺利瞬间出来,当我们想使用它的时候容易。我们有两种方法,如果你喜欢函数方法,可以直接执行这个状态类,但是只能由你自己使用,如果别人使用的时候就可能会修改状态类内部状态。

MarryState()
    .change('jump', 'shoot')   //添加跳跃和射击动作
    .goes()             //执行
    .goes()             //执行
    .change('shoot')  //添加射击动作
    .goes(); //执行

    // 触发一次动作
    // jump  
    // shoot
    // 触发一次动作
    // jump
    // shoot 
    // 触发一次动作  
    // shoot

为了安全我们还是实例化一下这个状态类,这样我们使用的就是对状态类的一个复制品,这样无论你怎么使用,都可以放心了。

var marry=new MarryState();
marry
    .change('jump', 'shoot')   //添加跳跃和射击动作
    .goes()             //执行
    .goes()             //执行
    .change('shoot')  //添加射击动作
    .goes(); //执行

看起来思路很清晰呀,我们改变了状态类的一个状态,就改变了状态对象的执行结果,看起来好像变了一个对象似的。

4.状态模式总结

状态模式既是解决程序中臃肿的分支判断语句问题,将每个分支转化为一种状态独立出来,方便每种状态的管理又不至于每次执行时遍历所有分支。在程序中到底产出哪种行为结果,决定于选择哪种状态,而选择哪种状态又是在程序运行时决定的。当然状态模式最终的目的即是简化分支判断流程。

4.3 策略模式

策略模式:将定义的一组算法封装起来,使其相互之间可以替换。封装的算法具有一定独立性,不会随客户端变化而变化。

1.需求分析

每年年底,公司商品促销页都要开展大型促销活动。

小明:小白,快到年底了,咱们的商品拍卖页要准备办一些活动来减轻库存压力。在圣诞节,一部分商品5折出售,一部分商品8折出售,还有9折出售,等到元旦。要搞个幸运反馈活动,普通用户满100返30,高级VIP用户满100返50

小白:促销就促销,干嘛弄这么多情况,我还要一个一个写,得工作到什么时候,难道我还要一个一个地实现每一种促销策略。如下代码:

//100返30
function return30(price) {
    
}
//100返50
function return50(price) {
    
}

//9折
function percent90(price) {
    
}

忽然间小白脑海中闪现上次学习的状态模式,它不就是用来处理多种分支判断的么?可又一想,对于圣诞节或者元旦,当天的一种商品只会有一种促销策略,而不用去关心其他促销状态,如果采用状态模式那么就会为每一种商品创建一个状态对象,这么做是不是就冗余了呢?于是找到小明,将自己遇到的问题以及想法说了一下

小明:你说的对,对于一种商品的促销策略只用一种情况,而不需要其他促销策略,此时用策略模式会更合理。

2.策略模式

小白:策略模式?也是一种处理多种分支判断的模式么?

小明:从结构上看,它与状态模式很像,也是在内部封装一个对象,然后通过返回的接口对象实现对内部对内部对象的调用,不同点是:策略模式不需要管理状态、状态间没有依赖关系、策略之间可以相互替换、在策略对象内部保存的是相互独立的一些算法。他就像是一个活诸葛,对于一件事情的处理,总有千万种计谋,每次都可以随心所欲地选择一种计谋来达到不同种结构。所以你也可以将你的促销策略放在活诸葛的心中,让他类帮你解决问题。

为实现对每种商品的策略调用,你首先要将这些算法封装在一个策略对象内,然后对每种商品的策略调用时,直接对策略对象中的算法调用即可,而策略算法又独立地分装在策略对象内。为方便我们的管理与使用,我们需要返回一个调用接口对象来实现对策略算法的调用。如下代码:

//价格策略对象
var PriceStrategy = function () {
    //内部算法对象
    var stragtegy = {
        //100返30
        return30: function (price) {
            //+price转化为数字类型
            return +price + parseInt(price / 100) * 30;
        },
        //100返50
        return50: function (price) {
            return +price + parseInt(price / 100) * 50;
        },
        //9折
        percent90: function (price) {
            //javascript在处理小数乘除法时有bug,故运算前转化为整数
            return price * 100 * 90 / 1000;
        },
        //8折
        percent80: function (price) {
            return price * 100 * 80 / 1000;
        },
        //5折
        percent50: function (price) {
            return price * 100 * 50 / 1000;
        }
    }

    //策略算法调用接口
    return function (algorithm, price) {
        //如果算法存在在,则调用
        return stragtegy[algorithm] && stragtegy[algorithm](price);
    }
}();

我们的活诸葛(策略对象)已经创建出来,接下来让活诸葛为我们奉献一种奇谋吧。当然我们也要告诉活诸葛我们需要的那类算法以及处理哪些东西,如下所示:

var price=PriceStrategy('return50','316.67');
console.log(price);
//466.67

策略模式使我们在外部看不到算法的具体实现,是不是说我们只关注算法的实现结果,不需要了解算法的实现过程。如果这样的话,只需要简单通过策略对象的接口方法直接调用内部封装的某种策略方法了,方便极了。

3.表单验证

很多地方都可以用到策略模式,比如下面代码的表单验证模块,验证方法就是一组正则算法:

//表单正则验证策略对象
var InputStrategy = function () {
    var stragtegy = {
        //是否为空
        notNull: function (value) {
            return /\s+/.test(value) ? '请输入内容' : '';
        },
        //是否是一个数字
        number: function (value) {
            return /\s+/.test(value) ? '请输入内容' : '';
        },
        //是否是本地电话
        phone: function (value) {
            return /\s+/.test(value) ? '请输入内容' : '';
        }
    }

    return {
        check: function (type, value) {
            return stragtegy[type] ? stragtegy[type](value) : 没有该类型的检测方法
        },
        addStrategy: function (type, fn) {
            stragtegy(type) = fn;
        }
    }
}();

上面的例子和前面的竞价活动有一个小小的区别,就是添加了一个添加策略接口,因为已有的策略再多,也是不能满足需求的。通过下面的代码就可以添加其他策略算法了。

InputStrategy.addStrategy('nickName',function(value){
    return /\s+/.test(value) ? '请输入内容' : '';
})

4.策略模式总结

策略模式最主要的特色是创建一系列策略算法,每组算法处理的业务都是相同的,只是处理的过程或者处理的结果不一样,所以他们又是可以相互替换的,这样就解决了算法与使用者之间的耦合。在测试层面讲,由于每组算法相互之间的独立性,该模式更方便于对每组算法进行单元测试,保证算法的质量。

对于策略模式的优点:

1.策略模式封装了一组代码簇,并且封装的代码相互之间独立,便于对算法的重复引用,提高了算法的复用率;

2.策略模式与继承相比,在类的继承中继承的方法是被封装在类中,因此当需求很多算法时候,就不得不创建出多种类,这样会导致算法与算法使用者耦合在一起,不利于算法的独立演化,并且在类的外部改变类的算法难度也是极大的。

3.同状态模式一样,策略模式也是一种优化分支判断语句的模式。采用策略模式对算法封装使算法更利于维护。

策略模式的缺点:

由于选择哪种算法的决定权在用户,所以对用户来说就必须了解每种算法的实现,这就增加了用户对策略对象的使用成本。其次,由于每种算法间相互独立,这样对于一些复杂的算法处理相同逻辑的部分无法实现共享,这就会造成资源的浪费,可以通过享元模式来解决。

对于分支语句的优化,目前我们已经学习了3种模式,分别为工厂方法模式,状态模式与策略模式。

对于工厂方法模式来说,它是一种创建型模式,他的最终目的是创建对象。

而状态模式与策略模式都是行为型模式,不过在状态模式中,其核心是对状态的控制来决定表现行为,所以状态之间通常是不能相互替代的,否则会产生不同的行为结果。

而策略模式核心是算法,由于每种算法要处理的业务逻辑相同,所以他们可以相互替换,当然策略模式并不关心使用者环境,因为同一种策略模式最终产出的结果是一定的。

4.4 观察者模式

观察者模式:又被称作发布-订阅模式或消息机制,定义了一种依赖关系,解决了主体对象与观察者之间功能的耦合

1.需求分析

时间过得真快,小白开始了团队代码开发,这样,需求的研发中经常遇到一个人负责一个模块,可是每个人负责的模块之间该如何进行信息沟通呢?

小白:小明,今天写新闻评论模块,它的需求是这样的,当用户发布评论时,会在评论展示模块末尾处追加新的评论,与此同时用户的消息模块的消息数量也会递增。如果用户删除留言区的信息时,用户的消息模块的信息数量也会递减,但是今天我浏览了一下这些模块的代码,发现他们是三位不同的工程师写的,都写在自己的独立闭包里,现在我要完成我的而需求,又不想他们的模块合并在一起,这样我改动量很大,可是我该如何解决呢?

小明:哦,听明白了,你是想实现你的需求而添加而添加一些功能代码,但又不想新添加的代码影响他人实现的功能。也就是说,你不想让你自己的模块与其他开发的模块严重耦合在一起吧。对于这类问题,观察者模式是比较理想的解决方法

小白:观察者模式?可以解开我与他们之间的功能偶偶和么?

小明:观察者模式也被称为消息机制或者发布-订阅者模式,为了解决主体对象与观察者之间功能的耦合,举个例子,目前每个国家都在研发并发射卫星,那么发射的卫星有什么用呢?

小白:是为了监控气象信息,监控城市信息等等吧?

小明:对的,为了就是监控,所以发射的卫星就可以看做是一个观察者或是一个消息系统。如果让这颗卫星为飞机导航,那么这个飞机就是一个被观察者或者说是一个主体对象。当然主体对象是可以变化的,比如飞机飞翔,就像你的需求中的信息一样,消息的内容每时每刻都可能变化,因此飞机经常会发出位置信息,比如从沈阳到香港,途中经过北京,那么当经过北京上空时会向卫星发射一则信息来指明自己所处位置。卫星接收这些信息后能确认这架飞机的具体位置。当然卫星仅仅处理这些事情是不够的,因为只有这架飞机和卫星知道该飞机的位置信息,而地面的中转站目前还是不知道的。为了让地面上的中转站知道飞机的运行情况,并且各地的飞机中转站可以根据接收到的飞机信息而坐相应的处理。于是卫星、飞机、中转站的问题模式可以简化成这样:

中转站与天空中的飞机要想知道这架飞机的运行情况,各地中转站都要在卫星上注册这架飞机的信息,以便能收到这架飞机的信息。于是每当飞机到达一个地方时,都会向卫星发出位置信息,然后卫星又将该信息广播到已经订阅过这架飞机的中转站。这样每个中转站便可以接收飞机的信息并做相应的处理来避免飞机事故发生。

2.创建一个观察者

小白:听你这么一说自己明白些,按你说的,把观察者或者信息系统看做一个对象,那么他应该包括2个方法吧,第一个是接收某架飞机发来的信息,第二个是向订阅过该飞机的中转站发送相应的消息吧。

小明:不过小白别忘了,不是每个中转站时时都要监控飞机的,比如飞机路过石家庄,那么它已经不再飞北京了,那么北京中转站就没有必要再继续监控飞机状态了,所以此时北京中转站就应该注销掉飞机之前注册的信息,因此我们还需要有一个取消注册的方法。当然不要忘记对于这些信息,我们还需要有一个保存信息的容器,所以我们还需要一个消息容器,我们分析到这里,观察者的雏形就出来了。

小明:首先我们需要把观察对象创建出来,他有一个消息容器,和三个方法,分别是订阅信息方法,取消订阅的信息方法,发送订阅的信息方法。如下代码:

//将观察者放在闭包中,当页面加载就立即执行
var Observer = (function () {
    //防止消息队列暴露而被篡改故将消息容器作为静态私有变量保存
    var _messages = {};
    return {
        //注册信息接口
        regist: function () {},
        //发布信息接口
        fire: function () {},
        //移除信息接口
        remove: function () {}
    }
})();

当观察者的雏形出来,我们剩下要做的事情就是一一实现这三个方法了,我们首先实现消息注册方法,注册方法的作用是将订阅者注册的消息推入到消息队列中,因此我们首先接收两个参数:消息类型和相应的处理动作,

在推入到消息队列时如果此消息不存在则应该创建一个该消息类型并将该消息放入消息队列中,如果此消息存在则应该将消息执行方法推入到该消息对应的执行方法队列中,这样做的目的也是保证多个模块注册同一则消息时能够顺利执行。

如下代码

  //注册信息接口
        regist: function (type,fn) {
            //如果此消息不存在则应该创建一个该消息类型
            if(typeof _messages[type]==='undefined') {
                _messages[type]=[fn];
            }else {
                //如果此消息存在,将动作方法推入到该消息对应的动作执行序列中
                _messages[type].push(fn);
            }
        }

对于发布信息方法,功能是当观察者发布一个信息时将所有订阅者订阅的信息依次执行。故应该接收两个参数,消息类型已经动作执行时需要传递的参数。当然在这里消息类型是必须的。

在执行消息动作序列之前检验消息的存在是很有必要的。然后遍历消息执行方法序列,并依次执行。然后将消息类别以及传递的参数打包后依次传入消息执行方法中。

如下代码:

        //发布信息接口
        fire: function (type, args) {
            //如果该消息没有被注册,则返回
            if (!_messages[type]) {
                return;
            }
            //定义消息信息
            var events = {
                type: type, //消息类型
                args: args || {} //消息携带数据
            }
            i = 0; //消息动作循环变量
            len = _messages[type].length; //消息动作长度
            //遍历消息动作
            for (; i < len; i++) {
                //依次执行注册的信息对应的动作序列
                _messages[type][i].call(this, events);
            }
        }

最后是信息注销方法,其功能是将订阅者注销的信息从消息队列中清除,因此我们也需要两个参数,即消息了类型以及执行的某一动作。当然为了避免删除消息动作时消息不存在情况的出现,对消息队列中消息的存在性校验也是很有必要的。

如下代码:

  remove: function (type, fn) {
            //如果消息队列存在
            if (_messages[type]) {
                //从最后一个消息动作遍历
                var i = _messages[type].length - 1;
                for (; i >= 0; i--) {
                    //如果存在该动作则在消息动作序列中移除相应动作
                    _messages[type][i] == fn && _messages[type].splice(i, 1);
                }
            }
        }

完整的代码如下:

//将观察者放在闭包中,当页面加载就立即执行
var Observer = (function () {
    //防止消息队列暴露而被篡改故将消息容器作为静态私有变量保存
    var _messages = {};
    return {
        //注册信息接口
        regist: function (type,fn) {
            //如果此消息不存在则应该创建一个该消息类型
            if(typeof _messages[type]==='undefined') {
                _messages[type]=[fn];
            }else {
                //如果此消息存在,将动作方法推入到该消息对应的动作执行序列中
                _messages[type].push(fn);
            }
        },
        //发布信息接口
        fire: function (type, args) {
            //如果该消息没有被注册,则返回
            if (!_messages[type]) {
                return;
            }
            //定义消息信息
            var events = {
                type: type, //消息类型
                args: args || {} //消息携带数据
            }
            i = 0; //消息动作循环变量
            len = _messages[type].length; //消息动作长度
            //遍历消息动作
            for (; i < len; i++) {
                //依次执行注册的信息对应的动作序列
                _messages[type][i].call(this, events);
            }
        },
        //移除信息接口
        remove: function (type, fn) {
            //如果消息队列存在
            if (_messages[type]) {
                //从最后一个消息动作遍历
                var i = _messages[type].length - 1;
                for (; i >= 0; i--) {
                    //如果存在该动作则在消息动作序列中移除相应动作
                    _messages[type][i] == fn && _messages[type].splice(i, 1);
                }
            }
        }
    }
})();

观察者对象或者消息系统创建之后,我们先简单测试一下,如下

//测试,首先订阅一条消息
Observer.regist('test',function(e){
    console.log(e.type,e.args);
})
//然后发布这则消息
Observer.fire('test',{msg:'传递参数'}); 
// test
// {msg: "传递参数"}

从上面的结果可以看出测试已经实现了我们预期的效果,不过可能还不太清楚观察者模式的作用,以及他是如何实现解耦的,下面看我们前面的例子,把需求实现一下,来亲身感受一下观察者模式

3.实战

小白:我正为不同的工程师将自己的功能代码写在不同的闭包模块中导致无法相互调用的问题所困扰着。

小明:你说的没错。同一个模块的功能理应放在一起,这样管理起来才会方便,所以对于追加留言的功能就应放在之前开发过的留言模块里;对于用户信息递增功能则应放在之前开发过的用户信息模块里。对于留言的提交,理应放在你的提交模块里。不过,既然我们选择用观察者模式来解决问题,首先就要分析哪些模块应该注册信息,哪些模块应该发布信息,这一点是很重要的。那么如何分类呢?

小白:发布与删除留言功能需求是用户主动触发,所以应该是观察者发布信息,而评论的追加以及用户消息的增减是被动触发的,所以他们应该是订阅者去注册信息,这样的话,用户信息模块既是信息的发送者也是信息的接收者,提交信息模块是信息的发送者,浏览模块是信息的接受者。

如下代码

//外观模式,简化获取元素
function $(id) {
    return document.getElementById(id);
}
//工程师A
(function () {
    //追加一则消息
    function addMsgItem(e) {
        var text = e.args.text, //获取消息中 用户添加的文本内容
            ul = $('msg'), //留言容器元素
            li = document.createElement('li'), //创建内容容器元素
            span = document.createComment('span'); //删除按钮
        li.innerHTML = text; //写入评论
        //关闭按钮
        span.onclick = function () {
            ul.removeChild(li);
            //发布删除留言功能
            Observer.fire('removeCommentMessage', {
                num: -1
            });
        }
        //添加删除按钮
        li.appendChild(span);
        //添加留言节点
        ul.appendChild(li);
    }

    //注册添加评论信息
    Observer.regist('addCommentMessage', addMsgItem);
})();

//工程师B
//实现用户信息递增功能,
(function () {
    //更改用户信息数目
    function changeMsgNum(e) {
        //获取需要增加的用户信息数据
        var num = e.args.num;
        //增加用户信息数目并写入到页面中
        $('msg_num').innerHTML = parseInt($('msg_num').innerHTML) + num;
    }
    //注册添加评论信息
    Observer.regist('addCommentMessage', changeMsgNum)
        .regist('removeCommentMessage', changeMsgNum);

})();

//工程师C
//对于一个用户来说,当他提交信息时,就要出发消息发布功能
(function () {
    //用户点击提交按钮
    $('user_submit').onclick = function () {
        //获取用户输入框中输入的信息
        var text = $('user_input');
        //如果消息为空则提交失败
        if (text.value === '') {
            return;
        }
        //发布一则评论信息
        Observer.fire('addCommentMessage', {
            text: text.value, //消息评论内容
            num: 1 //消息评论数目
        })
        text.value = '';
    }

})();

从上面可以看出,各个模块之间的耦合问题就这么简单地解决了,属于哪个模块的功能方法都可以写在原模块,一点也不担心其他 模块式怎么实现的,只需要收发消息即可以。

那么,观察者模式能不能实现类或对象之间的耦合呢?

4.对象间解耦

观察者模式是可以实现类或对象之间的耦合的,举个例子,比如在来公司前还在学校上课,那么在课堂上有学生和老师,就课堂老师提问学生的例子说明一下吧。我们首先创建学生类,学生是被提问对象,因此他们是订阅者。同时学生也有对问题的思考结果,以及回答问题的动作。如下代码:

//学生类
var Student = function (result) {
    var that = this;
    //学生回答结果
    that.result = result;
    //学生回答问题动作
    that.say = function () {
        console.log(that.result);
    }
}
//在课堂上学生是可以回答问题的,所以他们有回答问题的方法answer
//回答问题方法
Student.prototype.answer = function (question) {
    //注册参数问题
    Observer.regist(question, this.say);
}

//还有一类学生在课堂上睡着了,所以他们就不能回答问题了,所有有个睡觉方法
Student.prototype.sleep = function (question) {
    console.log(this.result + '' + question + '已被注销');
    //取消对老师的问题的监听
    Observer.remove(question, this.say);
}

//创建教师类,他会提问学生,所以他是一个观察者,所以需要有一个提问题的方法
var Teacher = function () {}
Teacher.prototype.ask = function (question) {
    //提问问题方法
    console.log('问题是:' + question);
    //发布提问问题
    Observer.fire(question);

}

//测试模拟
var student1 = new Student('学生1回答问题'),
    student2 = new Student('学生2回答问题'),
    student3 = new Student('学生3回答问题');
//三位同学订阅(监听)了老师提问的两个问题
student1.answer('什么是设计模式');
student1.answer('什么是观察者模式');
student2.answer('什么是设计模式');
student3.answer('什么是观察者模式');
student3.sleep('什么是观察者模式');

//老师提问问题
var teacher = new Teacher();
teacher.ask('什么是设计模式');
// 学生3回答问题什么是观察者模式已被注销
// 问题是:什么是设计模式
// 学生1回答问题
// 学生2回答问题
teacher.ask('什么是观察者模式');
// 问题是:什么是观察者模式
// 学生1回答问题

上面代码巧妙地运用观察者模式解决类之间的耦合。

5.观察者模式总结

观察者模式最主要的作用是解决类或对象之间的耦合,解耦2个互相依赖的对象,使其依赖于观察者的消息机制。这样对于任意一个订阅者来说,其他订阅者对象的改变不会影响到自身。对于每一个订阅者来说,其自身既可以是消息的发出者也可以是消息的执行者,这都依赖于调用观察者对象的三种方法(订阅消息、注销消息、发布消息)中的哪一种。

团队开发中,尤其是大型项目的模块化开发中,一位工程师很难做到熟知项目中的每个模块,此时为完成一个涉及多模块调用的需求,观察者模式的优势就显而易见了,模块间的信息传递不必要相互引用其他模块,只需要通过观察者模式注册或者发布消息即可。通过观察者模式,工程师间对功能的开发只需要按照给定的消息格式开发各自功能即可,而不必去担忧他人的模块。

4.5 职责链模式

职责链模式:解决请求的发送者与请求的接受者之间的耦合,通过职责链上的多个对象对分解请求流程,实现请求在多个对象之间的传递,直到最后一个对象完成请求的处理

发发发发发发反反复复

4.6 命令模式

命令模式:将请求与实现解耦并封装成独立对象,从而使不同的请求对客户端的实现参数化。

项目经理让小白做个活动页面,平铺式的结构,不过页面的每个模块都有些相似的地方,比如每个预览产品图片区域,都有一行标题,然后标题的下面是产品图片,只是图片的数量与排列不同。

1.需求分析

小白:小明,有什么方式可以自由地创建视图模块呀?

小明:自由创建的方式?

小白:嗯,就是有时候在模块里面想创建一个图片,有时候想创建多张。

小明:试试命令模式,通过执行命令语句自由创建图片,这种方式就可以满足你的需求了。

小白:命令模式,它的工作原理是什么样的,如何实现呢?

小明:命令模式就是将请求模块与实现模块解耦,没关系,详细解释一下你就明白了。命令模式是将创建模块的逻辑封装在一个对象里,这个对象提供一个参数化的请求接口,通过调用这个接口并传递一些参数实现调用命令对象内部中的一些方法。

请求部分很简单,只需要按照给定的参数格式书写指令即可,所以实现部分的封装才是重点,因为它要为请求部分提供所需方法。

小白:那这样是不是我首先要明确我需要哪些命令呀?

小明:明确命令固然重要,其实质上还是说要实现你的需求,以及哪些需求可以命令化,而这些需求往往又是动态的,比如你要创建的图片,可以有一张还是多张。所以你需要在命令对象内部合理封装这些处理方法,但是你还要提供一个命令接口,来合理化接收并处理你的命令。好了,分析下需求,看看那些是变动的,哪些可以命令化,然后构建命令对象吧。

2.命令对象

既然动态显示不同模块,所以创建元素这一需求就是变化的,因此创建元素方法、展示方法应该被命令化。

//命令实现模块
var viewCommand = (function () {
    var Action = {
        //创建方法
        create: function () {
        },
        //展示方法
        diplay: function () {}
    }
})();

既然命令对象框架搭建起来了,命令对象中的每个方法就一一实现吧,创建视图过程中如果单纯用DOM操作拼凑页面的开销实在有些大,索性格式化字符串模板来创建页面,不过要注意的是,在实现创建视图方法之前就应该给出页面中每个模板的字符串模板,并且需要有一个格式化字符串模板的方法formateString,如下代码所示:

//命令实现模块
var viewCommand = (function () {
    //方法集合
    var tpl = {
        //展示图片结构模板
        product: [
            '<div>',
            '<img src="{#src#}"/>',
            '<p>{#text#}</p>',
            '</div>'
        ],
        //展示标题结构模板
        title: [
            '<div class="title">',
            '<div class="main">',
            '<p>{#title#}</p>',
            '</div>'
        ]
    },
    //格式化缓存字符串
    html='';
    //格式化字符串
    function formateString(str,obj) {
       //替换{##}之间的字符
    }
    var Action = {
        //创建方法
        create: function (data,view) {
            //解析数据,如果数据是一个数组
            if(data.length) {
                //遍历数组
                for(var i=0,len=data.length;i<len;i++) {
                    //将格式化之后的字符串缓存在html中
                    html+=formateString(tpl[view],data[i]);
                }
            }else {
                //直接将格式化字符串缓存到html中
                html+=formateString(tpl[view],data);
            }
        },
        //展示方法
        diplay: function () {}
    }
})();

视图展示方法,如何实现呢,如下代码

 //展示方法
        diplay: function (container,data,view) {
            if(data) {
                //根据给定数据创建视图
                this.create(data,view);
            }
            //展示模块
            document.getElementById(container).innerHTML=html;
            //展示后清空缓存的字符串
        }

创建视图与展开视图模块的2个方法都已经实现了,那么接下来就是实现命令接口了,对于这个接口的参数应该包括两部分,第一部分是命令对象内部的方法 名称,第二部分是命令对象内部方法对应的参数,通过这2个参数就可以自由地实现对视图模块创建或者展示了。如下代码:

  return function execute(msg) {
        //解析命令,如果msg.param不是数组则将其转化为数组(apply方法要求第2个参数是数组)
        msg.param = Object.prototype.toString.call(msg.param) === "[Object Array]" ? msg.param : [msg.param];
        //Action内部调用的方法引用this,为保证作用域this传入Action
        Action[msg.command].apply(Action, msg.param);
    }

3.测试代码

//命令实现模块
var viewCommand = (function () {
    //方法集合
    var tpl = {
            //展示图片结构模板
            product: [
                '<div>',
                '<img src="{#src#}"/>',
                '<p>{#text#}</p>',
                '</div>'
            ],
            //展示标题结构模板
            title: [
                '<div class="title">',
                '<div class="main">',
                '<p>{#title#}</p>',
                '</div>'
            ]
        },
        //格式化缓存字符串
        html = '';
    //格式化字符串
    function formateString(str, obj) {
        //替换{##}之间的字符
        console.log(obj.text);
        return obj.text;
    }
    var Action = {
        //创建方法
        create: function (data, view) {
            //解析数据,如果数据是一个数组
            if (data.length) {
                //遍历数组
                for (var i = 0, len = data.length; i < len; i++) {
                    //将格式化之后的字符串缓存在html中
                    html += formateString(tpl[view], data[i]);
                }
            } else {
                //直接将格式化字符串缓存到html中
                html += formateString(tpl[view], data);
            }
        },
        //展示方法
        display: function (container, data, view) {
            if (data) {
                //根据给定数据创建视图
                this.create(data, view);
            }
            //展示模块
            console.log('display'+html)
            // document.getElementById(container).innerHTML = html;
            //展示后清空缓存的字符串
        }

    }

    return function execute(msg) {
        //解析命令,如果msg.param不是数组则将其转化为数组(apply方法要求第2个参数是数组)
        msg.param = Object.prototype.toString.call(msg.param) === "[Object Array]" ? msg.param : [msg.param];
        //Action内部调用的方法引用this,
        Action[msg.command].apply(Action, msg.param);
    }
})();

var titleData=[
    {
        src:'commad/02.png',
        text:'桃花'
    },
    {
        src:'commad/03.png',
        text:'杏花'
    }

]

var productData=[
    {
        src:'commad/02.png',
        text:'product'
    },
    {
        src:'commad/03.png',
        text:'product2'
    }
]
viewCommand({
    command: 'display',
    //参数说明:param1元素容器,param2 标题数据 param3元素模板
    param: ['title', titleData, 'title']
})

//可以创建一个图片
viewCommand({
    command: 'create',
    //参数说明:param1元素容器,param2 标题数据 param3元素模板
    param: [ {
        src:'commad/04.png',
        text:'fff花'
    },'product']
})

// display
// Object {src: "commad/04.png", text: "fff花"}
// product

//可以创建多个图片

viewCommand({
    command: 'display',
    //参数说明:param1元素容器,param2 标题数据 param3元素模板
    param: [ 'product',productData,'product']
})

3.实际例子

4.命令模式总结

命令模式是将执行的命令封装,解决命令的发起者与命令的执行者之间的耦合。每一条命令实质上是一个操作,命令的使用者不需要了解命令的执行者(命令对象)的命令接口是如何实现的、命令时如何接受的、命令是如何执行的。所有的命令都被存储在命令对象中。

命令模式的优点自然是解决命令使用者之间的耦合。新的命令很容易加入到命令系统中,供使用者使用。命令的使用具有一致性,多数的命令在一定程度上是简化操作方法的使用的。

命令模式是对一些操作的封装,这就造成每执行依次依次操作就要调用一次命令对象,增加了系统的复杂度。

4.7 访问者模式

访问者模式:针对于对象结构中的元素,定义在不改变该对象的前提下访问结构中元素的新方法。

1.需求分析

小白:小明,今天我用DOM2级事件为页面中为元素绑定了一些事件,在事件中为该元素设置了一些css样式,可是在标准浏览器下可以成功,在低版本ID下面不成功,你知道是什么原因吗?

小明:你看看,低版本IE是不是报错了

小白:打开浏览器,真是呀,提示说'this.style为空或不为对象',可是这里的this指代的不是这个元素么?

小明:w3c是这么规定的,可是IE浏览器总喜欢 自娱自乐,他们可不按照标准来,比如IE提供的方法attachEvent就不是按照W3C标准实现的,所以你遇到的问题很简单,在你的事件里面添加一条测试语句“aler(this===window)"你会发现IE提示的是true,这也就是说我们在attachEvent事件中this指向的不是这个元素而是window,所以你想获取事件对象,应该通过window.e来获取

//外观模式实现
function bindEvent(dom, type, fn) {
    //对于支持DOM2级事件处理程序addEventListener方法的浏览器
    if (dom.addEventListener) {
        dom.addEventListener(type, fn, false);
        //对于不支持DOM2级事件处理程序addEventListener方法提供attachEvent方法的浏览器
    } else if (dom.attachEvent) {
        dom.attachEvent('on' + type, fn);
    } else {
        //不支持上面2种方式的
        dom['on' + type] = fn;
    }
}

var demo = document.getElementById('demo');
bindEvent(dom, 'click', function () {
    this.style.background = 'red';
})

小白:我的天,IE怎么可以这样

小明:IE脾气很 倔强,使用访问者模式来解决事件回调函数中对该元素的访问问题

访问者模式的思想就是说我们在不改变操作对象的同时,为它添加新的操作方法,来实现对操作对象的访问。就像国家间相互访问,不会让全国人民都去,而且这种成本还是蛮大的。所以派遣一位大使访问便可解决问题。此处我们要为操作元素添加的新操作方法就是事件。听上去很复杂,没关系,但你看到它的实现时,会有种恍然大悟的感觉,来看看IE的实现方式吧。

function bindIEEvent(dom, type, fn, data) {
    var data = data || {};
    dom.attachEvent('on' + type, function (e) {
        fn.call(dom, e, data);
    })
}

小白,你看,其实实现的核心就是调用了一次call方法。我们知道call和apply的作用就是更改函数执行时的作用域,这正是访问者模式的精髓,通过这两种方法我们就可以让某个对象在其他作用域中运行,比如我们这里让事件源元素对象在我们事件回调函数中的作用域中运行,那么我们在回调函数访问的this当然指代的就是我们的事件源元素对象了。

2.事件自定义数据

小白:不过这里什么多了一个参数data呢?它表示什么意思呀?

小明:这是通过访问者模式思想对操作元素绑定的事件而进行的一次拓展,目的是为了向事件回调函数中传入自定义数据,我们知道,W3C给我们定义事件绑定的回调函数内部只能有一个参数对象供我们访问,它就是例子中的参数e-事件对象,通过它我们能访问到事件相关的一些信息,然而有时,这并不能满足我们的需求,比如当用户触发一个点击事件时,再给事件回调函数传递一些参数是很困难的。为解决这类问题,有时候我们将数据存储在外面,有时候将数据绑定在回调函数上。然而这些方案中有的存在可被篡改风险,有的做不到对数据的实时更新,所以在事件执行时,将一些必要数据传入到事件回调函数中是很有必要的。

call和apply方法在向对象添加访问的方式时是允许我们添加参数的。所以我们顺便将数据也一起传进来,不过你注意到没有,我在使用call方法时,是在回调函数内部调用的,所以不要忘记在call方法中携带事件对象,这样在回调函数中才能顺利访问到这个事件对象

小白:哦。这么说我再为IE添加事件时,事件源对象this,事件对象e,自定义数据等等我们都可以在回调函数中访问到了。

测试一下

function bindIEEvent(dom, type, fn, data) {
    var data = data || {};
    dom.attachEvent('on' + type, function (e) {
        fn.call(dom, e, data);
    })
}

function $(id) {
    return document.getElementById(id);
}

bindIEEvent($('btn'),'click',function(e,d){
    $('test').innerHTML=e.type+d.text+this.tagName;
},{text:'test dome'});

//小白测试了一下id为btn的按钮元素,发现p元素增加了一行内容
click
test demo
BUTTON

3.对象访问器

4.访问模式总结

访问者模式解决数据与数据的操作方法之间的耦合,将数据的操作方法独立于数据,使其可以自由化演化。因此访问者数据更适合于那些数据稳定,但是数据的操作方法易变的环境下。因此当操作环境改变时,可以自由修改操作方法以适应操作环境,而不用修改原数据,实现操作方法的拓展。同时对于同一个数据,它可以被多个访问对象所访问,这极大增加了操作数据的灵活性。

4.8 中介者模式

中介者模式:通过中介者对象封装一系列对象之间的交互,使对象之间不再相互引用,减低他们之间的耦合,有时中介者对象也可以改变对象之间的交互

1.需求分析

项目经理准备在用户首页上的导航模块添加一个设置层,让用户可以通过设置层,来设置导航展现形式,于是找来小白谈谈新需求。

小明:小白,有用户反馈首页中为导航添加消息提醒影响他们的视觉体验,说页面有些乱,有的人认为这是广告入侵。还是一些用户不想在导航中出现链接地址,这也会让他们感觉页面混乱。我们准备要对导航层添加一个设置层,让用户自由设置导航样式,这是是需求文档。

小白:好多模块都有导航,模块太多无法下手,我该从何做起。上次说观察者模式可以解决模块之间的耦合,你感觉可以吗?

小明:观察者模式?导航模块里面的内容有需要向设置层发送请求的需求?

小白:没有,设置层只是单向地控制导航模块里面导航的样式

小明:要是这样单向通信的,可以试试中介者模式,这个模式适合你的需求

小白:中介者模式?这是一个怎样的模式?为何观察者模式不可以呢?

小明:首先他们都是通过消息的收发机制实现的,不过在观察者模式中,一个对象既可以是消息的发送者也可以是消息的接受者,他们之前信息交流依托于信息系统实现的精髓。而中介者模式中消息的发送方只有一个,就是中介者对象,而且中介者对象不能订阅消息,只有那些活跃对象(订阅者)才可订阅中介者的消息。当然也可以看做是将消息系统封装在中介者对象内部,所以中介者对象只能是消息的发送者。就像男女相亲,总要找媒婆,但是没听说媒婆天天找男女青年要给他介绍对象。而观察者模式你需要些一个消息系统,增加了开发成本,所以建议使用中介者模式。

小白:如果用中介者模式来解决我的需求问题,那么设置层模块对象就应该是一个中介者对象了吧,他负责向各个导航模块对象发送用户设置的消息,而各个导航模块则应该作为消息的订阅者存在吧

小明:是的,既然明白其中的角色,试着将其实现,试试看可不可以解决需求问题

2.创建订阅者对象

这里的消息系统为中介者对象,先实现它吧,如下代码:

//中介者对象
var Mediator = function () {
    //消息对象
    var _msg = {};
    return {
        //订阅消息方法
        register: function (type, action) {
            //如果该消息存在
            if (_msg[type]) {
                //存入回调函数
                _msg[type].push(action);
            } else {
                //不存在,则建立该消息容器
                _msg[type] = [];
                //存入消息回调函数
                _msg[type].push(action);
            }
        },
        //发布消息方法
        send: function (type) {
            //如果该消息已经被订阅
            if (_msg[type]) {
                //遍历已被存储的消息回调函数
                for (var i = 0, len = _msg[type].length; i < len; i++) {
                    //执行该回调函数
                    _msg[type][i] && _msg[type][i]();
                }
            }
        }
    }
}

测试一下写的代码,订阅2个消息,然后让中介者发布这个消息看看是否成功?

//测试
Mediator.register('demo',function(){
    console.log('first');
});
Mediator.register('demo',function(){
    console.log('second');
});

//发布demo消息
Mediator.send('demo');

//first
//second

3.需求实现

在设置层模块监听每位用户对设置层内的导航展现形式的设置,并发送样式消息就可以了,如下代码;

//发布消息
//设置层模块
(function () {
    //消息提醒选框
    var hideNum = document.getElementById('hide_num'),
        //网址选框
        hideurl = document.getElementById('hide_url');

    //消息提醒选框事件
    hideNum.onchange = function () {
        //如果勾选
        if (hideNum.checked) {
            //中介者发布隐藏消息提醒功能消息
            Mediator.send('hideAllNavNum');
        } else {
            //中介者发布显示消息提醒功能消息
            Mediator.send('showAllNavNum');
        }
    }
    //网址选框事件
    hideurl.onchange = function () {
        //如果勾选
        if (hideurl.checked) {
            //中介者发布隐藏消息提醒功能消息
            Mediator.send('hideAllNavNum');
        } else {
            //中介者发布显示消息提醒功能消息
            Mediator.send('showAllNavNum');
        }
    }
})();

涉及到的模块订阅中介者提供的消息就可以了。对导航隐藏设置为display:none会不会影响到导航后面元素的样式呢?css在显隐上不仅仅给我们提供了display,还有一个属性visibility是占位隐藏的。这样就不会影响其他元素了。如下代码为用户收藏导航模块和推荐用户导航内的消息提醒功能。

//显隐导航小组件
var showHideNavWeight = function (mode, tag, showOrHide) {
    //获取导航样式
    var mod = document.getElementById(mode),
        //获取下面的标签名为tag的元素
        tag = mod.getElementsByTagName(tag),
        //如果设置为false或者hide则值为hidden,否则为visible
        showOrHide = (!showOrHide || showOrHide == 'hide') ? 'hidden' : 'visible';
    //占位隐藏这些标签
    for (var i = tag.length - 1; i >= 0; i--) {
        tag.style.visibility = showOrHide;
    }
}

//用户收藏导航模块
(function () {
    //..其他交互逻辑
    //订阅隐藏用户收藏导航消息提醒
    Mediator.register('hideAllNavNum', function () {
        showHideNavWeight('collection_nav', 'b', false);
    })
    //订阅显示用户收藏导航消息提醒
    Mediator.register('showAllNavNum', function () {
        showHideNavWeight('collection_nav', 'b', true);
    })

    //订阅隐藏用户收藏导航消息提醒
    Mediator.register('hideAllNavNum', function () {
        showHideNavWeight('collection_nav', 'span', false);
    })
    //订阅显示用户收藏导航消息提醒
    Mediator.register('showAllNavNum', function () {
        showHideNavWeight('collection_nav', 'span', true);
    })
})();


//推荐用户导航模块
(function () {
    //..其他交互逻辑
    //订阅隐藏用户收藏导航消息提醒
    Mediator.register('hideAllNavNum', function () {
        showHideNavWeight('recommend_nav', 'span', false);
    })
    //订阅显示用户收藏导航消息提醒
    Mediator.register('showAllNavNum', function () {
        showHideNavWeight('recommend_nav', 'span', true);
    })
})();



//常用导航模块
(function () {
    //..其他交互逻辑
    //订阅隐藏用户收藏导航消息提醒
    Mediator.register('hideAllNavNum', function () {
        showHideNavWeight('recommend_nav', 'b', false);
    })
    //订阅显示用户收藏导航消息提醒
    Mediator.register('showAllNavNum', function () {
        showHideNavWeight('recommend_nav', 'b', true);
    })
})();

4.中介者模式总结

同观察者模式一样,中介者模式的主要业务也是通过模块间或者对象间的复杂通信,来解决模块间或者对象间的耦合。对于中介者的本质是分装多个对象的交互,并且这些对象的交互一般是在中介者内部实现的。

与外观模式的封装特性相比,中介者模式对多个对象交互地封装,且这些对象一般处于同一层面上,并且封装的交互在中介者内部,而外观模式封装的目的是为了提供更简单的易用接口,而不会添加其他功能。

与观察者模式相比,显然两种模式都是通过消息传递实现对象间或者模块间的接口。观察者模式中的订阅者是双向的,既可以是消息的发布者,也可以是消息的订阅者。而在中介者模式中,订阅者是单向的,职能是消息的订阅者,而消息统一由中介者对象发布,所有的订阅者对象间接的被中介者管理。

4.9 备忘录模式

备忘录模式:在不破坏对象的封装性的前提下,在对象之外捕获并保存该对象内部的状态以便日后对象使用或者对象恢复到以前的某个状态。

1.需求分析

今天新闻模块要上线了,为保证产品质量,小明要检查一下新人写的代码,当浏览到某一页新闻逻辑代码的时候感觉有些不妥。叫来小白:小白,显示某一页新闻逻辑代码是你写的吗?

小白:嗯,有什么问题吗?

小明:按你目前的逻辑来看是可以走通的,不过对于资源的请求,在你的代码逻辑中倒是有一些浪费。比如,你用jquery代码库对上一页和下一页按钮定义了2个事件,请求上一篇或下一篇文章,并将响应得到的新闻内容数据显示在内容区域内。每次点击上一页和下一页都对后台发送了一次请求,有些浪费,可以通过备忘录模式将请求的数据缓存下来。

每次发送请求的时候对当前状态做一次记录,将请求下的数据以及对应的页码缓存下来,如果将来的某一时刻想返回到某一浏览过的新闻页,直接在缓存中查询即可。直接恢复记录过的状态而不必触发新的请求行为,这很高效。

如下代码所示:

//Page备忘录类
var Page=function() {
    //信息缓存对象
    var cache={};
    return function(page,fn) {
        //判断该页数据是否在缓存中
        if(cache[page]) {
            //恢复到该页状态,显示该页内容
            showPage(page,cache[page]);
            //执行成功回调函数
            fn&&fn();
        }else {
            //若缓存cache中无该页数据
            $.post('.data/getNewsData.php',{
                //请求携带数据page页码
                page:page
            },function(res){
                //成功返回
                if(res.errNo==0) {
                    //显示该页数据
                    showPage(page,res.data);
                    //将该页数据放到缓存中
                    cache[page]=res.data;
                    //执行成功回调函数
                    fn&&fn();
                }else {
                    //处理异常
                }
            })
        }
    }
}

如上代码,以后如果用户想回看某页新闻数据,就不需要发送不必要的请求了。

2.备忘录模式总结

其实备忘录模式的应用还挺多的,比如打开皮肤的换肤设置层,第二次就不要发送请求获取数据了。备忘录模式最主要的任务是对现有的数据或状态做缓存,为将来某个时刻使用或恢复做准备。在Javascript中,备忘录模式常常运用于对数据的缓存备份,浏览器端获取的数据往往是从服务器端请求获取到的,而请求过程往往是以时间与流量为代价的。因此对重复性数据反复请求不仅增加了服务器端的压力,而且造成浏览器端对请求数据的等待进而影响用户体验。

当数据量过大时,会严重占用系统提供的资源,会极大降低系统性能。此时对缓存器的缓存策略优化是很有必要的,复用率低的数据缓存下来是不值得的。因此资源空间的限制对是备忘录模式应用的一大障碍。

4.10 迭代器模式

posted @ 2020-06-05 20:51  melimeli  阅读(146)  评论(0编辑  收藏  举报