【问题标题】:Java semantics - Is there a way to write this better?Java 语义 - 有没有办法写得更好?
【发布时间】:2020-04-26 19:02:46
【问题描述】:

我正在构建一个 Spring 后端。我有一个控制器,它获取一个“搜索对象” - 一个具有 10 个字段的对象,其中只有一个应该被填充,所以搜索功能(我没有编写,但需要对其进行更改和重构)被编写像这样:

if( param1 != null ) user = getUserByParam1(param1);
else if ( param2 != null ) user = getUserByParam2(param2);
.
.
.
else if(lastName != null || lastName != null) user = getUserByName(firstName, lastName);
else user = getUserById(id);

if(user == null) throw costumException;
return user;

请注意最后的 2 种特殊情况——其中一种检查 2 个参数的可用性而不是一个,并将它们都发送到同一个函数(可以在其中一个字段中处理 null,但不能同时处理两者),以及默认值假设一个 ID 被传递的情况(如果它不是 - 它被if (user == null) 处理后抛出异常检查)。

有没有办法将这段代码重构为更具可读性/美观?我可以使用任何设计模式或已知方法来做到这一点吗?或者它实际上是编写这种功能的最佳方式?

我想了很多,但找不到更好的方法。我有一个想法,以某种方式将填充的字段名称及其值发送到另一个函数,该函数将在字段名称上“切换大小写”并将值发送到适当的函数,但它并没有真正节省太多代码(因为我仍然需要手动迭代所有字段以找到已填充的字段)而且我不确定它是否更具可读性。

我对 Java 也很陌生,所以我不知道所有可用的 API 和接口,也许你可以向我求助。

【问题讨论】:

  • 你有多少种不同的获取用户的案例?
  • 您的搜索对象是某种地图吗?
  • 检查是否可以使用 Java 8 中的谓词 API。
  • 这里的问题是端点试图做的太多了。 10 个参数在单个端点中处理太多了,并且由于它们是理想的互斥的,因此您应该让一个端点处理一个参数。复杂的参数对象和单个端点似乎并没有给您带来任何优势,至少基于提供的代码。非常相关:softwareengineering.stackexchange.com/questions/388026/…
  • 在软件工程或代码审查交流中问这个问题不是更好吗?

标签: java spring refactoring


【解决方案1】:

注意:这只是你相当可怕的方法的一种解决方法。如 cmets 中所述,您的端点试图做太多事情。最好创建许多只需要所需参数的不同端点。比只看 10 多个 if-else 语句更容易理解


您可以创建一个包含所有可能参数的数据-class

public class UserRequest {
    private String lastName;
    private String firstName;

    // more fields, constructors, getters, setters etc.
}

然后有一个 interface 看起来像这样:

public interface UserRequestResolver {
    User resolve(UserRequest request);
}

然后可以实现此接口以依赖于给定的参数(如果存在)。返回找到的User 或简单的null

下一步是,创建一个List<UserRequestResolver> resolvers,并添加不同的实现,Java8+ 可以使用 lambda:

resolvers.add(r -> r.getParam1() != null ? getUserByParam1(r.getParam1()) : null);
resolvers.add(r -> {
    if(r.getFirstName() == null && r.getLastName()) return null;
    return getUserByName(r.getFirstName(), r.getLastName());
});
// etc.

然后当收到UserRequest 时,您可以简单地遍历resolvers 并获取第一个不是null 的返回值:

for(UserRequestResolver resolver : resolvers) {
    User user = resolver.resolve(request);
    if(user != null) return user;
}
throw costumException;

如果你是Streams 的粉丝,那么你可以将上面的for循环替换为:

return resolvers.stream()
    .map(resolver -> resolver.resolve(request))
    .filter(Objects::nonNull)
    .findFirst()
    .orElseThrow(() -> costumException);

