Java 接口的所有子类都需要执行相同处理逻辑的推荐姿势

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 接口的所有子类都需要执行相同处理逻辑的推荐姿势相关的知识,希望对你有一定的参考价值。


一、背景

在实际开发过程中,有些时候我们可能会遇到这样的场景:我们定义接口给上游使用,不同的业务类型定义不同的子类型,实现该接口的某个函数,但是这些子类型会有很多公共的逻辑(公共的步骤)。

如果将这部分代码定义为工具方法,就需要在每个子类中都执行对应的调用。

如果有些公共步骤的返回值和接口中定义的返回值一致时,很容易出现漏调用的情况。

那么,该如何 “强制”子类型都要执行一些相同的步骤呢?

Java

二、描述

下面都是伪代码,大家不必纠结于具体细节,理解意思即可。

我们需要提供给上游这样一个接口, type 是指当前服务能够处理的类型,something 代表实际执行的业务功能。

public interface SomeInterface 

String type();

ResultDTO something(Param param);

第一个实现类:

public class AImpl implements SomeInterface 

@Override
public String type()
return "A";


@Override
public ResultDTO something(Param param)
// 特有逻辑
ResultDTO resultDTO = buildPart(param);

// 构造公共逻辑所需的参数
OtherParam otherParam = new OtherParam();

// 公有逻辑
return SomeUtils.fillCommon(resultDTO, otherParam);


private ResultDTO buildPart(Param param)
ResultDTO result = new ResultDTO();
// 执行查询

// 塞入特有属性

return result;

第二个实现类:

public class BImpl implements SomeInterface 

@Override
public String type()
return "B";


@Override
public ResultDTO something(Param param)
// 特有逻辑
ResultDTO resultDTO = buildPart(param);

// 构造公共逻辑所需的参数
OtherParam otherParam = new OtherParam();

// 公有逻辑
return SomeUtils.fillCommon(resultDTO, otherParam);


private ResultDTO buildPart(Param param)
ResultDTO result = new ResultDTO();
// 执行查询

// 塞入特有属性

return result;


使用时,构造 ​​Map<String,SomeInterface> type2BeanMap​​ ,然后根据当前的 type 去执行具体的实现 Bean。

具体可参考​​《巧用 Spring 自动注入实现策略模式升级版》​​

问题:如果我们新增 ​​CImpl​​​ 继承 ​​SomeInterface​​​ 就必须查看 ​​AImpl​​​ 或者 ​​BImpl​​ 源码才知道有一段公共逻辑,很容易遗漏这一段公共逻辑。

如果我们想让新建子类时,不需要担心遗漏这段公共的逻辑,该怎么办?

三、方案

如果大家对设计模式比较熟悉的话,这种场景我们很自然地会联想到​​模板模式​​。

我们将采用这种设计模式,对代码进行改造。

(1)我们将特有逻辑上提到接口中,在 default 方法中编排逻辑即可。

public interface SomeInterface 

String type();

// 目标方法
default ResultDTO something(Param param)
return SomeUtils.fillCommon(buildPart(param));


/**
* 特有逻辑
*/
MiddleParam buildPart(Param param);

定义为接口的好处是,不会影响到子类继承其他父类型(Java 是单继承机制)。

(2)可以将 ​​SomeInterface​​​ 改为抽象类(​​AbstractSomeService​​​),​​something​​​定义为 ​​public​​​ ,将 ​​builPart​​ 定义为抽象函数,让子类去重写。

public abstract class AbstractSomeService 

abstract String type();

// 目标方法
public ResultDTO something(Param param)
return SomeUtils.fillCommon(buildPart(param));


/**
* 特有逻辑
*/
abstract MiddleParam buildPart(Param param);

定义为抽象类的坏处是子类型无法再继承其他类,但理论上也不应该(不需要) 再继承其他类,好处是可以将​​buildPart​​​ 重写的访问修饰符范围降低,如改为 ​​protected​​。

注意:
(1) 本案例里的 ​​​SomeUtils.fillCommon​​​ 只是伪代码,公共逻辑可能封装在工具类中,也可能封装在某个 bean 中,在抽象类或者接口中可以通过​​ApplicationContextHolder​​​ 去获取并调用。
(2) 实际编码时,公共逻辑也未必在最后调用。
(3) 实际编码中,公共的步骤可能不止一个,但是方案是一致的,有几个定义几个抽象方法即可
(4)实际编码时,公共的步骤可能不需要返回值

定义中间参数:

@Data
public class MiddleParam

private ResultDTO semiResult;

private OtherParam otherParam;

第一个实现类:

public class AImpl extends AbstractSomeService 
@Override
public String type()
return "A";



@Override
protected MiddleParam buildPart(Param param)
MiddleParam middleParam = new MiddleParam();
// 执行查询

// 塞入特有属性

return middleParam;

第二个实现类:

