具有子分类和必需参数的构建器设计模式?

时间:2017-12-04 16:54:14

标签: java design-patterns builder

最近我遇到了构建器模式非常强大的情况,但我需要子类化。我查找了一些解决方案和一些建议的泛型,而其他人建议正常的子类。但是,我查看的所有示例都没有必填字段才能开始构建对象。我写了一个小例子来说明我陷入困境的地方。在每一个转弯我都会遇到问题,其中的东西会返回错误的类,不能覆盖静态方法,返回super()返回错误的数据类型等等。我有一种感觉除了过度使用外没有出路泛型。

在这种情况下,正确的方法是什么?

测试

import person.Person;
import person.Student;

public class Tester
{
    public static void main(String[] args)
    {
        Person p = Person.builder("Jake", 18).interest("Soccer").build();
        // Student s = Student.builder(name, age) <-- It's weird that we still have access to pointless static method
        // Student s = Student.builder("Johnny", 24, "Harvard", 3).address("199 Harvard Lane") <-- returns Person builder, not student
        Student s = ((Student.Builder)Student.builder("Jack", 19, "NYU", 1).address("Dormitory")).build(); // really bad
    }
}

人类

package person;

import java.util.ArrayList;
import java.util.List;

public class Person
{
    // Required
    protected String name;
    protected int age;

    // Optional
    protected List<String> interests = new ArrayList<>();
    protected String address = "";

    protected Person(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public List<String> getInterests() { return interests; }
    public String getAddress() { return address; }

    // person.person does not allow builder construction
    // unless all required fields are provided

    /* Problem: I have to repeat the constructor fields here, very annoying */
    public static Builder builder(String name, int age)
    {
        Person p = new Person(name, age);
        return new Builder(p);
    }

    public static class Builder
    {
        Person reference;

        protected Builder(Person reference)
        {
            this.reference = reference;
        }

        public Builder address(String address)
        {
            reference.address = address;
            return this;
        }

        public Builder interest(String interest)
        {
            reference.interests.add(interest);
            return this;
        }

        public Person build()
        {
            return reference;
        }
    }
}

学生班

package person;

import java.util.ArrayList;
import java.util.List;

public class Student extends Person
{
    // Required
    protected String school;
    protected int year;

    // Optional
    protected List<String> subjects = new ArrayList<>();

    // This looks good
    public Student(final String name, final int age, final String school, final int year)
    {
        super(name, age);
        this.school = school;
        this.year = year;
    }

    public String getSchool() { return school; }
    public int getYear() { return year; }
    public List<String> getSubjects() { return subjects; }

    /* Here's where my issues are:

      * Override doesn't compile on static methods but how else can I describe that I want to
      * override this functionality from the Person class?
      * 
      * Extending 'Person' does not enforce that I need to provide 'name', 'age', etc like it would 
      * if this was a normal design pattern using the 'new' keyword. I have to manually drag fields
      * from 'person' and place them here. This would get VERY messy with an additional class 
      * 
      * User can STILL call the Person builder on a Student object, which makes no sense. */
    /*@Override*/ public static Builder builder(String name, int age, String school, int year)
    {
        Student s = new Student(name, age, school, year);
        return new Builder(s);
    }

    public static class Builder extends Person.Builder
    {
        // Student reference; <--- this should not be needed since we already
        //                      have a variable for this purpose from 'Person.Builder'

        public Builder(final Student reference)
        {
            super(reference);
        }

        /* Things begins to get very messy here */
        public Builder subject(String subject)
        {
            ((Student)reference).subjects.add(subject);
            // I guess I could replace the reference with a student one, but
            // I feel like that infringes on calling super() builder since we do the work twice.
            return this;
        }

        @Override public Student build()
        {
            // I can either cast here or
            // rewrite 'return reference' every time.
            // Seems to infringe a bit on subclassing.
            return (Student)super.build();
        }
    }
}

2 个答案:

答案 0 :(得分:1)

你在这里写的是什么:

Student s = ((Student.Builder)Student.builder("Jack", 19, "NYU", 1).address("Dormitory")).build(); // really bad

确实不是很自然,你不需要施放 我们期待的是:

Student s = Student.builder("Jack", 19, "NYU", 1).address("Dormitory")).build(); 

除了你在Student.Builder的实现中所做的所有强制转换,也是在运行时可能失败的噪声和语句:

    /* Things begins to get very messy here */
    public Builder subject(String subject)   {
        ((Student)reference).subjects.add(subject);          
        return this;
    }

    @Override public Student build() {          
        return (Student)super.build();
    }

