不要在构造方法中调用可覆写的方法

如题,这句话来自于《Java解惑》(《Effective java》同一作者)。

在步入正题前我们先来看看kotlin的两个特性:

  • kotlin没有引入受检查的异常,设计这个特性一般认为是思考了Bruce Eckel的《Java是否需要受检的异常?》,以及Anders Hejlsberg的观点。
  • kotlin创建的类默认不可继承,设计这个特性一般认为是思考了《Effective java》:要么为了继承而设计,要么禁止继承。

我在想kotlin能从语言级别就设计支持它认为是对的观点,我为什么不能设计一种机制支持我认为是对的观点。下面来分析标题:不要在构造方法中调用可覆写的方法

  • 可覆写的方法,即指主题是可继承的基类,如果不可继承,谈不上可覆写;
  • 可覆写的方法,即指非static,非private的方法;
  • 直接调用或间接调用都不行;

那这么做有什么问题呢?问题是不要指望定义一个不可变字段并在你调用的那个可覆写的方法里“搭船”
首先,如果一个字段能定义为不可变字段,那就应该这么做,这样更有助于减少阅读代码和调试代码的工作量,也减少了多线程中状态。
其次,一个放在构造方法中的可覆写方法多半是需要强制保证执行的(设计模式中的)模板方法,是应该遵守以统一行为的。
换句话说,一旦你在构造方法中调用了可覆写的方法将限制你子类的能力。

书中举得例子如下:

class Point {
    protected final int x, y;
    private final String name; // Cached at construction time
    Point(int x, int y) {
        this.x = x;
        this.y = y;
        name = makeName(); // 3. Invoke subclass method
    }
    protected String makeName() {
        return "[" + x + "," + y + "]";
    }
    public final String toString() {
        return name;
    }
}

public class ColorPoint extends Point {
    private final String color;
    ColorPoint(int x, int y, String color) {
        super(x, y); // 2. Chain to Point constructor
        this.color = color; // 5. Initialize blank final-Too late
    }
    protected String makeName() {
        // 4. Executes before subclass constructor body!
        return super.makeName() + ":" + color;
    }
    public static void main(String[] args) {
        // 1. Invoke subclass constructor
        System.out.println(new ColorPoint(4, 2, "purple"));
    }
} 

此示例打印 [4,2]:null

解决方案:
书中给出的建议是惰性初始化,即在真正使用的时候初始化,把可覆写的方法踢出去。这不失为一种好的方案。
但是有的时候我们希望规定一套规则以统一行为,比如下面这样:

public ViewA() {
    initComponents();
    initDefaults();
    initListeners();
    initKeyboardActions();
}

再或者:

public ViewB() {
    initInflater();
    findViewByIds();
    setListeners();
    loadCustomTheme();
    fillDataCached();
}

这样的情况都是初始化的情况。我想到有两种思路。
第一种是使用Builder模式,将模板方法各个阶段都包装起来,但是要想避免绕过Builder的话,一般需把构造器设置成私有比较好,所以pass。
第二种是定一个Resource接口,里面定义一个initialize方法,让基类实现这个接口,将构造方法中的这些模板方法放到initialize方法里,然后保证创建对象实例的时候,调用initialize方法,以保证初始化完整。
对于第二种情况,如何保证创建对象实例的时候一定调用init方法呢?
一开始我想的是提供工厂方法,工厂方法里完成new对象并调用init的动作,但是问题来了,也许使用者绕过工厂直接创建对象呢?因为构造器是非私有的,于是我又在构造器上加了文档,但是,你放心吗?我是不放心。所以我又写了一个idea插件来做强制检查,你可以从这里看到我的努力:(加载的有点慢)
https://johnlee175.github.io/json2pojo/

后来我想,我都用了idea插件了,那么用接口有点侵入,所以又提供了@ManualInit和@InitMethod的注解,将基类和初始化方法标注起来,我用idea插件帮你检查对象实例化时是否调用了初始化方法,并结合工厂方法帮你提炼代码。

再后来遇到了一个坎,对于java来说这种没问题,对于android中自定义的基view来说有问题。因为android的view实例化是LayoutInflater经过反射帮你做的,我作为idea的插件无法获取到new表达式那个节点,这就使的是否调用以及何时调用初始化方法变得不可控。

android中常见的把控初始化的思路,比如mPrivateFlags,在未初始化前某个标志位未置位,初始化后检查这个标志位是否置位,如果没有就抛出异常。类似的做法可以参看Activity的onCreate如何保证super.onCreate被调用(在ActivityThread.java中),而且android也提供了@CallSuper注解。

可是这些思路有可能无法用在此处,首先你可能希望在运行期以前确认问题的存在(运行期能否及时发现问题取决于运气,有可能半个月以后那个地方被调到),其次是你在什么地方进行这个检查,可能时机和你想的不吻合。

我们怎么能解决这个问题呢?有个比较好的办法。见下面:

class A {
    public A() {
        // init A fields here
        // at last
        if (this.getClass().equals(A.class)) {
            initialize();
        }
    }
}
class B extends A {
    public B() {
        super();
        // init B fields here
        // at last
        if (this.getClass().equals(B.class)) {
            initialize();
        }
    }
}

只要继承体系的每个非this()系的构造方法都在尾部添加一个class比较,并依据比较结果来调用初始化方法即可,然后在初始化方法中做模板方法的规定和分发。
这样的当创建B的时候,调用顺序一定是A的构造->B的构造->B的initalize方法,这样的顺序执行。如果B覆写了initalize,为了保持顺序一致性,需要调用super.initialize(可以使用@CallSuper注解)。

但是这一行比较代码如何添加进去呢?gradle-plugin中通过transform api进行注入代码的方式无疑是最理想的,它能保证强制执行,不会造成遗忘,而又对使用者透明,不会造成反感,但是这样也会拖慢编译打包时间,尤其项目大了,推动工作比较困难。

另一种方法是使用idea插件做inspection和intention,如果开发者没写那个比较则inspection报错,intention也可以一键生成那一行代码。美中不足的是,这个很容易被破坏,比如我故意压制报错,去掉那行代码等。不过目前看这个推动起来相对gradle-plugin容易一些。
目前只能想想这方面的事,我只能说要建一套机制,保证好的开发实践方式减少不必要的限制。

猜你喜欢

转载自blog.csdn.net/kslinabc/article/details/81153589