面向接口编程

前面的话

  谈到接口的时候,一般会涉及如下几种含义。常常说一个库或者模块对外提供了某某API接口。经过主动暴露的接口来通讯,能够隐藏软件系统内部的工做细节。这也是最熟悉的第一种接口含义。第二种接口是一些语言提供的关键字,好比Java的interface。interface关键字能够产生一个彻底抽象的类。这个彻底抽象的类用来表示一种契约,专门负责创建类与类之间的联系。第三种接口便是谈论的“面向接口编程”中的接口,接口是对象能响应的请求的集合。本文将详细介绍面向接口编程javascript

 

Java抽象类

  由于javascript并无从语言层面提供对抽象类(Abstractclass)或者接口(interface)的支持,有必要从一门提供了抽象类和接口的语言开始,逐步了解“面向接口编程”在面向对象程序设计中的做用html

  有一个鸭子类Duck,还有一个让鸭子发出叫声的AnimalSound类,该类有一个makeSound方法,接收Duck类型的对象做为参数,代码以下:java

public class Duck {    // 鸭子类
  public void makeSound(){ 
    System.out.println( "嘎嘎嘎" );
  }
}

public class AnimalSound {
  public void makeSound( Duck duck ){    // (1) 只接受 Duck 类型的参数
    duck.makeSound();
  }
}
public class Test {
  public static void main( String args[] ){ 
    AnimalSound animalSound = new AnimalSound(); 
    Duck duck = new Duck();
    animalSound.makeSound( duck );    // 输出:嘎嘎嘎
  }
}

  目前已经能够顺利地让鸭子发出叫声。后来动物世界里又增长了一些鸡,如今想让鸡也叫唤起来,但发现这是一件不可能完成的事情,由于在上面这段代码的(1)处,即AnimalSound类的sound方法里,被规定只能接受Duck类型的对象做为参数:程序员

public class Chicken {    // 鸡类
  public void makeSound(){ 
    System.out.println( "咯咯咯" );
  }
}

public class Test {
  public static void main( String args[] ){ 
    AnimalSound animalSound = new AnimalSound(); 
    Chicken chicken = new Chicken(); 
    animalSound.makeSound( chicken );
  // 报错,animalSound.makeSound 只能接受 Duck 类型的参数
  }
}

  在享受静态语言类型检查带来的安全性的同时,也失去了一些编写代码的自由编程

  静态类型语言一般设计为能够“向上转型”。当给一个类变量赋值时,这个变量的类型既可使用这个类自己,也可使用这个类的超类。就像看到天上有只麻雀,既能够说“一只麻雀在飞”,也能够说“一只鸟在飞”,甚至能够说成“一只动物在飞”。经过向上转型,对象的具体类型被隐藏在“超类型”身后。当对象类型之间的耦合关系被解除以后,这些对象才能在类型检查系统的监视下相互替换使用,这样才能看到对象的多态性设计模式

  因此若是想让鸡也叫唤起来,必须先把duck对象和chicken对象都向上转型为它们的超类型Animal类,进行向上转型的工具就是抽象类或者interface。即将使用的是抽象类。先建立一个Animal抽象类:数组

public abstract class Animal{
  abstract void makeSound();    //抽象方法
}

  而后让Duck类和Chicken类都继承自抽象类Animal:安全

public class Chicken extends Animal{ 
  public void makeSound(){
    System.out.println( "咯咯咯" );
  }
}

public class Duck extends Animal{ 
  public void makeSound(){
    System.out.println( "嘎嘎嘎" );
  }
}

  也能够把Animal定义为一个具体类而不是抽象类,但通常不这么作。如今剩下的就是让AnimalSound类的makeSound方法接收Animal类型的参数,而不是具体的Duck类型或者Chicken类型:闭包

public class AnimalSound{
  public void makeSound( Animal animal ){ // 接收 Animal 类型的参数,而非 Duck 类型或 Chicken 类型
    animal.makeSound();
  }
}

