Подтвердить что ты не робот

Java Factory Шаблон с универсалами

Я бы хотел, чтобы мой BallUserInterfaceFactory возвращал экземпляр пользовательского интерфейса, который имеет собственный общий тип. Я застрял в примере ниже, получив ошибку:

Связанное несоответствие: общий метод getBaseballUserInterface (BASEBALL) типа BallUserInterfaceFactory не применим для аргументов (МЯЧ). Выведенный тип BALL не является допустимой заменой ограниченный параметр

public class BallUserInterfaceFactory {
    public static <BALL extends Ball> BallUserInterface<BALL> getUserInterface(BALL ball) {

        if(ball instanceof Baseball){
            return getBaseballUserInterface(ball);
        }
        //Other ball types go here

        //Unable to create a UI for ball
        return null;
    }

    private static <BASEBALL extends Baseball> BaseballUserInterface<BASEBALL> getBaseballUserInterface(BASEBALL ball){
        return new BaseballUserInterface<BASEBALL>(ball);
    }
}

Я понимаю, что он не может гарантировать, что BALL является бейсболом, и поэтому существует несовпадение типа параметра в вызове метода getBaseballUserInterface.

Если я передам параметр ball в вызове метода getBaseballUserInterface, я получаю ошибку:

Несоответствие типов: невозможно преобразовать из BaseballUserInterface<Baseball>до BallUserInterface<BALL>

Потому что он не может гарантировать, что то, что я возвращаю, является одним и тем же типом шара.

Мой вопрос: какова стратегия решения этой ситуации?

(Для полноты здесь приведены другие классы, необходимые в примере)

public class Ball {

}

public class Baseball extends Ball {

}

public class BallUserInterface <BALL extends Ball> {

    private BALL ball;

    public BallUserInterface(BALL ball){
        this.ball = ball;
    }
}

public class BaseballUserInterface<BASEBALL extends Baseball> extends BallUserInterface<BASEBALL>{

    public BaseballUserInterface(BASEBALL ball) {
        super(ball);
    }

}
4b9b3361

Ответ 1

Это ОЧЕНЬ ХОРОШИЙ вопрос.

Вы можете бросить бросок

    return (BallUserInterface<BALL>)getBaseballUserInterface((Baseball)ball);

Ответ теоретически ошибочен, поскольку мы вынуждаем BASEBALL=Baseball.

Он работает из-за стирания. На самом деле это зависит от стирания.

Я надеюсь, что есть лучший ответ, безопасный для reification.

Ответ 2

Это неправильный шаблон дизайна. Вместо использования одного универсального метода и лестницы if вы должны вместо этого использовать перегрузку. Перегрузка устраняет необходимость в лестнице if, и компилятор может убедиться, что правильный метод вызывается, а не ждать до выполнения.

например.

public class BallUserInterfaceFactory {

    public static BallUserInterface<Baseball> getUserInterface(
            Baseball ball) {
        return new BallUserInterface<Baseball>(ball);
    }

    public static BallUserInterface<Football> getUserInterface(
            Football ball) {
        return new BallUserInterface<Football>(ball);
    }
}

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


Чтобы избежать лестницы, вы можете использовать технику, известную как двойная отправка. По сути, мы используем тот факт, что экземпляр знает, к какому классу он относится, и называет соответствующий метод factory для нас. Для этого для работы Ball должен быть метод, который возвращает соответствующий BallInterface.

Вы можете либо сделать метод абстрактным, либо предоставить реализацию по умолчанию, которая генерирует исключение или возвращает значение null. Ball и Baseball теперь должны выглядеть примерно так:

public abstract class Ball<T extends Ball<T>> {
    abstract BallUserInterface<T> getBallUserInterface();
}

.

public class Baseball extends Ball<Baseball> {
    @Override
    BallUserInterface<Baseball> getBallUserInterface() {
        return BallUserInterfaceFactory.getUserInterface(this);
    }
}

