爷的眼睛闪亮
insideDotNet En_summerGarden

什么是链式接口(Fluent Interface)

根据wikipedia上的定义,Fluent interface是一种通过链式调用方法来完成方法的调用,其操作分为终结与中间操作两种。[1]

下面是一个Wikipedia上的例子。

Author author = AUTHOR.as("author");
create.selectFrom(author)
      .where(exists(selectOne()
                   .from(BOOK)
                   .where(BOOK.STATUS.eq(BOOK_STATUS.SOLD_OUT))
                   .and(BOOK.AUTHOR_ID.eq(author.ID))));

在设置多参数时,通过这种方式简化操作,提高可读性。在effective java 第2版中讲述了这种接口的一个实现方式“第2条:遇到多个构造器参数时要考虑用构建器”。通过创建一个构建类Builder来实现这种风格的接口。

例如effective java 第2版,第2条的例子。

public class NutritionFacts {
    private int servingSize;
    private int fat;

    private NutritionFacts(Builder builder) {
        this.servingSize = builder.servingSize;
        this.fat = builder.fat;
    }
    ...略
    public static class Builder {
        private int servingSize;
        private int fat;

        public Builder servingSize(int sS) {
            servingSize = sS;
            return this;
        }

        public Builder fat(int ft) {
            fat = ft;
            return this.
        }

        public NutritionFacts build() {
            return NutritionFacts(this);
        }
    }
}

可以看到,要使用这种风格的接口,那么每次都要创建Builder,然后实现属性设置,对象创建等重复操作。因此考虑通过程序自动完成这些重复操作,在使用时仅需要指定所需要实现的接口即可。

为了实现这种风格的操作需要完成两个主要的功能。 
1.链式调用设置属性。 
2.创建最终的对象。

通过Effective java 2 中的例子可以看到可以看到,链式调用的实现,是在调用属性设置方法时,每次返回Builder来实现,方法名根据要创建的目标对象的不同而变化,对于任何一个对象的创建都是通过build方法完成。

基于JDK动态代理的实现

下面以类Student为例。

public class Student {
    private int age;
    private String name;
    private char sex;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public char getSex() {
        return sex;
    }
    public void setSex(char sex) {
        this.sex = sex;
    }
    @Override
    public String toString() {
        return "Student [age=" + age + ", name=" + name + ", sex=" + sex + "]";
    }
}

定义Builder接口,通过类型参数T来表示最终创建的对象,而方法build来完成最终对象的创建。

public interface Builder<T> {
    T build();
}

创建Student的Builder接口,接口中的方法返回StudentBuilder最终实现链式属性设置。

public interface StudentBuilder extends Builder<Student>{
    StudentBuilder age(int age);
    StudentBuilder name(String name);
    StudentBuilder sex(char sex);
}

最终的目标是,只需要使用者定义自己的XXXBuilder,而无需自己去实现各种属性设置方法及对象创建方法来使用链式调用完成属性设置。

接下来通过JDK的动态代理机制来实现上述功能。定义FluentApi来提供链式调用功能。通过target传入使用者定义的XXXBuilder接口。

public class FluentApi<T> {
    /**
     * 目标接口
     */
    Class<T> target;

    private FluentApi(Class<T> target) {
        if(!target.isInterface()) {
            throw new IllegalArgumentException("must be interface");
        }
        this.target = target;
    }

    public static  <T> FluentApi<T>  target(Class<T> target) {
        return new FluentApi<T>(target);
    }
}

接着创建XXXBuilder接口的代理。为了实现链式调用,每次必须返回代理对象proxy本身。

public class JdkProxy implements InvocationHandler{
    private final String BUILDMETHOD = "build";

    Class<?> target;

    public JdkProxy(Class<?> target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        return proxy;
    }
}

在FluentApi增加Create方法创建XXXBuilder的代理。

    public T create() {
        JdkProxy jdkProxy = new JdkProxy(target);
        return (T)Proxy.newProxyInstance(target.getClassLoader(), 
                               new Class[]{target}, 
                               jdkProxy);
    }

经过上面简单的两步,各个方法进行链式调用功能就完成了。

public class Main {

    public static void main(String[] args) {
        FluentApi.target(StudentBuilder.class)
                 .create()
                 .age(1)
                 .sex('m')
                 .name("java")
                 .build();
    }
}

接下来实现属性的设置与对象创建。由于对象创建是在build方法调用时创建,因此调用非build方法时仅保存属性。所以需要判断方法到底是属性设置还是对象创建。

build方法仅需要与Builder接口中build方法比较即可,判断方法如下。

    boolean isBuildMethod(Method method) {
        try {
            Method buildMethod = Builder.class.getDeclaredMethod(BUILDMETHOD);
            return method.equals(buildMethod);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException("");
        }
    }

属性设置方法满足的条件是只有一个参数且返回类型为使用者传入的接口类型,判断方法如下。

    boolean isPropertySetter(Method method) {
        return method.getParameters().length == 1 && target.isAssignableFrom(method.getReturnType());
    }

这样就可以在invoke方法中判断方法的类型了。

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        if(isPropertySetter(method)) {
            System.out.println("PropertySetter");
            return proxy;
        }
        else if(isBuildMethod(method)) {
            System.out.println("BuildMethod");
            return 最终要创建的对象;
        }
        return null;
    }

