代码改变世界

读书笔记之 - javascript 设计模式 - 门面模式

2014-08-14 09:59  sai.zhao  阅读(462)  评论(1编辑  收藏  举报

门面模式有俩个作用:

  1. 简化类的接口
  2. 消除类与使用它的客户代码之间的耦合

在javascript中,门面模式常常是开发人员最亲密的朋友。它是几乎所有javascript库的核心原则,门面模式可以使库提供的工具更容易理解。使用这种模式,程序员可以间接地与一个子系统打交道,与直接访问子系统相比,这样做更不容易出错。

addEvent函数是一个基本的门面,你不用在每次为一个元素添加事件监听器的时候都得针对浏览器间的差异进行检查,有了这个便利,你可以把这个添加事件的底层细节抛在脑后,而把心思集中在如何构建自己的应用系统上。

用作便利方法的门面元素

门面模式给与开发人员的另一个好处表现在对函数的组合上。这些组合而得到的函数又叫便利函数。如下例子:

function a(){}
function b(){}
function e(){
    a();
    b();
}

你可能在想为什么一开始不把所有功能放到函数e中,答案是分离可以获得更多粒度控制和灵活性。组合a和b可能会对应用程序造成破坏或者产生意想不到的结果,如以DOM脚本编程中经常遇到的俩个普通事件方法为例:

  • event.stopPropagation()
  • event.preventDefault()

第一个stopPropagation功能是中止事件沿DOM树向上冒泡的传播过程

第二个方法preventDefault是阻止浏览器针对一个事件的默认行为。因为不同浏览器厂商为这俩个功能提供的接口略有差异,所以现在摆在我们面的就是一个门面模式实现便利方法的理想案例。

var DED = window.DED || {};
DED.util = {
    stopPropagation:function(e){
        if(e.stopPropagation){
            e.stopPropagation();
        }else{
            e.cancelBubble = true;
        }
    },
    preventDefault:function(e){
        if(e.preventDefault){
            e.preventDefault();
        }else{
            e.returnValue = false;
        }
    },
    stopEvent:function(e){
        DED.util.stopPropagation(e);
        DED.util.preventDefault(e);
    }
}

尽管看起来很像,但是门面模式并不是适配器模式。适配器模式是一种包装器,用来对接口进行适配以便在不兼容的系统中使用它。而创建门面模式则是图个方便,它并不用于达到与需要特定接口的客户端系统打交道这个目的,而是用于提供一个简化的接口。

示例:设置HTML元素的样式:

不停的写getElementById并且为每一个元素设置同样的属性,看起来相当的无聊,门面模式就可以派上用场,我们采用一种逆向的工作方式,先写出使用方法,然后设计代码。

setStyle(['foo','bar','baz'],'color','red');

可以看出,要创建setStyle函数,这里传递给它的第一个参数是一个包含着三个ID值得数组。第二个参数是要设置的样式属性,而第三个参数则是该属性的值。下面这个函数就是一个门面元素,它可以满足我们的需要:

function setStyle(elements,prop,val){
    for(var i=0,len=elements.length-1;i<len;++i){
        document.getElementById(elements[i]).style[prop] = val;
    }
}

我们可以设计一个更复杂的接口,把所有逻辑都组合在另一个门面元素中,以便一次函数调用就能处理所有这些问题。这个门面元素内部也要使用setStyle,但是客户代码对此一无所知。我们把它命名为setCSS:

setCSS(['foo'],{
    position:'absolute',
    top:'50px',
    left:'20px'
});

setCSS的实现方式如下:

function setCSS(el,styles){
    for (var prop in styles) {
        if(!styles.hasOwnProperty(prop)){
            continue;
        }else{
            setStyle(el,prop,styles[prop]);
        }
    }
}

设计一个事件工具:

前面曾经说过,在处理跨浏览器开发的问题时,最好创建一个门面模式,如果要设计一个大型库,那么最好把其中所有的工具元素聚拢在一起,这样更好用,访问起来也更简单。鉴于各种浏览器在事件处理方面表现出来的差异,开发一个事件工具很有必要。

我们先从一个基本的框架开始,这里要用到单体模式,它位于DED.util命名空间中,包含着我们要设计的各个静态方法:

DED.util.Event = {
    // bulk goes here ...
}

接下来我们将着手解决开发人员在与事件打交道的时候都会遇到的一些常见的问题,比如怎么获得事件目标元素和事件对象。当然,我们也会利用前面用来处理事件传播和事件默认行为的代码。下面是粗略的框架结构:

DED.util.Event = {
    getEvent:function(){},
    getTarget:function(){},
    stopPropagation:function(e){},
    preventDefault:function(e){},
    stopEvent:function(e){}
}

下面的代码对相关对象的能力和特性进行检查并加入一些代码分支,以图弥合浏览器之间的差异,其结果是创建了5个门面方法,这是一个更为一致的接口,有了它我们的工作就会变得更轻松。

DED.util.Event = {
    getEvent:function(e){
        return e||window.event;
    },
    getTarget:function(e){
        return e.target || e.srcElement;
    },
    stopPropagation:function(e){
        if(e.stopPropagation){
            e.stopPropagation();
        }else{
            e.cancelBubble = true;
        }
    },
    preventDefault:function(e){
        if(e.preventDefault){
            e.preventDefault();
        }else{
            e.returnValue = false;
        }
    },
    stopEvent:function(e){
        this.stopPropagation(e);
        this.util.preventDefault(e);
    }
}

现在这个事件工具设计好了,可以和前面的addEvent函数结合使用

addEvent('example','click',function(e){
    console.log(DED.util.Event.getTarget(e));
    DED.util.Event.stopEvent(e);
})

实现门面模式的一般步骤:

函数名字应该仔细考虑,与他们用途要相称,对那些有几个函数组合而成的函数,一个简单的方法就是把相关函数的名称串成一个函数名,并采用camel大写规范。或者也可以使用thisFunctionAndThatFunction这种形式。

处理浏览器的API的不一致性属于另一种情况,此时要做的就是把分支代码放在新创建的门面函数中,辅以对象检查或者浏览器嗅探技术。

门面模式的适用场合:

判断是否应该使用门面模式的关键在于辨认那些反复成组出现的代码,如果函数b出现在函数a之后这种情况经常出现,那么你也许应该考虑用一个门面函数把这俩个函数组合起来。

使用门面模式的好处就是,编写一次组合代码,可以反复使用。避免与下层子系统的紧密耦合。