如何正确防止在未调用所需初始化方法的情况下实例化子类?

3

有谁能帮助一位初学者程序员理解他的解决方案是否正确?

我的问题类似于以下两个问题:

在构造函数中调用可重写方法有什么问题?

C#工厂模式:如何确保只能通过工厂类创建对象实例?

问题:我想要有子类,这些子类仅在其初始化方法中有所不同。但是,我还想防止在没有初始化的情况下实例化这些类。换句话说,我想确保在子类实例化后总是会调用某些“initialize()”方法:

public abstract class Data {

 protected Parameter dataSource;     

  Data(parameter1){
     this.dataSource = parameter1;

  loadData(); // should be called to initialise class fields and ensure correct work of other class methods
  }

 protected abstract loadData(){
     ... //uses dataSource
 }
}

所以我决定在构造函数中执行初始化。这行得通(现在我知道这是一种非常不好的做法),直到我创建了一个子类,其中initialize方法使用了一些额外的参数:

public class DataFromSpecificSources extends Data {

 private Parameter dataSource2;

 public DataFromSpecificSources(parameter1, parameter2){
    this.dataSource2 = parameter2; // I can't put it here because the constructor is not called yet
    super(parameter1); // this, of course, will not work
  }

 @Override
 private void loadData(){
   ... // uses both dataSource 1 and 2
       // or just dataSource2
  }
}

当然,这样做是行不通的。于是我开始寻找正确的模式……在阅读之前发布的问题答案后,我决定使用工厂并将子类构造函数的可见性限制为包内: 我的解决方案:
// factory ensures that loadData() method will be called
public class MyDataFactory(){

 public Data createSubClass(parameter1,parameter2){
  Data subClass;

  if (parameter2 != null){
   subClass = new DataFromSpecificSources(parameter1, parameter2);
   subClass.loadData();
  } else {
   subClass = new AnotherSubClass(parameter1);
   subClass.loadData()
  }

  return subClass;
 }

}


 public abstract class Data {

 protected Parameter dataSource;     

  Data(parameter1){
     this.dataSource = parameter1;
  }

 // I don't call it in constructor anymore - instead it's controlled within the factory
 protected abstract loadData(){
     ... //uses dataSource
 }
}



public class DataFromSpecificSources {

 private Parameter dataSource2;

 protected DataFromSpecificSources(){}

 // now this constructor is only visible within package (only for the factory in the same package)
 DataFromSpecificSources(parameter1, parameter2){
    super(parameter1); // it does not initialise data anymore

    this.dataSource2 = parameter2;
  }

  @Override
  protected void loadData(){
   ... // uses dataSources 1 and 2
  }
}

现在工厂确保了子类的初始化(数据将被加载),并且不允许在其他包中实例化子类。其他类无法访问子类的构造函数,被强制使用工厂获取子类的实例。

我想问一下我的解决方案是否正确(逻辑上),并且用子类构造函数可视性限于包的工厂方法是正确的选择?还是有其他更有效的模式来解决这个问题?!


优雅的解决方案。但是工厂需要成为自己的类吗?工厂方法可以成为Data中的静态方法吗? - John B
@JohnB 因为他的实现选择取决于一个参数,所以我认为不是很好,工厂类是更好的选择。 - Brian
1个回答

3
使用工厂模式确实是朝着正确方向迈出的一步。然而,当您想要添加一个需要第三个参数的第三个类时会出现问题。现在您的工厂必须要么有第二个重载的createSubClass方法来接受第三个参数,要不然所有的代码都需要重新编写以提供第三个参数。此外,您强制任何使用工厂的人即使只需要单个参数类也必须为第二个参数指定null......当您到达需要15个参数的类时,您将如何记住每个参数的含义。
解决这个问题的方法是使用建造者模式。
public class MyDataBuilder(){
    private parameter1 = null;
    private parameter2 = null;

    public MyDataBuilder withParameter1(parameter1) {
        this.parameter1 = parameter1;
        return this;
    }

    public MyDataBuilder withParameter2(parameter2) {
        this.parameter2 = parameter2;
        return this;
    }

    public Data createSubClass(){
        Data subClass;

        if (parameter2 != null){
            subClass = new DataFromSpecificSources(parameter1, parameter2);
        } else {
            subClass = new AnotherSubClass(parameter1);
        }
        subClass.loadData();
        return subClass;
    }

}

现在创建Data实例的代码可以像这样工作:
Data data = new MyDataBuilder().withParameter1(param1).withParameter2(param2).create();

或者

Data data = new MyDataBuilder().withParameter1(param1).create();

而且当您添加参数3时,该代码是具有未来性的...... 如果需要,甚至可以使用非空默认值为参数3构建生成器。

接下来您会注意到,现在您拥有一个包含所有必需参数的很好的Builder对象...... 因此,现在您可以向Builder添加getter,并将Builder作为构造函数参数传递,例如:

public class DataFromSpecificSources {
   ...

   DataFromSpecificSources(MyDataBuilder builder){
       ...
   }

   ...

}

现在几乎有了标准构造函数签名。

现在来谈一些Java特定的改进。我们可以使构建器完全不需要了解子类!

使用DI框架,我们可以将实现Data接口/抽象类的类注入到构建器中,然后只需迭代每个类,直到找到支持构建器实例配置的类为止。

穷人版的DI框架是自JRE 1.6以来可用的/META-INF/services合同和ServiceLoader类(尽管核心逻辑自1.2以来就存在于Java中)。

您的构建器的创建方法将看起来像这样:

public Data create() {
    for (DataFactory factory: ServiceLoader.load(DataFactory.class)) {
        if (factory.canCreate(this)) {
           Data result = factory.newInstance(this);
           result.loadData();
           return result;
        }
    }
    throw new IllegalStateException("not even the default instance supports this config");
}

你是否真的需要这样做还有待商榷……但是,由于在查看其他人的代码时可能会遇到这种情况,所以现在向你指出这一点可能是一个好时机。

哦,我们必须添加一个工厂类供ServiceLoader查找,原因是ServiceLoader希望调用默认构造函数,而我们隐藏了默认构造函数,所以我们使用工厂类来完成工作并允许我们保持构造函数的隐藏。

没有什么阻止工厂类成为Data类的静态内部类(这使它们在创建的类上具有很高的可见性),例如:

public class UberData extends Data {
    private UberData(MyDataBuilder config) {
        ...
    }

    public static class Factory extends DataFactory {
        protected Data create(MyDataBuilder config) {
            return new UberData(config); 
        }
        protected boolean canCreate(MyDataBuilder config) {
            return config.hasFlanges() and config.getWidgetCount() < 7;
        }
    }
}

那么我们可以在 META-INF/services/com.mypackage.DataFactory 中列出如下内容:

com.mypackage.UberData.Factory
com.mypackage.DataFromSpecificSources.Factory
com.some.otherpackage.AnotherSubClass.Factory

这种解决方案的最大优点在于,只需将这些实现添加到运行时的类路径中即可添加其他实现... 即非常松散的耦合。

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