【问题标题】:Pattern to initialize base class in derived class constructor (or factory)在派生类构造函数(或工厂)中初始化基类的模式
【发布时间】:2011-07-21 23:03:20
【问题描述】:

假设您有一个派生类,其中基类是您无法修改的。基类有很多状态(很多非常量的私有成员)和很多构造函数,使用不同数量的参数来初始化状态的某个子集(当然,子集的大小因构造函数而异)。

现在我的派生类是基类的一个非常轻量级的包装器。假设它没有添加自己的状态,只是稍微修改了几个方法的行为(可能围绕super.originalMethod() 调用做一些额外的日志记录)。

我遇到的问题是我想获取基类的一个对象,并创建它的“副本”,具有相同的状态,但作为派生类的一个实例。

事实证明这很困难。我不能调用基类的“最完整”构造函数,通过调用 getter 从源传递所有状态,因为根据基类的构造方式,某些状态值可能会被此构造函数拒绝。例如,您可以使用 0-arg ctor 创建一个默认对象,任何许多值都将为空。但是,在允许您指定这些值的 ctor 中传递空值是不合法的。

此外,上面的方法很脆弱,因为如果对基类进行修改,会添加更多状态,以及“更完整”的构造函数(或不能在构造函数中设置的状态,只能通过访问器方法设置)已添加,副本将不再完整。

