【Java24】面向接口编程

利用接口可以降低程序各模块之间的耦合,从而提高系统的可扩展性和可维护性。下面介绍两种常见的面向接口编程的模式:

简单工厂模式

假设程序中有一个Computer类需要组合一个输出设备。现在有两种选择:

  1. 让Computer类组合Printer对象;
  2. 让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行的功能,通过声明父类,再利用类型转换指向不同子类是不行的。除非父类不是抽象类。但是父类不是抽象类,意味着父类的方法必须由子类重写,这就又造成了代码的冗余。

由此可知,没有“正确的”代码,只有“好”的代码。而且不同人的眼中,好与不好的评判标准还可能不一致。有人喜欢接口,有人喜欢继承,萝卜青菜各有所爱。

命令模式

利用接口变量能够指向不同对象的特性,我们还可以让接口包含一个抽象方法但不具体实现它。然后由它的不同的实现类去实现这个方法。这样的话,我们就可以把不同的方法做为一个参数来调用了:

  1. 首先利用接口来定义一个方法:
public interface Command
{
  void process(int element);
}

这个方法是抽象方法,只约束了输入参数必须是int。

  1. 我们可以实现(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);
  }
}
  1. 我们要用一个命令类来处理不同的命令。和简单工厂模式类似,它包含一个核心方法,就是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遍历来处理数组的每一个元素。

  1. 最终在外部类中是这么调用的:
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行,也就失去了以参数传递”不同行为“的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值