门面模式与Builder模式下必要与默认参数设置的思考

Author:Christopher_L1n | CSDN Blog | 未经允许,禁止转载

门面模式与Builder模式下必要与默认参数设置的思考

门面模式

门面模式可能是在用户体验上较为友好的一种设计模式,它只暴露出少量高度封装的方法,给API用户(后简称用户)一种开箱即用的感觉。下述代码只给用户提供run、stop方法,这两个方法高度封装了一些用户不需要感知的功能,为用户屏蔽了实现的复杂性。比如运行时,不需要让用户从轮胎开始组装到最终开动整辆车,只需要提供一个run和stop方法让用户启停车辆,这便是门面模式。

public class FacadeCar {
    private boolean wheelRun() {}
    private boolean wheelStop() {}
    private boolean engineRun() {}
    private boolean engineStop() {}
    public boolean run() { if (engineRun()) return wheelRun(); }
    public boolean stop() { if (wheelStop()) return engineStop(); }
}

我们可以选择性地开放一些方法的访问权限,让用户拥有深入开发的能力。

Builder模式设置参数

设置参数可能常用Builder模式或者JavaBean模式等,下述代码演示StringBuilder对象设置参数并生成最终String,它避免了创建中间String对象,最终通过build方法生成我们想要的对象,在一定程度上优化了程序的运行:

public class Demo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder();
        String str = sb.append("Demo").append(".").build();
        System.out.println(str);
    }
}

把append方法看作setter方法即为设置参数,比如:Obj = builder.setName("Christopher_L1n").setAge(99).build();但是必要参数应怎样强制用户配置呢?解决方法是在Builder的构造器中解决,我们不为用户提供默认构造器,而在Builder构造器中添加一些必要参数,如下述代码展示了一种Builder模式的设计:

// ./Person.java
public class Person {
    public final String name;  // Required.
    public final int age;  // Optional.
    public final int weight;  // Optional.
    public Person(Builder builder) {
    name = Builder.name;
    age = Builder.age;
    weight = Builder.weight;
    }

    public static Builder {
        public String name;
        public int age = -1; // Default.
        public int weight = -1;  // Default.

        public Builder(String name) {
            this.name = name;
        }

        public void setAge(int age) { this.age = age; return this; }

        public void setWeight(int weight) { this.weight; return this;}

        public Person build() {
            return new Person(this);
        }
    }
}

这样就显式地在构造器中配置了必要参数,隐式地配置了默认参数,同时我们得到了一个流式的API

// ./Person.java
public static void main(String[] args) {
    Person.Builder builder = new Person.Builder("Christopher_L1n");
    Person p = builder.setAge(Integer.MAX_VALUE)
        .setWeight(66)
        // Any Other setter method.
        .build();
}

JavaBean模式

JavaBean模式的设计类似Builder模式,只是它不提供流式的API,相信读者能够在看完它的调用方式之后就能自己设计这种模式,调用方法如下:

// ./AnoPerson.java
public static void main(String[] args) {
    AnoPerson p = new AnoPerson();
    p.setName("Christopher_L1n");
    p.setAge(Integer.MAX_VAULE);
    p.setWeight(66);
}

为了配置必要参数,也可以为它的构造器中添加参数,本文略。

门面模式+Builder/JavaBean模式

不论是JavaBean模式还是Builder模式,都存在不能忽略的问题:在各类参数较少时,用户尚可接受手动一次次调用setter方法,而当参数较多时,不论是必要参数还是其他参数的配置,对于用户都是一次次的机械重复,且有时用户忘记配置某一项可选参数,就会造成意想不到的结果且追溯的成本较高。
即使作为API的提供者可以把问题推给用户——这毕竟是用户的编程错误。但是否存在解决方式给用户更友好的开发体验呢?
问题的解决方式其实在本文一开始就提出了——门面模式:如果能够为用户提供一个setter方法,这个setter方法能够配置所有类型的参数,并且在必要参数未得到配置的情况下build一个对象实例将及时告知用户,这样对用户的开发过程就较为友好。毕竟用户有时并不需要知道我们细化提供了什么setter方法,用户只需要将他们要配置的参数一股脑地调用同个setter方法,我们**做过检查后若有误则抛出异常(本文忽略检查)**即可。

我们来实现这种模式:

// ./Person.java
public class Person {
    public final String name;  // Required.
    public final int age;  // Optional.
    public final int weight;  // Optional.
    public Person(FacadeBuilder builder) {
        name = builder.name;
        age = builder.age;
        weight = builder.weight;
    }

    public static class FacadeBuilder {
        public String name;
        public int age = -1; // Default.
        public int weight = -1;  // Default.

        public FacadeBuilder() {}

        public FacadeBuilder setName(Object name) throws ClassCastException {
            this.name = (String) name;
            return this;
        }

        public FacadeBuilder setAge(Object age) throws ClassCastException {
            this.age = (int) age;
            return this;
        }

        public FacadeBuilder setWeight(Object weight) throws ClassCastException{
            this.weight = (int) weight;
            return this;
        }

        public Person build() throws Exception {
            // Hardcode here.
            if (name == null) throw new Exception();
            return new Person(this);
        }

        public boolean setParam(String key, Object value) throws ClassCastException {
            // Hardcode here.
            switch(key) {
                case "age":
                    setAge(value);
                    return true;
                case "weight":
                    setWeight(value);
                    return true;
                case "name":
                    setName(value);
                    return true;
                default:
                    return false;
            }
        }
    }
}

