二学“面向接口编程”

面向接口编程——遵循契约办事

虽然之前学过【策略模式】,知道要面向接口,而不是面向实现,那么真的有get到精髓吗?

接口的本质是什么?——代码世界里的契约

接口里的一个个方法,就是契约的条款

OOP中和接口非常相似的就是抽象类,这经常被大家拿来比较,虽然我之前也写过了,但我还是想再说一遍

接口和抽象类——它们其实分工明确

首先它们相似在哪?——没有具体实现逻辑

那差在哪呢?我们先看例子先:

// 抽象类
abstract class Door {
    abstract void open();
    abstract void close();
}
abstract class Fish {
    abstract void swim();
    abstract void eat();
}

// 接口
interface IFlyable {
    void fly();
}
interface IComparer {
    int compare(Object a,Object b);
}

有点感觉了吧?抽象类重在它的类名本身,而接口更强调的是它里面的一个个方法,而名字只是辅助。

我们接着看:

interface IFlyable {
    void fly();
}
abstract class Animal {}
class Bird extends Animal implements IFlyable {}
class Plane implements IFlyable {}

懂了吧???接口是很轻量级的,附着于主体类上的一个像装备一样辅助的东西。

而抽象类则直接说明了,你是谁谁谁的一员,是单继承,但接口你可以多实现呀

  • 接口针对行为抽象,更强调“你能干啥”(动态)
  • 抽象类针对数据抽象,更强调“你是个啥”(静态)

(C++和Python中,接口和抽象类是合二为一的概念,都是抽象类,我们这边主要是说OOP纯度高的,以Java为例)

但是,抽象类命名也有对行为的抽象啊,那为什么还要多抽出一个接口?

  • 数据、行为分开了定义,更方便理解
  • 继承的时候,太重了,难免把不必要的也放进来

那接口怎么用呢?往下看。

接口的应用

先签约,后对接

该场景主要是为了有相互依赖调用关系的双(多)方能同时开发,开工前需要保证模块间能够顺利对接

案例背景:

【数据源模块】——>【数据库模块】【中间件模块】【日志模块】

(数据处理完之后,会根据业务需要写到三个模块内)

最早只能咋样,数据源模块的开发者对着另外三个模块的开发者吼:“你们都给我写个void writeResult(String data)的方法,实现具体逻辑,方法名别错了!我就只管传数据!“这显然不合适啊,那咋办,四组开发者定个协议咯,而这个协议,就是接口。这可是一大步飞跃啊,约定好的方法名,从word文档到了代码里,不实现接口,编译器还不让你过,难道不香吗???

(其实这是【观察者模式】,数据源模块是”被观察者“,其他模块是”观察者“,Container是中介,以Container为核心,实现解耦的消息通知系统)

  • 先在大家都能引用的Common模块里新增接口
interface IWriteResult {
    void writeResult(String data); 
}
  • 【数据源模块】里有个observers数组,用来装所有注册过的观察者(注册方式:通过配置文件,反射扫描,反射查找,创建实例)
// 表示实现了这个接口的对象
IWriteResult[] observers;
  • 解析完一条数据后
void sendResult(String data){
    for(IWriteResult observer:observers){
        observer.writeResult(data);
    }
}
  • 然后每个目标模块有自己完成writeResult()的业务逻辑,至此,我们就完成各个模块的对接啦

接口的贯穿从始至终,其价值体现在项目开发的过程中

专注抽象,脱离具体、

这个场景就不是多人同时开发了,而是架构师设计框架性代码时,采用接口专攻抽象的主体逻辑

我们就拿数组的排序来说:

// 给用户制定标准,用户自定义一个排序标准
Array.sort(IComparer comparer);

// 接口定义
interface IComparer {
    int compare(Object a,Object b);
}

// 内部实现
Array sort(IComparer comparer){
	int length = this.elements.length;
	int m = length;
    for(int i=0;i<length;i++){
        m = m - 1;
        for(int j=0;j<m;j++){
            // 最关键的抽象判断
            if(comparer(this.elements[j],this.elements[j+1])>0)
                swap(this.elements[j],this.elements[j+1]);
        }
    }
    return this.elements;
}

这就是面向抽象编程,而接口就是实现这一方式的关键

我们用户就根据需要实现接口:(比方这里是年龄)

class PersonComparer implements IComparer {
    public int compare(Person a,Person b){
        return a.age > b.age;
    }
}

说白了,IComparer这个接口,就是为了摆脱细节实现,把这一part留给用户

解开耦合,破除缠绕

这个场景主要用于不应该依赖其他底层模块的封装,一旦底层模块有调用上层模块的需求,可以借助接口抽象化

先来看个案例:

在父窗口parentWindow里嵌入一个子UI控件childControl,子控件有几个功能:

  • Reload菜单

  • Add菜单

  • Save菜单