public class Test {
  public static void main( String args[] ){ 
    AnimalSound animalSound = new AnimalSound ();
    Animal duck = new Duck();    // 向上转型
    Animal chicken = new Chicken();    // 向上转型
    animalSound.makeSound( duck );    // 输出:嘎嘎嘎
    animalSound.makeSound( chicken );    // 输出:咯咯咯
  }
}

  抽象类在这里主要有如下两个做用编程语言

  一、向上转型。让Duck对象和Chicken对象的类型都隐藏在Animal类型身后,隐藏对象的具体类型以后,duck对象和chicken对象才能被交换使用,这是让对象表现出多态性的必经之路

  二、创建一些契约。继承自抽象类的具体类都会继承抽象类里的abstract方法,而且要求覆写它们。这些契约在实际编程中很是重要,能够帮助编写可靠性更高的代码。好比在命令模式中,各个子命令类都必须实现execute方法,才能保证在调用command.execute的时候不会抛出异常。若是让子命令类OpenTvCommand继承自抽象类Command:

abstract class Command{
  public abstract void execute();
}

public class OpenTvCommand extends Command{ 
  public OpenTvCommand (){};
  public void execute(){ 
    System.out.println( "打开电视机" );
  }
}

  天然有编译器帮助检查和保证子命令类OpenTvCommand覆写了抽象类Command中的execute抽象方法。若是没有这样作,编译器会尽量早地抛出错误来提醒正在编写这段代码的程序员

  总而言之,不关注对象的具体类型,而仅仅针对超类型中的“契约方法”来编写程序,能够产生可靠性高的程序,也能够极大地减小子系统实现之间的相互依赖关系,这就是面向接口编程

  从过程上来看,“面向接口编程”实际上是“面向超类型编程”。当对象的具体类型被隐藏在超类型身后时,这些对象就能够相互替换使用,关注点才能从对象的类型上转移到对象的行为上。“面向接口编程”也能够当作面向抽象编程,即针对超类型中的abstract方法编程,接口在这里被当成abstract方法中约定的契约行为。这些契约行为暴露了一个类或者对象可以作什么,可是不关心具体如何去作

 

interface

  除了用抽象类来完成面向接口编程以外,使用interface也能够达到一样的效果。虽然不少人在实际使用中刻意区分抽象类和interface,但使用interface实际上也是继承的一种方式,叫做接口继承

  相对于单继承的抽象类,一个类能够实现多个interface。抽象类中除了abstract方法以外,还能够有一些供子类公用的具体方法。interface使抽象的概念更进一步,它产生一个彻底抽象的类,不提供任何具体实现和方法体,但容许该interface的建立者肯定方法名、参数列表和返回类型,这至关于提供一些行为上的约定,但不关心该行为的具体实现过程。interface一样能够用于向上转型,这也是让对象表现出多态性的一条途径,实现了同一个接口的两个类就能够被相互替换使用

  再回到用抽象类实现让鸭子和鸡发出叫声的故事。这个故事得以完美收场的关键是让抽象类Animal给duck和chicken进行向上转型。但此时也引入了一个限制,抽象类是基于单继承的,也就是说不可能让Duck和Chicken再继承自另外一个家禽类。若是使用interface,能够仅仅针对发出叫声这个行为来编写程序,同时一个类也能够实现多个interface

  下面用interface来改写基于抽象类的代码。先定义Animal接口,全部实现了Animal接口的动物类都将拥有Animal接口中约定的行为:

public interface Animal{ 
  abstract void makeSound();
}

public class Duck implements Animal{
  public void makeSound() {    // 重写 Animal 接口的 makeSound 抽象方法
    System.out.println( "嘎嘎嘎" );
  }
}

public class Chicken implements Animal{
  public void makeSound() {    // 重写 Animal 接口的 makeSound 抽象方法
   System.out.println( "咯咯咯" );
  }
}

public class AnimalSound {
  public void makeSound( Animal animal ){ 
    animal.makeSound();
  }
}

public class Test {
  public static void main( String args[] ){ 
    Animal duck = new Duck();
    Animal chicken = new Chicken();
    AnimalSound animalSound = new AnimalSound(); 
    animalSound.makeSound( duck );    // 输出:嘎嘎嘎 
    animalSound.makeSound( chicken );        // 输出:咯咯咯
  }
}

 

javascript

  由于javascript是一门动态类型语言,类型自己在javascript中是一个相对模糊的概念。也就是说,不须要利用抽象类或者interface给对象进行“向上转型”。除了number、string、boolean等基本数据类型以外,其余的对象均可以被当作“天生”被“向上转型”成了Object类型:

