面向接口编程
什么是接口
百度:接口泛指实体把自己提供给外界的一种抽象化物,用以由内部操作分离出外部沟通方法,使其能被内部修改而不影响外界其他实体与其交互的方式。
牛津字典: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 接口,对调用者也没有影响,因为接口没变。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通