// 双向引用
class ChildControl {
    ParentWindow parentWindow;
    void reloadClicked(){
        this.parentWindow.Reload();
    }

    void addClicked(){
        this.parentWindow.Add();
    }

    void saveClicked(){
        this.parentWindow.Save();
    }
}

class ParentWindow {
    void Reload(){}
    void Add(){}
    void Save(){}
}

父窗口和子控件莫名其妙耦合了,而且子控件还成不了轮子,很伤的,咋办?接口!

interface IDataOperation {
    void Reload();
    void Add();
    void Save();
}

子控件里我们只关心这个接口:(其实也就是把这三个操作具体实现的任务丢出去,我不管谁来整,反正让我鼠标敲下去有对应的正确反馈就好)

class ChildControl {
    // 让父窗口去实现这个接口
    IDataOperation dataOperation;
    void reloadClicked() {
        if(dataOperation != null)
            dataOperation.Reload();
    }
    // 其他两个省略
}

//  父窗口中实现这个接口
class ParentWindow implements IDataOperation{
    // 接受子控件的接口实现任务
    ChildControl childControl;
    void Reload(){
        this.dataArr = getDataFromDB();
    }
    // 其他两个省略
    // 交付任务
    this.childControl.dataOperation = this;
}

经常用于相互引用的耦合,我中有你,你中有我的场景

原来子控件的模块,还需要考虑父窗口怎么处理,有哪些方法;现在相反,由子控件来整个接口丢出去,让父窗口来实现

这样的接口具有特殊性,为了满足业务特定需求的,实现模块间解耦

同样的应用,可以参考model层和controller层,model层会调用controller层的方法(事件通知),但它并不知道controller层的信息,一样也是为了解耦。

总结

接口的语法是简单,但是难在用法太花太多了!!!

接口和函数指针

有人觉得很像,有人觉得是两个东西,仁者见仁吧

亲兄弟

以C为例:

int (*calculate)(int,int) = add;
int sum = (*calculate)(100,200);

函数指针也有个特点:自己不做决定,放权给别人来做

函数指针能做的,接口也能做(就是累点);除了OOP特性的东西,函数指针也能做接口的东西,好似两个亲兄弟

接口的优势

接口的业务承载能力明显比函数指针强,而函数指针更强调函数结构的定义

  • 优势一:函数指针不是OOP的产物,而接口是OOP的高级抽象

    • 接口里可以写很多函数,而函数指针不行
    • 接口间还能接口继承接口,而函数指针不行
    • 我们可以利用反射查询多少类实现了某个接口,而函数指针不行
  • 优势二:接口是个很好的“协议”

    • 语法上,接口清晰易懂,沟通成本低;函数指针反之
    • 可以有多个函数,函数指针只能一一对应
    • 实现该接口的模块必须实现细节,否则过不了编译;函数指针只有在动态运行时才可能会报错

函数指针的优势

  • 优势一:函数指针比接口还轻量

    • 函数指针短小精悍,动态赋值;而接口则需要在某个类上才能发挥作用。比如说加减乘除,函数指针只要一个指针和四个函数就可以了;而接口则需要写一个接口和加减乘除四个类,明显臃肿了很多(函数指针似乎更适合小场面)
  • 优势二:接口可能会造成空函数

    • 接口要求所有方法都要实现,万一我有一些不需要呢?
    • 函数指针一一对应,细粒度的匹配,不会造成浪费
  • 优势三:接口比函数指针更加笨重,函数指针适合业务语境很少的环境

假如有个接口A和一个接口B,A中有9个方法要实现,B中只有1个,但它们的地位却看似相等?B中的那个方法而且很可能只是主体类的一个边边角角小喽啰,没必要大动干戈,在主体类中去掉B接口,用函数指针不香吗?

而可惜的是Java没有,而C#是有的,但是…Java有Lambda表达式

Lambda表达式

为了模仿函数指针,JDK1.8增加了函数式编程新特性——Lambda表达式

  • Java8之前,我们都是用匿名内部类来模仿函数指针
  • Java8之后,我们用lambda表达式
    • 要求1:接口只有一个方法
    • 要求2:如果要简化的话,该方法只能有一行代码
interface ILambda {
    void out();
}

public class Test {
    public static void main(String[] args) {
        // 匿名内部类
        ILambda i1 = new ILambda(){
            @Override
            public void out() {
                System.out.println("i1");
            }          
        };
        i1.out();
        // Lambda表达式
        ILambda i2 = ()->{
            System.out.println("i2");
        };
        i2.out();
        // 简化(如果只有一行)
        ILambda i3 = ()->System.out.println("i3");
        i3.out();
    }
}

你可能感兴趣的:(程序设计思想,设计模式,接口)