Java 接口的所有子类都需要执行相同处理逻辑的推荐姿势
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
这种函数名,可以明确感知到当前是对部分逻辑进行处理,且不需要在当前子类中执行公共逻辑的调用。
四、总结
本文案例比较简单,主要思想是使用模板模式来实现公共步骤的编排。
希望大家遇到类似场景时,可以使用更优雅的方式,更健壮的方式去实现,而不是依靠“口口相传” 或者让别人看你代码才知道该怎么写。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
接口
在抽象类中,抽象方法本质上是定义接口规范:即规定高层类的接口,从而保证所有子类都有相同的接口实现,这样,多态就能发挥出威力。
如果一个抽象类没有字段,所有方法全部都是抽象方法:
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 接口的所有子类都需要执行相同处理逻辑的推荐姿势的主要内容,如果未能解决你的问题,请参考以下文章