我想要的是像`clone(),而是初始化一个相同类型的新对象,初始化派生类的基类成员。我想这样的事情是不存在的。有关可能提供等效功能的模式的任何建议?

请记住,我不能修改基类。如果可以的话,这会容易得多。

【问题讨论】:

    标签: java design-patterns constructor clone factory


    【解决方案1】:

    如果您可以覆盖所有公共方法,则可以将源对象保存为委托

    Class D extends B
        B src;
        D(B src){ super(whatever); this.src=src; }
    
        public method1(){ src.method1(); }
    

    【讨论】:

    • 确实如此。缺点是 D 类在这种情况下具有无用的(始终为空/默认)成员。不过很有趣,它解决了 hoipolloi 回答的未实现接口问题。
    • 我喜欢用一句话描述看似复杂的问题的解决方案。
    【解决方案2】:

    支持组合而不是继承,可能通过创建一个包装类(如下所示)。如果您的基类使用接口,您的包装类可以实现相同的接口并将调用委托给基类(装饰器)。

    但是,没有像您描述的继承那样强大的策略。正如您所指出的,即使您使用反射来执行深层复制,实现也可能会发生变化。您破坏了封装,您的代码将与基类紧密耦合。

    public static void main(final String[] args) {
        final Base base = new Base("Hello");
        base.printState(); // Prints "Hello"
        final Wrapper wrapper = new Wrapper(base);
        wrapper.printState(); // Prints "Wrapper says Hello"
        wrapper.clone().printState(); // Prints "Wrapper says Hello"
    }
    
    private static class Wrapper {
    
        private final Base base;
    
        public Wrapper(final Base base) {
            this.base = base;
        }
    
        public Wrapper clone() {
            return new Wrapper(base);
        }
    
        public void printState() {
            System.out.printf("Wrapper says ");
            base.printState();
        }
    }
    
    private static class Base {
    
        private Object state;
    
        public Base(final Object state) {
            if (state == null) {
                throw new IllegalArgumentException("State cannot be null");
            }
            this.state = state;
        }
    
        public void printState() {
            System.out.println(state);
        }
    }
    

    【讨论】:

    • 是的,如果可以的话,我肯定会实现这个代理模式。不幸的是,我派生的基类是公共 API,并没有实现任何我可以实现的有趣接口。也就是说,我需要我的对象是基类型的一个实例,因为我必须在我无法更改的方法中使用它。
    • @BeeOnRope:如果您受制于设计不佳的框架,那么我认为您不走运。有一些解决方案,但它们很脆弱,无法支持超类中的 API 更改。
    • 感谢您的全面回答!
    【解决方案3】:

    正如其他人所指出的,很自然地想到通过使用委托并将其实现为代理或装饰器来解决此问题。处理这些模式的标准方法要求您有一个接口,而不是一个具体的基类,就像 Java 的动态代理一样。

    但是,您可以使用cglibjavassist 使用具体类来完成类似的事情。

    有了足够的运行时 JVM 修补,可能通过上述方法之一,或者使用 AspectJ,我认为您甚至可以让现有类实现新定义的接口。

    Hibernate 为所有持久类创建代理而不要求它们实现接口,我相信它使用 cglib 来做到这一点。

    【讨论】:

    • 这很有趣 - 对我的目的来说可能有点矫枉过正,但我​​会在未来考虑其他情况。
    【解决方案4】:

    我注意到有些人建议您同时使用组合和继承(请参阅下面的这种反模式的示例)。

    请仅在万不得已时才这样做。除了引入冗余状态之外,您的子对象还将暴露完全被忽略的状态和行为。这将导致一个非常具有误导性的 API。

    public static void main(final String[] args) {
        final Base base = new Base("Hello");
        base.printState(); // Prints "Hello"
        final Wrapper wrapper = new Wrapper(base);
    
        wrapper.changeState("Goodbye");
    
        wrapper.printState(); // Prints "Wrapper says Hello"
        wrapper.clone().printState(); // Prints "Wrapper says Hello".
    
        // It seems my state change was completely ignored. What a confusing API...
    }
    
    private static class Wrapper extends Base {
    
        private final Base base;
    
        public Wrapper(final Base base) {
            super("Make something up; this state isn't used anyway");
            this.base = base;
        }
    
        public Wrapper clone() {
            return new Wrapper(base);
        }
    
        public void printState() {
            System.out.printf("Wrapper says ");
            base.printState();
        }
    }
    
    private static class Base {
    
        private Object state;
    
        public Base(final Object state) {
            if (state == null) {
                throw new IllegalArgumentException("State cannot be null");
            }
            this.state = state;
        }
    
        public void changeState(final Object state) {
            this.state = state;
        }
    
        public void printState() {
            System.out.println(state);
        }
    }
    

    编辑:实际上,不要这样做。曾经。这是一个可怕的、可怕的策略。如果您无法管理与基类状态的所有交互(这再次使其成为一个非常脆弱的解决方案),那么将会发生非常糟糕的事情。例如,如果我修改基类如下:

    private static class Base {
    
        ...
    
        // A new method
        public Object getState() {
            return state;
        }
    
        ...
    }
    

    亲爱的……

    final Wrapper wrapper = new Wrapper(new Base("Foo"));
    System.out.println(wrapper.getState()); // Prints "Make something up; this state isn't used anyway"
    

    【讨论】:

    • 同意,但有时你的双手会尝试现有代码的结构。最后,我发现我可以通过在类路径中更早地包含我的 .class 来覆盖整个有问题的类,所以这让我能够以一种稍微不那么骇人听闻的方式修补行为。
    • ...但我仍然会接受这个答案,因为它是我能看到的唯一一个适用于给定约束的答案。
    • @BeeOnRope:当 classpath 恶作剧被认为是一种“不那么老套的方式”时,这种情况令人遗憾!但是,当我们的手被绑起来时,这很糟糕。出于兴趣,您尝试使用什么框架?祝你好运!
    • 好吧,在这种情况下,它不那么笨拙,因为它使我能够完全替换有问题的类,而不是简单地扩展它。当我扩展它时,我需要搜索基类型的每个创建站点(因为没有使用一致的工厂模式)并修改它以创建我的派生类。显然,这是非常脆弱的。完全替换后,我根本不需要担心这一点,所以我认为以后会少些hacky,也不容易出现(静默)问题。有问题的库是 Quartz。
    【解决方案5】:

    这看起来像是Proxy 的工作。 (可能如果你用谷歌搜索,你可以找到更好的代理实现,但我认为标准的代理实现很好。)

    像这样实现InvocationHandler

    class Handler implements InvocationHandler
    {
        private Thingie thingie ;
    
        public Handler ( Thingie thingie )
        {
            this . thingie = thingie ;
        }
    
        public Object invoke ( Object proxy , Method method , Object [ ] args ) thro
    ws Throwable
        {
            if ( method . getName ( ) . equals ( "target" ) )
                {
                    LOG . log ( this ) ;
                }
            return method . invoke ( this . thingie , args ) ;
        }
    }
    

    【讨论】:

    • 不幸的是,这只适用于我想根据基类实现接口 - 这里我需要直接实现基类。
    • 那我想你想要commons.apache.org/proxy。在我看来,它比其他选项更好,因为您可以将原始对象及其基类视为黑匣子。您无需了解其内部工作原理 - (1) 为您减少实施工作; (2) 如果基类发生变化,代理会处理所有这些。
    • @emory:如果 OP 可以摆脱使用组合,它会引入不必要的复杂性和依赖性。否则,我同意,它似乎代表了一个优雅的解决方案(比组合和继承解决方案要好得多)。
    【解决方案6】:

    如果基类不提供对克隆的内置支持,那么就没有任何真正好的方法。恕我直言,如果正确的模式是将类分为三类:

    -1- 在不破坏类不变量的情况下,根本无法有意义地克隆的类。

    -2- 可以在不破坏类不变量的情况下克隆的类,但可以用于派生其他无法有意义地克隆的类。

    -3- 可以克隆的类,仅用于派生同样可以克隆的类。

    -2- 或 -3- 类型的类应该提供一个受保护的虚拟克隆方法,该方法将调用父级的实现(如果它们是一个)或 Object.Clone(如果没有父级实现)然后执行任何特定于类的清理。 -3- 类型的类应该提供一个公共克隆方法,该方法将调用虚拟方法并将结果类型转换为正确的类型。 -1- 类型的可继承类从 -2- 类型的类派生而来,应该使用函数以外的东西来遮蔽受保护的克隆方法。

    如果无法将受保护的克隆方法添加到父类中,则无法构造一个在父类实现细节方面不会脆弱的可克隆派生类。但是,如果根据上述模式构造父类,则可以在派生类中干净地实现克隆。

    【讨论】:

    • 基类中有一个可访问的 clone() 方法,但我不想克隆基类,而是想创建一个与基类状态相同的派生类给定的基类(不是给定的派生类)。
    • 如果基类的 clone 方法使用 Object.Clone 创建新对象,那么在派生类对象上调用它会创建一个新的派生类对象。如果创建基类克隆方法的人使用了复制构造函数,那么创建派生类对象状态的克隆的唯一方法是自己重新实现复制构造函数的核心。
    • 当然,但我根本不明白这与问题有什么关系。我有一个类型为BaseClass 的对象,称之为b,具有可访问的clone() 方法。我想创建一个DerivedClass 类型的对象,其BaseClass 状态与b 对象相同。 clone() 有什么帮助?我不能在b 上调用它,因为我会得到一个BaseClass 类型的对象。我不能在派生类的任何实例上调用它,因为不存在(具有我想要的状态)。
    【解决方案7】:

    由于继承的工作方式,我认为您根本无法以这种方式分配它。假设您的基类是“A”类型。您创建类型为“B”的包装类。您可以将“B”的实例分配给类型“A”,但不能将“A”的实例分配给类型“B”。

    【讨论】:

    • 我不确定这是否相关。赋值仅适用于引用(和原始类型),您不能将任何对象的“实例”分配给任何其他对象。
    猜你喜欢
    • 2023-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-05
    • 2011-09-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多