var ary = new Array();
var date = new Date();

  若是javascript是一门静态类型语言,上面的代码也许能够理解为:

Array ary = new Array();
Date date = new Date();

  或者:

Object ary = new Array();
Object date = new Date();

  不多有人在javascript开发中去关心对象的真正类型。在动态类型语言中,对象的多态性是与生俱来的,但在另一些静态类型语言中,对象类型之间的解耦很是重要,甚至有一些设计模式的主要目的就是专门隐藏对象的真正类型

  由于不须要进行向上转型,接口在javascript中的最大做用就退化到了检查代码的规范性。好比检查某个对象是否实现了某个方法,或者检查是否给函数传入了预期类型的参数。若是忽略了这两点,有可能会在代码中留下一些隐藏的bug。好比尝试执行obj对象的show方法,可是obj对象自己却没有实现这个方法,代码以下:

function show( obj ){
  obj.show();    // Uncaught TypeError: undefined is not a function
}

var myObject = {};    // myObject 对象没有 show 方法
show( myObject );

或者:
function show( obj ){
  obj.show();    // TypeError: number is not a function
}

var myObject = {    // myObject.show 不是 Function 类型
  show: 1
};
show( myObject );

  此时,不得不加上一些防护性代码:

function show( obj ){
  if ( obj && typeof obj.show === 'function' ){ 
    obj.show();
  }
}

  或者:

function show( obj ){ 
  try{
    obj.show();
  }catch( e ){
  }
}
var myObject = {};    // myObject 对象没有 show 方法
// var myObject = {    // myObject.show 不是 Function 类型
// show: 1
// };

show( myObject );

  若是javascript有编译器帮助检查代码的规范性,那事情要比如今美好得多,不用在业务代码中处处插入一些跟业务逻辑无关的防护性代码。做为一门解释执行的动态类型语言,把但愿寄托在编译器上是不可能了。若是要处理这类异常状况,只有手动编写一些接口检查的代码

【接口检查】

  鸭子类型是动态类型语言面向对象设计中的一个重要概念。利用鸭子类型的思想,没必要借助超类型的帮助,就能在动态类型语言中轻松地实现面向接口编程。好比,一个对象若是有push和pop方法,而且提供了正确的实现,它就能被看成栈来使用;一个对象若是有length属性,也能够依照下标来存取属性,这个对象就能够被看成数组来使用。若是两个对象拥有相同的方法,则有很大的可能性它们能够被相互替换使用

  在Object.prototype.toString.call([])==='[object Array]'被发现以前,常常用鸭子类型的思想来判断一个对象是不是一个数组,代码以下:

var isArray = function( obj ){ 
  return obj &&
    typeof obj === 'object' && 
    typeof obj.length === 'number' &&
    typeof obj.splice === 'function'
};

  固然在javascript开发中,老是进行接口检查是不明智的,也是没有必要的,毕竟如今还找不到一种好用而且通用的方式来模拟接口检查,跟业务逻辑无关的接口检查也会让不少javascript程序员以为不值得和不习惯

 

TypeScript

  虽然在大多数时候interface给javascript开发带来的价值并不像在静态类型语言中那么大,但若是正在编写一个复杂的应用,仍是会常常怀念接口的帮助。下面以基于命令模式的示例来讲明interface如何规范程序员的代码编写,这段代码自己并无什么实用价值,在javascript中,通常用闭包和高阶函数来实现命令模式

  假设正在编写一个用户界面程序,页面中有成百上千个子菜单。由于项目很复杂,决定让整个程序都基于命令模式来编写,即编写菜单集合界面的是某个程序员,而负责实现每一个子菜单具体功能的工做交给了另一些程序员。那些负责实现子菜单功能的程序员,在完成本身的工做以后,会把子菜单封装成一个命令对象,而后把这个命令对象交给编写菜单集合界面的程序员。已经约定好,当调用子菜单对象的execute方法时,会执行对应的子菜单命令。虽然在开发文档中详细注明了每一个子菜单对象都必须有本身的execute方法,但仍是有一个粗心的javascript程序员忘记给他负责的子菜单对象实现execute方法,因而当执行这个命令的时候,便会报出错误,代码以下:

