面向接口编程

什么是接口

百度:接口泛指实体把自己提供给外界的一种抽象化物,用以由内部操作分离出外部沟通方法,使其能被内部修改而不影响外界其他实体与其交互的方式。

牛津字典:Interface: A point where two systems, subjects, organizations, etc. meet and interact.

维基百科:In computing, an interface is a shared boundary across which two or more separate components of a computer system exchange information.

以程序编程来定义接口

接口是和调用方的一种约定,它是一个高度抽象的类型,不用和具体的实现细节绑定在一起。接口要做的是定义好约定,告诉调用方自己可以做什么,但不用知道它的内部实现。

接口定义一套规范,描述一个“物”的功能,要求如果现实中的“物”想成为可用,就必须实现这些基本功能。

接口这样描述自己:“对于实现我的所有类,看起来都应该像我现在这个样子。”

采用一个特定接口的所有代码都知道对于那个接口会调用什么方法。这便是接口的全部含义。

接口常用来作为类与类之间的一个“协议”。接口是抽象类的变体,接口中所有方法都是抽象的,没有一个有程序体。接口除了可以包含方法外,还能包含常量。

接口不关心实现,因为接口为抽象而生,接口也是一种契约。

因此在程序里,接口的方法必须被全部实现。

接口有什么好处

接口在软件设计中主要有两大好处:

制定标准

标准规范的制定离不开接口,制定标准的目的就是为了让定义和实现分离,而接口作为完全的抽象,是标准制定的不二之选。

这个世界的运转离不开分工协作,而分工协作的前提就是标准化。试想一下,你家的电脑能允许你把显卡从NVIDIA换成七彩虹;你家的灯泡坏了,你可以随便找一个超市买一个新的就可以换上;你把数据从Oracle换成了MySQL,但是你基于JDBC写的代码都不用动。等等这些事情的背后都是因为接口,以及基于接口定制的标准化在起作用。

提供抽象

接口还有一个特征就是抽象。抽象可以让接口的调用者和实现者可以完全的解耦。

解耦的好处是调用者不需要依赖具体的实现、不用关心实现的细节。这样,不管是实现细节的改动,还是替换新的实现,对于调用者来说都是透明的。

这种扩展性和灵活性,是软件设计中,最美妙的设计艺术之一。一旦你品尝过这种“依赖接口”的设计来带的美好,就不大会再愿意回到“依赖实现”的简单粗暴。平时我们说的“面向接口编程原则”和“依赖倒置原则”说的都是这种设计。

什么时候要用到接口设计

通常在大型项目里,会把代码进行分层和分工。核心开发人员和技术经理编写核心的流程和代码,往往是以接口的形式给出,而基础开发人员则针对这些接口,填充代码,如数据库操作等。这样,核心人员把更多精力投入到了技术攻关和业务逻辑中。前端针对接口编程,只管在 Action 层调用 Service,不管实现细节;而后端则要负责 Dao 和 service 层接口实现。这样,就实现了代码的分工和合作。

下面这几种情况,在系统架构时,就需要接口设计:

有扩展性需求的时候

可扩展设计,主要是利用了面向对象的多态特性,所以这里的接口是一个广义的概念,如果用编程语言的术语来说,它既可以是Interface,也可能是Abstract Class。

这种扩展性的诉求在软件工作中可以说无处不在,小到一个工具类。例如,我现在系统中需要一个开关的功能,开关的配置目前是用数据库做配置的,但是后续可能会迁移到Diamond配置中心,或者SwitchCenter上去。

简单做法,直接用数据库的配置去实现开关功能,如下图所示:

但是这样做的问题很明显,当需要切换新的配置实现的话,就不得对原来的应用代码做修改了。更恰当的做法应该是提供一个Switch的接口,让不同的实现去实现这个接口,从而在切换配置实现的时候,应用代码不再需要更改了。

需要解耦的时候

上面介绍的关于Switch的例子,从表面上来看,是扩展性的诉求。但不可扩展的本质原因正是因为耦合性。当我们通过Switch Interface来解开耦合之后,扩展性的诉求也就迎刃而解了。

发现这种耦合性,对系统的可维护性至关重要。有一些耦合比较明显(比如Switch的例子)。但更多的耦合是隐式的,并没有那么明显,而且在很长一段时间,它也不是什么问题,但是,一旦它变成一个问题,将是一个非常头痛的问题。

一个真实的典型案例,就是java的logger,早些年,大家使用commons-logging、log4j并没有什么问题。然而,此处一个隐患正在生长——那就是对logger实现的强耦合。

当logback出来之后,事情开始变得复杂,当我们想替换一个新的logger vendor的时候,为了尽量减少代码改动,不得不上各种Bridge(桥接),到最后日志代码变成了谁也看不懂的代码迷宫。下图就是我费了九头二虎之力,才梳理清楚的一个老业务系统的日志框架依赖情况。

试想一下,假如一开始我们就能遇见到这种紧耦合带来的问题。在应用和日志框架之间加入一层抽象解耦。后续的那么多桥接,那么多的向后兼容都是可以省掉的麻烦。而我们所要做的事情,实际上也很简单——就是加一个接口做解耦而已(如下图所示):

要给外界提供API的时候

