У меня есть конечная точка в моем RestAPI, которая может получать 3 дополнительных параметра, а затем находить информацию в базе данных с этими параметрами, также можно комбинировать параметры. Итак, в своем контроллере я беру 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 ответ
Хотя он работает, главная проблема этого подхода — его чрезвычайная сложность и конкретность. Если новый параметр 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 (и соответствующие изменения в реализации). Это упростит введение новых параметров.