开发者

Problem with final field initialization in java: avoiding duplication of code using multiple constructors

开发者 https://www.devze.com 2023-01-09 15:28 出处:网络
I have a class with some private final fields: public class ClassA { private final Object field1; private final Object field2;

I have a class with some private final fields:

public class ClassA {
  private final Object field1;
  private final Object field2;
  ...
}

The class has several different constructors, with a variety of arguments:

public ClassA(Object arg1, Object arg2);
public ClassA(int arg1, String arg2, boolean arg3);

These constructors calculate the values to put in the final fields.

Ideally, I would like to do something like this:

public ClassA(Object arg1, Object arg2) {
  ... // error check params
  Object value1 = ... // calculate value 1 based on args
  Object value2 = ... // calculate value 2 based on args

  init(value1, value2);
}

public ClassA(int arg1, String arg2, boolean arg3) {
  ... // error check params
  Object value1 = ... // calculate value 1 based on args
  Object value2 = ... // calculate value 2 based on args

  init(value1, value2);
}

private void init(Object arg1, Object arg2) {
  ... // error checks on combination of calculated arg1 and arg2 values
  ... // in reality there are more than 2 args, and this logic is fairly complex
  field1 = arg1;
  field2 = arg2;
  ... // other common initialization code, depends on field1 and field2
}

In order to reuse the assignments and common initialization code. However, since the fields are final, they can only be assigned in the constructor calls.

Can't get away from using public constructors to make objects the in this case, so any type of factory methods aren't possible. It would be nice to keep the combination error checks on the arg1, arg2 values in the same block as the assignments of them to the ClassA fields. Otherwise I would split init() into 2 functions, one pre-assignment of final fields and one post assignment of final fields, and make my constructors look like:

public ClassA(int arg1, String arg2, boolean arg3) {
  ... // error check params
  Object value1 = ... // calculate value 1 based on args
  Object va开发者_StackOverflow中文版lue2 = ... // calculate value 2 based on args

  preinit(value1, value2);  // error checks combination of values
  field1 = value1;
  field2 = value2;
  postinit();  // other common initialization code
}

Suggestions? Is there any way to avoid this, or am I stuck splitting the init() function?


First, I would try to look at builder patter, but if you insist to make it as constructor and the fields should be final, use private constructor:

public class ClassA {
    private final Object field1;
    private final Object field2;

    public ClassA(Object arg1, Object arg2) {
        this(calc1(arg1, arg2), calc2(arg1, arg2), true);
    }

    public ClassA(int arg1, String arg2, boolean arg3) {
        this(calc1(arg1, arg2, arg3), calc2(arg1, arg2, arg3), true);
    }

    private ClassA(Object arg1, Object arg2, boolean a) {
        field1 = arg1;
        field2 = arg2;
    }

    private static Object calc1(int arg1, String arg2, boolean arg3) {
        return ... // calculate value 1 based on args
    }

    private static Object calc2(int arg1, String arg2, boolean arg3) {
        return ... // calculate value 2 based on args
    }

    private static Object calc1(Object arg1, Object arg2) {
        return ... // calculate value 1 based on args
    }

    private static Object calc2(Object arg1, Object arg2) {
        return ... // calculate value 2 based on args
    }

}


Why don't you create the objects value1 and value2 in a static factory method? Then you could have distinct factory methods replacing the current constructors, and a single (private) constructor doing what is now done in init:

public static createWithObjectParams(Object arg1, Object arg2) {
  ... // error check params
  Object value1 = ... // calculate value 1 based on args
  Object value2 = ... // calculate value 2 based on args

  return new ClassA(value1, value2);
}

public static createWithPrimitiveParams(int arg1, String arg2, boolean arg3) {
  ... // error check params
  Object value1 = ... // calculate value 1 based on args
  Object value2 = ... // calculate value 2 based on args

  return new ClassA(value1, value2);
}

private ClassA(Object arg1, Object arg2) {
  ... // error checks on combination of calculated arg1 and arg2 values
  ... // in reality there are more than 2 args, and this logic is fairly complex
  field1 = arg1;
  field2 = arg2;
  ... // other common initialization code, depends on field1 and field2
}


Instead of having a separate init method, you can instead chain constructors together. So, calculate field and field2, then call a constructor that initializes them. Basically, replace init with:

private classA(Object arg1, Object arg2) {
  ... // error checks on combination of calculated arg1 and arg2 values
  ... // in reality there are more than 2 args, and this logic is fairly complex
  field1 = arg1;
  field2 = arg2;
  ... // other common initialization code, depends on field1 and field2
}

Of course, there can only be 1 constructor with 2 Objects as parameters.


You can push everything down to a 2 arg constructor, so that it is the only constructor that actually initializes the final fields and does validation.

E.g.

public ClassA(int arg1, String arg2, boolean arg3) {
   this(deriveO1(arg1, arg2, arg3));
   this(deriveO2(arg1, arg2, arg3));
}

public ClassA(Object arg1, Object arg2) {
   field1 = arg1;
   field2 = arg2;
   // do initialization checks, you can have this in a separate method, or check directly here.   
}

If You don't want the two Object version being called directly (e.g. say you want to do pre-validation before assignment) then declare a "dummy" argument. It feels a bit wrong, but it's really the only choice if you want to have public and private constructors with semantically the same arguments (they have to be syntacticly distinct.)

public ClassA(int arg1, String arg2, boolean arg3) {
   Object o1 = ...; 
   Object o2 = ...;
   this(o1, o2, false);
}

public ClassA(Object arg1, Object arg2) {
   this(arg1, arg2, false);
}

private ClassA(Object arg1, Object arg2, boolean dummy) {
   field1 = arg1;
   field2 = arg2;
   // do initialization checks, you can have this in a separate method, or check directly here.   
}


Use static factory methods and a simple private constructor, which just assigns the values to the fields, and if necessary does other initializations.


Don't know if this is an option, but you could use Dependency Injection, i.e. you create the objects you're fields referring to somewhere else and just pass them in in the constructor. This makes your class much more easy to test and much more flexible.

public class ClassA {
  private final Object field1;
  private final Object field2;

  public Class A(Object field1, Object field2) {
        this.field1 = field1;
        this.field2 = field2;
  }
}


public class AppFactory {
    private AppFactory() {}

    public static ClassA newInstance(Object arg1, Object arg2) {
        // create the fields there
        ...

        // pass them in
        return new ClassA(field1, field2);         
    }

    public static ClassA newInstance(int arg1, String arg2, boolean arg3) {
        // create fields there, in a different way

        // pass them in
        return new ClassA(field1, field2);
    }
}

That way, you could:

  • Refer to the constructor parameters by their supertypes.
  • In the factory, return subtypes of ClassA
  • Pass in mock objects (dummy objects) for the fields, in order to test the class in isolation
  • many more benefits
0

精彩评论

暂无评论...
验证码 换一张
取 消