前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面向接口编程

面向接口编程

作者头像
一粒小麦
发布2019-11-21 20:10:39
6560
发布2019-11-21 20:10:39
举报
文章被收录于专栏:一Li小麦

面向接口编程

当我们谈到接口的时候,可能会联想到三样事物:

我们经常说一个库或者模块对外提供了某某API。通过主动暴露的接口来通信,可以隐藏软件系统内部的工作细节。这也是我们最熟悉的第一种接口含义。

第二种接口是一些语言提供的关键字,比如Java的interface。interface关键字可以产生一个完全抽象的类。这个完全抽象的类用来表示一种契约,专门负责建立类与类之间的联系。

第三种接口即是我们谈论的“面向接口编程”中的接口,接口的含义在这里体现得更为抽象。用《设计模式》中的话说就是:接口是对象能响应的请求的集合。

本文讨论第二种和第三种。

JAVA的抽象类

假设java的世界里,有一只鸭子(Duck),还有一只让鸭子发声的(AnimalSound)类,它有一个makeSound方法,接受Duck类型的对象为参数,当让这个鸭子叫的时候,用代码描述就是:

代码语言:javascript
复制
    public class Duck {

      public void makeSound(){

        System.out.println('嘎');

      }

    }


    public class AnimalSound {

      // 只接受Duck类型参数

      public void makeSound(Duck duck){

        duck.makeSound();

      }

    }


    public class Test{

      public static void main(String args[]){

        AnimalSound animalSound=new AnimalSound();

        Duck duck=new Duck();

        animalSound.makeSound(duck); // 输出:嘎

      }

    }
超类实现继承

后来这个世界多了一只鸡,现在我们想让鸡也叫唤起来,但发现这是一件现有代码不可能完成的事情,因为在上面这段代码中AnimalSound类的sound方法里,被规定只能接受Duck类型的对象作为参数。

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

像java这样的静态语言,通常可以设置为"向上传型"——当给一个类变量赋值时,这个变量的类型既可以使用这个类本身,也可以使用这个类的超类。

就像看到鸭子叫,我们既可以说“一只鸭子在叫”,也可以说“一只鸟在叫”,甚至可以说成“一只生物在叫”。通过向上转型,对象的具体类型被隐藏在“超类型”身后。当对象类型之间的耦合关系被解除之后,这些对象才能在类型检查系统的监视下相互替换使用,这样才能看到对象的多态性。

所以如果想让鸡也叫唤起来,必须先把duck对象和chicken对象都向上转型为它们的超类型Animal类,进行向上转型的工具就是抽象类或者interface。

代码语言:javascript
复制
    public abstract class Animal{

      abstract void makeSound(); // 抽象方法

    }


    // 让Duck类和Chicken类都继承自抽象类Animal:

    public class Duck extends Animal {

      public void makeSound(){

        System.out.println('嘎');

      }

    }


    public class Chicken extends Animal {

      public void makeSound(){

        System.out.println('咯');

      }

    }

因此,AnimalSound的makeSound方法需要接受Animal类而非Duck类

代码语言:javascript
复制
    public class AnimalSound {

      // 只接受Duck类型参数

      public void makeSound(Animal animal){

        animal.makeSound();

      }

    }


    public class Test{

      public static void main(String args[]){

        AnimalSound animalSound=new AnimalSound();

        Animal duck=new Duck();

        animalSound.makeSound(duck); // 输出:嘎


        Animal chicken=new Chicken();

        animalSound.makeSound(chicken); // 输出:咯

      }

    }

很明显,Animal是抽象类。它的主要作用在于向上传型,完成多态的功能,更重要的是建立契约——这些契约行为暴露了一个类或者对象能够做什么,但是不关心具体如何去做。

继承自抽象类的具体类都会继承抽象类里的abstract方法,并且要求覆写它们。这些契约在实际编程中非常重要,可以帮助我们编写可靠性更高的代码。比如在上面代码中,各个子类都必须实现makeSound方法,才能保证在不会抛出异常。

总而言之,不关注对象的具体类型,而仅仅针对超类型中的“契约方法”来编写程序,可以产生可靠性高的程序,也可以极大地减少子系统实现之间的相互依赖关系。这就是我们本文要讨论的主题:

面向接口编程,而不是面向实现编程。