您的主要问题是Builder类与构建方法之间的耦合 需要考虑的一件重要事情是,在编译时,方法绑定(编译器选择的方法)是根据调用目标的声明类型和声明的参数类型执行的。
实例化类型仅在运行时考虑应用动态绑定:在运行时对象上调用编译时限制的方法。

所以Student.Builder中定义的覆盖是不够的:

@Override public Student build() {
    return (Student)super.build();
}

当你调用时:

Student.builder("Jack", 19, "NYU", 1).address("Dormitory").build();

在编译时,address("Dormitory")会返回一个类型为Person.Builder的变量,因为该方法在Person.Builder中定义:

public Builder address(String address){
    reference.address = address;
    return this;
}

并且未在Student.Builder中覆盖 在编译时,对声明为build()的变量调用Person.Builder会返回一个声明为Person的声明的对象,因为该方法在Person.Builder中声明为:

public Person build(){
    return reference;
}

当然在运行时,返回的对象将是Student

Student.builder("Jack", 19, "NYU", 1)隐藏了Student而不是Person

为了避免从实现和客户端转换为Student.builder,请优先考虑组合而不是继承:

public static class Builder {

  Person.Builder personBuilder;
  private Student reference;

  public Builder(final Student reference) {
    this.reference = reference;
    personBuilder = new Person.Builder(reference);
  }

  public Builder subject(String subject) {
    reference.subjects.add(subject);
    return this;
  }

  // delegation to Person.Builder but return Student.Builder
  public Builder interest(String interest) {
    personBuilder.interest(interest);
    return this;
  }

  // delegation to Person.Builder but return Student.Builder
  public Builder address(String address) {
    personBuilder.address(address);
    return this;
  }

  public Student build() {
    return (Student) personBuilder.build();
  }

}

您现在可以写:

Student s = Student.builder("Jack", 19, "NYU", 1)
                   .address("Dormitory")
                   .build(); 

甚至是:

Student s2 = Student.builder("Jack", 19, "NYU", 1)
                    .interest("Dance")
                    .address("Dormitory")
                    .build();

组合通常会引入更多代码作为继承,但它会产生代码 更加强大和适应性强。

作为旁注,你的实际问题已经足够接近我1个月前回答的另一个问题了 The question and its answers可能会让您感兴趣。

答案 1 :(得分:0)

一些想法作为背景

  1. 静态方法不是很好, 他们使单元测试更加困难。
  2. 可以将构建器作为静态嵌套类放置,但如果使用构建器构造类,则应该使构造函数不公开。
  3. 我更喜欢让构建器成为同一个包中的一个单独的类,并使构造函数(由构建器创建的类)包访问。
  4. 限制构建器构造函数参数。
  5. 我不喜欢为构建器使用类层次结构。
  6. 每个人和学生班都有一个建设者。
  7. 某些代码

    public class PersonBuilder
    {
        private String address;
        private int age;
        private final List<String> interestList;
        private String name;
    
        public PersonBuilder()
        {
            interestList = new LinkedList<>();
        }
    
        public void addInterest(
            final String newValue)
        {
            // StringUtils is an apache utility.
            if (StringUtils.isNotBlank(newValue))
            {
                interestList.add(newValue);
            }
    
            return this;
        }
    
        public Person build()
        {
            // perform validation here.
            // check for required values: age and name.
    
            // send all parameters in the constructor.  it's not public, so that is fine.
            return new Person(address, age, interestList, name);
        }
    
        public PersonBuilder setAddress(
            final String newValue)
        {
            address = newValue;
    
            return this;
        }
    
        public PersonBuilder setAge(
            final int newValue)
        {
            age = newValue;
    
            return this;
        }
    
        public PersonBuilder setInterestList(
            final List<String> newValue)
        {
            interestList.clear();
    
            if (CollectionUtils.isNotEmpty(newValue))
            {
                interestList.addAll(newValue);
            }
    
            return this;
        }
    
        public PersonBuilder setName(
            final String newValue)
        {
            name = newValue;
    
            return this;
        }
    }
    
    
    public class Person
    {
        private Person()
        {
        }
    
        Person(
            final String addressValue,
            final int ageValue,
            final List<String> interestListValue,
            final String name)
        {
            // set stuff.
            // handle null for optional parameters.
        }
    
        // create gets or the fields, but do not create sets.  Only the builder can set values in the class.
    }