public class BImpl extends AbstractSomeService 

@Override
public String type()
return "B";


@Override
protected MiddleParam buildPart(Param param)
MiddleParam middleParam = new MiddleParam();
// 执行查询

// 塞入特有属性

return middleParam;


这样通过类似 ​​buildPart​​ 这种函数名,可以明确感知到当前是对部分逻辑进行处理,且不需要在当前子类中执行公共逻辑的调用。

四、总结

本文案例比较简单,主要思想是使用模板模式来实现公共步骤的编排。

希望大家遇到类似场景时,可以使用更优雅的方式,更健壮的方式去实现,而不是依靠“口口相传” 或者让别人看你代码才知道该怎么写。


创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。

Java


接口

在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。

如果一个抽象类没有字段,所有方法全部都是抽象方法:

abstract class Person 
public abstract void run();
public abstract String getName();

就可以把该抽象类改写为接口:​​interface​​。

在Java中,使用​​interface​​可以声明一个接口:

interface Person 
void run();
String getName();

所谓​​interface​​,就是比抽象类还要抽象的纯抽象接口,因为它连字段都不能有。因为接口定义的所有方法默认都是​​public abstract​​的,所以这两个修饰符不需要写出来(写不写效果都一样)。

当一个具体的​​class​​去实现一个​​interface​​时,需要使用​​implements​​关键字。举个例子:

class Student implements Person 
private String name;

public Student(String name)
this.name = name;


@Override
public void run()
System.out.println(this.name + " run");


@Override
public String getName()
return this.name;

我们知道,在Java中,一个类只能继承自另一个类,不能从多个类继承。但是,一个类可以实现多个​​interface​​,例如:

class Student implements Person, Hello  // 实现了两个interface
...

术语

注意区分术语:

Java的接口特指​​interface​​的定义,表示一个接口类型和一组方法签名,而编程接口泛指接口规范,如方法签名,数据格式,网络协议等。

抽象类和接口的对比如下:

abstract class

interface

继承

只能extends一个class

可以implements多个interface

字段

可以定义实例字段

不能定义实例字段

抽象方法

可以定义抽象方法

可以定义抽象方法

非抽象方法

可以定义非抽象方法

可以定义default方法

接口继承

一个​​interface​​可以继承自另一个​​interface​​。​​interface​​继承自​​interface​​使用​​extends​​,它相当于扩展了接口的方法。例如:

interface Hello 
void hello();


interface Person extends Hello
void run();
String getName();

此时,​​Person​​接口继承自​​Hello​​接口,因此,​​Person​​接口现在实际上有3个抽象方法签名,其中一个来自继承的​​Hello​​接口。

继承关系

合理设计​​interface​​和​​abstract class​​的继承关系,可以充分复用代码。一般来说,公共逻辑适合放在​​abstract class​​中,具体逻辑放到各个子类,而接口层次代表抽象程度。可以参考Java的集合类定义的一组接口、抽象类以及具体子类的继承关系:

┌───────────────┐
│ Iterable │
└───────────────┘
▲ ┌───────────────────┐
│ │ Object │
┌───────────────┐ └───────────────────┘
│ Collection │ ▲
└───────────────┘ │
▲ ▲ ┌───────────────────┐
│ └──────────│AbstractCollection │
┌───────────────┐ └───────────────────┘
│ List │ ▲
└───────────────┘ │
▲ ┌───────────────────┐
└──────────│ AbstractList │
└───────────────────┘
▲ ▲
│ │
│ │
┌────────────┐ ┌────────────┐
│ ArrayList │ │ LinkedList │
└────────────┘ └────────────┘

在使用的时候,实例化的对象永远只能是某个具体的子类,但总是通过接口去引用它,因为接口比抽象类更抽象:

List list = new ArrayList(); // 用List接口引用具体子类的实例
Collection coll = list; // 向上转型为Collection接口
Iterable it = coll; // 向上转型为Iterable接口

default方法

在接口中,可以定义​​default​​方法。例如,把​​Person​​接口的​​run()​​方法改为​​default​​方法:

// interface

 Run

实现类可以不必覆写​​default​​方法。​​default​​方法的目的是,当我们需要给接口新增一个方法时,会涉及到修改全部子类。如果新增的是​​default​​方法,那么子类就不必全部修改,只需要在需要覆写的地方去覆写新增方法。

​default​​方法和抽象类的普通方法是有所不同的。因为​​interface​​没有字段,​​default​​方法无法访问字段,而抽象类的普通方法可以访问实例字段。

以上是关于Java 接口的所有子类都需要执行相同处理逻辑的推荐姿势的主要内容,如果未能解决你的问题,请参考以下文章

再回顾java面向对象

学懂Java面向对象编程-6

java动态代理

使用Go来模拟Java中的接口 实现类

Java中的重载和重写的区别

JAVA中接口与抽象类