【讨论】:

  • 确实 if/else-ladder 可以更改为不同的形式,例如这里介绍的更复杂的形式。我相信,如果 10 种不同的检索用户的方式并非都共享“尽可能多的代码”(在这种情况下,if/else 仍然是最简单的一个)。很好的答案。
  • @Kayaman 我同意,我提出了一个可能吸引眼球的选项,但引入了很多复杂性。即:1)引入classinterface,2)必须初始化resolvers。还要注意的是,这个解决方案会比一些简单的单行 if-else 语句慢很多
  • 感谢您的详细解答!我真的很喜欢这种方式,但你真的认为我的端点做得太多了吗?毕竟,在我看来,“单一责任原则”在这里非常适用,因为我所做的只是“搜索”——通过不同的参数。这也将引入前端负载,因为这是一种“搜索”表单,我们只需要插入一个字段。 FE 必须自己进行迭代,这肯定会比后端更慢、更复杂。 @LinosaysReinstateMonica
  • @Gibor 确实做得太多了,因为您提供了多种不同的搜索方法,它们都相互排斥(您不能同时搜索 param1param2)。我不知道你是如何收到你的搜索条件的,但如果你问我,整个事情听起来有点臭。关于您对前端负载的担忧,您必须知道 javascript 的速度非常快。一些迭代对它来说没问题;)
  • @LinosaysReinstateMonica 是的,我明白你在说什么......我只是想我可以稍微调整一下你的方法,就像非一对一的搜索条件(如姓名和姓氏)会首先,给我驱动程序列表,然后我继续应用非空解析器,直到列表大小变为 1。它实际上可以帮助我,因为我需要保留当前函数以支持单个对象的返回并创建另一个端点支持返回多个对象(不能合并并返回大小为 1 的列表,因为此搜索功能已在产品中大量使用)。我会努力实现的,谢谢!
【解决方案2】:

根据您用于访问数据的方式,您可以使用getByExample 查询。您有一个将 here 与 Spring 数据一起使用的示例。有了这个,你只需要从 api 获取用户类(你必须从你的 api 接收搜索查询的对象,这个对象必须有点像用户对象,因为参数看起来很相似)。

然后你有你的大 if/else 你从 api 传递用户搜索对象并在存储库中执行 getByExample

user = getByExample(userSearchObject)

但这一切都假设您在 api 中只收到一个填充对象的字段。这将是最简洁的方式。

否则,我建议将所有参数包装到 api 中的类中,如下所示:

public class UserSearchQuery() {
    private String param1;
    private String param2;
    ...
    
    // getters + setters
}

然后使用条件查询和条件查询构建器(一篇关于它的文章here)。

在您的服务中,您只需这样做:

public class UserService() {

private UserRepository repository;
   public User search(UserSearchQuery query) {
      return repository.search(query);
   }
}

在回购中:

public class UserRepository() {
   private EntityManager em;
   
   public User search(UserSearchQuery query) {
      CriteriaBuilder builder = em.getCriteriaBuilder();
      CriteriaQuery<User> query = builder.createQuery(User.class);

      Root<User> user = query.from(User.class);
      List<Predicate> predicates = new ArrayList<>();
     
      if (query.getParam1() != null) {
          predicates.add(builder.equal(user.get("param1"), query.getParam1()));
      }
      ...
      // and the other ones goes here
   }
}

这样做,您的用户将只有 1 个搜索方法,而不再是 10 个方法,每个参数一个,并且要添加搜索条件,您只需向 UserSearchQuery 类添加一个新参数并添加一个新谓词.此外,如果有一天您希望能够使用多个参数作为搜索条件,那么它已经完成了。

【讨论】:

  • 感谢您的回答!这听起来很神奇,但它需要在我们的后端进行大量更改,因为我们在任何地方都大量使用这些搜索功能。我会记住这个 API,因为我们将在几个月内进行大规模的重构——这真的可以帮助我们减少函数的数量并使一切变得更加通用。再次感谢!
  • 很高兴能帮到你!那么我认为现在你没有很多选择,除了一个大的 if-else ...
【解决方案3】:

您可以发送请求参数中的所有字段,然后使用命令设计模式或策略设计模式针对不同的情况实现不同的实现。

命令模式示例如下:

https://sourcemaking.com/design_patterns/command

策略设计模式: https://sourcemaking.com/design_patterns/strategy

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多