可以看到,这已经为用户屏蔽了过多的setter方法,用户可以将他需要配置的调用setParam方法即可,只是略有些别扭:由于setParam方法设置的值为Object类型,用户不易得知应该传入什么类型的值才算正确,故为此依旧保留了原来的Builder模式,让深度用户保留有自己的设置参数习惯。
但这就为我们API开发者带来了新的问题:setParam方法需要我们去维护,写一大长串的switch case语句属实令自己感到厌烦。同时,如果维护了其他setter方法,但忘记在setParam增删分支,是比较难在开发过程发现的(当然写好单元测试能很快发现,但未必所有项目组都会遵守规范)。其实问题主要在switch case属于硬编码,且另外一处标注了hardcode的代码即检查必要参数是否已配置也属于硬编码,我们都容易忘记维护。
那么有什么方法在开发时避免这个问题呢?如果存在某种方式标记我们需要检查的方法,将它们自动维护进在一个容器中,用户传入对应的key(需要配置的参数名)时在这个容器中自动地找到对应的setter方法并调用,同时将必要参数自动维护进某容器内检查是否已配置,能够解决这个问题。

改进

在Java中,注解的本质便是标记:它为某个字段/方法/类/包等元素添加额外的信息,以便我们在某个阶段使用被注解的元素。
我们的需求在上述已经提到:自动维护。在已知注解的前提下,我们可以通过反射来在运行时获取某种注解下的元素,于此同时,注解应配置为RUNTIME级别以便VM在运行时保留该注解的信息。
由于注解在方法上时,我们可以在开发流程上要求自己使用注解标记某setter方法配置某参数的,某getter方法获取必要参数的,这也就多了一个维度的信息(对应什么参数)。由此,我们方法级别分别创建两个注解:

  • RequiredGetter: 用于标记配置必要参数的getter方法。
  • AllSetter: 用于标记所有setter方法。
  • 这两类注解均包含key参数,即对应上一小节末的key,也就是对应配置的字段。
// ./RequiredGetter.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiredGetter {
    String key();
}

// ./AllSetter.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AllSetter {
    String key();
}

这样我们在开发中只需要遵守自己的规范:在增加参数时,添加AllSetter注解标记这个方法是用来配置哪个参数,添加RequiredGetter注解标记必要参数的getter方法。最后,我们在类中添加两个容器对象通过反射获取到使用这两个注解的方法,修改setParam方法作为统一的setter入口,修改build方法用来最终校验必要参数是否已配置,如此即可减少未来开发的难度:

// ./ReconstructPerson.java
public class ReconstructPerson {
    public final String name;  // Required.
    public final int age;  // Optional.
    public final int weight;  // Optional.

    public ReconstructPerson(FacadeBuilder builder) {
        name = builder.name;
        age = builder.age;
        weight = builder.weight;
    }

    public static class FacadeBuilder {
        public String name;
        public int age = -1; // Default.
        public int weight = -1;  // Default.
        private final HashMap<String, Method> setter = new HashMap<>();
        private static final HashMap<String, Method> required = new HashMap<>();
        {
            // Auto-maintained.
            for (Method m: FacadeBuilder.class.getDeclaredMethods()) {
                AllSetter as;
                if ((as = m.getAnnotation(AllSetter.class)) != null){
                    setter.put(as.key(), m);
                    continue;
                }
                RequiredGetter rg;
                if ((rg = m.getAnnotation(RequiredGetter.class)) != null) required.put(rg.key(), m);
            }
        }

        public FacadeBuilder() {}

        @AllSetter(key = "name")
        public FacadeBuilder setName(Object name) throws ClassCastException {
            this.name = (String) name;
            return this;
        }

        @AllSetter(key = "age")
        public FacadeBuilder setAge(Object age) throws ClassCastException {
            this.age = (int) age;
            return this;
        }
        @AllSetter(key = "weight")
        public FacadeBuilder setWeight(Object weight) throws ClassCastException{
            this.weight = (int) weight;
            return this;
        }

        @RequiredGetter(key = "name")
        public Object getName() { return name; }

        public Object getAge() { return age; }

        public Object getWeight() { return age; }

        public ReconstructPerson build() throws Exception {
            for (String key : required.keySet()) {
                Method m = required.get(key);
                // First param is which object should invoke this method.
                if (m.invoke(this) == null)
                    throw new RuntimeException(String.format("Haven\'t set value for %s.", key));
            }
            return new ReconstuctPerson(this);
        }

        /**
        * @throw Exception setter failed.
        * @return true: setter success; false: setter failed.
         */
        public boolean setParam(String key, Object value) throws Exception {
            if (setter.containsKey(key)) {
                // Can be simplied.
                Method m = setter.get(key);
                // First param is which object should invoke this method.
                m.invoke(this, value);
                return true;
            }
            throw new RuntimeException("Input an invalid key.");
        }
    }

    public static void main(String[] args) throws Exception {
        ReconstructPerson.FacadeBuilder builder = new ReconstructPerson.FacadeBuilder();
        // Ignore return value.
        builder.setParam("age", 11);
        // builder.setParam("name", "Christopher_L1n");
        // Raise a RuntimeException.
        ReconstructPerson p = builder.build();
        // System.out.println(p.age);
    }
}

总结

如此,用户只需要将他们需要设置的参数显式地放在他们应用的某个位置(便于维护),用个循环调用setParam方法即可完成参数的配置,我们作为API的开发者在未来维护时也变得轻松。
当然这种方法也有缺点:setParam方法传入的value均为Object类型,用户不易得知应该传入什么类型的值,只好以文档来显式地约束用户。

发布了15 篇原创文章 · 获赞 3 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Christopher_L1n/article/details/103518172