java - 在不调用所需的初始化方法的情况下防止子类实例化的正确方法?

标签 java design-patterns inheritance overriding factory

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

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

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
 }
}

所以我决定对构造函数进行初始化。它一直有效(现在我知道这是一个非常糟糕的做法),直到我创建了一个子类,其中初始化方法使用了一些额外的参数:

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
  }
}

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

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

最佳答案

使用工厂绝对是朝着正确方向迈出的一步。我看到的问题是,当您想添加带有第三个参数的第三个类时会发生什么。现在您的 Factory 要么必须有第二个重载的 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 = new MyDataBuilder().withParameter1(param1).withParameter2(param2).create();

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

当您添加 parameter3 时,该代码是面向 future 的......如果您需要的话,您甚至可以让构建器为 parameter3 设置非空默认值。

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

public class DataFromSpecificSources {
   ...

   DataFromSpecificSources(MyDataBuilder builder){
       ...
   }

   ...

}

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

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

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

穷人的 DI 框架是自 JRE 1.6 以来可用的 /META-INF/services 契约和 ServiceLoader 类(尽管核心逻辑从 Java 开始1.2)

您的构建器的创建方法看起来有点像

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

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

关于java - 在不调用所需的初始化方法的情况下防止子类实例化的正确方法?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12427131/

相关文章:

Java 8 : Applying Stream map and filter in one go

java - Jpcap处理程序不起作用

c# - 需要一个带有用户反馈的 ASP.NET MVC 长时间运行的进程

javascript - 什么时候真正使用对象的 `.constructor`属性

c++ - 将派生类转换为基类时 dynamic_cast 失败

java - 如何重构重复的一行代码java

python - 如何调用函数以特定模式在图形窗口中运行?

java - 调整复合(设计模式)形状的大小

c# - 覆盖某些 .Net Framework 控件的绘图以更改其边框颜色?

java - 单击按钮将回收器 View 滚动到特定位置