Чтобы сделать вещи немного опрятными, лучше сделать пакет getBallUserInterface приватным и предоставить общий getter в BallUserInterfaceFactory. Затем factory может управлять дополнительными проверками, например, для null и любых исключений. например.

public class BallUserInterfaceFactory { 
    public static BallUserInterface<Baseball> getUserInterface(
            Baseball ball) {
        return new BallUserInterface<Baseball>(ball);
    }   
    public static <T extends Ball<T>> BallUserInterface<T> getUserInterface(
            T ball) {
        return ball.getBallUserInterface();
    }
}

Шаблон посетителя

Как отмечалось в комментариях, одна из проблем, упомянутых выше, требует, чтобы классы Ball имели знание UI, что крайне нежелательно. Однако вы можете использовать шаблон посетителя, который позволяет использовать двойную отправку, а также отделяет различные классы Ball и пользовательский интерфейс.

Сначала необходимы необходимые классы посетителя и factory:

public interface Visitor<T> {
    public T visit(Baseball ball);
    public T visit(Football ball);
}

public class BallUserInterfaceVisitor implements Visitor<BallUserInterface<? extends Ball>> {
    @Override
    public BallUserInterface<Baseball> visit(Baseball ball) {
        // Since we now know the ball type, we can call the appropriate factory function
        return BallUserInterfaceFactory.getUserInterface(ball);
    }   
    @Override
    public BallUserInterface<Football> visit(Football ball) {
        return BallUserInterfaceFactory.getUserInterface(ball);
    }
}

public class BallUserInterfaceFactory {
    public static BallUserInterface<? extends Ball> getUserInterface(Ball ball) {
        return ball.accept(new BallUserInterfaceVisitor());
    }
    // other factory functions for when concrete ball type is known
}

Вы заметите, что посетитель и функция factory должны использовать подстановочные знаки. Это необходимо для безопасности типа. Поскольку вы не знаете, какой тип шара был передан, метод не может быть уверен в том, какой пользовательский интерфейс возвращается (кроме него является пользовательским интерфейсом мяча).

Во-вторых, вам нужно определить абстрактный метод accept на Ball, который принимает Visitor. Каждая конкретная реализация Ball должна также реализовать этот метод для правильной работы шаблона посетителя. Реализация выглядит точно так же, но система типов обеспечивает отправку соответствующих методов.

public interface Ball {
    public <T> T accept(Visitor<T> visitor);
}

public class Baseball implements Ball {
    @Override
    public <T> T accept(Visitor<T> visitor) {
        return visitor.visit(this);
    }
}

Наконец, немного кода, который может объединить все это:

Ball baseball = new Baseball();
Ball football = new Football();

List<BallUserInterface<? extends Ball>> uiList = new ArrayList<>();

uiList.add(BallUserInterfaceFactory.getUserInterface(baseball));
uiList.add(BallUserInterfaceFactory.getUserInterface(football));

for (BallUserInterface<? extends Ball> ui : uiList) {
    System.out.println(ui);
}

// Outputs:
// [email protected]
// [email protected]

Ответ 3

public class BaseballUserInterface extends BallUserInterface<Baseball> {

    public BaseballUserInterface(Baseball ball) {
        super(ball);
    }
}

Вы используете BallUserInterface в результате метода factory. Таким образом, он может быть скрыт, какой конкретный шар используется:

public class BallUserInterfaceFactory {

public static BallUserInterface<?> getUserInterface(Ball ball) {

        if(ball instanceof Baseball){
            return getBaseballUserInterface((Baseball)ball);
        }

        return null;
    }

    private static BaseballUserInterface getBaseballUserInterface(Baseball ball){
        return new BaseballUserInterface(ball);
    }
}

Если клиент заинтересован в типе шара, вы должны предложить метод factory с конкретным шаром в качестве параметра:

public static BaseballUserInterface getUserInterface(Baseball ball){
    return new BaseballUserInterface(ball);
}