我们通常使用的各种开放平台的SDK,或者分布式服务中RPC的二方库,其包含的主要成分也是接口,其实现不在本地,而是在远程服务提供方。

类似于这种API的情况,系统可以设计成微服务架构,但在设计时开发者一定要把接口想清楚。

当原来单体应用里的各种耦合的业务模块,一旦被服务化之后,就自然而然的变成“面向接口”的了。

面向接口编程

编程有三大范式:面向过程编程、面向对象编程和函数式编程,但面向接口编程并不是一种新的编程范式。

在web开发的工作,mvc也是常用的开发模式,这种开发模式通常将代码分成三层(控制层、业务逻辑层、数据库访问层),无论是较早的SSH、SSM、还是现在流行的spring boot框架,都提倡使用面向接口编程,这样可以降低层与层之间的耦合。

在面向对象的语言进行大型系统的设计时,需要考虑各个对象之间的交互问题,接口本质是一种规范和约束,反映了系统设计者对系统的抽象理解

换一种说法,面向接口编程,写业务不需要遵守,但需要设计和实现分离的时候,面向接口编程却是一种解决问题的很好方式。

框架设计与业务开发,什么时候使用面向接口编程比较好,简单聊一下这两点。

框架设计与业务开发

框架设计

框架设计时,通常设计和实现是分离的。面向接口的设计,能保证框架的扩展性。比如设计物流运输系统,针对运输这个过程,系统设计人员经过抽象,将物流公司的运输行为分解为几个过程:接单、送达、结算、用户评价,那么可以定义一个接口约束这种行为:

public interface Transport {

    //接单
    void receiveOrder();

    //运达
    void arrive();

    //结算
    void pay();

    //用户反馈
    void userComment();
}

业务系统中有各种配送方式,比如外包(A运输公司、B运输公司等)、自己派送,直接实现上面的接口,然后再写具体的业务实现。框架在执行业务的时候,根据实现接口方法,执行相应的业务逻辑。

public class TransportExecutor {

    public void transport(int selectedType){
        if(selectedType==1){
            //外包 
            //模拟随机分配订单到运输公司
            int flag=new Random().nextInt(10);
            //根据flag读取配置,取出component的name
            String companyName= PropertyUtil.getCompanyByFlag(flag);
            Transport transport=SpringBeanUtil.getBeanByName("name");
            transport.receiveOrder();
            transport.arrive();
            transport.pay();
            transport.userComment();
        }else{
           //自己运输
        }
    }
}

业务开发

面对业务开发时,在应用中各层之间相互调用,不存在定义和实现分离的情况,这时候不需要使用面向接口编程,因为会增加一定的工作量。大家统一约束(例如:业务层类命名直接以service结尾,不用接口和实现分离),需要调用其他人写的业务方法,直接注入实现即可,其他人修改实现,不会影响你的调用。

@Service
public class TransportService {

    @Autowired
    //此处直接注入实现类
    private UserLoginService userLoginService;

    public void transport(int selectedType){
       userLoginService.checkLoin();//调用实现类中的方法

    }
}

go使用接口编程

package main

import "fmt"

type person struct {
	name string
	age  uint
	addr address
}

type address struct {
	province string
	city     string
}

// Stringer 是 Go SDK 的一个接口,属于 fmt 包。
type Stringer interface {
	String() string
}

// 结构体person实现了Stringr接口
func (p person) String() string {
	return fmt.Sprintf("the name is %s,age is %d", p.name, p.age)
}

// 结构体address实现了Stringr接口
func (addr address) String() string {
	return fmt.Sprintf("the addr is %s%s", addr.province, addr.city)
}
// printString 这个函数的优势就在于它是面向接口编程的,只要一个类型实现了 Stringer 接口,都可以打印出对应的字符串,而不用管具体的类型实现。
func printfString(s fmt.Stringer)  {
	fmt.Println(s.String())
}

func main()  {
	p := person{
		age: 18,
		name: "牛奔",
		addr: address{
			province: "上海",
			city: "上海",
		},
	}
	printfString(p)
	printfString(p.addr)
}

输出

the name is 牛奔,age is 18
the addr is 上海上海

工厂函数

工厂函数一般用于创建自定义的结构体,便于使用者调用。通过工厂函数创建自定义结构的方式,可以让调用者不用太关注结构体内部的字段,只需要给工厂函数传参即可。工厂函数也可以用来创建一个接口,它的好处就是可以隐藏内部具体类型的实现,让调用者只需关注者接口使用即可。

以 errors.New 这个 Go 语言自带的工厂函数为例

//工厂函数,返回一个error接口,其实具体实现是*errorString
func New(text string) error {

    return &errorString{text}

}

//结构体,内部一个字段s,存储错误信息
type errorString struct {

    s string

}

//用于实现error接口
func (e *errorString) Error() string {

    return e.s

}

errorString 是一个结构体类型,它实现了 error 接口,所以可以通过 New 工厂函数,创建一个 *errorString 类型,通过接口 error 返回。

这就是面向接口的编程,假设重构代码,哪怕换一个其他结构体实现 error 接口,对调用者也没有影响,因为接口没变。

巨人的肩膀

关于面向接口编程,你真的弄懂了吗?
谈谈面向接口编程

posted @ 2019-05-12 22:35  牛奔  阅读(300)  评论(0编辑  收藏  举报