从过程上来看,“面向接口编程”其实是“面向超类型编程”。当对象的具体类型被隐藏在超类型身后时,这些对象就可以相互替换使用,我们的关注点才能从对象的类型转移到对象的行为上。

interface

上面的代码中,我们通过引入超类Animal来解决问题。但是有个缺点在于此Animal抽象类是基于单继承的,也就是说我们不可能让Duck和Chicken再继承自另一个家禽类。如果使用interface,可以仅仅针对发出叫声这个行为来编写程序,同时一个类也可以实现多个interface。

代码语言:javascript
复制
    public interface class Animal{

      abstract void makeSound(); // 抽象方法

    }


    // 让Duck类和Chicken类都继承自抽象类Animal:

    public class Duck implements Animal {

      public void makeSound(){

        System.out.println('嘎');

      }

    }


    public class Chicken implements Animal {

      public void makeSound(){

        System.out.println('咯');

      }

    }


    // ...

    // 其它代码同上

回到js

本文花费大量篇幅在java的代码描述上,是为了说明面向接口编程的两个要点:

  • 通过向上转型来隐藏对象的真正类型,以表现对象的多态性。
  • 约定类与类之间的一些契约行为。

本系列文章也多次提到,JavaScript不是一个真正有类的语言。面向接口编程在JavaScript中的最大作用就退化到了检查代码的规范性。比如检查某个对象是否实现了某个方法,或者检查是否给函数传入了预期类型的参数。如果忽略了这两点,有可能会在代码中留下一些隐藏的bug。比如我们尝试执行obj对象的show方法,但是obj对象本身却没有实现这个方法,

代码语言:javascript
复制
Uncaught TypeError:: aaa.show() is not a function.

这是很多jser都会遇到过的错误。为此我们不得不写一些防御性的代码:

代码语言:javascript
复制
aaa.show && aaa.show()

这时候你会想,如果能有静态语言的类型检查机制,我就不用在业务代码中写那么多无关的东西了。

在js中,假设你要判断一个对象是否数组,可能会这么做:

代码语言:javascript
复制
    var isArray=function(obj){

      return obj&&

        typeof obj==='object'&&

        typeof obj.length==='number'&&

        typeof obj.splice==='function'

    };

如果它满足数组的方法,它就可以当数组来用了。现在看一个更加"高级"的语言是如何实现的。

基于TypeScript的命令模式

TypeScript是微软开发的一种编程语言,是JavaScript的一个超集。TypeScript代码最终会被编译成原生的JavaScript代码执行。通过TypeScript,我们可以使用静态语言的方式来编写JavaScript程序。用TypeScript来实现一些设计模式,显得更加原汁原味。本系列文章结束后,笔者也将对ts进行系统梳理。

假设我们正在编写一个用户界面程序,页面中有成百上千个子菜单。因为项目很复杂,我们决定让整个程序都基于命令模式来编写,即编写菜单集合界面的是某个程序员,而负责实现每个子菜单具体功能的工作交给了另外一些程序员。

那些负责实现子菜单功能的程序员,在完成自己的工作之后,会把子菜单封装成一个命令对象,然后把这个命令对象交给编写菜单集合界面的程序员。他们已经约定好,当调用子菜单对象的execute方法时,从而执行对应的子菜单命令。

代码语言:javascript
复制
    // 定义抽象类

    interface Command{

      execute:Function;

    }


    class RefreshMenuBarCommand implements Command{

      constructor(){}

      execute(){

        console.log('刷新菜单界面');

      }

    }


    class AddSubMenuCommand implements Command{

      constructor(){}

      execute(){

        console.log('增加二级菜单');

      }

    }

假设某个程序员一时疏忽,在删除二级菜单方法中漏写了execute:

代码语言:javascript
复制
    class DelSubMenuCommand implement sCommand{

      constructor(){}

      //忘记重写execute方法

    }


    const delSubMenuCommand=new DelSubMenuCommand();


    delSubMenuCommand.execute();


    //输出:Uncaught Type Error:undefined is not a function

ts的编译器给出了报错提示。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-11-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 面向接口编程
    • JAVA的抽象类
      • 超类实现继承
      • interface
    • 回到js
      • 本文花费大量篇幅在java的代码描述上,是为了说明面向接口编程的两个要点:
        • 基于TypeScript的命令模式
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档