0

As a homework project I have been asked to create a bunch of classes in a hierarchy, which take different parameters in their constructors, and allow a user to interactively create objects of such classes. For simplicity, let's assume that the classes are Base and Child, with Child extending Base. Any future class in the project would extend Base or one of its subclasses.

Although the focus of the project is not writing elegant or maintainable code, I am trying write such code.

Ideally, after figuring out what object the user wants to instantiate, I would have a way of figuring out which parameters are necessary to create such an object and pass that list to the method in charge of retrieving them from the user. I had thought of a static method, getNecessaryArguments() such as:

public class Base {
    public static List<String> getNecessaryArguments() {
         return Arrays.asList("name", "age");
    }

    public Base(String name, String age) {
        ...
    }
}

public class Child extends Base {
    public static List<String> getNecessaryArguments() {
         final List<String> baseArgs = Base.getNecessaryArguments();
         baseArgs.addAll(Arrays.asList("position"));
    }

    public Child(final String name, final String age, final String position) {
        super(name, age);
        ...
    }

I would then do something like UserInterface.requestParameters(XXX.getNecessaryArguments()).

There are several obvious issues with this code, though:

  1. Two lists of arguments need to be maintained in parallel for each class: the actual arguments to the constructor and arguments returned by getNecessaryArguments().
  2. The class which Child extends has to be explicitly mentioned in its getNecessaryArguments(), since super() cannot be used in static contexts.

This all opens up the possibility of out-of-sync issues if the design changes in the future.

What would be the best way of handling these issues so that the code can adapt to some design changes? I thought of reflection, but I think you cannot retrieve argument names from it, only the method signature.

Cœur
  • 37,241
  • 25
  • 195
  • 267
user2891462
  • 3,033
  • 2
  • 32
  • 60
  • The correct keyword to search for is "reflection". However you should reduce its use to a minimum because reflection is slow and allows lots of errors that the compiler cannot check. Also passing user input directly to classes can be dangerous in networking environments. See this tutorial: https://www.oracle.com/technical-resources/articles/java/javareflection.html – Stefan Feb 23 '20 at 12:21
  • @Stefan Thanks for the link. I know very little about reflection, but I believe it only allows you to inspect the signature of a method, so I would not be able to know what to ask the user ("Name", "Age" would need to become something counterintuitive like "Arg0, Arg1"...): https://stackoverflow.com/questions/2237803/can-i-obtain-method-parameter-name-using-java-reflection (My bad, I realise it's now possible under certain circumstances using Java 8). – user2891462 Feb 23 '20 at 12:27
  • Instead of constructor arguments you yould provide some setter methods, then the method names can be used as a label. E.g. ```setName()``` -> "Enter Name". – Stefan Feb 23 '20 at 12:43
  • @Stefan Thought about that, but I would rather avoid mutability. – user2891462 Feb 23 '20 at 12:46
  • I just found out that Java 8 can list the parameter names, see https://www.baeldung.com/java-parameter-reflection – Stefan Feb 23 '20 at 12:47
  • 1
    You have to call `Base.getNecessaryArguments()` explicitly, because it is also a deliberate decision to have exactly the same arguments as the super class constructor at the beginning of the constructor list. There is no actual requirement for this. – Holger Feb 28 '20 at 10:03

3 Answers3

2

You may replace the List by a Map. Then, it can hold both, the parameter names and the actual values. It can be used to copy the current properties of an object, to edit one or more of them, and pass the it to the constructor of a new object.

When the objects have methods to query their current state, you can replace the static methods by a static final template objects, holding the default values of the properties which can be queried via said method.

public class Base {
    static final Base TEMPLATE = new Base();

    final String name, age;
    protected Base() {
        name = "no name";
        age = null;
    }
    public Base(String name, String age) {
        this.name = name;
        this.age = age;
    }
    public Base(Map<String,String> map) {
        name = map.get("name");
        age = map.get("age");
    }
    public Map<String,String> arguments() {
        HashMap<String,String> map = new HashMap<>();
        map.put("name", name);
        map.put("age", age);
        return map;
    }
}
public class Child extends Base {
    static final Child TEMPLATE = new Child();

    final String position;
    protected Child() {
        position = "unspecified";
    }
    public Child(final String name, final String age, final String position) {
        super(name, age);
        this.position = position;
    }
    public Child(Map<String, String> map) {
        super(map);
        this.position = map.get("position");
    }

