利用接口可以降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。下面介绍两种常见的面向接口编程的模式:
简单工厂模式
假设程序中有一个Computer类需要组合一个输出设备。现在有两种选择:
- 让Computer类组合Printer对象;
- 让Computer类组合一个Output对象。
使用前者,一旦发生代码重构,比如用一个新的打印类BetterPrinter来替换掉Printer类,那么Computer类的代码就要跟着改动。想象下,假如有很多类都组合了Printer对象,那么意味着需要修改很多类的代码。工作量极大。
而通过Output对象来“间接”调用Printer类的实例,关系变为如下形式:
由于接口的特性,Output的对象实际上个引用类型变量,它可以指向不同的实现类的对象:
Output hp = new HP_Printer();
Output cn = new Canon_Printer();
如此一来,只要Output内的抽象方法不变,具体由哪个类实现都可以。这样的话,如果将图1中Printer类替换为BeeterPrinter类,并不需要修改Computer、MobileDevice、Laptop中的代码,因为对他们来说,成员对象都是Output的引用类型变量:
public class Computer
{
private Output out;
public Computer(Output out)
{
this.out = out;
}
// 定义一个模拟获取字符串输入的方法
public void keyIn(String msg)
{
out.getData(msg);
}
public void print()
{
out.out();
}
}
对这样的Computer,它其实不在乎out
到底指向的是哪个打印机。
我们新建一个类来创建几个Computer对象,模拟它们连接到不同的打印机上:
public class OutputFactory
{
public static void main(String[] args)
{
Output hp = new HP_Printer();
Output cn = new Canon_Printer();
var c1 = new Computer(hp);
c1.keyIn("今天星期二");
c1.print();
var c2 = new Computer(cn);
c2.keyIn("明天星期三");
c2.print();
}
}
注意观察main方法中的第5-6行,分别创建了两个指向不同对象的Output引用变量。我们还可以进一步,用一个类来创建指向不同对象的Output引用变量,它的核心功能就是返回一个Output引用变量:
public class OutputFactory
{
public Output getOutput()
{
return new HP_Printer();
}
public static void main(String[] args)
{
var of = new OutputFactory();
var c = new Computer(of.getOutput());
c.keyIn("今天星期二");
c.print();
}
}
观察上面的代码,利用OutputFactory
的方法getOutput()
来返回一个HP_Printer
的实例。这么做的好处是,若想让实例c
挂载另一个打印机,只需修改第5行为:return new Canon_Printer()
。从main方法来看,没有任何变化;也不需要修改class Computer
里的任何代码。
这种模式,把生成指向不同实现类的实例的逻辑集中在一个集中管理(上述例子中的
getOutput
)。从外部看,就像是先实例化了一个加工厂(new OutputFactory()
),然后由这个加工厂来生产出指向不同实现类实例(如HP_Printer或Canon_Printer的实例)的接口对象。这种模式,叫做简单工厂模式。这么做的好处是:如实现类的代码发生改变,或者指向了不同的实现类,接口代码不会改变、调用接口的代码也不会改变。唯一受到影响的是工厂里的“加工代码”。
想一想,抽象类其实也能做到类似的效果。但请注意一点,抽象类是不能实例化的。所以第一版代码中,想要实现类似第5-6行的功能,通过声明父类,再利用类型转换指向不同子类是不行的。除非父类不是抽象类。但是父类不是抽象类,意味着父类的方法必须由子类重写,这就又造成了代码的冗余。
由此可知,没有“正确的”代码,只有“好”的代码。而且不同人的眼中,好与不好的评判标准还可能不一致。有人喜欢接口,有人喜欢继承,萝卜青菜各有所爱。
命令模式
利用接口变量能够指向不同对象的特性,我们还可以让接口包含一个抽象方法但不具体实现它。然后由它的不同的实现类去实现这个方法。这样的话,我们就可以把不同的方法做为一个参数来调用了:
- 首先利用接口来定义一个方法:
public interface Command
{
void process(int element);
}
这个方法是抽象方法,只约束了输入参数必须是int。
- 我们可以实现(implement)这个接口,为这个方法
process
赋予不用的行为:
public class PrintCommand implements Command
{
public void process(int element)
{
System.out.println("迭代输出目标数组的元素:" + element);
}
}
public class SquareCommand implements Command
{
public void process(int element)
{
System.out.println("数组元素的平方是:" + element * element);
}
}
- 我们要用一个命令类来处理不同的命令。和简单工厂模式类似,它包含一个核心方法,就是
process()
,这个方法不确定对数组执行什么命令,而是将Command实例做为参数,传入方法中:
public class ProcessArray
{
public void process(int[] target, Command cmd)
{
for (var t : target)
{
cmd.process(t);
}
}
}
这个方法并不是直接用一个明确的方法来处理输入数组target
。相反地,它间接地接受一个Command实例,根据Command引用变量指向的实现类实例,调用对应的方法cmd.procee()
。注意,这里利用了foreach遍历来处理数组的每一个元素。
- 最终在外部类中是这么调用的:
public class CommandTest
{
puiblic static void main(String[] args)
{
var pa = new ProcessArray();
int[] target = {3, -4, 6, 4};
// 注意将PrintCommand实例做为参数传入process方法
pa.process(target, new PrintCommand());
// 类似地,传入SquareCommand实例
pa.process(target, new SquareCommand());
}
}
注意看第8和第10行,如果PrintCommand()
和SquareCommand()
不是Command
接口的实现类的话,就会影响到ProcessArray
类中的第3行,也就失去了以参数传递”不同行为“的效果。