创建型设计模式(Creational Patterns) 这类模式主要关注对象的创建过程
✨单例模式 单例设计模式(Singleton)理解起来非常简单,就是一个类只允许创建一个对象或者实例,那么这个类就是一个单例类,这种模式就叫做单例设计模式,简称单例模式。
为什么要使用单例模式?
表示全局唯一
有些数据在系统中应该有且仅有只能保存一份,那就应该设计为单例类,如:配置类,全局计数器。
处理资源访问冲突
日志输出功能就是用单例模式实现的
如何实现一个单例? 在编写单例代码的时候需要注意以下4点:
构造器需要私有化
暴露一个公共的获取单例对象的接口
是否支持懒加载(延迟加载)
是否线程安全
常见的单例设计模式,有以下5种写法:
饿汉式 饿汉式的实现方法较为简单,在类加载的时候,instance静态实例就已经创建并初始化好了,所以,instance实例的创建过程是线程安全的。
1 2 3 4 5 6 7 8 9 10 public class EagerSingleton { private final static EagerSingleton INSTANCE = new EagerSingleton (); private EagerSingleton () {} public static EagerSingleton getInstance () { return INSTANCE; } }
懒汉式 1 2 3 4 5 6 7 8 9 10 public class LazySingleton { private static LazySingleton instance; private LazySingleton () {} public static LazySingleton getInstance () { if (null == instance){ instance = new LazySingleton (); } return instance; } }
懒汉式相对于饿汉式的优势是支持延迟加载,但是上面这个写法,线程不安全,很有可能会有超过一个线程同时执行了new LazySingleton(),由此引出双重检查锁
双重检查锁 第一次创建需要上锁,一旦创建好了,就不需要再上锁,这种写法既可以满足延迟加载,也可以满足线程安全,推荐使用这种单例设计模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class DclSingleton { private volatile static DclSingleton instance; private DclSingleton () {} public static DclSingleton getInstance () { if (null == instance){ synchronized (DclSingleton.class){ if (null == instance){ instance = new DclSingleton (); } } } return instance; } }
静态内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 public class InnerSingleton { private InnerSingleton () {} public static InnerSingleton getInstance () { return InnerSingletonHolder.INSTANCE; } private static class InnerSingletonHolder { private final static InnerSingleton INSTANCE = new InnerSingleton (); } }
InnerSingletonHolder是一个静态内部类,当外部类 InnerSingleton被加载的时候,并不会创建 InnerSingletonHolder实例对象。只有当调用 getInstance() 方法时,InnerSingletonHolder才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
枚举 基于枚举类型的单例是一种最简单的实现方式,这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性,还能防止反序列化重新创建新的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Singleton { private Singleton () {} public static enum EnumSingleton { INSTANCE; private Singleton singleton = null ; private EnumSingleton () { singleton = new Singleton (); } public Singleton getSingleton () { return singleton; } } }
反射入侵 事实上,我们想要阻止其他人构造实例仅仅私有化构造器还是不够的,因为我们还可以使用反射获取私有构造器进行构造,当然使用枚举的方式是可以解决这个问题的,对于其他的书写方案,可以在类的构造方法中添加个if判断
1 2 3 4 5 6 7 if (instance != null ){ throw new RuntimeException ("该对象是单里的,无法创建多个" ); return instance; }
序列化与反序列化安全 到目前为止,其实还没实现真正意义上的单例,看下面例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testSerialize () throws IllegalAccessException,NoSuchMethodException,IOException,ClassNotFoundException { Singleton singleton = Singleton.getInstance(); FileOutputStream fout = new FileOutputStream ("D://singleton.txt" ); ObjectOutputStream out = new ObjectOutputStream (fout); out.writeObject(singleton); FileInputStream fin = new FileInputStream ("D://singleton.txt" ); ObjectInputStream in = new ObjectInputStream (fin); Object o = in.readObject(); System.out.println("他们是同一个实例吗?{}" ,o == singleton) }
解决方法:readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在,需要在类中添加这么一个方法:
1 2 3 4 public Object readResolve () { return singleton; }
✨工厂方法模式 简单工厂 简单工厂模式(也称为静态工厂模式)是最简单的工厂模式,它由一个工厂类负责创建其他类的对象。调用者只需传递给工厂类一个参数,工厂类便可根据这个参数动态实例化一个对象。这种模式可以有效地隐藏类之间的关系,提高代码的可维护性和可扩展性。
在看工厂方法模式之前,先来看下简单工厂模式,现在有一个场景,我们需要一个资源加载器,他要根据不用的url进行资源加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 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) { String type = getPrefix(url); return ResourceFactory.create(type,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(方法名字不重要)创建即可这样的好处是:
工厂将创建的过程进行封装,不需要关系创建的细节,更加符合面向对象思想
这样主要的业务逻辑不会被创建对象的代码干扰,代码更易阅读
产品的创建可以独立测试,更将容易测试
独立的工厂类只负责创建产品,更加符合单一原则
工厂方法 工厂方法模式(也称为多态性工厂模式)更具灵活性和可扩展性。工厂方法模式将对象的创建过程分散到各个子类中,每个子类只负责创建特定的对象,从而实现了开放封闭原则。此外,由于将实例化逻辑分散到子类中,工厂方法模式更加符合面向对象设计的原则,可以更好地支持单元测试和扩展。
接上面例子,如果有一天,我们的if分支逻辑不断膨胀,有变为肿瘤代码的可能,就有必要将 if 分支逻辑去掉,那又该怎么办呢?比较经典的处理方法就是利用多态。按照多态的实现思路,对上面的代码进行重构。我们会为每一个Resource 创建一个独立的工厂类,形成一个个小作坊,将每一个实例的创建过程交给工厂类完成,重构之后的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 public interface IResourceLoader { Resource load (String url) ; } public class HttpResourceLoader 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 ClassPathResourceLoader 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); } } public class ResourceLoader { 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 ()); } public Resource load (String url) { String prefix = getPrefix(url); return resourceLoaderCache.get(prefix).load(url); } private String getPrefix (String url) { if (url == null || "" .equals(url) || !url.contains(":" )){ throw new ResourceLoadException ("此资源url不合法." ); } String[] split = url.split(":" ); return split[0 ]; } }
这样子修改完,代码看起来比第一个版本舒服多了,但是以后如果要新增一种产品,那是不是就要在static代码块里修改代码了,这又不符合开闭原则了,为此,可以搞一个配置文件,将我们的工厂类进行配置,如下:
1 2 3 4 5 //新建一个配置文件,叫resourceLoader.properties吧 http =HttpResourceLoader的路径 file =FileResourceLoader的路径 classpath =ClassPathResourceLoader的路径 default =DefaultResourceLoader的路径
修改静态代码块的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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的影子了且完全满足开闭原则。
总结
简单工厂模式适用于创建数量较少的对象,工厂方法模式适用于创建更多的对象
工厂方法模式比起简单工厂模式更加符合开闭原则