    @Override
    public Map<String, String> arguments() {
        final Map<String, String> map = super.arguments();
        map.put("position", position);
        return map;
    }
}

You can use it like

Map<String, String> values = Child.TEMPLATE.arguments();
UI.editValues(values);
Child ch = new Child(values);

This leads to the Builder pattern, as there are reasons to replace the generic Map with dedicated classes. These classes can define properties with different types and also enforce constraints. Bonus points if the class can report the constraints via an interface.

But it requires a specialization of the builder type for every actual type to build. There a lots of different variations of the pattern. One directly derived from the Map usage would be:

public class Base {
    public static class Builder {
        String name = "no name";
        int age;
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = Objects.requireNonNull(name);
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            if(age < 0) throw new IllegalArgumentException();
            this.age = age;
        }
    }

    final String name;
    final int age;
    public Base(String name, int age) {
        if(age < 0) throw new IllegalArgumentException();
        this.name = Objects.requireNonNull(name);
        this.age = age;
    }
    public Base(Builder b) {
        name = b.name;
        age = b.age;
    }
    public void fill(Builder b) {
        b.setName(name);
        b.setAge(age);
    }
}
public class Child extends Base {
    public static class Builder extends Base.Builder {
        String position = "unspecified";
        public String getPosition() {
            return position;
        }
        public void setPosition(String position) {
            this.position = Objects.requireNonNull(position);
        }
    }

    final String position;
    public Child(final String name, final int age, final String position) {
        super(name, age);
        this.position = Objects.requireNonNull(position);
    }
    public Child(Builder b) {
        super(b);
        this.position = b.position;
    }
    public void fill(Builder b) {
        super.fill(b);
        b.setPosition(position);
    }
}

This can be used like

Child.Builder b = new Child.Builder();
UI.editValues(b);
Child c = new Child(b);

Note that these builders follow the Bean pattern regarding properties, which is quiet useful for generic user interfaces. Of course, this implies that the UI processes mutable objects, but that would apply to List and Map variants as well. The key point is that the state of the builder will be used to construct immutable objects at the right times, e.g. before passing them to subsequent processing code.

Holger
  • 285,553
  • 42
  • 434
  • 765
0

Java Beans, with ConstructorProperties would have been my choice, might be interesting to see some ideas. For instance to have parallel to a bean class Foo, have a describing class FooBean.

However the homework is about you own ideas worked out in code. Instead of something complex try to find something without fluff.

One can compile conserving the parameter names, as commented. This would be a special case: where the developers must change their compiler options.

On alternatively could make a run-time scoped annotation for every parameter, so one can via reflection receive that annotation which value is the parameter name. That would be the unproblematical solution.

You use a static getter for a piece of class based information. This is a bit hard to handle, no inheritance for instance.

Alternatively you could create a class for that constructor describing information BeanClassInfo, and have in a single class BeanRegistry:

public class BeanRegistry {

    private Map<Class<? extends Base>, BeanClassInfo> classInfos = new HashMap<>();

    public void registerClass(Class<? extends Base> type, BeanClassInfo classInfo) {
        classInfos.put(type, BeanClassInfo);
    }
    ...
}

public class BeanClassInfo {
    List<String> argNames = ...
}

public class Base {
    protected static BeanRegistry registry = new BeanRegistry();
}

public class Child extends Base {
    static {
        BeanClassInfo classInfo = new BeanClassInfo();
        ...
        registry.registerClass(Child.class, classInfo);
    }

static { ... } is a static initializer.

As you see you have one single Base.beanRegistry object, easily found. The child classes must do something to "register" themselves.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
0

In the comments you wrote:

I would rather avoid mutability

For this case you could create a Builder for each immutable class. For instance a String is immutable but the StringBuilder that creates a String is mutable.

In addition, the variable information can be saved in the Builder, since the Builder must know all the fields that a class needs for its construction.

class Child extends Base {

  private final String position;

  public Child(final String name, final String age, final String position) {
    super(name, age);
    this.position = position;
  }

  /* ... */
}

class ChildBuilder {

  private String name;
  private String age;
  private String position;

  public List<String> parameters() {
    return Arrays.asList("name", "age", "position");
  }

  public ChildBuilder withName(String name) {
    this.name = name;
    return this;
  }

  public ChildBuilder withAge(String age) {
    this.age = age;
    return this;
  }

  public ChildBuilder withPosition(String position) {
    this.position = position;
    return this;
  }

  public Child build() {
    return new Child(name, age, position);
  }
}
Roman
  • 4,922
  • 3
  • 22
  • 31