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

Свободный API с наследованием и дженериками

Я пишу свободный API для настройки и создания последовательности объектов "сообщения". У меня есть иерархия типов сообщений.

Чтобы иметь возможность получить доступ к методу подклассов при использовании свободного API, я использовал generics для параметризации подклассов и создания всех свободно используемых методов (которые начинаются с "с" ) возвращают общий тип. Обратите внимание, что я пропустил большую часть тела свободного метода; в них много конфигурации.

public abstract class Message<T extends Message<T>> {

    protected Message() {

    }

    public T withID(String id) {
        return (T) this;
    }
}

Конкретные подклассы также переопределяют общий тип.

public class CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>> {

    protected CommandMessage() {
        super();
    }

    public static CommandMessage newMessage() {
        return new CommandMessage();
    }

    public T withCommand(String command) {
        return (T) this;
    }
}

public class CommandWithParamsMessage extends
    CommandMessage<CommandWithParamsMessage> {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName,
        String paramValue) {
        contents.put(paramName, paramValue);
        return this;
    }
}

Этот код работает, т.е. я могу создать экземпляр любого из классов и использовать все текущие методы:

CommandWithParamsMessage msg = CommandWithParamsMessage.newMessage()
        .withID("do")
        .withCommand("doAction")
        .withParameter("arg", "value");

Вызов быстрых методов в любом порядке - главная цель здесь.

Однако компилятор предупреждает, что все return (T) this являются небезопасными.

Тип безопасности: снятие флажка из сообщения в T

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

другие вопросы здесь, на SO, которые касаются аналогичной проблемы. Они указывают на решение, где все промежуточные классы абстрактны и объявляют метод, подобный protected abstract self(). Тем не менее, в конце концов, это не безопасно.

4b9b3361

Ответ 1

Ваш код в основном является небезопасным использованием Generics. Например, если я пишу новый класс, который расширяет сообщение, скажем, Threat, и имеет новый метод doSomething(), а затем создаю сообщение, параметризованное этим новым классом, и он создает экземпляр Message, а затем пытается его опубликовать к его подклассу. Однако, поскольку это экземпляр Message, а не Threat, попытка вызвать это сообщение вызовет исключение. Так как сообщение Message не может выполнять SOmething().

Кроме того, здесь также нет необходимости использовать Generics. Обычное старое наследование будет работать нормально. Поскольку подтипы могут переопределять методы, делая их типы возврата более конкретными, вы можете иметь:

public abstract class Message {

    protected Message() {

    }

    public Message withID(String id) {
        return this;
    }
}

И затем

public class CommandMessage extends Message {

    protected CommandMessage() {
        super();
    }

    public static CommandMessage newMessage() {
        return new CommandMessage();
    }

    public CommandMessage withCommand(String command) {
        return this;
    }
}

Это будет работать отлично, при условии, что вы вызываете свои аргументы в правильном порядке:

CommandWithParamsMessage.newMessage()
    .withID("do")
    .withCommand("doAction")
    .withParameter("arg", "value");

не удастся, но

CommandWithParamsMessage.newMessage().withParameter("arg", "value")
.withCommand("doAction").withID("do")

Будет успешным, поскольку он только "вверх", наконец, возвращает класс "сообщение". Если вы хотите, чтобы это не было "uptype", просто перепишите унаследованные команды, и теперь вы можете вызывать методы в любом порядке, так как все они возвращают исходный тип.

например.

public class CommandWithParamsMessage extends
CommandMessage {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName,
        String paramValue) {
        contents.put(paramName, paramValue);
        return this;
    }

    @Override
    public CommandWithParamsMessage withCommand(String command){
        super.withCommand(command);
        return this;
   }

    @Override
    public CommandWithParamsMessage withID(String s){
        super.withID(s);
        return this;
    }
}

Теперь вы будете свободно возвращать CommandWithParamsMessage с любым из двух бесплатных вызовов выше.

Помогает ли это решить вашу проблему, или я неправильно понял ваши намерения?

Ответ 2

Я уже делал что-то подобное. Это может стать уродливым. Фактически, я пробовал это больше раз, чем использовал; обычно он стирается, и я стараюсь найти лучший дизайн. Тем не менее, чтобы помочь вам двигаться немного дальше по дороге, попробуйте это:

Пусть ваши абстрактные классы объявят метод вроде:

protected abstract T self();

Это может заменить this в ваших операторах return. Подклассы должны будут возвращать то, что соответствует оценке для T - но это не гарантирует, что они возвратят тот же объект.

Ответ 3

Если вы меняете подписи таким образом, вы не должны получать никаких предупреждений и не нуждаться в каких-либо переводах:

abstract class Message<T extends Message<T>> {

    public T withID(String id) {
        return self();
    }

    protected abstract T self();
}

abstract class CommandMessage<T extends CommandMessage<T>> extends Message<T> {

    public T withCommand(String command) {
        // do some work ...
        return self();
    }
}

class CommandWithParamsMessage extends CommandMessage<CommandWithParamsMessage> {

    public static CommandWithParamsMessage newMessage() {
        return new CommandWithParamsMessage();
    }

    public CommandWithParamsMessage withParameter(String paramName, String paramValue) {
        // do some work ...
        return this;
    }

    @Override protected CommandWithParamsMessage self() {
        return this;
    }
}

Ответ 4

Это не решение вашей исходной проблемы. Это всего лишь попытка зафиксировать ваше фактическое намерение и набросать подход, где исходная проблема не появляется. (Мне нравятся дженерики, но имена классов, такие как CommandMessage<T extends CommandMessage<T>> extends Message<CommandMessage<T>>, заставляют меня дрожать...)

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

