【问题标题】:Right way to prevent subclass instantiation without calling a desired initialisation method?在不调用所需初始化方法的情况下防止子类实例化的正确方法?
【发布时间】:2012-09-07 18:32:56
【问题描述】:

有人可以帮助新手程序员了解他的解决方案是否正确吗?

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

What's wrong with overridable method calls in constructors?

Factory pattern in C#: How to ensure an object instance can only be created by a factory class?

问题:我想要子类,它们的不同之处仅在于它们的初始化方法。但是,我还想防止在没有初始化的情况下实例化这些类。换句话说,我想确保在子类实例化后会始终调用某些“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中的静态方法吗?
  • @JohnB 由于他对实现的选择取决于参数,我会说不,工厂类是更好的选择。

标签: java design-patterns inheritance overriding factory


【解决方案1】:

使用工厂绝对是朝着正确方向迈出的一步。我看到的问题是,当您想要添加带有第三个参数的第三个类时会发生什么。现在你的工厂要么必须有第二个重载的createSubClass 方法来获取第三个参数,要么你的所有代码都必须重写以提供第三个参数。此外,您强制使用工厂的任何人为第二个参数指定null,即使他们只需要单个参数类....当您到达采用 15 个参数的类时,您将如何记住哪个参数是哪个

解决方案是改用 Builder 模式。

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 对象...所以现在您可以将 getter 添加到 Builder 并将 Builder 作为构造函数参数传递,例如

public class DataFromSpecificSources {
   ...

   DataFromSpecificSources(MyDataBuilder builder){
       ...
   }

   ...

}

这样你现在几乎有了一个标准的构造函数签名

现在进行一些 Java 特定的改进。我们可以让builder根本不需要知道子类!

使用 DI 框架,我们可以将实现 Data 接口/抽象类的类注入到 Builder 中,然后遍历每个类,直到找到支持 Builder 实例配置的类。

穷人的 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");
}

你是否想走到那个极端值得怀疑......但由于你在查看其他人的代码时可能会在某个时间点遇到它,现在可能是向你指出它的好时机。

哦,我们之所以要添加Factory类供ServiceLoader查找是因为ServiceLoader期望调用默认构造函数,而我们隐藏了默认构造函数,所以我们使用Factory类来完成工作我们并允许我们隐藏构造函数。

没有什么可以阻止工厂类成为数据类中的静态内部类(这使它们对正在创建的类具有很好的可见性),例如

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

这种类型的解决方案最好的一点是它允许通过在运行时将这些实现添加到类路径来添加额外的实现......即非常松散的耦合

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-12-20
    • 2015-08-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多