接口

头像
南赐
    阅读 15 分钟
    1

    前言

    这一系列的文章将主要基于js设计模式这本书的要点还有一些翻阅的博客文章,借鉴来源会注明,外加自己的一些demo与直觉.不同于其他设计模式类的书,js设计模式是一本讲述设计模式在动态语言js中的实现的书它从设计的角度教人编写代码,书中的许多实例代码来自YUI实战项目,对js面向对象的特性阐述到位,详细剖析了面向对象底层实现机制.随着ajax技术的兴起,Web应用的许多业务逻辑从服务器转移到浏览器端执行,,js越来越大的规模和复杂程度需要fe rd更好地运用面向对象特性.js设计模式 和 ajax设计模式 做个比较:后者研究了运用ajax设计模式开发Web应用的各种设计模式,前者研究了通用面向对象设计模式在js中的实现,不仅仅用于浏览器端编程,还可以有其他环境下的实现.
    本书比较大的缺点在于作者阐述对各种设计模式的实现时没有选择js面向对象编程最自然的继承实现方式原型继承,而是选择了该书出版时广大读者较为熟悉的类继承.
    设计模式运用在java,cpp等等各类程序语言上已经有许多年了,应用于各种语言和语法上表现出一致性,基本结构是相同的,只是细节上略有差别,但对js则不一样,那些常见的面向对象特性还需要靠晦涩的技巧来实现,所以设计模式也同样需要这样的手段.

    目录结构

    这本书大体上分为两部分,第一部分给出实现具体设计模式所需要的面向对象特性的基础知识,主要包括接口,封装,信息隐藏,继承,单体模式等等.第二部分专注于各种具体的设计模式及其在js中的应用,主要介绍了工厂模式,桥接模式,组合模式,门面模式...几种常见的模式.

    第一部分

    第一部分前6章是js面向对象的基础知识,第1章富有表现力的js主要介绍js实现同一任务的编程风格多样性,函数式编程实现面向对象,运用设计模式的缘由.第2章接口分析了js如何采用最佳特性来模仿接口,探讨了接口检查的各种方式,然后给出一个用来检查对象是否具有必要方法的可重用类.第3章封装和信息隐藏探讨了js中创建对象的各种不同方式,以及如何创建public,private,protected方法.第4章继承讲述了js中如何创建子类,对比了类继承和原型继承及其应用场合.第5章单体模式讨论了js中的单体模式,命名空间,代码组织以及分支技术--根据运行时环境动态定义方法.第6章方法的链式调用,对有没有采用该技术来创建一个js库作比较.

    第二部分

    第7章工厂模式,它有助于消除那些彼此实例化对方的类之间的耦合,然后改用一个方法来确定要实例化哪个类.主要讨论了两点,一个是如何用一个类(通常是一个单体)来生成实例的简单工厂模式,一个是如何用子类来确定一个成员对象应该是哪种具体类实例的工厂模式.第8章桥接模式讨论了一种把两个对象连接在一起而且还能避免二者间强耦合的方法.桥接元素允许两个对象独立变化,演示了如何用桥接元素把函数松散得绑定到事件.第9章组合模式非常适合于创建Web动态页面,演示了如何达到只用一条命令即可在许多对象上激发复杂或者递归性的行为,以及如何把一系列对象组织成复杂的层次体系.第10章门面模式用来为对象创建一个更完善的接口,把现有接口转换为一个更便于使用的接口.这一章解释了大多数js库都是为js在具体浏览器中的实现提供的一个门面,也阐述了如何用这种模式创建便利方法和事件工具库.第11章适配器模式可以让现有接口契合实际需要.适配器也称包装器,用来把不匹配的接口替换为一个可用于现有系统中的接口.该章探讨了如何用适配器弥补js库之间的差异并金华一中库到另一种库之间的过渡过程,分析了一个电子邮件API并创建了一个有助于升级到新版本的适配器.第12章装饰者模式引入了一种可以为对象添加属性而不必创建新的子类的方法.用于把对象透明地包装到另一种具有相同接口的对象中.还研究了装饰者的结构以及如何将其与工厂模式结合以自动创建内嵌对象,该章有一个性能分析器示例,示范如何用装饰者模式动态实现接口.第13章享元模式用于优化,该章示范了如何通过把打屁独立对象转变为少量共享对象以大幅削减实现应用软件所需的对象数目,该章还创建了一个Web日历和一个可重用的工具提示类,示范如何按照享元模式进行重构.第14章代理模式用于控制对象的访问代理.该章研究了如何为本体real object创建一个代理对象以作为替身,并使其能被远程访问,还探讨了该模式的种种用法.第15章观察者模式对对象的状态进行观察,发生变化时能得到通知.也称为发布者-订阅者publisher-subscriber模式,用于让对象对事件进行监听以便对其作出响应,该章讨论了在使用一个动画库时可以订阅的各种事件.第16章命令模式对方法调用进行封装:进行参数化和传递,然后在需要的时候再加以执行.应用场合主要包括创建创建用户界面--尤其是那种需要不受限制的取消操作的用户界面.该章还讨论了该模式的结构.第17章职责莲模式用来消除请求的发送者和接受者之间的耦合,研究了如何使用该模式处理事件的捕获和冒泡,如何创建弱耦合模块和优化事件监听器的绑定.

    简易函数说明

    对于书中的代码实例,作者使用了一些简易函数来执行安装事件监听器,派生子类,处理Cookie,引用HTML元素等任务,没有使用YUI或者jQuery等js库来提供类似功能,读者可以使用自己喜欢的任何库结合使用.下面是这些简易函数的说明:

    • $(id) 根据ID获取HTML元素的引用,其参数可以是字符串,字符串的数组等等

    • addEvent(obj, type, func), 把函数fund作为元素obj的事件监听器,type表示该函数监听的事件.

    • addLoadEvent(func),将函数fund关联到window对象的load事件.

    • getElementByClass(searchClass, node, tag),获取所有class属性值为searchClass的元素的引用.node和tag这两个参数可有可无,可以用来缩小搜索范围,函数的返回值是一个数组.

    • insertAfter(parent, node, referenceNode),插入元素node,其父元素为parent,其位置在referenceNode之后.

    • getCookie(name),获取与名为name的Cookie相关联的字符串.

    • setCookie(name, value, expires, path, domain, secure),把与名为name的Cookie相关联的字符串设置为value,除name和value外的其它参数均可有可无.

    • deleteCookie(name),将名为name的Cookie的过期时间设置到过去,即删除这个Cookie.

    • clone(object),创建object的一个副本,用于原型继承,见第4章.

    • extend(subClass, superClass),执行一些必要的工作,使SubClass成为superClass的子类,见第4章.

    • augment(receivingClass, givingClass),将givingClass中的方法输入receivingClass中,见第4章.

    第一章

    js灵活性

    js编程风格多样化,既可以函数式编程,也可以面向对象式.可以模仿其他语言的编程模式和惯用法.以启动和停止动画为例,定义一个类,用来创建可以保存状态并且具有一些仅对其内部状态进行操作的方法的动画对象:

    //Anim Class
    var Anim = function () {
        ...
    };
    Anim.prototype.start = function () {
        ...
    };
    Anim.protype.stop = function () {
        ...
    }
    //Usage
    var myAnim = new Anim();
    myAnim.start();
    ...
    myAnim.stop();

    上面的代码定义了一个名为Anim的类,并把两个方法赋给该类的prototype属性.也可以更便捷地把类的定义封装在一条声明里面:

    //Anim class, with a slightly different syntax for declaring methods. 
    var Anim = function() {
        ...
    }
    Anim.prototype = {
        start: function () {
            ...
        },
        stop: function () {
            ...
        }
    };

    换种风格:

    //Add a method to the function object that can be used to declare methods.
    Function.prototype.method = function(name, fn) {
        this.prototype[name] = fn;
    };
    //Anim class, with methods created using a convenience method.
    var Anim = function () {
        ...
    };
    Anime.method('start', function () {
        ...
    });
    Amin.method('stop', function () {
        ...
    });

    Function.prototype.method用于为类添加新方法, 它有两个参数, 第一个是字符串, 表示新方法的名称; 第二个是用作新方法的函数. 可以修改一下Function.Prototype.method, 使其可以被链式调用, 这只需要返回this即可:

    //This version allows the calls to be chained.
    Function.prototype.method = function (name, fn) {
        this.prototype[name] = fn;
        return this;
    };
    //Anim class,with methods created using a convenience method and chaining. 
    var Anim = function () {
        ...
    };
    Anim.method('start', function () {
        ...
    });
    Anim.method('stop', function () {
        ...
    });

    以上是完成同一项任务的不同方法, 他们的风格略有差异, 不同风格在代码篇幅, 编码效率, 和执行性能方面各有特点, 不同人偏好不同的风格.

    弱类型语言

    在js中, 定义变量时不必声明其类型. 但是变量依然具有类型, 一个变量具有的类型取决于其包含的数据. 有三种原始类型: 布尔型, 数值型(不同于其他语言, js不区分整数和浮点数)和字符串型. 另外还有对象类型和包含可执行代码的函数类型, 前者是一种复合数据类型(数组是一种特殊的对象, 包含着一批值的有序集合),最后还有null空类型和undefined.原始类型按值传递,其他类型按引用传递.
    js中的变量既可以根据所赋的值改变类型,还可以进行原始类型之间的转换.toString可以把数值或者布尔值转换为字符串.parseFloat和parseInt可以把字符串转换为数值,双重非可以把字符串或者数值转换为布尔值.
    js这样的弱类型语言变量带来了极大的灵活性,但是会根据需要进行转换,一般说来不会有类型错误.

    函数是一等对象

    js中的函数可以存储在变量中,可以作为参数传给其他函数,可以做为返回值从其然函数传出,还可以在运行时进行构造.这些特性带来了极大的灵活性和表达能力.
    可以用function () {...}创建匿名函数,没有函数名但是可以赋值给变量.

    //An anonymous function, executed immediately.
    (function () {
        var foo = 10;
        var bar = 2;
        console.log(foo * bar);
    })();

    该函数在定义之后立即执行,甚至不用赋值给一个变量,出现在函数声明之后的一对括号立即对函数进行调用,实际上括号当中也可以有参数:

    //An anonymous function with arguments.
    (function (foo, bar) {
        console.log(foo * bar);
    )(10, 2);
    `
    这个匿名函数与前一个等价,只不过变量没有在函数内部用var声明,而是作为参数从外部传入,函数也可以返回值,然后赋值给一个变量:
    `//An anonymous function returns a value.
    var baz = (funciton(foo, bar) {
        return foo * bar;
    })(10, 2);
    //baz equals to 20.

    匿名函数最好的用途是用来创建闭包,closure是一个受到保护的变量空间,由内嵌函数生成,js有函数级的作用域:定义在函数内部的变量在函数外部不能被访问.js的作用域又是词法性质的,函数运行在定义它的作用域中,而不是在调用它的作用域中,把这两个因素结合起来就能通过把变量包裹在匿名函数中加以保护.可以这样创建类的private变量:

    //An anonymous function used as a closure. 
    var baz;
    (function () {
        var foo = 10;
        var bar = 2;
        baz = function () {
            return foo * bar;
        };
    })();
    baz();
    //baz can access foo and bar, even though it is executed outside of the anonymous function.

    变量foo和bar定义在匿名函数中,因为函数baz定义在这个比保重,所以它能访问两个变量,即使是在该闭包执行结束后.

    对象易变

    在js中,除了布尔,数值,字符串三种原始数据类型(即便是他们也可以进行包装为对象),一切都是易变的对象:为函数添加属性等等.

    function displayError (message) {
        displayError.numTimesExecuted++;
        console.log(message);
    };
    displayError.numTimesExecuted = 0;

    你也可以对先前定义的类和实例化的对象进行修改:

    //Class Person.
    function Person (name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype = {
        getName: function () {
            return this.name;
        },
        getAge: function () {
            return this.age;
        }
    }
    //Instantiate the class.
    var alice = new Person('Alice', 93);
    var bill = new Person('Bill', 30);
    //Moddify the class.
    Person.prototype.getGreeting = function () {
        return 'Hi' + this.getName() + '!';
    };
    //Modify a specific instance.
    alice.displayGreeting = function () {
        console.log(this.gerFreeting());
    };

    这个例子中,类的getGreeting方法是在已经创建了类的两个实例之后才添加的,但这两个实例仍然能够获得这个方法,其原因在于prototype对象的工作机制.对象alice得到了displayGreeting方法,其他事里却没有.
    与对象易变性相关的还有内省introspection:在运行时检查所具有的属性和方法,还可以使用这种信息动态实例化类和执行其方法(反射reflection),甚至不需要在开发时知道他们的名称.书中js大多数用来模仿传统面向对象特性的技术都依赖于对象的易变性和反射.在C++,Java不能对已经实例化的对象进行扩展,不能对已经定义好的类进行修改,而在js中,任何东西都可以在运行时修改,当然也有弊端:可以定义一个具有一套方法的类,却无法确定这些方法会一直保持不变,这是js很少进行类型检查的原因.

    继承

    js使用的是基于对象--原型继承,可以用来模仿类继承,编码时应根据手头任务的实际情况和两种继承的实际性能表现进行选择.

    js中的设计模式

    1995年GoF出版的"设计模式"整理记录了对象间相互作用的各种方式,用以创建不同类型对象的套路被称为design pattern.
    js强大的表现力赋予了程序员在运用设计模式编码时的灵活性,js中使用设计模式的主要原因如下:

    1. 可维护性,有助于降低模块间的耦合强度,使得重构,更换模块,大型团队中的合作变得更容易.

    2. 沟通,为处理不同类型的对象提供了一套通用的术语,比如该系统它使用了工厂模式,可以在较高层面上对带有特定模式的名称进行讨论,不必涉及过多的细节.

    3. 性能,他们起优化作用,可以大幅提高程序的运行速度,减少需要传送到客户端的代码量,最经典的享元模式和代理模式.
      你也可能不使用因为如下缘由design pattern:

    4. 复杂性,获得可维护性往往需要付出代价,新手更不容易理解.

    5. 性能,多数模式性能上都有一点点拖累,不过能不能接受取决于项目需要.

    6. 懂得应该在什么时候选择恰当的模式较为困难,盲目套用会带来许多不安全问题.

    小节

    这一章是基础概念,研究意义,研究内容等等方面的定位与确认,并不太适合作出相关实践性的项目或者举出demo.

    第二章 接口

    js中没有内置的创建或者实现接口的方法,也没有内置的方法用于判断一个对象是否实现了与另一个对象相同的一套方法,使得对象很难互换使用,不过js很灵活,添加这些特性并不难.

    接口定义

    它提供了一种用以说明一个对象具有哪些方法的手段,可以表明这些方法的语义,但是并不解释应该如何实现. e.g.如果一个接口包含setName方法,那么可以认为该方法的实现有一个字符串参数,并且会把该参数赋值给name变量.
    有了接口,就能按照对象的特性进行分组,即使一批对象存在极大差异,只要他们都实现Comparable接口,那么在Object.compare(anotherObject)方法中就可以互换使用这些对象.

    优点

    • 描述作用,促进代码重用.表明一个类实现了哪些方法,从而帮助使用该类.

    • 如果事先知道了接口,就能减少在集成两个对象的过程中出现的问题,可以事先声明一个类应该具有哪些特性和操作,程序员A针对所需要的类定义一个接口,然后把它交给B,B可以随意编码代码只要他定义的类实现了那个接口.

    • 测试和调试更为轻松.使用接口可以让类型不匹配错误的查找变得更容易,如果一个对象没有实现必要的方法,就会得到明确的错误提示,逻辑错误可以被限制在方法自身,而不是在对象的构成之中.接口还能让代码更稳固,因为对接口的任何改变在所有实现它的类中都必须体现出来,如果接口添加一个操作,某个实现它的类并没有相应的添加这个操作,就会报错.

    缺点

    • 接口的使用在一定程度上强化了类型的作用,减少了js的灵活性.

    • js中任何实现接口的方法都会有性能上的一些影响,缘由额外的方法调用的开销,实现方法中使用两个for循环来遍历每个接口的每个方法,对于大型接口和实现许多不同接口的对象会消耗较多时间.

    • 接口的最大问题是没法强迫其它人遵循你定义的接口,其他语言中接口内置,某人定义了实现一个接口的类,那么编译器会确保该类的确实现了这个接口,在js中必须手动地去保证某个类实现了一个接口,编码规范和辅助类可以帮忙但无法根治.只有团队成员都愿意使用接口并进行检查,接口的很多价值才能体现出来.

    其他面向对象语言处理接口的方式

    以Java为例,下面是java.io包中的一个接口:

    public interface DataOutput {
        void writeBoolean(boolean value) throws  IOException;
        void writeByte(int value) throws IOException;
        void writeChar(int value) throws IOException;
        void writeShort(int value) throws IOException;
        void writeInt(int value) throws IOException;
        ...
    }

    它列出了一个类应该实现的一批方法,包括方法的参数和可能会抛出的异常,每一行都像是一个方法声明,只不过是以一个分号而不是一对大括号结尾.
    创建一个实现该接口的类需要使用关键字implements:

    public class DataOutStream extends FilterOutputStream implements DataOutput {
        public final void writeBoolean (blooean value) throws IOException {
            write (value ? 1 : 0);
        }    
        ...
    }

    该类声明并具体实现了接口中列出的每一个方法,漏掉任何一个方法都会导致在编译时显示错误.下面是Java编译器在发现一个接口错误时可能产生的输出信息:

    MyClass should be declared abstract; it does not define writeBoolean(boolean) in MyClass.

    从上面的Java代码可以看出接口包含的信息:需要实现的方法名以及这些方法的参数,类的定义明确地声明他们实现了这个接口(通常使用implements关键字),一个类可以实现不止一个接口,如果接口中某个方法没有被实现,则会产生一个错误有的语言产生在编译时,有的在运行时,错误包含三类信息:类名,接口名,未被实现的方法名.
    因为js中没有interface和implements关键字(es5的严格模式把interface视作保留字),不能在运行时对接口约定是否得到遵守进行检查,所以我们可以通过使用辅助类和显式检查来进行模仿.

    在js中模仿接口

    注释法,属性检查法,鸭式辨型法,三种方法当中没有完美的,结合起来可以令人满意.

    用注释描述接口

    最简单,但是效果最差,使用interface和implements关键字,但是放在注释当中,以免引起语法错误:

    /*
    interface Composite {
        function add(child);
        function remove(child);
        function getChild(index);
    }
    interface FormItem {
        function save();
    }
    */
    var CompositeForm = function(id, method, action) {
        //implements Composite, FormItem
        ...
    };
    //Implement the Composite interface.
    CompositeForm.prototype.add = function (child) {
        ...
    };
    CompositeForm.prototype.remove = function (child) {
        ...
    };
    //Implement the FormItem interface.
    CompositeForm.prototype.save = function (child) {
        ...
    };

    可以看出,注释法没有为确保CompositeForm实现了所有方法而进行检查,不抛出错误,对测试和调试没有任何帮助.归属于程序文档范畴,对接口约定的遵守依靠自觉.
    但是注释法也有优点:易于实现,不需要额外的类或者函数,可以提高代码的重用性,因为程序员可以把实现同样接口的类互换使用.

    用属性检查模仿接口

    这种方法更严谨一些,类明确声明自己实现了哪些接口,那些与类打交道的对象可以针对这些声明进行检查,那些接口自身仍然只是注释,但是现在你可以通过检查一个属性得知某个类自称实现了什么接口:

    /*
    interface Composite {
        function add(child);
        function remove(child);
        function getChild(index);
    }
    interface FormItem {
        function save();
    }
    */
    var CompositeForm = function (id, method, action) {
        this.implementsInterfaces = ['Composite', 'FormItem'];
        ...
    };
    ...
    function addForm (formInstance) {
        if(!implements(formInstance, 'Composite', 'FormItem')) {
            throw new Error("Object does not implement a required interface.");
        }
        ...
    }
    //The implements function, which checks to see if an object declares that it 
    //implements the required interfaces.
    function implements (object) {
        for (var i = 1; i < arguments.length; i++) {
            //Loop through all arguments after the first one.
            var interfaceName = arguments[i];
            var interfaceFound = false;
            for (var j = 0; j < object.implementsInterface.length; j++) {
                if  (object.implementsInterfaces[j] == interfaceName) {
                    interfaceFound = true;
                    break;
                }
            }
            if (!interfaceFound) {
                return false;
                //An interface was not found.
            }
        }
        return true;
        //All interfaces were found.
    }

    在这个例子中,CompositeForm宣称自己实现了Composite和FormItem接口,把接口名称加入一个implementsInterfaces数组,类显示声明自己支持什么接口,任何一个要求其参数属于特定类型的函数都可以对这个属性进行检查,并在所需接口未在声明之列时抛出错误.
    优点:对类所实现的接口提供了文档说明,如果所需要的接口不在一个类宣称支持的接口之列,会报错.
    缺点:未确保真正实现了自称实现的接口,只知道它是否说自己实现了接口.在创建一个类时声明它实现了一个接口,但是后来在实现该接口所规定的方法时却漏掉了其中的某一个,这一种错误很常见,此时所有检查都能通过,但是那个方法却不存在,是一个隐患.

    用鸭式辨型模仿接口

    类是否声明自己支持哪些接口并不重要,只要他具有这些接口中的方法就行.James Whitcomb Riley曾说过'像鸭子一样走路并且嘎嘎叫的就是鸭子',也就是说鸭式辨型法把对象实现的方法集作为判断他是不是某个类的实例的唯一标准,在检查一个类是否实现了某个接口时特别有用.如果对象具有与接口定义的方法同名的所有方法,那么就可以认为它实现了这个接口,用一个辅助函数来确保对象具有所必需的方法:

    //Interface.
    var Composite = new Interface('Composite', ['add', 'remove', 'getChild']);
    var FormItem = new Interface('FormItem', ['save']);
    //CompositeForm class
    var CompositeForm = function (id, method, action) {
        ...
    };
    ...
    function addForm (formInstance) {
        ensureImplements(formInstance, Composite, FormItem);
        //This function will throw an error if 
        //a required method is not implemented.
        ...
    }

    与另外两种方法不同,这种方法并不借助于注释.ensureImplements函数需要至少两个参数,其一是想要检查的对象,其余参数是对那个对象进行检查的接口.该函数检查其第一个参数所代表的对象是否实现了那些接口所声明的所有方法,如果发现漏掉任何一个方法,就会抛出错误,其中包含所缺少的那个方法和未被正确实现的接口的名称.这种检查可用于代码中任何需要确保某个对象实现某个接口的地方.
    尽管鸭式辨型可能最有用,但是类并不声明自己实现了哪些接口,降低了代码的重用性,而且也缺乏其他两种方法的自我描述性,他需要一个辅助类Interface和一个辅助函数ensureImplements,只关心方法的名称,并不检查其参数的名称,数目,类型.

    书中所采用的接口实现方法

    综合使用了第一种和第三种,用注释声明类支持的接口,从而提高代码重用性及其文档完善性.采用辅助类Interface和类方法Interface.ensureImplements来对对象实现的方法进行显式检查.示例:

    //Interface.
    var Composite = new Interface('Composite', ['add', 'remove', 'getChild']);
    var FormItem = new Interface('FormItem', ['save']);
    //CompositeForm class
    var CompositeForm = fucntion(id, method, action) {//implements Composite
        //, FormItem
        ...
    };
    ...
    function addForm (formInstance) {
        Interface.ensureImplements(formInstance, Composite, FormItem);
        //This function will throw an error if a required method 
        //is not implemented, halting execution of the function.
        //All code beneath this line will be executed only if the checks pass.
        ...
    }

    如果Interface.ensureImplements发现有问题,就会抛出一个错误,要么被其他代码捕捉到并得到处理要么中断程序的执行.

    Interface类

    下面是书中使用的Interface类定义:

    //Constructor.
    var Interface = function(name, methods) {
        if(arguments.length != 2){
            throw new Error("Interface constructor called with " + arguments.length + "arguments, but expected exactly 2.");
        }
        this.name = name;
        this.methods = [];
        for(var i = 0; len = methods.length; i < len; i++) {
            if(typeof methods[i] !== 'string') {
                throw new Error("Interface constructor expects method names to be " + "passed in as a string.");
            }
            this.methods.push(methods[i]);
        }
    };
    //Static class method.
    Interface.ensureImplements = function (object) {
        if (arguments.length < 2) {
            throw new Error("Function Interface.ensureImplements called with " + arguments.length +"arguments, but expected at least 2.");
        }
        for (var i = 1, len = arguments.length; i < len; i++) {
            var interface = arguments[i];
            if(interface.constructor !== interface) {
                throw new Error("Function Interface.ensureImplements expects arguments" + "two and above to be instance of Interface.");
            }
            for (var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++) {
                var method = interface.methods[j];
                if(!object[method] || typeof object[method] !== 'function'){        
                    throw new Error("Function Interface.ensureImplements: object " + "does not implement the " + interface.name + " interface.Method " + method + " was not found.");
                }
            }
        }
    };

    可以看出,该类的所有方法其参数都有严格要求,如果参数未通过检查将抛出错误,我们特地加入这种检查的目的在于:如果没有抛出错误,那么你可以肯定接口已经得到了正确的声明和实现.

    Interface类的使用场合

    说实话,之前我还在想好像真的就没怎么用过所谓的接口,或许框架源码涉及得较多其他地方涉及地较少,模块化也没怎么用过接口.下面来谈谈接口到底是怎么真正派上用场的.
    许多js程序员根本不用接口或者它提供的那种检查,只能说接口在运用设计模式实现复杂系统的时候最能体现价值,看似降低了js的灵活性,实际上因为使用接口可以降低对象间耦合程度所以提高了代码灵活性.可以让函数变得更灵活,因为既能向函数传递任何类型的参数还能保证它只会使用那些具有必要方法的对象.
    在大型项目中,接口至关重要,常常需要使用还未编写出来的API或者需要提供一些占位代码stub以免延误开发进度,接口在这种场合中的重要性表现在许多方面,它们记载着API,可以作为程序员正式交流的工具,在占位代码中被替换为最终的API时就能立刻知道所需方法是否得到实现.在开发过程中如果API发生变化,只要新的API实现同样的接口,它就能天衣无缝地替换原有API.
    现在,项目中用到来自Internet,无法直接控制的代码越来越普遍,部署在外部环境中的程序库/搜索,email,map等服务的API就是这类代码的例子.即使来源可信,也必须谨慎使用,确保其变化不会在自己的代码中引起问题.一种应对之策就是为所依赖的每一个API创建一个Interface对象,然后对接收到的每一个对象都进行检查,以确保其正确实现了那些接口:

    var DynamicMap = new Interface('DynamicMap', ['centerOnPoint', 'zoom', 'draw']);
    function displayRoute (mapInstance) {
        Interface.ensureImplements(mapInstance, DynamicMap);
        mapInstance.centerOnPoint(12, 34);
        mapInstance.zoom(5);
        mapInstance.draw();
        ...
    }

    示例中,displayRoute函数要求传入的参数具有3个特定方法,通过使用一个Interface对象和调用Interface.ensureImplements方法,可以确保这些方法已经得到实现,否则将会见到错误.这个错误可以用try/catch捕获,然后可能会被用于发送一条ajax请求,将外部API引起的问题告知用户.

    Interface类的用法

    判断在代码中使用接口是否划算最重要,也最困难.对于小型项目来说接口的好处也许并不明显只是徒增复杂度.自行权衡利弊后,如果认为利大于弊,需要在项目中使用接口,可以按如下方法:

    1. 将Interface类--Interface.js文件引入html文件.

    2. 逐一检查代码中所有以对象为参数的方法,搞清要求这些对象参数具有哪些方法.

    3. 为每一个不同的方法创建一个Interface对象.

    4. 剔除所有针对构造器的显式检查,因为鸭式辨型的使用所以对象类型不再重要.

    5. 以Interface.ensureImplements取代原来的构造器检查.
      按以上方法使用接口之后的好处:代码的耦合度降低,不再依赖于任何特定的类实例,而是检查所需要的特性是否都已就绪(不管具体如何实现),于是你在对代码进行优化和重构时将拥有更大的自由.

    依赖于接口的设计模式

    • 工厂模式,对象工厂所创建的具体对象会因具体情况,使用接口可以确保所创建的这些对象可以互换使用.对象工厂可以保证其生产出来的对象都生产了必需的方法.

    • 组合模式,其中心思想在于可以将对象群体与其组成对象同等对待,通过让他们实现同样的接口来做到,如果不进行某种形式的鸭式辨型或者类型检查,组合模式就会失去作用.

    • 装饰者模式,通过透明地为另一对象而发挥作用,这是通过实现与另外那个对象完全相同的接口而做到的,对于外界而言,一个装饰者和他所包装的对象看出区别,使用Interface类来确保所创建的装饰者对象实现了必须的方法.

    • 命令模式,代码中所有的命令对象都要实现同一批方法(execute,run,undo等等),通过使用接口,为执行这些命令对象而创建的类可以不必知道这些对象具体是什么,只要知道他们实现了正确的接口即可.于是你可以创建出模块化度高而耦合度低的用户界面和API.

    小结

    我google了一下,有关接口模式的开发实践并不是很多,但是就算实际小型项目当中用的不多,接口模式依然还是许多其他模式的基础依赖,地位重要.
    最后分享一个蛮有用的工具,上git的好东西.


    南赐
    125 声望4 粉丝

    大多数情况下会在segmentfault上面活跃,日后主攻小程序,storybook + vue 组件开发,前端测试,持续集成,node.js,eggjs等等。期待与大家深入交流技术~