Но если я правильно понял ваше намерение, вы могли бы подумать о том, чтобы позволить подтипам обрабатываться быстрыми вызовами.

Идея здесь в том, что вы изначально можете только создать простой Message:

Message m0 = Message.newMessage();
Message m1 = m0.withID("id");

В этом сообщении вы можете вызвать метод withID - единственный метод, который имеет все общие сообщения. В этом случае метод withID возвращает Message.

До сих пор это сообщение не является ни CommandMessage, ни какой-либо другой специализированной формы. Однако, когда вы вызываете метод withCommand, вы, очевидно, хотите построить CommandMessage - так что теперь вы просто возвращаете CommandMessage:

CommandMessage m2 = m1.withCommand("command");

Аналогично, когда вы вызываете метод withParameter, вы получаете CommandWithParamsMessage:

CommandWithParamsMessage m3 = m2.withParameter("name", "value");

Эта идея примерно (!) вдохновлена ​​записью , которая находится на немецком языке, но код прекрасно показывает, как эта концепция может быть используемый для создания запросов типа "Отбирать-Откуда".

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

import java.util.HashMap;
import java.util.Map;


public class FluentTest
{
    public static void main(String[] args) 
    {
        CommandWithParamsMessage msg = Message.newMessage().
                withID("do").
                withCommand("doAction").
                withParameter("arg", "value");


        Message m0 = Message.newMessage();
        Message m1 = m0.withID("id");
        CommandMessage m2 = m1.withCommand("command");
        CommandWithParamsMessage m3 = m2.withParameter("name", "value");
        CommandWithParamsMessage m4 = m3.withCommand("otherCommand");
        CommandWithParamsMessage m5 = m4.withID("otherID");
    }
}

class Message 
{
    protected String id;
    protected Map<String, String> contents;

    static Message newMessage()
    {
        return new Message();
    }

    private Message() 
    {
        contents = new HashMap<>();
    }

    protected Message(Map<String, String> contents) 
    {
        this.contents = contents;
    }

    public Message withID(String id) 
    {
        this.id = id;
        return this;
    }

    public CommandMessage withCommand(String command) 
    {
        Map<String, String> newContents = new HashMap<String, String>(contents);
        newContents.put("command", command);
        return new CommandMessage(newContents);
    }

}

class CommandMessage extends Message 
{
    protected CommandMessage(Map<String, String> contents) 
    {
        super(contents);
    }

    @Override
    public CommandMessage withID(String id) 
    {
        this.id = id;
        return this;
    }

    public CommandWithParamsMessage withParameter(String paramName, String paramValue) 
    {
        Map<String, String> newContents = new HashMap<String, String>(contents);
        newContents.put(paramName, paramValue);
        return new CommandWithParamsMessage(newContents);
    }

}

class CommandWithParamsMessage extends CommandMessage 
{
    protected CommandWithParamsMessage(Map<String, String> contents) 
    {
        super(contents);
    }

    @Override
    public CommandWithParamsMessage withID(String id) 
    {
        this.id = id;
        return this;
    }

    @Override
    public CommandWithParamsMessage withCommand(String command) 
    {
        this.contents.put("command", command);
        return this;
    }
}

Ответ 5

Компилятор предупреждает вас об этой небезопасной операции, потому что она не может фактически проверить правильность вашего кода. Это делает его, по сути, небезопасным, и вы ничего не можете сделать, чтобы предотвратить это предупреждение. Несмотря на то, что небезопасная операция не проверяется компилятором, она все равно может быть законна во время выполнения. Если вы обойдете проверку компилятора, то ваша работа - проверить свой собственный код для использования правильных типов, для чего предназначена аннотация @SupressWarning("unchecked").

Чтобы применить это к вашему примеру:

public abstract class Message<T extends Message<T>> {

  // ...

  @SupressWarning("unchecked")
  public T withID(String id) {
    return (T) this;
  }
}

отлично, потому что вы можете с уверенностью сказать, что этот экземпляр Message всегда имеет тип, который представлен T. Но компилятор Java не может (пока). Как и в случае других предупреждений о подавлении, ключ к использованию аннотации заключается в том, чтобы свести к минимуму его объем! В противном случае вы можете легко сохранить подавление аннотации случайно после внесения изменений кода, которые делают вашу быструю ручную проверку безопасности типа недействительной.

Когда вы возвращаете экземпляр this только, вы можете легко передать задачу на конкретные методы, как рекомендовано в другом ответе. Определите метод protected, например

@SupressWarning("unchecked")
public T self() {
  (T) this;
}

и вы всегда можете вызвать мутатор, как здесь:

public T withID(String id) {
  return self();
}

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

interface Two<T> { T make() }
interface One { <S> Two<S> do(S value) }

class MyBuilder<T> implements One, Two<T> {

  public static One newInstance() {
    return new MyBuilder<Object>(null);
  }

  private T value; // private constructors omitted

  public <S> Two<S> do(S value) {
    return new MyBuilder<S>(value);
  }

  public T make() {
    return value;
  }
}

Вы можете, конечно, создать более умные конструкции, где вы избежите неиспользуемых полей. Если вы хотите взглянуть на примеры того, как я использую этот подход, посмотрите на мои два проекта, которые довольно сильно используют беглые интерфейсы:

  • Byte Buddy: API для определения класса Java во время выполнения.
  • PDF-конвертер: конверсионное программное обеспечение для преобразования файлов с использованием MS Word с Java.