活动地址:CSDN21天学习挑战赛
5. 接口
5.1 什么是接口
Interface
,接口是Java
语法中最强大,也最为让人迷惑误解的一种语法,在加之从Java 8
这个版本以来,接口的语法有了一些变化和更新,使得不少程序员对于接口经常使用不当。
接口在设计之初,是为了能编写出
松耦合
、易扩展
、可测试
的应用程序。
//当在A类中,使用B类时,我们可以说,A依赖B,或者A与B有耦合 //当我们修改B类逻辑时,A类相应的也有很大可能要进行修改 //如果还有其它类依赖A的话,那么将会像多米诺骨牌一样,因为B的一处修改,整个依赖的生态链都会发生蝴蝶效应带来的修改 //即使是B类的修改,A类及其相关类无需修改任何逻辑也能正常运行,但是它们都需要被重新编译、重新部署,这都需要消耗系统资源和时间
针对以上场景,是否能有一种语法来降低类与类之间的耦合度,让其耦合度更松散一些(松耦合)呢?
就好比,小车的引擎坏了,只需要更换或者修理引擎就行了,而不需要动其它部件。
之前,我们学习过的抽象
,即隐藏实现细节,只对外暴露必要的成员,使用private
这样的修饰符,可以让A
尽可能少的“认识”B
,这样就能降低两者之间的耦合度。但这还不够,使用interface
我们可以更进一步的来降低这种耦合度。
public interface Draggable {
void drag();
}
接口就像是一个类,但是里面一般只定义方法,而且这个方法没有任何实现细节。换句话说,接口只定义它应该做什么。
要解藕AB
的话,我们只需要在之间定义一个interface
,当B
的具体逻辑修改时,A
将不再会受到影响,因为A
根本就不了解B
是如何实现的。这就是所谓的面向接口编程
。
-
interface,主要用来定义应该做什么(what),例如数据压缩、加密解密、排序、查找
-
class,主要用来定义具体怎么做(how)
5.2 高耦合度案例 - 个税计算
public class TaxCalculator {
private double income;
public TaxCalculator(double income) {
this.income = income;
}
public double calculatorTax() {
return income * 0.3;
}
}
public class TaxReport {
private TaxCalculator calculator;
public TaxReport() {
calculator = new TaxCalculator(10_000);
}
public void show() {
var tax = calculator.calculatorTax();
System.out.println(tax);
}
}
这个案例由于TaxReport
使用了TaxCalculator
,造成了后者依赖前者,因此存在如下几个问题:
-
TaxCalculator
的构造器或者方法的签名如果发生改变,那么将会导致另一方需要修改代码。 -
即使第一种情况没有发生,但是计算个税的逻辑如果发生了细微调整,那么也会导致使用它的类重新编译。
5.3 创建于实现接口
public interface TaxCalculator {
double calculateTax();
}
接口中的方法默认是public
和abstract
的。
重新修改TaxCalculator
,使其实现这个接口:
public class TaxCalculatorImpl implements TaxCalculator {
private double income;
public TaxCalculatorImpl(double income) {
this.income = income;
}
@Override
public double calculateTax() {
return income * 0.3;
}
}
在实现接口时,必须实现接口中定义的所有方法,且方法的签名保持一致。
关于接口,还需要我们了解的是,我们可以让一个类在继承一个父类的同时,根据程序的需要,实现一个或者更多个接口。
5.4 依赖注入
Dependency Injection
,又简称DI
,根据这一原则,我们的类不应该实例化它的依赖。(不要依赖具体的逻辑)
在TaxReport
的构造器中,我们不应该自己实例化它的依赖,而只应该使用它,DI
原则认为,实例化和使用是两件独立的事情,我们应该尽量分离两者。
例如:在餐厅中,厨师和服务员互相之间有依赖关系,但是服务员确定好客户的点膳后,需要厨师来制作膳食,他不应该自己去厨房制作,而只是把点膳的信息传递给厨师;反之,厨师做好膳食以后,需要把膳食递交给服务员,而不应该自己跑去大堂传膳。每个人的职责都是单一且专业的。
那么,在上述案例中,我们需要把实例化TaxCalculator
的逻辑交给另一个类来完成,比如Main
,然后传递这个实例给TaxReport
使用。
这个传递依赖的具体实现(实例)给使用者的过程,就称之为依赖注入。
依赖注入有以下3种实现方式:
-
构造器注入
-
Setter注入
-
方法注入
5.4.1 构造器注入
public class TaxReport {
private TaxCalculator calculator;
public TaxReport(TaxCalculator calculator) {
this.calculator = calculator;
}
public void show() {
var tax = calculator.calculateTax();
System.out.println(tax);
}
}
public class Main {
public static void main(String[] args) {
var calculator = new TaxCalculatorImpl(10_000);
var report = new TaxReport(calculator);
report.show();
}
}
5.4.2 Setter注入
public class TaxReport {
private TaxCalculator calculator;
public void setCalculator(TaxCalculator calculator) {
this.calculator = calculator;
}
public void show() {
var tax = calculator.calculateTax();
System.out.println(tax);
}
}
public class Main {
public static void main(String[] args) {
var calculator = new TaxCalculatorImpl(10_000);
var report = new TaxReport();
report.setCalculator(calculator);
report.show();
}
}
5.4.3 方法注入
public class TaxReport {
public void show(TaxCalculator calculator) {
var tax = calculator.calculateTax();
System.out.println(tax);
}
}
public class Main {
public static void main(String[] args) {
var calculator = new TaxCalculatorImpl(10_000);
var report = new TaxReport();
report.show(calculator);
}
}
5.5 接口隔离原则
将包含很多方法的大接口切割为多个包含少量且必要方法的小接口。
public interface UIWidget {
void drag();
void resize();
}
public class Dragger {
public void drag(UIWidget widget){
widget.drag();
}
}
当我们修改resize
方法,例如给它增加参数,虽然Dragger
没有使用这个方法,但也会导致这个类重新编译。
因此,在这种场景下,我们应该将drag
方法单独封装到另一个接口去,使之与其它没有必要一起使用的方法隔离开来。
如果有一个场景,我们必须要同时使用两个方法,可以让相关接口形成继承关系来达到代码复用:
public interface Draggable {
void drag();
}
public interface UIWidget extends Draggable {
void resize();
}
与类的单根继承不同,一个接口可以继承多个接口。
5.6 Project - MyTube
见项目资源文件夹 Mytube.zip
请定义相关接口并调整项目代码,降低VideoProcessor
与其依赖的耦合度。
5.7 接口新语法
-
定义常量
-
默认方法
-
私有方法
5.8 接口与抽象类
5.9 面向接口编程的益处
-
灵活切换实现方式
-
是的程序更容易扩展,同时最小化扩展带来的负面影响
-
更容易进行独立的单元测试