工厂模式分为三种更加细分的类型:简单工厂、工厂方法和抽象工厂。
简单工厂叫作静态工厂方法模式(Static Factory Method Pattern)。
假设一个场景,需要一个资源加载器,要根据不同的url进行资源加载,但是如果将所有的加载实现代码全部封装在了一个load方法中,就会导致一个类很大,同时扩展性也非常差,当想要添加新的前缀解析其他类型的url时,发现需要修改大量的源代码,
代码如下:
定义两个需要之后会用到的类:
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Resource {
private String url;
}
public class ResourceLoadException extends RuntimeException{
public ResourceLoadException() {
super("加载资源是发生问题。");
}
public ResourceLoadException(String message) {
super(message);
}
}
源码如下:
public class ResourceLoader {
public Resource load(String filePath) {
String type = getPrefix(filePath);
Resource resource = null;
if("http".equals(type)){
// ..发起请求下载资源... 可能很复杂
return new Resource(url);
} else if ("file".equals(type)) {
// ..建立流,做异常处理等等
return new Resource(url);
} else if ("classpath".equals(type)) {
// ...
return new Resource(url);
} else {
return new Resource("default");
}
return resource;
}
private String getPrefix(String url) {
if(url == null || "".equals(url) || !url.contains(":")){
throw new ResourceLoadException("此资源url不合法.");
}
String[] split = url.split(":");
return split[0];
}
}
在此案例中,存在很多的if分支,如果分支数量不多,且不需要扩展,这样的编写方式当然没错,然而在实际的工作场景中,业务代码可能会很多,分支逻辑也可能十分复杂,这个时候简单工厂设计模式就要发挥作用了。
不管有多少个分支逻辑,本质就是一个,创造一个资源产品,只需要创建一个工厂类,将创建资源的能力交给工厂即可:
public class ResourceFactory {
public static Resource create(String type,String url){
if("http".equals(type)){
// ..发起请求下载资源... 可能很复杂
return new Resource(url);
} else if ("file".equals(type)) {
// ..建立流,做异常处理等等
return new Resource(url);
} else if ("classpath".equals(type)) {
// ...
return new Resource(url);
} else {
return new Resource("default");
}
}
}
有了上边的工厂类,将【创建资源产品】这个单一的能力赋予产品工厂,这样能更好的符合单一原则。有了工厂之后,主要逻辑就会简化:
public class ResourceLoader {
public Resource load(String url){
// 1、根据url获取前缀
String prefix = getPrefix(url);
// 2、根据前缀处理不同的资源
return ResourceFactory.create(prefix,url);
}
private String getPrefix(String url) {
if(url == null || "".equals(url) || !url.contains(":")){
throw new ResourceLoadException("此资源url不合法.");
}
String[] split = url.split(":");
return split[0];
}
}
这就是简单工厂设计模式,提取一个工厂类,工厂会根据传入的不同的类型,创建不同的产品,好处如下:
将创建对象的过程交给工厂类、其他业务需要某个产品时,直接使用create(方法名字不重要)创建即可这样的好处是:
1、工厂将创建的过程进行封装,不需要关系创建的细节,更加符合面向对象思想
2、这样主要的业务逻辑不会被创建对象的代码干扰,代码更易阅读
3、产品的创建可以独立测试,更将容易测试
4、独立的工厂类只负责创建产品,更加符合单一原则
q:但是有的人会想,如果需要修改或者添加新的功能,还是要修改源代码呀,这不符合开闭原则?
确实如此,但是原则这种东西,一定要结合业务创建,在创建对象的过程相对简单,业务改动不是很频繁的情况下,适当的不按原则出牌才是更好的选择,只是偶尔修改一下 ResourceLoaderFactory代码,稍微不符合开闭原则,也是完全可以接受的。因为这样可以更加简单的编码,在进行软件开发时编码难度也是一个很重要的考量标准。一定要在合理设计和过度设计之间进行权衡,明白一点,适合的才是最好的。
绝大部分工厂类都是以“Factory”单词结尾,但也不是必须的,比如 Java 中的 DateFormat、Calender。除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好。
工厂方法(Factory Method)
如果if分支逻辑不断膨胀,有变为肿瘤代码的可能,就有必要将 if 分支逻辑去掉,?比较经典的处理方法就是利用多态。按照多态的实现思路,对代码进行重构。为每一个 Resource 创建一个独立的工厂类,形成一个个小作坊,将每一个实例的创建过程交给工厂类完成,重构之后的代码如下所示:
之前是一个大而全的工厂,一个工厂需要创建不同的产品,工厂方法讲究的是工厂也要专而精,一个工厂只创建一种资源(产品),奔驰工厂只负责生产奔驰,宝马工厂只负责生产宝马。每一种url加载成不同的资源产品,那每一种资源都可以由一个独立的ResourceFactory生产。为了实现这一种场景,需要将生产资源的工厂类进行抽象:
public interface IResourceLoader {
Resource load(String url);
}
并为每一种资源创建与之匹配的实现:
public class ClassPathResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}
public class FileResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}
public class HttpResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}
public class FtpResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}
public class DefaultResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}
这就是工厂方法模式的典型代码实现。当新增一种读取资源的方式时,只需要新增一个实现,并实现 IResourceLoader 接口即可。所以,工厂方法模式比起简单工厂模式更加符合开闭原则。
当然现在觉得这没什么用,到时候使用的时候还不是需要如下的方式,这个工厂不就是脱裤子放屁吗?
public class ResourceLoader {
public Resource load(String url){
// 1、根据url获取前缀
String prefix = getPrefix(url);
ResourceLoader resourceLoader = null;
// 2、根据前缀选择不同的工厂,生产独自的产品
// 版本一
if("http".equals(prefix)){
resourceLoader = new HttpResourceLoader();
} else if ("file".equals(prefix)) {
resourceLoader = new FileResourceLoader();
} else if ("classpath".equals(prefix)) {
resourceLoader = new ClassPathResourceLoader()
} else {
resourceLoader = new DefaultResourceLoader();
}
return resourceLoader.load();
}
private String getPrefix(String filePath) {
if (filePath == null || "".equals(filePath)) {
throw new RuntimeException("The file path is illegal");
}
filePath = filePath.trim().toLowerCase();
String[] split = filePath.split(":");
if (split.length > 1) {
return split[0];
} else {
return "classpath";
}
}
}
此时为每个产品引入了工厂,发现需要为创建工厂这个行为付出代价,在创建工厂这件事上,仍然不符合开闭原则,为了解决上述的问题又不得不去创建一个工厂的缓存来统一管理工厂实例,以后使用工厂会更加的简单,代码如下:
private static Map<String,IResourceLoader> resourceLoaderCache = new HashMap<>(8);
// 版本二
static {
resourceLoaderCache.put("http",new HttpResourceLoader());
resourceLoaderCache.put("file",new FileResourceLoader());
resourceLoaderCache.put("classpath",new ClassPathResourceLoader());
resourceLoaderCache.put("default",new DefaultResourceLoader());
}
ResourceLoader的核心方法就可以简化成这个样子了:
public Resource load(String url){
// 1、根据url获取前缀
String prefix = getPrefix(url);
return resourceLoaderCache.get(prefix).load(url);
}
如果觉得还是不够,修改需求还是不够灵活,仍然需要修改static中的代码,可以这样做,搞一个配置文件如下,将我们的工厂类进行配置,如下:
http=com.ydlclass.factoryMethod.resourceFactory.impl.HttpResourceLoader
file=com.ydlclass.factoryMethod.resourceFactory.impl.FileResourceLoader
classpath=com.ydlclass.factoryMethod.resourceFactory.impl.ClassPathResourceLoader
default=com.ydlclass.factoryMethod.resourceFactory.impl.DefaultResourceLoader
这样可以在static中这样编写代码,完全满足开闭原则:
static {
InputStream inputStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("resourceLoader.properties");
Properties properties = new Properties();
try {
properties.load(inputStream);
for (Map.Entry<Object,Object> entry : properties.entrySet()){
String key = entry.getKey().toString();
Class<?> clazz = Class.forName(entry.getValue().toString());
IResourceLoader loader = (IResourceLoader) clazz.getConstructor().newInstance();
resourceLoaderCache.put(key,loader);
}
} catch (IOException | ClassNotFoundException | NoSuchMethodException | InstantiationException |
IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
以后想新增或删除一个resourceLoader只需要写一个类实现IResourceLoader接口,并在配置文件中进行配置即可。此时此刻已经看不到if-else的影子了。
实际产品可能是及其复杂的,同样需要对整个产品线进行抽象,为不同的
public abstract class AbstractResource {
private String url;
public AbstractResource(){}
public AbstractResource(String url) {
this.url = url;
}
protected void shared(){
System.out.println("这是共享方法");
}
/**
* 每个子类需要独自实现的方法
* @return 字节流
*/
public abstract InputStream getInputStream();
}
具体的产品需要继承这个抽象类:
public class ClasspathResource extends AbstractResource {
public ClasspathResource() {
}
public ClasspathResource(String url) {
super(url);
}
@Override
public InputStream getInputStream() {
return null;
}
}
其他产品同理,工厂类也需要面向产品的抽象进行编程了:
public class ClassPathResourceLoader implements IResourceLoader {
@Override
public AbstractResource load(String url) {
// 中间省略复杂的创建过程
return new ClasspathResource(url);
}
}
编写测试用例进行测试:
@Test
public void testFactoryMethod(){
String url = "file://D://a.txt";
ResourceLoader resourceLoader = new ClassPathResourceLoader();
AbstractResource resource = resourceLoader.load(url);
log.info("resource --> {}",resource.getClass().getName());
}
抽象工厂(Abstract Factory)
抽象工厂模式(Abstract Factory Pattern),该设计模式的应用场景比较特殊,他的重要性比不上简单工厂和工厂方法,其定义如下:
抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式生产需要的对象是一种不错的解决方案。
在简单工厂和工厂方法中,往往只需要创建一种类型的产品,但是如果需求改变,需要增加多种类型的产品,即增加产品族,上边的需求是创建各种类型的资源,本小节我们再增加一个维度,如图片资源、视频资源、文本资源等。
如果不停的增加产品维度,最后导致的结果就是产品数量不停的爆炸,以笛卡尔集的方式指数级增长,如下:
- 1、HttpPictureResource
- 2、HttpVideoResource
- 3、FilePictureResource
- 4、FileVideoResource
- ......
按照之前的逻辑,有5个产品,加3个维度,就会产生15个产品和15个产品工厂,类会迅速爆炸起来,这显然不合适。
待完成。。。。。