前言:
这篇文章的思路就是抛出问题,再思考解决方案,最后利用设计模式解决问题
项目背景:
聚合搜索平台的主要功能就是一个有强大搜索能力的一个项目
用户输入一个词,同时可以搜索出用户,文章和图片这种功能
门面模式:
然后现在的情况是:
用户输入一个词,我们是将三个部分全都搜索出来
但是我们怎么样能让前端又能一次搜出所有数据、又能够分别获取某一类数据(比如分页场景)。
我们的解决办法就是让前端传一个参数:type
枚举type:比如有user用户,post文章,picture文章
这就引出了这篇文章的第一个设计模式:门面模式
门面模式(Facade Pattern)是一种结构性设计模式,它为复杂子系统提供一个简化的接口。通过创建一个门面类,门面模式可以隐藏系统的复杂性,使得外部调用变得更简单。
主要特点
- 简化接口:门面模式通过搭建一个统一的接口来简化对底层复杂系统的访问。
- 降低耦合:它减少了客户端与复杂系统之间的依赖性,客户端只需要与门面交互,而不需要直接与内部组件进行交互。
- 提高可维护性:由于系统内部的细节被封装在门面类中,后期对系统进行修改时,客户代码无需修改。
用户无需去理会后端业务逻辑有多复杂,只需要知道自己需要获取那一种的信息
这也有点像微服务项目的网关
用户不需要知道那么多服务器的ip地址
只需要把自己的请求发送给网关即可。
我们来看代码:
具体代码实现:
首先我们需要有一个SearchController来接收请求
还要有一个dto和vo用来接收请求参数和作为参数返回给前端。
还有一个状态的枚举
@Data
public class SearchRequest implements Serializable {
/**
* 查询
*/
private String searchText;
/**
* 查询接口的类型
*/
private String type;
private static final long serialVersionUID = 1L;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SearchVO implements Serializable {
private List<UserVO> userList;
private List<PostVO> postList;
private List<Picture> pictureList;
private static final long serialVersionUID = 1L;
}
/**
* 用户角色枚举
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
* @from <a href="https://yupi.icu">编程导航知识星球</a>
*/
public enum SearchTypeEnum {
POST("帖子","post"),
USER("帖子","user"),
PICTURE("帖子","picture");
private final String text;
private final String value;
SearchTypeEnum(String text, String value) {
this.text = text;
this.value = value;
}
/**
* 获取值列表
*
* @return
*/
public static List<String> getValues() {
return Arrays.stream(values()).map(item -> item.value).collect(Collectors.toList());
}
/**
* 根据 value 获取枚举
*
* @param value
* @return
*/
public static SearchTypeEnum getEnumByValue(String value) {
if (ObjectUtils.isEmpty(value)) {
return null;
}
for (SearchTypeEnum anEnum : SearchTypeEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
public String getValue() {
return value;
}
public String getText() {
return text;
}
}
状态的枚举里面就三个参数:
user对应用户 post对应文章 picture对应图片
然后具体看controller层的代码:
@Component
@Slf4j
@RequiredArgsConstructor
public class SearchFacade {
private final UserService userService;
private final PostService postService;
private final PictureService pictureService;
public SearchVO searchAll(@RequestBody SearchRequest searchRequest, HttpServletRequest request){
String type = searchRequest.getType();
SearchTypeEnum searchTypeEnum = SearchTypeEnum.getEnumByValue(type);
final String value = searchTypeEnum.getValue();
ThrowUtils.throwIf(StringUtils.isBlank(value), ErrorCode.PARAMS_ERROR);//传入的值是非法的
SearchVO searchVO = new SearchVO();
final String searchText = searchRequest.getSearchText();
if(type==null){
UserQueryRequest queryRequest = new UserQueryRequest();
queryRequest.setUserName(searchText);
final Page<UserVO> userVOPage = userService.listUserVOByPage(queryRequest);
PostQueryRequest postQueryRequest = new PostQueryRequest();
postQueryRequest.setSearchText(searchText);
final Page<PostVO> postVOPage = postService.listPostVOByPage(postQueryRequest,request);
final Page<Picture> picturePage = pictureService.SearchPicture(searchText, 1, 10);
searchVO.setUserList(userVOPage.getRecords());
searchVO.setPostList(postVOPage.getRecords());
searchVO.setPictureList(picturePage.getRecords());
}else {
switch (searchTypeEnum) {
case POST -> {
PostQueryRequest postQueryRequest = new PostQueryRequest();
postQueryRequest.setSearchText(searchText);
final Page<PostVO> postVOPage = postService.listPostVOByPage(postQueryRequest,request);
searchVO.setPostList(postVOPage.getRecords());
break;
}
case USER -> {
UserQueryRequest queryRequest = new UserQueryRequest();
queryRequest.setUserName(searchText);
final Page<UserVO> userVOPage = userService.listUserVOByPage(queryRequest);
searchVO.setUserList(userVOPage.getRecords());
break;
}
case PICTURE -> {
final Page<Picture> picturePage = pictureService.SearchPicture(searchText, 1, 10);
searchVO.setPictureList(picturePage.getRecords());
break;
}
}
}
return searchVO;
}
}
整体的代码逻辑其实很简单:
首先就是先把参数取出来进行判断
如果传入的type不合法(就不是枚举中提到的三种)直接抛异常
然后如果是type为空,就查询全部列表
就把SearchVO里面的三个变量
private List<UserVO> userList; private List<PostVO> postList; private List<Picture> pictureList;
都填充上
然后如果不是,我们就用switch来判断你是想要对应哪一个service来查询
整体逻辑很简单。
适配器模式:
上面的逻辑已经可以解决问题了,不过这一篇文章是优化
所以我们还是有优化点的
我们首先还是抛出问题
上面这段代码的问题:
- 很冗余(重复的逻辑写了很多)
- 不易于扩展(如果还有其它的搜索要求,比如视频,公众号这种,想要添加进来需要再继续写逻辑(堆屎山))
- 如果有其它的服务想要接入这个服务非常的麻烦,怎么说这个麻烦呢,首先我们自己的每个service中的方法都千奇百怪的(不过这个项目还好)如果再加入其它服务,就很乱
综上所诉,我们的解决办法就是需要一个标准,就是每个人接口都按照我这个标准给我东西,然后你才能接入。
这就涉及到了适配器模式:
适配器模式(Adapter Pattern)是一种结构性设计模式,旨在解决不兼容接口之间的交互问题。适配器模式通过创建一个适配器类,使得原本由于接口不兼容而无法一起工作的类可以协同工作。
可以理解为生活中的转换头。
主要特点
- 接口转换:适配器模式允许将一个类的接口转换成客户端所期望的另一种接口。
- 解耦:客户端和被适配者之间的耦合度降低,客户端无需了解被适配者的具体实现。
- 增强系统的灵活性:通过适配器,系统可以灵活地引入新的类,而无需改动已有的代码。
适用场景
- 当你想使用一些现有的类,而它们的接口不符合你的要求时。
- 你想创建一个可复用的类,该类可以与其他不相关的类一起工作。
- 系统需要与一些接口不兼容的类进行交互时。
我们的具体实现就是需要一个接口:
public interface DataSource<T> {
public Page<T> doSearch(String searchText,long pageNum,long pageSize);
}
里面有一个方法doSearch方法
里面有三个参数:搜索内容,分页号,分页大小
项目中的每一个Service都需要实现这个接口,都需要向我传入这三个参数
这就是一个标准
我们根据上面的枚举,我们需要三个Service来实现这个接口
我们来看具体的代码实现:
@Service
@Slf4j
public class UserDataSource implements DataSource {
@Autowired
private UserService userService;
@Override
public Page doSearch(String searchText, long pageNum, long pageSize) {
UserQueryRequest userQueryRequest = new UserQueryRequest();
userQueryRequest.setUserName(searchText);
userQueryRequest.setPageSize((int) pageSize);
userQueryRequest.setCurrent((int) pageNum);
Page<UserVO> userVOPage = userService.listUserVOByPage(userQueryRequest);
return userVOPage;
}
}
这里也有一个小技巧
我们这里的doSearch需要三个参数
但是UserService中查询用户的接口的参数是userQueryRequest
这和doSearch需要的参数差的有点大
所以我们可以在这个UserDataSource 里面调用UserService的listUserVOByPage方法,然后我们传入我们需要的参数即可。
@Service
@Slf4j
public class PostDataSource implements DataSource {
@Autowired
private PostService postService;
@Autowired
private HttpServletRequest request;
@Override
public Page doSearch(String searchText, long pageNum, long pageSize) {
PostQueryRequest postQueryRequest = new PostQueryRequest();
postQueryRequest.setSearchText(searchText);
postQueryRequest.setPageSize((int) pageSize);
postQueryRequest.setCurrent((int) pageNum);
//todo HttpRequest的这个参数值是null
Page<PostVO> postVOPage = postService.listPostVOByPage(postQueryRequest, request);
return postVOPage;
}
}
这里也有注意点,PostService的listPostVOByPage需要一个参数:HttpServletRequest request
(这个时候需要做出选择,看能不能通过其它办法解决,不能的话就需要考虑是否要把这个方法接入)
但是我们这里可以解决,就是在这个PostDataSource 中引入HttpServletRequest
这里就涉及到一个点:就是如何在不是controller层拿到request请求
这里有两种方法:
一种是直接依赖注入
@Autowired
private HttpServletRequest request;
还有一种是通过一个RequestContextHolder
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attrs.getRequest();
@Service
public class PictureDataSource implements DataSource{
@Override
public Page doSearch(String searchText, long pageNum, long pageSize) {
String url = String.format("https://cn.bing.com/images/search?q=%s&form=HDRSC2&first=%s",searchText,pageSize);
Document doc = null;
try {
doc = Jsoup.connect(url).get();
} catch (IOException e) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR);
}
Elements elements = doc.select(".iusc");
final Elements elements1 = doc.select("a.inflnk");
List<Picture> pictureList = new ArrayList<>();
for (int i = 0; i < elements.size(); i++) {
//获取图片
final String m = elements.get(i).attr("m");
Map<String,Object> map = JSONUtil.toBean(m,Map.class);
final String murl = (String) map.get("murl");
//获取标题
final String title = elements1.get(i).attr("aria-label");
Picture picture = new Picture();
picture.setMurl(murl);
picture.setTitle(title);
pictureList.add(picture);
if(pictureList.size()>=pageSize){
break;
}
}
Page<Picture> picturePage= new Page<>(pageNum,pageSize);
picturePage.setRecords(pictureList);
return picturePage;
}
}
经过这个适配器模式之后else中的代码:
else {
Map<String, DataSource> dataSourceMap = new HashMap<>();
dataSourceMap.put("post",postDataSource);
dataSourceMap.put("user",userDataSource);
dataSourceMap.put("picture",pictureDataSource);
DataSource dataSource = dataSourceMap.get(type);
final Page<?> page = dataSource.doSearch(searchText, 1, 10);
final List<?> records = page.getRecords();
searchVO.setDatasourceList(page.getRecords());
}
这里用了一个map来记录不同的适配器实现类。
注册模式:
注册模式:首先注册模式是单例模式的一个扩展
上面的代码还可以抽象:
我们根据上面写的map,创造一个datasourceregistry,进行初始化map,并且用@PostConstrucet注解来在所有的bean注册之后执行(因为map中的元素<String,Bean对象>)
@Component
@Slf4j
@RequiredArgsConstructor
public class DataSourceRegistry {
private final UserDataSource userDataSource;
private final PostDataSource postDataSource;
private final PictureDataSource pictureDataSource;
private Map<String,DataSource> dataSourceMap = new HashMap<>();
@PostConstruct
private void initMap(){
dataSourceMap.put("post",postDataSource);
dataSourceMap.put("user",userDataSource);
dataSourceMap.put("picture",pictureDataSource);
}
public DataSource getDataSource(String type){
return dataSourceMap.get(type);
}
}
这里还用到了一个@PostConstruct注解
为什么呢
我们仔细看代码
这里的map收集的都是bean对象,所以我们肯定要先注册bean对象然后才能存到map里面去
@PostConstruct的注解作用就是这个:在bean对象注册完之后再执行。