Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I avoid creating useless pass-through constructors in child classes just to pass arguments to "super()"?

In Java, as far as I'm aware, a child class does NOT inherit a constructor that has arguments.

E.g.

public class Parent {
    public Parent(int x) {
        DoSomethingWithX(x);
    }
}

public class Child extends Parent {
    // Compile fails with "Implicit super constructor Parent() is undefined 
    // for default constructor. Must define an explicit constructor
}

The only way to fix it is to create a useless pass-through constructor in Child class:

public class Child extends Parent {
    public Child(int x) {
         super(x);
    }
}

Problem:

If I have a complex hierarchy of subclasses, with 6-10 subclasses, adding such a meaningless pass-through constructor with arguments to every single one of subclasses seems to be a bad idea!

  • The code plain out looks stupid (10 copies of same method in 10 classes)
  • Worse, as with ANY code repetition, the code becomes fragile - any change (e.g. adding 1 more parameter) needs to be made in 10 places instead of one.

Question:

Is there a way to avoid this issue for a large class hierarchy?

Note:

I'm aware of one solution (have a setter, that has to be called separate from constructor) for the parameters. But this solution has several big downsides and isn't acceptable because of them.

like image 570
DVK Avatar asked Sep 27 '16 19:09

DVK


People also ask

Can you use this () and super () both in a constructor?

“this()” and “super()” cannot be used inside the same constructor, as both cannot be executed at once (both cannot be the first statement). “this” can be passed as an argument in the method and constructor calls.

What happens if you do not explicitly activate the superclass constructor in your child class constructor?

Note: If a constructor does not explicitly invoke a superclass constructor, the Java compiler automatically inserts a call to the no-argument constructor of the superclass. If the super class does not have a no-argument constructor, you will get a compile-time error.

What happens if super () is not coded?

If we call "super()" without any superclass Actually, nothing will be displayed. Since the class named Object is the superclass of all classes in Java. If you call "super()" without any superclass, Internally, the default constructor of the Object class will be invoked (which displays nothing).

What happens if we write this () as a first statement in our constructor?

The Eclipse compiler says, Constructor call must be the first statement in a constructor . So, it is not stopping you from executing logic before the call to super() . It is just stopping you from executing logic that you can't fit into a single expression. There are similar rules for calling this() .


2 Answers

Something like 15 years ago I had a similar problem to yours with a very broad (but only slightly deep) hierarchy (and yes, folks, there was a reason it was the way it was). But since they all derived from a base that we periodically needed to add more information to, it was quickly apparent that adding parameters to the base constructor was prohibitively painful.

Not inheriting constructors by default is a good thing, but sometimes you want to inherit them. I've sometimes wanted a compile-time annotation I could add to a class to tell the compiler "auto-generate constructors for any of super's constructors I don't explicitly implement". But we don't have it, and a JSR would take years to go through if it were successful at all...

Not having that magic annotation, the solution we used was exactly the one @psabbate mentioned in his/her comment:

what if your constructors receive a Map, or a CustomClass class that contains every parameter you could possible need. That way if you need to change parameters and arguments you can only change the class you are interested about.

...but with a specific type hierarchy for the parameters classes (not Map).

For completeness, an example:

// The standard parameters needed
class StandardParams {
    private String thisArg;

    public StandardParams(String thisArg) {
        this.thisArg = thisArg;
    }

    public String getThisArg() {
        return this.thisArg;
    }
}

// The base class
class Base {
    public Base(StandardParams args) {
        System.out.println("Base: " + args.getThisArg());
    }
}

// A standard subclass
class Sub1 extends Base {
    public Sub1(StandardParams args) {
        super(args);
        System.out.println("Sub1 thisArg: " + args.getThisArg());
    }
}

To me it's not even "least bad." Having a type that represents the base information that the hierarchy needs is expressive.

If a subclass needs more information than the standard parameters (which came up for us), you have the option of a second parameters class type that you use as a second parameter (but then you have two types of constructors in the tree) or using inheritance in the parameters class hierarchy; we used the latter:

// Extended parameters (naturally you make these names meaningful)
class ExtendedParams extends StandardParams {
    private String thatArg;

    public ExtendedParams(String thisArg, String thatArg) {
        super(thisArg);
        this.thatArg = thatArg;
    }

    public String getThatArg() {
        return this.thatArg;
    }
}

// A subclass requiring extended parameter information
class Sub2 extends Base {
    public Sub2(ExtendedParams args) {
        super(args);
        System.out.println("Sub2 thisArg: " + args.getThisArg());
        System.out.println("Sub2 thatArg: " + args.getThatArg());
    }
}

In my case IIRC, we only had three parameters classes (the standard one and two subs for particular branches in the tree) across something like 30 main classes in the hierarchy.

A final note: For us, there was a core set of things we needed to provide to the parameters class when constructing it which didn't vary and there were something like eight options that had reasonable, basic defaults but you could override. To avoid an explosion of constructors in the parameter classes, we ended up doing a bit of a poor man's builder pattern on them ("poor man's" because we made the classes their own builders rather than separating it out; it just wasn't necessary to be that rigorous about it, the things that changed had inexpensive defaults and so the instance was always in a valid state even when being built). So construction for us looked something like this:

Thingy t = new Thingy(
    new ThingyParams(basic, construction, info)
    .withAnswer(42)
    .withQuestion("Life, the Universe, and Everything")
);
like image 144
2 revs Avatar answered Sep 19 '22 20:09

2 revs


Use a factory method and reflection:

public class A {
    protected A() {
        // No longer do initialization in constructor
        // initialize in init(), which is guaranteed to be called
    }

    protected A init(int i) {
        // do all initialization with i in here
        return this;
    };

    public static <T extends A> T create(Class<T> clazz, int i) {
        try {
            return (T) clazz.newInstance().init(i);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new IllegalArgumentException("not going to happen", e);
        }
    }
}

Creating the instance is then:

B b = A.create(B.class, 86);

Depending on the discipline level you want, sub-classes can either be as simple as:

public class B extends A {
    // nothing special needed
}

but allows direct instantiation B b = new B() not requiring the int, or you can tighten everything up to prevent that like this:

public class B extends A {
    protected B() {
        super();
    }
    // nothing else special needed
}

which requires all instantiation to go via the factory method.

Changing your code at the call points can be easily refactored using your IDE.

If your base class changes to require more/different initializing variables, you only have to change the factory method (nothing changes in the sub classes), or you can add a new factory method, keeping the old one for backward compatibility.

like image 22
Bohemian Avatar answered Sep 23 '22 20:09

Bohemian