接下来当满足isPropertySetter时,将属性保存,因此增加一个map来保存待设置的值。Map <String,Object> properties = new HashMap <String,Object> 。其中key为属性的名字,而value为属性值。由于在定义接口XXXBuilder时约定里面的属性设置方法是与要创建的对象的各个属性名保持一致,因此取方法名作为key即可,而属性参数值作为value。

最后调用build方法时,完成对象的创建。而为了创建对象,需要获取Builder<T>接口中的类型参数,这样才能完成对象的创建。

    Class<?> getParameterType(Class<?> clzz) {
        Type[] types = clzz.getGenericInterfaces();
        for(Type type : types) {
            if(type instanceof ParameterizedType) {
                ParameterizedType paramType = (ParameterizedType)type;
                if(paramType.getRawType() == Builder.class) {
                    return (Class<?>)paramType.getActualTypeArguments()[0];
                }
            }
        }
        return null;
    }

由于使用者传入的XXXBuilder实现了Builder<T>接口,因此只要检查XXXBuilder所实现的接口中哪个为参数类型ParameterizedType并且参数擦出后的原始类型为Builder即可。当类型满足这些条件时,再取出参数类型,这样就获取到了所需要的对象类型,就可以创建对象实例了[3]。

接下来将properties存放的属性赋给对象的各个成员变量即可。

    private void setPropperties(Object obj) throws Exception {
        for(Map.Entry<String, Object> entry : properties.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            Field field = obj.getClass().getDeclaredField(key);
            boolean isAccess = field.isAccessible();
            try {
                field.setAccessible(true);
                field.set(obj, value);
            }finally {
                field.setAccessible(isAccess);
            }
        }
    }

这样一个链式调用风格小框架主体功能就完成了,在使用时使用者仅需要定义属性设置接口,并满足方法名与属性名保持一致,随后就可以通过链式调用的方式来为对象设置属性并完成创建功能,而不需要重复编写属性设置与对象创建的代码了。

参考:

[1] fluent interface定义,https://en.wikipedia.org/wiki/Fluent_interface 
[2] effective java 第2版,第2条 
[3] jdk 1.8 api参考手册

代码:

public class FluentApi<T> {
    /**
     * 目标接口
     */
    Class<T> target;

    private FluentApi(Class<T> target) {
        if(!target.isInterface()) {
            throw new IllegalArgumentException("must be interface");
        }
        this.target = target;
    }

    public static  <T> FluentApi<T>  target(Class<T> target) {
        return new FluentApi<T>(target);
    }

    @SuppressWarnings("unchecked")
    public T create() {
        JdkProxy jdkProxy = new JdkProxy(target);
        return (T)Proxy.newProxyInstance(target.getClassLoader(), 
                               new Class[]{target}, 
                               jdkProxy);
    }
}
public class JdkProxy implements InvocationHandler{
    private final String BUILDMETHOD = "build";

    Class<?> target;

    Class<?> object;
    Map<String,Object> properties = new HashMap<String,Object>();

    public JdkProxy(Class<?> target) {
        this.target = target;
        this.object = getParameterType(target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(method.getName());
        if(isPropertySetter(method)) {
            properties.put(method.getName(), args[0]);
            System.out.println("PropertySetter");
            return proxy;
        }
        else if(isBuildMethod(method)) {
            Object obj = object.newInstance();
            setPropperties(obj);
            System.out.println("BuildMethod");
            return obj;
        }
        return null;
    }

    boolean isPropertySetter(Method method) {
        return method.getParameters().length == 1 && target.isAssignableFrom(method.getReturnType());
    }

    boolean isBuildMethod(Method method) {
        try {
            Method buildMethod = Builder.class.getDeclaredMethod(BUILDMETHOD);
            return method.equals(buildMethod);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException("");
        }
    }

    private Class<?> getParameterType(Class<?> clzz) {
        Type[] types = clzz.getGenericInterfaces();
        for(Type type : types) {
            if(type instanceof ParameterizedType) {
                ParameterizedType paramType = (ParameterizedType)type;
                if(paramType.getRawType() == Builder.class) {
                    return (Class<?>)paramType.getActualTypeArguments()[0];
                }
            }
        }
        return null;
    }

    private void setPropperties(Object obj) throws Exception {
        for(Map.Entry<String, Object> entry : properties.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            Field field = obj.getClass().getDeclaredField(key);
            boolean isAccess = field.isAccessible();
            try {
                field.set(obj, value);
                field.setAccessible(true);
            }finally {
                field.setAccessible(isAccess);
            }
        }
    }
}
public interface Builder<T> {
    T build();
}


public interface StudentBuilder extends Builder<Student>{
    StudentBuilder age(int age);
    StudentBuilder name(String name);
    StudentBuilder sex(char sex);
}
public class Main {

    public static void main(String[] args) {
        Student student = 
        FluentApi.target(StudentBuilder.class)
                 .create()
                 .age(1)
                 .sex('m')
                 .name("java")
                 .build();
        System.out.println(student);
    }
}
posted on 2017-12-25 15:31  爷的眼睛闪亮  阅读(1068)  评论(0编辑  收藏  举报