Как я могу упростить этот набор операторов if? (Или, что заставляет его чувствовать себя так неловко?)

Мой коллега показал мне этот фрагмент кода, и мы оба задавались вопросом, почему мы не можем удалить дублированный код.

private List<Foo> parseResponse(Response<ByteString> response) {
    if (response.status().code() != Status.OK.code() || !response.payload().isPresent()) {
      if (response.status().code() != Status.NOT_FOUND.code() || !response.payload().isPresent()) {
        LOG.error("Cannot fetch recently played, got status code {}", response.status());
      }
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Здесь альтернативное представление, чтобы сделать его немного менее многословным:

private void f() {
    if (S != 200 || !P) {
        if (S != 404 || !P) {
            Log();
        }
        return;
    }
    // ...
    // ...
    // ...
    return;
}

Есть ли более простой способ написать это, не дублируя !P? Если нет, существует ли какое-то уникальное свойство ситуации или условий, из-за которых невозможно исключить из !P

Ответы

Ответ 1

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

private List<Foo> parseResponse(Response<ByteString> response) {
    Status status = response.status();
    int code = status.code();
    boolean payloadAbsent = !response.payload().isPresent();

    if (code != Status.OK.code() || payloadAbsent) {
      if (code != Status.NOT_FOUND.code() || payloadAbsent) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
      }
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Изменить: Как отмечает Стюарт в комментариях ниже, если возможно напрямую сравнить response.status() и Status.OK, то вы можете устранить посторонние вызовы на .code() и использовать import static для прямого доступа к статусам:

import static Namespace.Namespace.Status;

// ...

private List<Foo> parseResponse(Response<ByteString> response) {
    Status status = response.status();
    boolean payloadAbsent = !response.payload().isPresent();

    if (status != OK || payloadAbsent) {
      if (status!= NOT_FOUND || payloadAbsent) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
      }
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Что касается вопроса о том, что делать с дублированием логики payloadAbsent, Захари предоставил некоторые хорошие идеи, которые совместимы с тем, что я предложил. Еще один вариант заключается в том, чтобы сохранить основную структуру, но сделать причину для проверки полезной нагрузки более явной. Это упростило бы логику и избавило бы вас от необходимости использовать || во внутреннем if. OTOH, я не очень увлечен этим подходом:

import static Namespace.Namespace.Status;

// ...

private List<Foo> parseResponse(Response<ByteString> response) {
    Status status = response.status();
    boolean failedRequest = status != OK;
    boolean loggableError = failedRequest && status!= NOT_FOUND ||
        !response.payload().isPresent();

    if (loggableError) {
      LOG.error("Cannot fetch recently played, got status code {}", status);
    }
    if (failedRequest || loggableError) {
      return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Ответ 2

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

if (!P) {
    Log();
    return A;
}
if (S != 200) {
    if (S != 404) {
        Log();
    }
    return A;
}
return B;

Или (это было предпочтительным OP)

if (S == 404 && P) {
    return A;
}
if (S != 200 || !P) {
    Log();
    return A;
}
return B;

Или (я лично предпочитаю это, если вы не против переключателя)

if (P) {
    switch (S) {
        case 200: return B;
        case 404: return A;
    }
}
Log ();
return A;

Вы можете сжимать оператор if, удаляя фигурные скобки и перемещая однослойное тело в ту же строку, что и оператор if. Однострочные if-утверждения, однако, могут вводить в заблуждение и неправильную практику. Из того, что я собираю из комментариев, ваше предпочтение было бы против этого использования. Хотя однострочные if-утверждения могут конденсировать логику и давать внешний вид более чистого кода, ясность и намерение кода должны быть оценены "экономичным" кодом. Чтобы быть ясным: я лично чувствую, что существуют ситуации, когда однострочные if-утверждения подходят, однако, поскольку исходные условия очень длинные, я бы настоятельно советовал против этого в этом случае.

if (S != 200 || !P) {
    if (S != 404 || !P) Log();
    return A;
}
return B;

В качестве бокового узла: если Log(); оператор был единственной доступной ветвью во вложенных операторах if, вы могли бы использовать следующий логический идентификатор для уплотнения логики (Distributive).

(S != 200 || !P) && (S! = 404 || !P) <=> (S != 200 && S != 404) || !P

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

Ответ 3

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

Я бы, вероятно, использовал:

    boolean goodPayload = response.status().code() == Status.OK.code() && response.payload().isPresent();
    if (!goodPayload) {
        // Log it if payload not found error or no payload at all.
        boolean logIt = response.status().code() == Status.NOT_FOUND.code()
                || !response.payload().isPresent();
        if (logIt) {
            LOG.error("Cannot fetch recently played, got status code {}", response.status());
        }
        // Use empty.
        return Lists.newArrayList();
    }

Ответ 4

Код для меня - это то, что вы запрашиваете объект Response вместо того, чтобы сообщать об этом. Спросите себя, почему метод Parse является внешним по отношению к объекту Response вместо того, чтобы быть его методом (или, скорее, супер-классом). Может ли метод Log() быть вызван в конструкторе объекта Response вместо его метода Parse? В момент, когда свойства payload().isPresent() status().code() и payload().isPresent() вычисляются в конструкторе, можно было бы назначить по-разному разбираемый объект частному свойству, чтобы только простой (и одиночный) if... else... остается в Parse()?

Когда кто-то благословлен возможностью писать на объектно-ориентированном языке с наследованием реализации, необходимо запросить каждый условный оператор (и выражение!), Чтобы посмотреть, будет ли он кандидатом быть поднятым либо в конструктор, либо в метод (s), которые вызывают конструктор. Упрощение, которое может следовать за всеми вашими классами, поистине огромно.

Ответ 5

Главное, что на самом деле является избыточным, - это полезная нагрузка P (!). Если вы написали это как логическое выражение, у вас есть:

(A || !P) && (B || !P)

Как вы заметили, P! Кажется, повторяется, и это бесполезно. В булевой алгебре вы можете рассматривать AND вроде умножения, и OR вроде подобного добавления. Таким образом, вы можете развернуть те круглые скобки, как с помощью простой алгебры:

A && B || A && !P || !P && B || !P && !P

Вы можете сгруппировать все выражения ANDed, которые имеют: P вместе:

A && B || (A && !P || !P && B || !P && !P)

Поскольку все эти термины имеют! P в них, вы можете вынести это как умножение. Когда вы это сделаете, вы замените его на истину (как и вы, 1, потому что в 1 раз что-то само по себе, правда И что-то само собой):

A && B || !P && (A && true || B && true || true && true)

Обратите внимание: "true && true" - это одно из выражений OR'd, поэтому вся группировка всегда верна, и вы можете упростить:

A && B || !P && true
-> A && B || !P

Может быть, я ржав на правильной нотации и терминологии. Но это суть этого.

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

Итак, поставив их вместе, вы получите:

boolean statusIsGood = response.status().code() != Status.OK.code() 
  && response.status().code() != Status.NOT_FOUND.code();

if (!statusIsGood || !response.payload().isPresent()) {
  log();
}

Обратите внимание, что переменная называется "statusIsGood", хотя вы ее отрицаете. Потому что переменные с отрицательными именами действительно запутывают.

Имейте в виду, вы можете сделать вышеупомянутое упрощение для действительно сложной логики, но вы не всегда должны это делать. В итоге вы получите технически правильные выражения, но никто не может понять, почему, глядя на него.

В этом случае, я думаю, упрощение разъясняет намерение.

Ответ 6

ИМХО проблема в основном повторение и вложение. Другие предложили использовать чистые переменные и использовать функции, которые я рекомендую, но вы также можете попытаться отделить внимание от своих валидаций.

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

private List<Foo> parseResponse(Response<ByteString> response)
{
    if (!response.payload.isPresent()) {
        LOG.error("Response payload not present");
        return Lists.newArrayList();
    }
    Status status = response.status();
    if (status != Status.OK || status != Status.NOT_FOUND) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
        return Lists.newArrayList();
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Ответ 7

Вы можете инвертировать оператор if, чтобы сделать его более понятным, как в:

private void f() {
    if (S == 200 && P) {
        return;
    }

    if (S != 404 || !P) {
        Log();
    }

    return;
}

Затем вы можете реорганизовать условные выражения с использованием значимых имен методов, таких как "responseIsValid()" и "responseIsInvalid()".

Ответ 8

Вспомогательные функции могут упростить вложенные условные обозначения.

private List<Foo> parseResponse(Response<ByteString> response) {
    if (!isGoodResponse(response)) {
        return handleBadResponse(response);
    }
    // ...
    // ...
    // ...
    return someOtherList;
}

Ответ 9

Самой неловкой частью являются геттеры, такие как response.status(), вызываемые много раз, когда логика, кажется, требует единственного, постоянного значения. Предположительно это работает, потому что геттеры гарантированно всегда возвращают одно и то же значение, но оно misexpresses намерение кода и делает его более хрупким по отношению к текущим предположениям.

Чтобы исправить это, код должен получить response.status() один раз,

var responseStatus = response.status();

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

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

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

Ответ 10

Отказ от ответственности: я не стану подвергать сомнению подпись представленной функции, а также функциональность.

Мне это кажется неудобным, потому что функция выполняет много работы сама по себе, а не делегирует ее.

В этом случае я бы предложил поднять часть проверки:

// Returns empty if valid, or a List if invalid.
private Optional<List<Foo>> validateResponse(Response<ByteString> response) {
    var status = response.status();
    if (status.code() != Status.NOT_FOUND.code() || !response.payload().isPresent()) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
        return Optional.of(Lists.newArrayList());
    }

    if (status.code() != Status.OK.code()) {
        return Optional.of(Lists.newArrayList());
    }

    return Optional.empty();
}

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

После этого parseResponse становится легким:

private List<Foo> parseResponse(Response<ByteString> response) {
    var error = validateResponse(response);
    if (error.isPresent()) {
        return error.get();
    }

    // ...
    // ...
    // ...
    return someOtherList;
}

Вместо этого вы можете использовать функциональный стиль.

/// Returns an instance of ... if valid.
private Optional<...> isValid(Response<ByteString> response) {
    var status = response.status();
    if (status.code() != Status.NOT_FOUND.code() || !response.payload().isPresent()) {
        LOG.error("Cannot fetch recently played, got status code {}", status);
        return Optional.empty();
    }

    if (status.code() != Status.OK.code()) {
        return Optional.empty();
    }

    return Optional.of(...);
}

private List<Foo> parseResponse(Response<ByteString> response) {
    return isValid(response)
        .map((...) -> {
            // ...
            // ...
            // ...
            return someOtherList;
        })
        .orElse(Lists.newArrayList());
}

Хотя лично я нахожу дополнительное гнездование немного раздражающим.

Ответ 11

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

if (not ({200,404}.contains(S) && P)){
    log();
    return;
}
if (S !=200){
    return;
}
// other stuff

Ответ 12

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

Set<Code> codes = {200, 404};
if(!codes.contains(S) && !P){
    log();
}
return new ArrayList<>();

Коррекция в состоянии.

Ответ 13

Можно повторить повторный тест P. Следующий (псевдокод) логически эквивалентен коду в вашем вопросе.

private List<Foo> f() {
    List<Foo> list(); /*whatever construction*/
    if (P) {
        if (S==200) {
            // ...
            // ...
            // ...
            list.update(/*whatever*/);
        }
        else if (S!=404) {
           Log();
        }
    }
    else {
       Log();
    }
    return list;
}

С точки зрения удобочитаемости я бы пошел со следующим (опять-таки псевдокодом):

private bool write_log() {
    return (S!=200 && S!=404) || !P
}
private bool is_good_response() {
    return S==200 && P
}
private List<Foo> f() {
    List<Foo> list(); /*whatever construction*/
    if (write_log()) {
       Log();
    }
    if (is_good_response()) {
        // ...
        // ...
        // ...
        list.update(/*whatever*/);
    }
    return list;
}

с возможно более подходящими названными функциями.

Ответ 14

Ветвление на целое число путем сравнения его с конечным набором явных значений лучше всего решать с помощью switch:

if (P) {
    switch (S) {
        case 200: return B;
        case 404: return A;
    }
}

Log();
return A;

Ответ 15

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

private List<Foo> parseResponse(Response<ByteString> response) {

    if (response.status().code() == Status.NOT_FOUND.code() && !response.payload().isPresent()) // valid state, return empty list
        return Lists.newArrayList();

    if (response.status().code() != Status.OK.code()) // status code says something went wrong
    {
        LOG.error("Cannot fetch recently played, got status code {}", response.status());
        return Lists.newArrayList();
    }

    if (!response.payload().isPresent()) // we have an OK status code, but no payload! should this even be possible?
    {
        LOG.error("Cannot fetch recently played, got status code {}, but payload is not present!", response.status());
        return Lists.newArrayList();
    }

    // ... got ok and payload! do stuff!

    return someOtherList;
}

Ответ 16

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

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

    private void f() {
        if(!P) {
            Log();          // duplicating Log() & return but keeping conditions separate
            return;
        } else if (S != 200) {
            if (S != 404) {
                Log();
            }
            return;
        }

        // ...
        // ...
        // ...
        return;
    }

Простота кода имеет некоторые субъективные элементы, и читаемость чрезвычайно субъективна. Учитывая, что, если я собираюсь написать этот метод с нуля, это то, что я бы дал моим предубеждениям.

private static final String ERR_TAG = "Cannot fetch recently played, got status code {}";

private List<Foo> parseResponse(Response<ByteString> response) {
    List<Foo> list = Lists.newArrayList();

    // similar to @JLRishe keep local variables rather than fetch a duplicate value multiple times
    Status status = response.status();
    int statusCode = status.code();
    boolean hasPayload = response.payload().isPresent();

    if(!hasPayload) {
        // If we have a major error that stomps on the rest of the party no matter
        //      anything else, take care of it 1st.
        LOG.error(ERR_TAG, status);
    } else if (statusCode == Status.OK.code()){
        // Now, let celebrate our successes early.
        // Especially in this case where success is narrowly defined (1 status code)
        // ...
        // ...
        // ...
        list = someOtherList;
    } else {
        // Now we're just left with the normal, everyday failures.
        // Log them if we can
        if(statusCode != Status.NOT_FOUND.code()) {
            LOG.error(ERR_TAG, status);
        }
    }
    return list;        // One of my biases is trying to keep 1 return statement
                        // It was fairly easy here.
                        // I won't jump through too many hoops to do it though.
}

Если я удалю свои комментарии, это почти удваивает строки кода. Некоторые утверждают, что это не могло сделать код более простым. Для меня это так.

Ответ 17

Я не уверен, что делает код: честно говоря, регистрируя только статус 404 и возвращая пустой список, когда код не 200, похоже, что вы пытаетесь избежать NPE...

Я думаю, что это лучше:

private boolean isResponseValid(Response<ByteString> response){
    if(response == null){
        LOG.error("Invalid reponse!");
        return false;
    }

    if(response.status().code() != Status.OK.code()){
        LOG.error("Invalid status: {}", response.status());
        return false;
    }

    if(!response.payload().isPresent()){
        LOG.error("No payload found for response!");
        return false;
    }
    return true;
}

private List<Foo> parseResponse(Response<ByteString> response) throws InvalidResponseException{
    if(!isResponseValid(response)){
        throw InvalidResponseException("Response is not OK!");
    }

    // logic
}

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

Кроме того, попробуйте использовать соглашения об именах Java:

LOG.error("")    // should be log.error("")
Status.OK.code() // why couldn't it be a constant like Status.OK, or Status.getOkCode()?