繁体   English   中英

如何避免if-else树代码?

[英]How to avoid if-else tree code?

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

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

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

        if (title == null && author == null) {
            log.info("Empty request");
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } else if (title != null && author == null) {
            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);
        } else if (author != null && title == null) {
            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);
        } else {
            List<Book> books = bookService.getBooksByTitleAndAuthor(title, author);
            if (books.isEmpty()) {
                log.info("No books with matching this title and author");
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
            return new ResponseEntity<>(books, HttpStatus.OK);
        }

    }

由于您的计划最终将支持更多参数,因此您最好的选择可能是查看Hibernate的Criteria 这允许您动态构造查询。 它不会避免使用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

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

另一个提示,一旦你传入一定数量的参数(例如4-5),就会创建一个参数对象 ,将你的参数包装成一个可以传递的单个漂亮的对象。

我建议使用Specification或Querydsl
它受到域驱动设计/模型的启发,实现依赖于JPA Criteria,允许灵活地构建查询。

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

BookSpecifications

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

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与实际的Entity模型(如“author”或“title”)。

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 ,以便能够调用接受Specification<T>参数的Repository方法,例如:

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);
            }
        };
    }

}

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

我只写这个:

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

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

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

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

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

@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);
}

使用此解决方案,您将在响应后立即对待。


一个提示

如果未发送任何参数,则返回错误请求,指出此服务至少需要一个参数。

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

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子句。

这是一个想法。 代码可能需要一些工作,特别是我不知道你是否可以使用静态地图,但是对于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。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM