Java - 强制子类在构造函数后调用super方法

7
我希望一堆子子类在构造函数结束后调用超级方法,就像这样:

public abstract class Superclass {

    ...

    public Superclass(...) {
        ...    // do stuff before initializing subclass
    }

    protected void dispatch() {     //method to be called directly after creating an object
        doStuff();
        ...
    }

    public abstract void doStuff();
}

public class Subclass extends Superclass {

    ...

    public Subclass(...) {
        super(...);     //has to be the first line
        ...             //assign variables etc.
        dispatch();     //has to be called after variables are assigned etc.
    }

    public void doStuff() {
        //do stuff with assigned variables etc.
    }
}
dispatch() 函数包含一系列在对象创建后必须应用于所有子类的操作。但我无法将此函数移动到超级构造函数中,因为它调用了来自需要已分配变量的子类的方法。但由于 super() 要求是子类构造函数的第一行,所以在调用超级构造函数之前我不能设置任何变量。
目前这种方式可以实现,但我认为在每个子类构造函数的结尾调用 dispatch() 不是一个好的设计概念。是否有更优雅的解决方案?或者我是否应该重新思考我的设计概念呢?

你不能这样做,而且这是一个糟糕的主意。如果你再用 SubSubClass 对 Subclass 进行子类化呢?那么在 SubSubClass 的构造函数被调用之前调用 dispatch,如果 SubSubClass 重写了 doStuff,那么该方法将在类初始化之前被调用。 - Erwin Bolwidt
5个回答

6
你的请求违反了几项Java最佳实践,例如:
  • 不要在构造函数中进行复杂的配置,只填充私有(final)成员变量,并且仅执行非常基本的一致性检查(如果有的话)。

  • 不要从构造函数中调用non privatenon final方法,甚至间接地也不要调用。

因此,我强烈建议重新考虑你的类设计。很可能你的类太大,承担了太多职责。


3
它现在可以工作,但我认为在每个子类构造函数的末尾调用dispatch()是一个不好的概念。有更优雅的解决方法吗?或者我应该完全重新思考我的概念吗?
正如Timothy Truckle所强调的那样,你的构造函数逻辑太复杂了。通过使用模板方法来初始化子类实例,您可以使事情变得简单得多并达到您的目标。请注意,您已经在doStuff()中使用了这种模式。 子类构造函数确实是您的问题:您想减少每个子类中必需的样板代码,并使其可读性和维护性更好。因此,在超类中引入一个新的模板方法,并从超类的构造函数中调用它。这个方法将做与构造函数相同的事情,但可以以更灵活的方式调用。仅出于技巧而引入的人工方法dispatch()也不是必需的。整个逻辑可以从超类构造函数中进行编排。
超类可能如下所示:
public abstract class Superclass {

    ...

    public Superclass(...) {
        ...    // do stuff before initializing subclass
        init();
        doStuff();
    }

    public abstract void init();

    public abstract void doStuff();
}

在子类中,将“:”替换为:
public Subclass(...) {
    super(...);     //has to be the first line
    ...             //assign variables etc.
    dispatch();     //has to be called after variables are assigned etc.
}

通过:

public Subclass(...) {
    super(...);   // let the super constructor to orchestrate the init logic  
}

public void init(){
    // move the constructor logic here
}

结果更加简单,因为这个设计将子类初始化“算法”相关的职责集中在一个地方:超类构造函数。
关于您的评论:

这确实比我做的要优雅得多。谢谢!编辑: 刚刚注意到,这不能用于具有不同构造函数参数的子类。有什么解决办法吗?

对于这样的要求,为了使事情简单明了,你需要分两步来完成:
  • 实例化对象
  • 在引用上调用init()方法。
它看起来可能像这样:
SuperClass o = new Subclass(argFoo, argBar); 
o.init();

这种方式的问题是你不能确定init()方法是否被调用。你可以添加一个标志,在每次对象上的方法被调用时检查该标志。但这样做非常麻烦且容易出错,建议避免。
为了改进这个问题,我可能会使用包装器模式。
你也可以使用拦截器/切面,但这不是一个好的使用案例:初始化处理不是横跨多个对象的,而是与对象行为密切相关的。保持它可见更有意义。
使用包装器,可能像这样:
SuperClass o = new MyWrapper(new Subclass(argFoo, argBar));

在这里,MyWrapperSuperClass的子类,它包装了一个SuperClass对象的实例:

public class MyWrapper implements SuperClass{

   private SuperClass wrapped;

   public MyWrapper (SuperClass wrapped){
       this.wrapped = wrapped;
       this.wrapped.init();
   }

   // then delegate each superclass method to the wrapped object
   public void doStuff(){
       this.wrapped.doStuff();
   }

  // and so for...

}

这看起来比我做的更加优雅。谢谢! 编辑:刚注意到,这在子类具有不同构造函数参数时无法工作。有什么解决办法吗? - StrikeAgainst
1
但是这违反了一个事实,即您不应该从超类构造函数中调用可重写方法,对吗? - Thiyagu
@user7,没错:通常不建议这样做,但是在这里,init()方法替代了子类的构造函数。因此,您不必担心在子类实例上调用方法会导致状态不一致,因为它没有调用其构造函数。 - davidxxx

0

您可以通过提供一个静态方法来抽象化使用您的超类,该方法将执行您想要的代码,但还会检查其是否已正确设置:

public abstract class SuperClass{
    private boolean instantiated;

    public SuperClass(...){
        ...
    }

    public abstract void doStuff();

    private void dispatch(){
        if(!instantiated){
            instantiated = true;
            doStuff();
        }
    }

    public static void executeActionOnSuperClass(SuperClass s){
        s.dispatch(); // call instantiation if not already done
        s.executeAnAction();
    }
}

还有子类:

public class SubClass extends SuperClass{
    public SubClass(...){
        super(...);
    }

    public void doStuff(){
         ...
    }
}

然后可以像这样执行:

SuperClass.executeAnActionOnSuperClass(new SubClass(...));

虽然这通常是一种反模式,应该尽量少使用。


0

如果在调用方法时需要任何子类实例化完成,则Lorelorelore是正确的。否则,您可以使用您所拥有的。我建议如果其他人需要使用代码,可以添加足够的注释。


0
我遇到了同样的问题,工厂强制执行了额外的初始化逻辑。
internal class ExpenseFactory
{
    internal static Expense CreateExpense(ExpenseArguments args)
    {
        Expense? expense = null;
        switch (args.ExpenseType)
        {
            case ExpenseType.EQUAL:
                expense = new EqualExpense(Guid.NewGuid().ToString(), args.Amount, args.PaidBy, args.SharedBy);
                break;
            case ExpenseType.EXACT:
                expense = new ExactExpense(Guid.NewGuid().ToString(), args.Amount, args.PaidBy, args.SharedBy, args.ExactAmounts);
                break;
            case ExpenseType.PERCENT:
                expense = new PercentExpense(Guid.NewGuid().ToString(), args.Amount, args.PaidBy, args.SharedBy, args.Percentages);
                break;
            default:
                throw new ArgumentException("Ivalid expense arguments");
        }

        expense.InitSplits(); // additional init logic implemented by each subclass and declared as abstract in parent class Expense 
        return expense;
    }
}

我认为工厂方法是一个适合复杂初始化逻辑的地方。完整的代码示例可以在这里找到。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接