user12530264
user12530264

Reputation:

Is there a way to refactor these constructors?

So I have an object, lets call it myObject

Here are the constructors to my object

private static class myObject {
        public myObject(int argA) {
            this.argA = argA;
        }

        public myObject(int argA, boolean argB) {
            this.argA = argA;
            this.argB = argB;
        }

        public myObject(int argA, int argC, int argD) {
            this.argA = argA;
            this.argC = argC;
            this.argD = argD;
        }

        public myObject(int argA, String argE) {
            this.argA = argA;
            this.argE = argE;
        }


        public int argA = 1;
        public boolean argB;
        public int argC = 4;
        public int argD = 5;
        public String argE;

Basically I have default values and the constructor overrides these default values when required. This makes it very clean in the code when I call these constructors I can just

myObject newObject = new myObject(4);

However, an API is giving me a list of arguments to create this object with

List objectParams1 = Arrays.asList(1,3,4)
List objectParams2 = Arrays.asList(1,false)
List objectParams3 = Arrays.asList(1,"tomato")
myObject newObjectWithTheseParameters1 = ?;
myObject newObjectWithTheseParameters2 = ?;
myObject newObjectWithTheseParameters3 = ?;

Creating this object with a list of params is very difficult as it does not know which constructor to use. Is the builder method the way to go with this? However this will make code base much larger as I have to call this constructor ~100 times..

myObject objectA = myObject.builder().withargA(4).withArgB(true).build();

Upvotes: 1

Views: 221

Answers (4)

Andy Turner
Andy Turner

Reputation: 140544

You only have four cases, so it's quite easy to write a static factory method:

static myObject create(List<?> args) {
  int argA = (int) args.get(0);
  switch (args.size()) {
    case 1:
      return new myObject(argA);
    case 2:
      if (args.get(1) instanceof Boolean) {
        return new myObject(argA, (boolean) args.get(1)) 
      }
      return new myObject(argA, (String) args.get(1));
    case 3:
        return new myObject(argA, (int) args.get(1), (int) args.get(2));
    default:
      throw new IllegalArgumentException();
  }
}

Then:

myObject newObjectWithTheseParameters1 = create(objectParams1);
// etc.

This is pretty gross (it can fail in all sorts of ways at runtime, if the list has the wrong number of elements, or elements of the wrong type, or the boxed primitive elements are null), but I don't really see what other choice you have if the parameters come from a List.


An alternative without doing the explicit checking would be to use reflection to obtain a constructor:

Class<?>[] classes =
    args.stream()
        .map(Object::getClass)
        .map(YourClass::unboxedClass)
        .toArray(Class<?>[]::new);

where unboxedClass is a method which translates Integer.class and Boolean.class into int.class and boolean.class. Then:

return myObject.getClass().getConstructor(classes).newInstance(args);

(and handle all the checked exceptions).

Upvotes: 2

user
user

Reputation: 7604

You can use the builder pattern here if you wish. This may look ugly and boilerplate-y, but it means you don't need a separate constructor for each case, and it will allow you to chain your builder, since each instance method in Builder returns this. It also doesn't look too bad if you keep the method names short.

You can use it like this (notice that you can leave out c or any other field because the defaults are set in the Builder class):

MyObject object = 
  new Builder()
    .a(4)
    .b(true)
    .d(0)
    .e("56")
    .build();

The modified MyObject class:

class MyObject {
  public int a;
  public boolean b;
  public int c;
  public int d;
  public String e;

  public MyObject(int a, boolean b, int c, int d, String e) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
    this.e = e;
  }
}

The Builder class

class Builder {
  public int a = 1;
  public boolean b;
  public int c = 4;
  public int d = 5;
  public String e;

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

  public Builder b(boolean b) {
    this.b = b;
    return this;
  }

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

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

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

  public MyObject build() {
    return new MyObject(a, b, c, d, e);
  }
}

Another way to implement the builder, with a Map and casting afterwards. It's typesafe, but the class above with fields is probably better because it doesn't involve unnecessary boxing and unboxing with primitives.

class Builder {

  private Map<String, Object> map = new HashMap<>();

  {
    map.put("a", 1);
    map.put("b", false);
    map.put("c", 4);
    map.put("d", 5);
    map.put("e", null);
  }

  public Builder a(int a) {
    map.put("a", a);
    return this;
  }

  public Builder b(boolean b) {
    map.put("b", b);
    return this;
  }

  public Builder c(int c) {
    map.put("c", c);
    return this;
  }

  public Builder d(int d) {
    map.put("d", d);
    return this;
  }

  public Builder e(String e) {
    map.put("e", e);
    return this;
  }

  public MyObject build() {
    return new MyObject(
      (Integer) map.get("a"), 
      (Boolean) map.get("b"), 
      (Integer) map.get("c"), 
      (Integer) map.get("d"), 
      (String) map.get("e"));
  }
}

Upvotes: 0

Rob Evans
Rob Evans

Reputation: 2874

Not claiming this is the correct way but you could set all values in a main constructor, and then call the main constructor from constructors defined with other signatures using the this keyword:

The only thing to really notice here is that I've set some "default" values where no value is provided in the varying constructors.

private static class myObject {

    public int argA = 1;
    public boolean argB;
    public int argC = 4;
    public int argD = 5;
    public String argE;

    public myObject(int argA, boolean argB, int argC, int argD, String argE) {
        this.argA = argA;
        this.argB = argB;
        this.argC = argC;
        this.argD = argD;
        this.argE = argE;
    }

    public myObject(int argA) {
        this(argA, false, 0, 0, null);
    }

    public myObject(int argA, boolean argB) {
        this(argA, argB, 0, 0, null);
    }

    public myObject(int argA, int argC, int argD) {
        this(argA, false, argC,  argD, null);
    }

    public myObject(int argA, String argE) {
        this(argA, false, 0, 0, argE);
    }
}

If you're going to add a lot of these types of constructors, you may end up with signature clashes which won't work. Builder is good when you need to specify a bunch of optional parameters that vary + some that are mandatory. Relatively simple to implement as every method returns itself (this), and you just update the fields as necessary, then finally call .build() to create the object.

Upvotes: 2

cegredev
cegredev

Reputation: 1579

Create a constructor that takes all arguments (make it private if you want) and call that one from all others. The arguments you don't have will be set to their default values:

private static class MyObject {

    private boolean b;
    private int a, c, d;
    private String e;

    private MyObject(int a, boolean b, int c, int d, String e) {
        this.a = a;
        this.b = b;
        this.c = c;
        this.d = d;
        this.e = e;
    }

    public MyObject(int a, int c, int d) {
        this(a, false, c, d, null);
    }

    public MyObject(int a, boolean b) {
        this(a, b, 3, 4, null);
    }

    public MyObject(int a, String e) {
        this(a, false, 3, 4, e);
    }

}

There are however a few downsides to this:

  1. It harms readability and can confuse you.
  2. If you want to change the default values for certain arguments, you'll have to remember to change them in every constructor. You can work around this by storing the defaults in static final variables, but still not ideal.

You should also consider naming your variables differently, as a, b, c, d and e or argA, argB, argC, argD, argE don't really convey much information.

Upvotes: 0

Related Questions