如何避免if-else树代码?

时间:2018-04-27 14:29:16

标签: java spring rest

我有一个休息控制器,其中包含一个根据“标题”和“作者”参数查找图书的方法。

你能否给我一些提示如何摆脱if-else的建设? 现在它并不复杂,但将来可能会增加参数的数量,从而导致混乱。

map $arg_amp $x {
    1        foo;
    2        bar;
    default  baz;
}

...

location / {
    add_header X-Foo $x;
    return 204;
}

6 个答案:

答案 0 :(得分:2)

由于您的计划最终会支持更多参数,因此最好的选择可能是调查Hibernate's Criteria class。这允许您动态构造查询。它不会避免使用if语句,但避免使用else语句,并且很容易支持新参数。在您的存储库/ DAO级别:

Criteria criteria = session.createCriteria(Book.class);
if (author != null && !author.isEmpty()) {
    criteria.add(Restriction.eq("author", author));
}
if (title != null && !title.isEmpty()) {
    criteria.add(Restriction.eq("title", title));
}
criteria.addOrder(Order.asc("publishDate"));
return (List<Book>) criteria.list();

这有一些显着的好处:

  1. 要支持新参数,您只需将参数添加到控制器,然后将参数传递到存储库,并将参数添加到此块。

  2. 您最终可以对您的排序进行配置,例如:

    ?sort=title:asc&author=Bobby%20Tables

  3. 但是,有一些缺点,最明显的是它依赖于字符串值来引用您的属性。如果您的属性名称更改(请注意这是POJO属性,而不是数据库列名称),则此代码将需要更改。但是,我认为这是一种非常罕见的情况,除了最新的数据库模式仍在不断变化的新项目中,并且在数据库模式建立之后,这种缺点很少会引起问题。

    另一个提示,一旦你点击了一定数量的传入参数(例如4-5),创建a parameter object就可以将你的参数包装成一个可以传递的好的对象。

答案 1 :(得分:1)

我会推荐Specification or Querydsl 它受到域驱动设计/模型的启发,实现依赖于JPA Criteria,可以灵活地构建查询。

这是一个例子(没有经过测试,但你应该得到一般的想法)。

书籍规范

用于动态创建规范的工厂类。

public class BookSpecifications {

    private BookSpecifications(){
    }

    public static Specification<Book> withAuthor(String author) {
        return new Specification<Book>() {
            public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.equal(root.get("author"), author);
            }
        };
    }

    public static Specification<Book> withTitle(String title) {
        return new Specification<Book>() {
            public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.equal(root.get("title"), title);
            }
        };
    }
}

请注意,使用从Entity类在编译时生成的常量值是一种更好的方法。 Yo可以使用Book.authorBook.title来引用实体的属性,而不是使用在编译时未检查的某些String与实际的实体模型(如“作者”或“标题”)。

BookController

在控制器端,您应该避免以最小的逻辑减少逻辑,并且有利于将处理委托给服务类,该服务类将创建所需的规范并将其传递给存储库。

public class BookController {
    @GetMapping
    public ResponseEntity<List<Book>> searchBooksByTitleAndAuthor(
            @RequestParam(value = "title", required = false) String title,
            @RequestParam(value = "author", required = false) String author) {


        List<Book> books = bookService.findAll(title, author);

        if (books.isEmpty()) {
            log.info("No books with this specification ");
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
        return new ResponseEntity<>(books, HttpStatus.OK);

    }
}

BookService

整个逻辑在这里(单元测试应该集中在这里)。

public class BookService {

    private BookRepository bookRepository;

    public BookService(BookRepository bookRepository){
         this.bookRepository = bookRepository;
    }

    public List<Book> findAll(String title, String author) {

       if (Stream.of(title, author)
                 .filter(s-> s == null || s.equals(""))
                 .count() == 0) {
            return new ArrayList<>();
        }

        Specification<Book> specification = createSpecification(specification, title, () -> BookSpecifications.withTitle(title));
        specification = createSpecification(specification, author, () -> BookSpecifications.withAuthor(author));
        List<Book> books = bookRepository.findAll(specification);
        return books;

    }

    Specification<Book> createSpecification(Specification<Book> currentSpecification, String arg, Callable<Specification<Book>> callable) {

        // no valued parameter so we return
        if (arg == null) {
            return currentSpecification;
        }

        try {

           // Specification instance already created : reuse it
            if (currentSpecification != null) { 
                return currentSpecification.and(callable.call());
            }

           // Specification instance not created yes : create a new one
            return callable.call();
        } catch (Exception e) {
            // handle the exception... if any
        }

    }
}

在搜索中添加新的实体属性非常简单 在方法和流中添加参数,以测试是否至少填充了条件搜索,然后将新调用链接到createSpecification()

// existing
Specification<Book> specification = createSpecification(specification, title, () -> BookSpecifications.withTitle(title));
specification = createSpecification(specification, author, () -> BookSpecifications.withAuthor(author));
// change below !
specification = createSpecification(specification, anotherColumn, () -> BookSpecifications.withAnotherColumn(anotherColumn));

BookRepository

最后一步:让您的BookRepository界面扩展JpaSpecificationExecutor,以便能够调用接受Repository参数的Specification<T>方法,例如:

List<T> findAll(@Nullable Specification<T> spec);

那应该没问题:

@Repository
public interface BookRepository extends JpaRepository<Book, Long> , JpaSpecificationExecutor<Book>  {    
}

请注意,如果可能要求许多实体属性,则使用更动态的方法可能会很有趣:

public class BookSpecifications {