<html>
<body>
    <button id="exeCommand">执行菜单命令</button>
    <script>
        var RefreshMenuBarCommand = function(){};
        RefreshMenuBarCommand.prototype.execute = function(){
            console.log( '刷新菜单界面' );
        };
        var AddSubMenuCommand = function(){};
        AddSubMenuCommand.prototype.execute = function(){
            console.log( '增长子菜单' );
        };
        var DelSubMenuCommand = function(){};
        /*****没有实现DelSubMenuCommand.prototype.execute *****/
        // DelSubMenuCommand.prototype.execute = function(){
        // };

        var refreshMenuBarCommand = new RefreshMenuBarCommand(),
        addSubMenuCommand = new AddSubMenuCommand(),
        delSubMenuCommand = new DelSubMenuCommand();
        var setCommand = function( command ){
            document.getElementById( 'exeCommand' ).onclick = function(){
                command.execute();
            }
        };
        setCommand( refreshMenuBarCommand );
        // 点击按钮后输出:"刷新菜单界面"
        setCommand( addSubMenuCommand );
        // 点击按钮后输出:"增长子菜单"
        setCommand( delSubMenuCommand );
        // 点击按钮后报错。Uncaught TypeError: undefined is not a function
</script>
</body>
</html>

  为了防止粗心的程序员忘记给某个子命令对象实现execute方法,只能在高层函数里添加一些防护性的代码,这样当程序在最终被执行的时候,有可能抛出异常来提醒咱们,代码以下

var setCommand = function( command ){
    document.getElementById( 'exeCommand' ).onclick = function(){
        if ( typeof command.execute !== 'function' ){
            throw new Error( "command 对象必须实现execute 方法" );
        }
        command.execute();
    }
};

  若是确实不喜欢重复编写这些防护性代码,还能够尝试使用TypeScript来编写这个程序。TypeScript是微软开发的一种编程语言,是javascript的一个超集。跟CoffeeScript相似,TypeScript代码最终会被编译成原生的javascript代码执行。经过TypeScript,可使用静态语言的方式来编写javascript程序。用TypeScript来实现一些设计模式,显得更加原汁原味。TypeScript目前的版本尚未提供对抽象类的支持,可是提供了interface。下面就来编写一个TypeScript版本的命令模式

  首先定义Command接口:

interface Command{
  execute:Function;
}

  接下来定义RefreshMenuBarCommand、AddSubMenuCommand和DelSubMenuCommand这3个类,它们分别都实现了Command接口,这能够保证它们都拥有execute方法:

    class RefreshMenuBarCommand implements Command{
        constructor (){
        }
        execute(){
            console.log( '刷新菜单界面' );

        }
    }
    class AddSubMenuCommand implements Command{
        constructor (){
        }
        execute(){
            console.log( '增长子菜单' );
        }
    }
    class DelSubMenuCommand implements Command{
        constructor (){
        }
            // 忘记重写execute 方法
    }

    var refreshMenuBarCommand = new RefreshMenuBarCommand(),
    addSubMenuCommand = new AddSubMenuCommand(),
    delSubMenuCommand = new DelSubMenuCommand();
    refreshMenuBarCommand.execute(); // 输出:刷新菜单界面
    addSubMenuCommand.execute(); // 输出:增长子菜单
    delSubMenuCommand.execute(); // 输出:Uncaught TypeError: undefined is not a function

  忘记在DelSubMenuCommand类中重写execute方法时,TypeScript提供的编译器及时给出了错误提示

  这段TypeScript代码翻译过来的javascript代码以下:

var RefreshMenuBarCommand = (function () { 
  function RefreshMenuBarCommand() {}
  RefreshMenuBarCommand.prototype.execute = function () { 
    console.log('刷新菜单界面');
  };
  return RefreshMenuBarCommand;
})();

var AddSubMenuCommand = (function () { 
  function AddSubMenuCommand() {}
  AddSubMenuCommand.prototype.execute = function () { 
    console.log('增长子菜单');
  };
  return AddSubMenuCommand;
})();

var DelSubMenuCommand = (function () { 
  function DelSubMenuCommand() {}
  return DelSubMenuCommand;
})();

var refreshMenuBarCommand = new RefreshMenuBarCommand(), 
    addSubMenuCommand = new AddSubMenuCommand(), 
    delSubMenuCommand = new DelSubMenuCommand();

refreshMenuBarCommand.execute(); 
addSubMenuCommand.execute(); 
delSubMenuCommand.execute();
相关文章
相关标签/搜索