Шаблон для инициализации базового класса в конструкторе производного класса (или фабрике) - PullRequest
4 голосов
/ 22 июля 2011

Представьте, что у вас есть производный класс, где базовый класс - это то, что вы не можете изменить.Базовый класс имеет много состояний (много непостоянных закрытых членов) и множество конструкторов с различным количеством аргументов для инициализации некоторого подмножества состояния (размер подмножества, конечно, зависит от конструктора).

Теперь мой производный класс - очень легкая оболочка над базовым классом.Давайте предположим, что он не добавляет свое собственное состояние и лишь незначительно изменяет поведение пары методов (возможно, делает некоторые дополнительные записи в журнал при вызове super.originalMethod()).

Проблема, которую я имею, заключается в том, что я хочу принятьобъект базового класса и создать его «копию» с тем же состоянием, но в качестве экземпляра моего производного класса.

Это оказывается трудным.Я не могу вызвать «самый полный» конструктор базового класса, передавая все состояние из источника, вызывая геттеры, поскольку в зависимости от того, как был построен базовый класс, некоторые значения состояния могут быть отклонены этим конструктором.Например, вы можете создать объект по умолчанию с 0-arg ctor, любые значения будут нулевыми.Однако не разрешено передавать нулевые значения в ctor, который позволяет вам указывать эти значения.

Кроме того, вышеописанный метод хрупок, потому что если происходит изменение базового класса, которое добавляет больше состояния, идобавлен «еще более полный» конструктор (или состояние, которое не может быть установлено в конструкторе, но только с помощью методов доступа), копия больше не будет завершена.

То, что я хочу, похоже на `clone (), а скорее инициализирует новый объект того же типа, инициализирует членов базового класса производного класса.Я думаю, что такой вещи не существует.Любые предложения по шаблону, которые могут предложить что-то эквивалентное?

Имейте в виду, я не могу изменить базовый класс.Если бы я мог, это было бы намного проще.

Ответы [ 7 ]

4 голосов
/ 22 июля 2011

Если вы можете переопределить все открытые методы, вы можете сохранить исходный объект в качестве делегата

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

    public method1(){ src.method1(); }
3 голосов
/ 22 июля 2011

Избавьте композицию от наследования, возможно, создав класс-оболочку (как показано ниже). Если ваш базовый класс использует интерфейсы, ваш класс-оболочка может реализовывать те же интерфейсы и делегировать вызовы базовому классу (декоратору).

Тем не менее, нет надежной стратегии, которую можно описать с помощью наследования. Даже если вы использовали отражение для выполнения глубокой копии, как вы указали, реализация может измениться. Вы нарушили инкапсуляцию, и ваш код был бы тесно связан с базовым классом.

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);
    }
}
1 голос
/ 24 июля 2011

Я заметил, что некоторые люди рекомендуют использовать как композицию, так и наследование (пример этого анти-паттерна приведен ниже).

Пожалуйста, делайте это только в крайнем случае . В дополнение к введению избыточного состояния ваш дочерний объект будет отображать состояние и поведение, которые полностью игнорируются. Это приведет к очень вводящему в заблуждение 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"
1 голос
/ 22 июля 2011

Как уже отмечали другие, естественно решить эту проблему, используя делегирование и реализуя его в качестве прокси или декоратора. Стандартные способы работы с этими шаблонами требуют наличия в основе интерфейса, а не конкретного класса, как это делает динамический прокси Java.

Тем не менее, вы можете выполнить аналогичные вещи с конкретными классами, используя cglib или javassist .

При достаточной обработке JVM во время выполнения, возможно, с помощью одного из вышеперечисленных, или с AspectJ , я думаю, вы даже можете заставить свой существующий класс реализовать вновь определенный интерфейс.

Hibernate создает прокси для всех постоянных классов, не требуя, чтобы они реализовывали интерфейс, и я полагаю, что для этого он использует cglib.

0 голосов
/ 22 июля 2011

Если базовый класс не предоставляет встроенную поддержку клонирования, то не существует действительно хорошего способа. ИМХО, если правильный шаблон - это разделить классы на три категории:

-1- Классы, которые принципиально не могут быть клонированы каким-либо образом, без нарушения инвариантов классов.

-2- Классы, которые можно клонировать, не нарушая инварианты классов, но которые можно использовать для получения других классов, которые не могут быть клонированы осмысленно.

-3- Классы, которые можно клонировать и которые будут использоваться только для получения классов, которые также могут быть клонированы.

Классы типа -2- или -3- должны предоставлять защищенный метод виртуального клонирования, который будет вызывать реализацию родителя (если у них одна), или Object.Clone (если нет родительской реализации), а затем делать любая классовая очистка. Классы типа -3- должны предоставлять общедоступный метод клонирования, который будет вызывать виртуальный метод и типизировать результат в соответствующий тип. Наследуемые классы типа -1, которые наследуются от классов типа -2, должны затенять защищенный метод клонирования чем-то другим, кроме функции.

Если нет способа добавить защищенный метод клонирования в родительский класс, нет способа создать клонируемый производный класс, который не будет хрупким в отношении деталей реализации родительского класса. Однако если родительский класс создается в соответствии с приведенным выше шаблоном, клонирование будет реализовано чисто в производных классах.

0 голосов
/ 22 июля 2011

Я не думаю, что вы вообще сможете назначить его таким образом из-за того, как работает наследование. Допустим, ваш базовый класс относится к типу «А». Вы создаете свой класс-обертку типа "B". Вы можете назначить экземпляр «B» для типа «A», но вы не можете назначить экземпляр «A» для типа «B».

0 голосов
/ 22 июля 2011

Это похоже на работу для Прокси . (Вероятно, если вы Google, вы можете найти лучшую реализацию прокси, но стандартная, на мой взгляд, хороший 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 ) ;
    }
}
...