在派生类构造函数(或工厂)中初始化基类的模式

时间:2011-07-21 23:03:21

标签: java design-patterns constructor clone factory

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

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

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

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

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

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

请记住,我无法修改基类。如果可以,这将更容易。

7 个答案:

答案 0 :(得分:4)

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

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

    public method1(){ src.method1(); }

答案 1 :(得分:3)

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

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

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

答案 2 :(得分:1)

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

但是,您可以使用cglibjavassist在具体课程中完成类似的事情。

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

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

答案 3 :(得分:1)

我注意到有些人建议你同时使用组合和继承(见下面的反模式示例)。

请仅作为最后的手段。除了引入冗余状态之外,您的子对象还将公开完全被忽略的状态和行为。这将导致一个非常误导的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"

答案 4 :(得分:0)

这看起来像是Proxy的工作。 (可能如果你谷歌,你可以找到一个更好的代理实现,但标准的是在我看来很好的enuf。)

像这样实现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 ) ;
    }
}

答案 5 :(得分:0)

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

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

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

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

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

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

答案 6 :(得分:-1)

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