一、引言
最近项目中使用了最新版的elasticSearch,通过官网了解到最新版的elasticSearch的JavaAPI不再推荐使用之前版本的org.elasticsearch.client.RestHighLevelClient 而是推荐使用co.elastic.clients.elasticsearch.ElasticsearchClient 于是主动去了解下这个新版的API的使用方法,例如使用这个新版API查询文档的时候 代码如下:
java
SearchResponse<Product> response = esClient.search(s -> s
.index("products")
.query(q -> q
.match(t -> t
.field("name")
.query(searchText)
)
),
Product.class
);
不知道各位读者首先看到这段代码的感受是什么,我的第一感觉就是这个API可读性明显变高了,就是你完全不懂es但是你看这段代码大致你也能猜到是什么意思(查询一个名字叫做products的索引,查询条件是按照name查询),这种FluentAPI 的设计思路进行在其他开源工具里面也有体现,例如okHttp的API设计。
java
OkHttpClient.Builder builder=new OkHttpClient.Builder();
OkHttpClient okHttpClient=builder
.readTimeout(5*1000, TimeUnit.SECONDS)
.writeTimeout(5*1000, TimeUnit.SECONDS)
.connectTimeout(5*1000, TimeUnit.SECONDS)
.build();
这种fluentAPI设计,显然提高了代码可读性,实现上述fluentAPI 有一种非常简单的写法就是使用lombok的@Builder注解来做,利用所谓的建造者模式来实现上述功能,例如下面的例子
java
@Builder
public class Student {
private String name;
private String address;
private Integer age;
public static void main(String[] args) {
Student student = Student.builder()
.name("张三")
.address("北京")
.age(18)
.build();
System.out.println(student);
}
}
注解实现的背后是生成了下面的代码,核心思路是通过StudentBuilder 内部类的方式,这个内部类的每个方法里面返回了this从而实现了这种fluent的调用模式。
java
public class Student {
private String name;
private String address;
private Integer age;
public static void main(String[] args) {
Student student = builder().name("张三").address("北京").age(18).build();
System.out.println(student);
}
Student(final String name, final String address, final Integer age) {
this.name = name;
this.address = address;
this.age = age;
}
public static StudentBuilder builder() {
return new StudentBuilder();
}
public static class StudentBuilder {
private String name;
private String address;
private Integer age;
StudentBuilder() {
}
public StudentBuilder name(final String name) {
this.name = name;
return this;
}
public StudentBuilder address(final String address) {
this.address = address;
return this;
}
public StudentBuilder age(final Integer age) {
this.age = age;
return this;
}
public Student build() {
return new Student(this.name, this.address, this.age);
}
public String toString() {
return "Student.StudentBuilder(name=" + this.name + ", address=" + this.address + ", age=" + this.age + ")";
}
}
}
但是这样就足够了吗?假定我们规定这样Student类在创建的时候必须先设置姓名再设置地址最后设置年龄,应该怎么做控制,显然现在的这种设计模式是限制不了的:
java
// 下面的两种方式都能通过编译
// 当类中的属性存在初始化的先后顺序时 仅靠单一的建造者模式无法做限定,
Student student1 = Student.builder().name("张三").address("北京").age(18).build();
Student student2 = Student.builder().address("北京").name("张三").age(18).build();
二、fluentAPI的顺序性设计
坦白说一开始思考如何实现fluentAPI的顺序性的时候确实毫无思路,恰好项目开发还使用到了Cola的状态机,定义一个状态机的逻辑如下
java
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
不由得觉得这段代码的API设计确实比较优秀,而且从代码本身的逻辑呢也能猜到这个状态机定义的规则是什么意思。并且在cola的状态机的定义代码中,明确限制了使用了from方法后只能用to方法,这种限定究竟是怎么做到的,于是本着自己不会就去抄源码的精神,详细看了下cola的状态机到底是怎么实现fluentAPI的顺序性问题的。
介绍Cola状态机的源码之前我们需要先知道Cola对于状态机概念的定义
核心概念如下:
css
State:状态
Event:事件,状态由事件触发,引起变化
Transition:流转,表示从一个状态到另一个状态
External Transition:外部流转,两个不同状态之间的流转
Internal Transition:内部流转,同一个状态之间的流转
Condition:条件,表示是否允许到达某个状态
Action:动作,到达某个状态之后,可以做什么
StateMachine:状态机
有了这些概念之后,比如上图中的状态A到状态B的状态流转就可以按照下面的方式去定义
java
// 三个泛型参数依次为状态、事件、状态切换上下文(可用于条件判断或者动作执行的代码逻辑)
StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
builder.externalTransition()
.from(States.STATEA)
.to(States.STATEB)
.on(Events.EVENT1)
// when 方法代表状态切换条件
.when(checkCondition())
// perform 代表状态切换之后需要执行的逻辑
.perform(doAction());
Cola 状态机实现FluentAPI的顺序性的基本方法就是利用 Java中的接口具备多继承性来解决的。
vbnet
public interface ExternalTransitionBuilder<S, E, C> {
/**
* Build transition source state.
* @param stateId id of state
* @return from clause builder
*/
From<S, E, C> from(S stateId);
}
public interface From<S, E, C> {
/**
* Build transition target state and return to clause builder
* @param stateId id of state
* @return To clause builder
*/
To<S, E, C> to(S stateId);
}
首先我们看下from方法的返回是From类型的实例,而这个实例只有一个to方法,所以在fluent调用链调用完from方法以后只能使用to方法。看起来确实非常简单 但是这种思路如何和建造者模式结合起来呢。于是打开源码看下 ExternalTransitionBuilder这个建造者究竟是怎么实现的。
可以发现 ExternalTransitionBuilder 作为接口有一个 TransitionBuilderImpl 的实现类,这个实现类实现了所有From、To、On等接口中规定的逻辑。可以看下这个类的全部代码:
java
class TransitionBuilderImpl<S,E,C> extends AbstractTransitionBuilder<S,E,C> implements ExternalTransitionBuilder<S,E,C>, InternalTransitionBuilder<S,E,C> {
private State<S, E, C> source;
private Transition<S, E, C> transition;
public TransitionBuilderImpl(Map<S, State<S, E, C>> stateMap, TransitionType transitionType) {
super(stateMap, transitionType);
}
@Override
public From<S, E, C> from(S stateId) {
source = StateHelper.getState(stateMap, stateId);
return this;
}
@Override
public To<S, E, C> within(S stateId) {
source = target = StateHelper.getState(stateMap, stateId);
return this;
}
@Override
public When<S, E, C> when(Condition<C> condition) {
transition.setCondition(condition);
return this;
}
@Override
public On<S, E, C> on(E event) {
transition = source.addTransition(event, target, transitionType);
return this;
}
@Override
public void perform(Action<S, E, C> action) {
transition.setAction(action);
}
}
于是答案已经呼之欲出了,这里的建造者实现类中 fluentAPI依然返回this 但是通过继承的关系链进行了Java"上转型",在上转型以后方法的调用集就会缩小,从而实现了限定候选方法集的作用。
三、fluentAPI设计实战
现在回到开头的问题,如果我们要定义一个Student实例 但是要求必须先定义名字再定义地址最后定义年龄,我们的代码就可以这样写
java
public interface IName {
IAddress address(String address);
}
public interface IAddress {
IAge age(Integer age);
}
public interface IAge {
Student build();
}
public interface StudentBuilder {
IName name(String name);
}
public class StudentBuilderImpl extends AbstractStudentBuilder implements StudentBuilder {
private String name;
private String address;
private Integer age;
@Override
public IAge age(Integer age) {
this.age = age;
return this;
}
@Override
public IAddress address(String address) {
this.address = address;
return this;
}
@Override
public IName name(String name) {
this.name = name;
return this;
}
@Override
public Student build(){
Student student = new Student();
student.setName(name);
student.setAddress(address);
student.setAge(age);
return student;
}
}
public class MainTest {
public static void main(String[] args) {
StudentBuilder studentBuilder = new StudentBuilderImpl();
// 不按照顺序定义就会直接报错
// studentBuilder.name("张三").age(18).address("北京").build();
Student student = studentBuilder.name("lisi").address("北京").age(18).build();
System.out.println(student);
}
}
虽然现实情况下不可能定义一个Student实例还需要讲究顺序 但是在某些规则引擎API设计或者强调初始化顺序的场景中这种解决fluentAPI顺序性问题的解决方案还是很值得学习的。
四、总结
本文通过研究ColaStateMachine的源码实现,介绍了一种fluentAPI在设计层面如何解决顺序性调用的一些技巧,即建造者类通过间接实现"链式"接口的方式,并利用了Java在上转型以后候选方法会变少的特性来保证我们链式调用创建对象的时候方法调用的顺序性。
也请各位读者思考下,如果我们需要定义完name属性以后可以选择定义age和addres两个属性的值 但是不允许在定义完name之后定义className这个属性值又该怎么改写呢?