工厂模式
概念
工厂是用来干嘛的,按照一定的流程创造出对应的产品的嘛。当然,本身工厂也是一个抽象的概念。光讲概念容易不清晰,看一段代码的演进过程吧。
原代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Resource {
private String url;
}
public class ResourceLoader {
/**
* 加载对应的资源
*/
public Resource load(String url){
// 解析出对应的前缀
String prefix = parsePrefix(url);
/*
处理不同前缀的资源加载
*/
Resource resource = null;
// 不同分支中的只是一些伪代码,真实情况下可能比较复杂
if("http".equals(prefix)){
resource = new Resource("http");
}else if("ftp".equals(prefix)){
resource = new Resource("ftp");
}else if("classpath".equals(prefix)){
resource = new Resource("classpath");
}else if("file".equals(prefix)){
resource = new Resource("file");
}
return resource;
}
/**
* 解析url的前缀
* url的前缀可能是:http、ftp、classpath、file
* 伪代码
*/
private String parsePrefix(String url){
return url;
}
}
上述代码的最大问题其实不在于什么if else
语句中的逻辑很复杂,复杂了可以去抽离它嘛,最主要的问题在于太臃肿了,我们可以对其进行解耦,使用一个工厂类:
静态工厂类 —— 小优化
public class ResourceFactory {
public static Resource createResource(String prefix){
if("http".equals(prefix)){
return new Resource("http");
}
if("ftp".equals(prefix)){
return new Resource("ftp");
}
if("classpath".equals(prefix)){
return new Resource("classpath");
}
if("file".equals(prefix)){
return new Resource("file");
}
return null;
}
}
public class ResourceLoader {
/**
* 加载对应的资源
*/
public Resource load(String url){
// 解析出对应的前缀
String prefix = parsePrefix(url);
/*
处理不同前缀的资源加载
*/
return ResourceFactory.createResource(prefix);
}
/**
* 解析url的前缀
* url的前缀可能是:http、ftp、classpath、file
* 伪代码
*/
private String parsePrefix(String url){
return url;
}
}
这样呢,将类的职责单一化,更利于理解和维护。上述的做法其实就是创建了一个静态工厂类。
但是,上述代码还是不符合开闭原则:对修改关闭,扩展开放。
说句实话,如果真的代码不是很复杂,且业务变动不频繁,改点代码无所谓的。但是为了学习而学习嘛,所以继续改造一下:
工厂方法 —— 迈出了一大步
public interface IResourceLoader {
/**
* 加载资源
*/
Resource load(String url);
}
public class HTTPResourceLoader implements IResourceLoader{
@Override
public Resource load(String url) {
return null;
}
}
public class FTPResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
return null;
}
}
public class ClasspathResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
return null;
}
}
public class FileResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
return null;
}
}
之前是一大个静态工厂负责全部 Resource对象 的创建,现在搞了几个更具体的工厂,负责不同 Resource对象 的创建。
不过,其实你可以发现,我们的if else
还是没有去掉:
public Resource load(String url){
// 解析出对应的前缀
String prefix = parsePrefix(url);
/*
处理不同前缀的资源加载
*/
if("http".equals(prefix)){
return new HTTPResourceLoader().load(url);
}
if("ftp".equals(prefix)){
return new FTPResourceLoader().load(url);
}
if("classpath".equals(prefix)){
return new ClasspathResourceLoader().load(url);
}
if("file".equals(prefix)){
return new FileResourceLoader().load(url);
}
return null;
}
细心观察可以发现,不就是根据前缀去取对应的loader,然后执行对应的load方法嘛?
优化一下:
private static final Map<String,IResourceLoader> LOADER_MAP = new HashMap<>();
static {
LOADER_MAP.put("http",new HTTPResourceLoader());
LOADER_MAP.put("ftp",new FTPResourceLoader());
LOADER_MAP.put("classpath",new ClasspathResourceLoader());
LOADER_MAP.put("file",new FileResourceLoader());
}
/**
* 加载对应的资源
*/
public Resource load(String url){
// 解析出对应的前缀
String prefix = parsePrefix(url);
/*
处理不同前缀的资源加载
*/
return LOADER_MAP.get(prefix).load(url);
}
可是,还是要改代码哇,确实,那我们来改配置文件嘛:
http: 类路径
ftp: 类路径
classpath: 类路径
file: 类路径
然后,static 静态代码块中去读取配置文件中内容,然后利用反着获取对应的工厂实例,添加到LOADER_MAP
中,即可。
当然,这样也是有些麻烦的,后面笔者会以自身的一个社区项目来演示在spring中如何搞出一个经典的工厂方法。
回顾一下,上文中,我们的 Resource 只是一个简简单单的产品类,实际上,可能有各式各样不同的产品,所以我们可以对产品类也进行抽象:
public abstract class AbstractResource {
private String url;
public AbstractResource(){}
public AbstractResource(String url){
this.url = url;
}
protected void common(){
System.out.println("公共方法,演示用");
}
public abstract InputStream getInputStream();
}
public class HTTPResource extends AbstractResource{
@Override
public InputStream getInputStream() {
return null;
}
}
// 其他产品实现类省略
public interface IResourceLoader {
/**
* 加载资源
*/
AbstractResource load(String url);
}
public class HTTPResourceLoader implements IResourceLoader{
@Override
public AbstractResource load(String url) {
return new HTTPResource();
}
}
public AbstractResource load(String url){
// 解析出对应的前缀
String prefix = parsePrefix(url);
/*
处理不同前缀的资源加载
*/
return LOADER_MAP.get(prefix).load(url);
}
现在不同的工厂就创建自己的产品,美滋滋。
(许多时候,我们就是会使用一些通用的东西,如果需要使用某个产品特殊的能力,那么也可以进行对应的类型强转)
能学到这里,工程模式基本上差不多了,不过我们还是要学习一下抽象工厂:
抽象工厂 —— 质的飞跃
这里就不准备写代码了,文字描述一下吧。
上文总,我们有不同类型的资源,FileResource、HTTPResource等,如果业务复杂一点,需要进行更加细化的区分,比如说 VideoResource、TextResource等,难道我们还要去写对应的工厂类嘛?
我们会发现,VideoResource、TextResource等资源包含在FileResource、HTTPResource内,说白了就是文件资源中会有视频资源或者文本资源,从http上也能获取这两种资源,可以理解为是这些资源的子集。
所以,我们可以先对产品进行抽象,搞出不同的抽象类出来,比如说,VideoAbstractResource、TextAbstractResource等,这些抽象类继承AbstractResource抽象类,规范一点,AbstractResource抽象类实现一个Resource接口等。
产品抽象完之后,我们开始动工厂:
public interface IResourceLoader {
/**
* 加载文本资源
*/
AbstractResource loadTextResource(String url);
/**
* 加载音视频资源
*/
AbstractResource loadVideoResource(String url);
}
对应的工厂类实现即可,然后根据业务处理书写自己的逻辑。
赠送:社区项目中的工厂模式
设计背景
社区中有一个刷题服务,在库表设计的时候,选择将题目主体和题目选项进行分离,因为不同的题型(单选、多选、判断、简答)它们的选项内容是不一样的,简答还没有选项。
无论是题目的增、删、改、查、对应的选项内容都要进行相应的变动,按照常规的逻辑就是:
if(题目类型 == '单选'){
...
}else if(题目类型 == '多选'){
...
}else if(题目类型 == '判断'){
...
}else if(题目类型 == ''){
...
}
题目的增、删、改、查接口都要这么写的话,我的天,代码太膨胀了,十分不利于维护,而且题目的CRUD还设计到多张表的操作,所以十分有必要进行相关逻辑的解耦。
工厂 + 策略的改造
/**
* 题型处理接口
* @author kaiven
*/
public interface SubjectTypeHandler {
/**
* 获取自身的题目处理类型
*/
SubjectTypeEnum getSubjectType();
/**
* 选项信息查询函数
*/
List<SubjectOption> queryHandle(Integer subjectId);
/**
* 新增选项列表
*/
boolean addHandle(List<SubjectOption> subjectOptionList, SubjectInfo subjectInfo);
/**
* 删除选项列表
*/
void removeHandle(Integer infoId);
}
这里定义了统一的题型工厂接口,不同的题型策略类可以去实现自己的逻辑,没什么好说的。
@Component
public class SubjectTypeFactory implements InitializingBean {
@Autowired
private List<SubjectTypeHandler> handlerList;
private final Map<SubjectTypeEnum, SubjectTypeHandler> handlerMap;
{
handlerMap = new HashMap<>();
}
public SubjectTypeHandler getHandlerByType(Integer type) {
if(type == null || type <= 0){
throw new IllegalArgumentException("type参数错误");
}
SubjectTypeEnum subjectType = SubjectTypeEnum.getSubjectTypeByCode(type);
if(subjectType == null){
throw new BusinessException("无此题目类型");
}
return handlerMap.get(subjectType);
}
@Override
public void afterPropertiesSet() throws Exception {
for(SubjectTypeHandler handler : handlerList) {
handlerMap.put(handler.getSubjectType(), handler);
}
}
}
这就是结合spring的经典工厂写法,可以仔细体会一下:
@Autowired
private List<SubjectTypeHandler> handlerList;
自动注入所有的策略类,
public class SubjectTypeFactory implements InitializingBean {
@Autowired
private List<SubjectTypeHandler> handlerList;
private final Map<SubjectTypeEnum, SubjectTypeHandler> handlerMap;
{
handlerMap = new HashMap<>();
}
@Override
public void afterPropertiesSet() throws Exception {
for(SubjectTypeHandler handler : handlerList) {
handlerMap.put(handler.getSubjectType(), handler);
}
}
}
实现InitializingBean
接口,在Bean初始化的过程中,执行afterPropertiesSet
函数,这个函数就是将所有的策略类注入到map中去。
总结
学习策略模式,是为了学习而去学习,多找一些例子,自己写一下,想要在实际的业务中运用,还是要多写代码,多思考业务逻辑,多考虑,这玩意儿就是经验的积累。
2024.12.31
writeBy kaiven