前言
通常代码中为某些类设置链式方法可以简化调用,并增强代码的优雅性和易读性,将一个设置方法(Setter)转换为链式方法最简单的方式是将返回类型改为类的本身,并在结尾返回this,比较典型的应用场景是工厂模式。
设计一个机器人的行为接口。
public interface Automata {
void run(int x,int y); //跑
void jump(float strength); //跳
void work(); //工作
void boom(); //炸了
void stop(); //停止
}
设计一个抽象工厂类。
public abstract class AutomataFactory<T extends Automata>{
T autoMata;
public T create(){
return autoMata;
}
}
设计一个机器人类和它的工厂类。
public class Robot implements Automata{
protected String no; //编号
protected String type; //型号
protected String appearDate; //出厂日期
protected int expireYear; //过保质期年限
protected int expireMonth; //过保质期月限
protected String licenceCode; //许可编号
protected String factoryCode; //工厂编号
//省略很多其他属性...
@Override
public void run(int x, int y) { System.out.println("run to ("+x+","+y+")"); }
@Override
public void jump(float strength) { System.out.println("jump with strength "+strength+"!"); }
@Override
public void work() {System.out.println("work work!");}
@Override
public void boom() {System.out.println("Booooom!");}
@Override
public void stop() {System.out.println("Shutdown!");}
@Override
public String toString() {
return "Robot{" +
"no='" + no + '\'' +
", type='" + type + '\'' +
", expireYear=" + expireYear +
", expireMonth=" + expireMonth +
", appearDate='" + appearDate + '\'' +
", licenceCode='" + licenceCode + '\'' +
", factoryCode='" + factoryCode + '\'' +
'}';
}
public static class Factory extends AutomataFactory<Robot>{
private final String mFactoryCode; //工厂编号
public Factory(String code){
this.mFactoryCode = code;
this.autoMata = new Robot();
robot.factoryCode = mFactoryCode;
}
public Factory setNo(String no) {
autoMata.no = no;
return this;
}
public Factory setType(String type) {
autoMata.type = type;
return this;
}
public Factory setExpireYear(int expireYear) {
autoMata.expireYear = expireYear;
return this;
}
public Factory setExpireMonth(int expireMonth) {
autoMata.expireMonth = expireMonth;
return this;
}
public Factory setAppearDate(String appearDate) {
autoMata.appearDate = appearDate;
return this;
}
public Factory setLicenceCode(String licenceCode) {
autoMata.licenceCode = licenceCode;
return this;
}
}
}
通过工厂类创建一个机器人,可以这么写。
Robot robot = new Robot.Factory("BnL").setNo("eva80").setType("WALE").setAppearDate("2008-06-27").setExpireYear(88).setExpireMonth(8).setLicenceCode("xxXXxxXXxx")
.create();
这就是一种简易的链式调用,这种写法的一个好处是便于在不同场景配置类的不同属性,另一个好处在于保持连续调用设置方法后,仍能在这段代码的结尾返回需要的对象,
知名的响应式框架RxJava和Retrofit使用了大量的工厂方法,安卓开发者应该对这种写法不陌生,比如一个后端接口"192.168.0.11:8090/api/User/GetUserById"。(通过id获取用户对象)
根据userId请求此接口,返回的结果可能是一个json数据,用Retrofit调用此接口,需要先创建一个UserService类。
public interface UserService{
@GET("User/GetUserById")
Observable<Response<User>> getUserById(@Query("id") String id); //User外层被一个自定义的Response类包裹
}
在通过id获取用户信息的地方。
public class TestRetrofit{
public static void main(String[] args) {
new Retrofit.Builder()
.baseUrl("192.168.0.11:8090/api/") //设置接口根路径
.client(new OkHttpClient.Builder() //OkHttp客户端仍然是用工厂模式创建
.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) //添加日志拦截
.connectTimeout(45,TimeUnit.SECONDS) //设置超时时长
.readTimeout(45,TimeUnit.SECONDS) //设置读取时长
.build()) //创建OkHttp客户端
.addConverterFactory(GsonConverterFactory.create()) //添加Gson转换器
.addCallAdapterFactory(RxJava2CallAdapterFactory.create()) //添加异步调度
.build() //创建retrofit客户端
.create(UserService.class) //生成UserService请求对象
.subscribeOn(Schedulers.io()) //在子线程订阅事件
.observeOn(AndroidSchedulers.mainThread()) //在主线程观察接口返回的结果
.subscribe(new Observer<Response<User>>(){ //订阅接口请求结果
@Override
public void onSubscribe(@NonNull Disposable disposable) {
//在订阅时执行的回调
}
@Override
public void onNext(Response<User> response) {
//在获取接口返回结果执行的回调
User user = response.getData();
//将数据填充到页面
}
@Override
public void onError(@NonNull Throwable e) {
//在接口访问出现异常时执行的回调
}
@Override
public void onComplete() {
//在接口访问结束后执行的回调
}
});
}
}
初学者看到这串代码只会感觉非常繁琐,但是仔细阅读之后会发现这种设计的精妙之处——将异步的网络请求浓缩到一串代码中,大大地增加了代码的可复用性。
通常在业务场景,会将Retrofit客户端封装到一个单例类中,并对Observer进行自定义,设计一个抽象类实现Observer接口,只保留onNext方法抽象化,忽略onSubscribe和onComplete两个不太常用的回调,在onError中处理网络异常(比如弹出一些消息提示),以简化调用。
用工厂方式设计一个支持任何对象链式调用的工具
在某些情况下,需要对一个对象进行链式调用,但是对象本身没有提供链式的调用方法,如何进行自定义呢?在不重写对象方法的前提下,可以设计一个简易的工厂类去处理,借助反射调用这个对象的方法,然后输出这个对象。
public class ComplianceFactory<T> {
Class<T> tClass; //执行链式方法对象的类型
T data; //执行链式方法的对象
//构造器
public ComplianceFactory(Class<T> tClass){
this.tClass = tClass;
}
//输入对象
public ComplianceFactory<T> input(T origin){
this.data = origin;
return this;
}
//Unsafe 执行方法,不限制方法的参数类型
public ComplineFactory<T> invoke(String functionName,Object...objects){
Class<?> params[] = new Class<?>[objects.length];
for(int i=0;i<objects.length;i++){
params[i] = objects[i].getClass();
}
try {
Method method = tClass.getDeclaredMethod(functionName,params);
method.invoke(data,objects);
} catch (NoSuchMethodException nsme) {
nsme.printStackTrace();
} catch (IllegalAccessException iae) {
iae.printStackTrace();
} catch (InvocationTargetException ite) {
ite.printStackTrace();
}
return this;
}
// 执行方法,限制方法的参数类型
public ComplianceFactory<T> invoke(String functionName, Class<?>[] classes, Object...objects){
Class<?> params[] = classes;
try {
Method method = tClass.getDeclaredMethod(functionName,params);
method.invoke(data,objects);
} catch (NoSuchMethodException nsme) {
nsme.printStackTrace();
} catch (IllegalAccessException iae) {
iae.printStackTrace();
} catch (InvocationTargetException ite) {
ite.printStackTrace();
}
return this;
}
//输出对象
public T output(){
return data;
}
}
写一个测试的类去检验这个工具的可行性
package starter;
import bean.Robot;
import chain.ComplianceFactory;
public class RobotFactoryMain {
public static void main(String[] args) {
Robot robot = new Robot.Factory("BnL").setNo("eva80").setType("WALE").setAppearDate("2008-06-27").setExpireYear(88).setExpireMonth(8).setLicenceCode("xxXXxxXXxx").create();
System.out.println(
"Robot is :"
+ new ComplianceFactory<Robot>(Robot.class)
.input(robot)
.invoke("run",22,33)
.invoke("work")
.output()
);
}
}
运行结果如图所示
为什么会报错呢?这是因为如果原始类型作为对象(Object)传入参数表,getClass会自动将原始类型(如int,float,double)自动识别为对应的包装器类(如Integer,float,double)。
package starter;
public class RobotFactoryMain {
public static void main(String[] args) {
System.out.println(int.class);
System.out.println(testGetClass(22));
System.out.println(float.class);
System.out.println(testGetClass(22.0f));
System.out.println(double.class);
System.out.println(testGetClass(22.0));
}
static Class<?> testGetClass(Object object){
return object.getClass();
}
}
运行结果如下
这个时候就需要限制对参数表包含原始类型的方法,强制设置参数表的类型,修改测试类。
package starter;
import bean.Robot;
import chain.ComplianceFactory;
public class RobotFactoryMain {
public static void main(String[] args) {
Robot robot = new Robot.Factory("BnL").setNo("eva80").setType("WALE").setAppearDate("2008-06-27").setExpireYear(88).setExpireMonth(8).setLicenceCode("xxXXxxXXxx").create();
System.out.println(
"Robot is :"
+ new ComplianceFactory<Robot>(Robot.class)
.input(robot)
.invoke("run",new Class[]{int.class,int.class},22,33)
.invoke("jump",new Class[]{float.class},11.0f)
.invoke("work")
.output()
);
}
}
运行结果如下
这个工具能够让任何对象链式调用自身的方法,但是存在两个缺陷,一个是反射调用需要记住方法名和参数表的类型,相对而言比较繁琐,而且会增加报错的风险,另一个是调用反射执行方法的过程中创建中间了变量。
kotlin的链式调用方式
有没有一种办法使一个对象在一次的方法调用过程中既能任意调用自身的方法,又能在方法结束后返回自身呢?对于这个问题,kotlin是这样处理的。
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T { //T是任意对象,入参是一个方法块,inline修饰符让方法体默认为lambda
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block() //执行方法块
return this //返回对象自身
}
kotlin是JetBrain基于Java开发的面向JVM并且兼容java的语言,kotlin提供了很多新的特性,比如类型推断、DSL、协程等,这些都是java支持力度不太足够的,而kotlin实现任何对象的公共方法可以链式调用是基于“拓展方法”——另一个java所欠缺的特性。
kotlin的apply如何实现方法的链式调用呢?比如用kotlin写一个简易安卓的页面,不声明任何布局文件,并且只用一段代码。
public class KtApplyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(FrameLayout(this).apply { //外层:FrameLayout
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) //尺寸:填充整个屏幕
setPadding(32, 32, 0, 0) //边距:上和左为32
background = ContextCompat.getDrawable(this@KtApplyActivity, R.color.white) //背景为白色
addView(LinearLayout(this@KtApplyActivity).apply { //内部添加一个LinearLayout
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT) //尺寸:填充整个屏幕
orientation = LinearLayout.VERTICAL //方向:垂直摆放空间
background = ContextCompat.getDrawable(this@KtApplyActivity, R.color.colorAccent) //背景:粉红色
addView(TextView(this@KtApplyActivity).apply { //添加一个文字控件
//尺寸:自适应
layoutParams = ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).apply { setPadding(8,4,0,4) }
setTextColor(Color.WHITE) //字体颜色:白色
textSize = 24f //字体大小:24
text = "Hello kotlin!" //文字
setShadowLayer(4f,4f,4f,Color.GRAY) //添加一个灰色阴影
setTypeface(Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)) //设置字体为粗体+斜体
})
//添加一个图片控件
addView(ImageView(this@KtApplyActivity).apply {
scaleType = ImageView.ScaleType.CENTER_CROP //设置裁剪方式
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT) //尺寸:自适应
setImageResource(R.mipmap.ic_kotlin) //设置图片素材
})
})
})
}
}
运行效果如下。
为什么kotlin的apply方法块中可以任意执行对象的方法,不需要借助对象显式调用?正如官方文档对此方法的描述那样,在apply的内部,Lambda持有的是对象的this,结果返回的仍然是对象的this,而在java的Lambda表达式中,没有办法持有调用者的this。所以说kotlin对Lambda的支持相对java而言是更加健全的。
Calls the specified function [block] with `this` value as its receiver and returns `this` value.
仿照kotlin的链式调用方法
如何仿照Kotlin的写法,写一个工具类可以让任何类可以被链式调用呢?可以借助java8的一个特性 : 函数式接口(Functional Interface)。
函数式接口是一个注解,可以修饰任何只有一个方法的接口,比如Runnable。
为什么被Functional Interface注解的接口只能有一个方法?
其实这个说法不太准确,可以在这个接口中声明多个方法,但是其他的方法必须要有默认的方法体(default),而且要有一个方法是抽象的(需要被外部实现)。从编译层面保证这个接口可以被“Lambda化”。
@FunctionalInterface
public interface Block<T>{
void block(final T t);
}
设计一个通用的工厂类,可以将任何类作为入参。
public class ComplianceFactory<T> {
T data;
public ComplianceFactory(){
}
public ComplianceFactory<T> input(T origin){
this.data = origin;
return this;
}
public ComplianceFactory<T> invoke(Block<T> block){
block.block(data);
return this;
}
public T output(){
return data;
}
}
写一个测试类,流程非常简单,创建一个工厂,输入一个Robot对象,执行它的一些方法,并打印出来。
import chain.ComplianceFactory;
public class RobotMain {
public static void main(String[] args) {
System.out.println(new ComplianceFactory<Robot>()
.input(new Robot.Factory("BnL").
setNo("eva80").setType("WALE").setAppearDate("2008-06-27").setExpireYear(88).setExpireMonth(8).setLicenceCode("xxXXxxXXxx")
.create())
.invoke(o -> {
o.run(32,16);
o.jump(5.0f);
o.work();
o.boom();
o.stop();
}).output());
}
}
运行结果如下
至此,一个简易的链式调用工厂类已经构建完毕,可以将其应用到任何需要“一次性调用”的地方以简化代码的编写。
对此工具类的优化
正所谓大道至简,链式调用的核心思想在于简化调用过程。对于当前版本的工具类,确实还有优化的空间。为了将调用简化到极致,我们可以首先优化构造器的设计。
public ComplianceFactory(T origin,Block<T> block){
input(origin);
invoke(block);
}
将原始数据和回调接口设置为构造方法的入参,节省了调用input和invoke两个方法。
new ComplianceFactory<Robot>().input(new Robot()).invoke(o->{/*do something here*/}).output()
可以借此将上述的代码可以简化为
new ComplianceFactory<Robot>(new Robot(),o->{/*do something here*/}).output();
还可以继续简化吗?答案仍是可以,使用静态方法将output方法的调用也省略掉。
public static <T> T ret(T origin,Block<T> block){
return new ComplianceFactory<T>(origin,block).output();
}
这样代码又可以被简化为
ComplianceFactory.ret(new Robot(),o->{/*do something here*/});
通过调用ret静态方法,可以直接返回一个在回调中被操作的对象。
优化,永不止境。