[英]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();
这有一些显着的好处:
要支持新参数,只需将参数添加到控制器,然后将参数传递到存储库,并将参数添加到此块。
您最终可以对排序进行配置,例如:
?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.author
或Book.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.