    private BookSpecifications(){
    }

    public static Specification<Book> withAttribute(String name, T value) {
        return new Specification<Book>() {
            public Predicate toPredicate(Root<Book> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                return cb.equal(root.get(name), value);
            }
        };
    }

}

但推迟检测编码错误有其缺点 实际上,规范构建中的错误(例如:值类型不兼容)只能在运行时检测到。

答案 2 :(得分:0)

我只想写这个:

@GetMapping
public List<Book> searchBooksByTitleAndAuthor(@RequestParam String title, @RequestParam String author) {
    return bookService.getBooksByTitleAndAuthor(title, author);
}

我会将ResponseEntity创建和HttpStatus管理留给Spring。对于null或空值管理,我将其留给后面的服务或数据库查询。

此外,您在RequestParam注释中编写的参数是默认值,可以简化。

最后,为什么要记录所有查询?重点是什么?如果您的目标是生产,那么您的信息日志将被大量查询发送垃圾邮件,而商业信息无论如何都不属于技术日志。

答案 3 :(得分:0)

没有很好的解决方案。如果我是你,我会将其重构为:

@GetMapping
public ResponseEntity<List<Book>> searchBooksByTitleAndAuthor(
        @RequestParam(value = "title", required = false) String title,
        @RequestParam(value = "author", required = false) String author) {
    List<Book> books;
    if (StringUtils.isBlank(title) && StringUtils.isBlank(author)) {
        log.info("Empty request");
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    } else if (!StringUtils.isBlank(title) && !StringUtils.isBlank(author)) {
        books = bookService.getBooksByTitleAndAuthor(title, author);
    } else if (StringUtils.isBlank(author)) {
        books = bookService.getBooksByTitle(title);
    } else {
        books = bookService.getBooksByAuthor(author);
    }
    if (books.isEmpty()) {
        log.info("No books found with title = " + title + " and author = " + author);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
    return new ResponseEntity<>(books, HttpStatus.OK);
}

使用此解决方案,您将在响应后进行处理。

一个提示

如果没有发送参数,则返回错误请求,说明此服务至少需要一个参数。

答案 4 :(得分:0)

一点点筑巢可能会有所帮助。 这将最小化重复测试。 一些代码:

if (StringUtils.isNotBlank(blam) || StringUtils.isNotBlank(kapow))
{
    if (StringUtils.isBlank(blam))
    {
        // kapow is not blank.
    }
    else if (StringUtils.isBlank(kapow))
    {
        // blam is not blank.
    }
    else
    {
        // neither kapow nor blam is blank.
    }
}
else
{
   // both empty.  error.
}

我喜欢Brian的回答, 但我永远不会建议使用Hibernate。 MyBatis还支持条件where子句。

答案 5 :(得分:0)

这是一个想法。代码可能需要一些工作,特别是我不知道你是否可以使用静态地图,但是对于Spring,你可能还有一个单例。

private static long toKey(Object ... args) {
    long key = 0L;
    for(int i = 0; i < args.length; i++) {
        if(args[i] != null) {
            key |= (1L << i);
        }
    }
    return key;
}

private static interface BookFinder {
    ResponseEntity<List<Book>> search(String title, String author);
}

private static Map<Long, BookFinder> _keyToFinderMap = new HashMap<>();

static {
    _keyToFinderMap.put(toKey(null, null), new BookFinder() {
        public ResponseEntity<List<Book>> search(String title, String author) {
            log.info("Empty request");
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        }
    });
    _keyToFinderMap.put(toKey("", null), new BookFinder() {
        public ResponseEntity<List<Book>> search(String title, String author) {
            log.info("Retrieving books by title");
            List<Book> books = bookService.getBooksByTitle(title);
            if (books.isEmpty()) {
                log.info("No books with this title");
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
            return new ResponseEntity<>(books, HttpStatus.OK);
        }
    });
    // Other cases
};

@GetMapping
public ResponseEntity<List<Book>> searchBooksByTitleAndAuthor(
        @RequestParam(value = "title", required = false) String title,
        @RequestParam(value = "author", required = false) String author) {
    return _keyToFinderMap.get(toKey(title, author)).search(title, author);
}

如果添加新参数,只需添加新的书籍查找器即可。不,如果声明。我会将NO_CONTENT移到searchBooksByTitleAndAuthor,但我不想更改您的日志记录语句。否则,这将简化发现者。

toKey方法当然可以改变,实施并不重要。它必须将输入组合映射到唯一键。建议的方法最多可处理64个参数,这些参数可以是null / not null。