lundi 4 décembre 2017

Builder Design Pattern with sub-classing and required parameters?

Recently I came into a situation where the builder pattern was very strong, but I had the need to subclass. I looked up some solutions and some suggested generics while others suggested normal subclassing. However, none of the examples I looked at had required fields in order to even begin building an object. I wrote a tiny example to illustrate where I'm getting stuck. At every turn I kept running into a wall of problems where things would return the wrong class, can't override static methods, returning super() returns the wrong data type, etc. I have a feeling there is no way out except excessive use of generics.

What is the correct way to go in this situation?

Tester

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
    }
}

Person Class

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;
        }
    }
}

Student Class

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();
        }
    }
}

Aucun commentaire:

Enregistrer un commentaire