Обработка нескольких необязательных параметров в Spring

У меня есть конечная точка в моем RestAPI, которая может получать 3 дополнительных параметра, а затем находить информацию в базе данных с этими параметрами, также можно комбинировать параметры. Итак, в своем контроллере я беру Map , который содержит эти параметры, и затем передаю его на уровень обслуживания, где я проверяю, какие параметры были переданы, а затем я вызываю конкретный метод DAO, в зависимости от того, какая комбинация параметров делает У меня есть. Параметры A и B относятся к типу String, а C — к Map. Итак, на уровне обслуживания это выглядит так:

public Set<ResultEntry> getByParameters(Map<String, Object> query) {
    Set<ResultEntry> result = new HashSet<>();
    if (query.containsKey("a") && query.containsKey("b") && query.containsKey("c")) {
        result.addAll(myDao.getByABC(
                (String) query.get("a"), (String) query.get("b"),
                (Map<String, String>) query.get("c")
        ));
    } else if (query.containsKey("a") && query.containsKey("b") && !query.containsKey("c")) {
        result.addAll(myDao.getByAB(
                (String) query.get("a"), (String) query.get("b")
        ));
    } else if (query.containsKey("a") && !query.containsKey("b") && query.containsKey("c")) {
        result.addAll(myDao.getByAC(
                (String) query.get("a"), (Map<String, String>) query.get("c")
        ));
    } else if (!query.containsKey("a") && query.containsKey("b") && query.containsKey("c")) {
        result.addAll(myDao.getByBC(
                (String) query.get("b"), (Map<String, String>) query.get("c")
        ));
    } else if (query.size() == 1 && query.containsKey("a")) {
        result.addAll(myDao.getByA((String) query.get("a")));
    } else if (query.size() == 1 && query.containsKey("b")) {
        result.addAll(myDao.getByB((String) query.get("b")));
    } else if (query.size() == 1 && query.containsKey("c")) {
        result.addAll(myDao.getByC((Map<String, String>) query.get("c")));
    }
    return result;
}

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

1 ответ
1

Хотя он работает, главная проблема этого подхода — его чрезвычайная сложность и конкретность. Если новый параметр d войдет в игру, будет адски поддерживать!

Что можно сделать, чтобы улучшить текущую реализацию, не меняя подхода?

  • есть много повторов .containsKey(arg) звонки. Их можно извлечь в соответствующие booleanкак hasA, hasB -> код будет более читабельным.

  • есть много повторяющихся приведений. (String) query.get("a") а остальные могут быть извлечены в соответствующие локальные var a = (String) query.get("a") -> он также станет короче и читабельнее.

  • Я думаю, что result переменной можно избежать, если myDao.get* методы уже возвращаются Setс.

Как можно улучшить дизайн?

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

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

Имя метода для вызова myDao можно рассчитать следующим образом:

private String calculateDaoMethodToInvoke(boolean hasA, boolean hasB, boolean hasC) {
  var builder = new StringBuilder("getBy");
  if (hasA) {
    builder.append("A");
  }
  if (hasB) {
    builder.append("B");
  }
  if (hasC) {
    builder.append("C");
  }
  return builder.toString();
}

Мы также можем вычислить аргументы для вызова этого метода с помощью:

private Object[] calculateDaoMethodArgs(String a, String b, Map<String, String> c) {
  return Stream.of(a, b, c)
               .filter(Objects::nonNull)
               .toArray();
}

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

Мы можем определить оболочку вокруг Class.getMethod вызов, чтобы получить ссылку на метод myDao для вызова:

private Method extractDaoMethod(String daoMethodName, Object[] params) {
  try {
    var paramTypes = Arrays.stream(params)
                           .map(Object::getClass)
                           .toArray(Class[]::new);
    return Dao.class.getMethod(daoMethodName, paramTypes);
  } catch (NoSuchMethodException ex) {
    throw new IllegalStateException(ex);
  }
}

И его вызов также можно обернуть:

private Collection<ResultEntry> getByParams(String daoMethodName, Object[] params) {
  var method = extractDaoMethod(daoMethodName, params);
  try {
    return (Collection<ResultEntry>) method.invoke(myDao, params);
  } catch (IllegalAccessException | InvocationTargetException ex) {
    throw new IllegalStateException(ex);
  }
}

Ну наконец то, то getByParameters метод сводится к следующему:

public Set<ResultEntry> getByParameters(Map<String, Object> query) {
  var a = (String) query.get("a");
  var b = (String) query.get("b");
  var c = (Map<String, String>) query.get("c");

  var daoMethodName = calculateDaoMethodToInvoke(a != null, b != null, c != null);
  var daoArgs = calculateDaoMethodArgs(a, b, c);
  return new HashSet<>(getByParams(daoMethodName, daoArgs));
}

Обратите внимание на то, что более жестких if - else if цепочки, алгоритм стал плоским.

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

Возможны дальнейшие улучшения. Например, подписи calculateDaoMethodToInvoke а также calculateDaoMethodArgs можно изменить, чтобы принять varargs (и соответствующие изменения в реализации). Это упростит введение новых параметров.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *