前言
本文旨在快速梳理常用的设计模式,了解每个模式主要针对的是哪些情况以及其基础特征,每个模式前都有列举出一个或多个可以深入阅读的参考网页,以供读者详细了解其实现。
分为三篇文章:
- 上篇:设计模式基础理念和创建型设计模式
- 中篇:行为型设计模式
- 下篇:结构型设计模式
快速回忆
创建型
- 单例(Singleton)
- 工厂模式
- 简单工厂(Simple Factory)
- 工厂方法(Factory Method)
- 抽象工厂(Abstract Factory)
- 生成器(Builder)
- 原型模式(Prototype)
理念
首先搞清楚一点,设计模式不是高深技术,不是奇淫技巧。设计模式只是一种设计思想,针对不同的业务场景,用不同的方式去设计代码结构,其最最本质的目的是为了解耦,延伸一点的话,还有为了可扩展性和健壮性,但是这都是建立在解耦的基础之上。
高内聚低耦合
高内聚:系统中A、B两个模块进行交互,如果修改了A模块,不影响模块B的工作,那么认为A有足够的内聚。
低耦合:就是A模块与B模块存在依赖关系,那么当B发生改变时,A模块仍然可以正常工作,那么就认为A与B是低耦合的。
创建型
单例模式
意图
确保一个类只有一个实例,并提供该实例的全局访问点。
类图
使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。
私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。
实现
懒汉式(延迟实例化)—— 线程安全/双重校验
一.私有化构造函数
二.声明静态单例对象
三.构造单例对象之前要加锁(lock一个静态的object对象)或者方法上加synchronized。
四.需要两次检测单例实例是否已经被构造,分别在锁之前和锁之后
使用lock(obj)
public class Singleton { private Singleton() {} //关键点0:构造函数是私有的 private volatile static Singleton single; //关键点1:声明单例对象是静态的 private static object obj= new object(); public static Singleton GetInstance() //通过静态方法来构造对象 { if (single == null) //关键点2:判断单例对象是否已经被构造 { lock(obj) //关键点3:加线程锁 { if(single == null) //关键点4:二次判断单例是否已经被构造 { single = new Singleton(); } } } return single; } } 复制代码
使用synchronized (Singleton.class)
public class Singleton { private Singleton() {} private volatile static Singleton uniqueInstance; public static Singleton getUniqueInstance() { if (uniqueInstance == null) { synchronized (Singleton.class) { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } } } return uniqueInstance; } } 复制代码
可能提问
0.为何要检测两次?
如果两个线程同时执行 if 语句,那么两个线程就会同时进入 if 语句块内。虽然在if语句块内有加锁操作,但是两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,也就是说会进行两次实例化,从而产生了两个实例。因此必须使用双重校验锁,也就是需要使用两个 if 语句。
1.构造函数能否公有化?
不行,单例类的构造函数必须私有化,单例类不能被实例化,单例实例只能静态调用。
2.lock住的对象为什么要是object对象,可以是int吗?
不行,锁住的必须是个引用类型。如果锁值类型,每个不同的线程在声明的时候值类型变量的地址都不一样,那么上个线程锁住的东西下个线程进来会认为根本没锁。
3.uniqueInstance 采用 volatile 关键字修饰
uniqueInstance = new Singleton(); 这段代码其实是分为三步执行。
分配内存空间 初始化对象 将 uniqueInstance 指向分配的内存地址 复制代码
但是由于 JVM 具有指令重排的特性,有可能执行顺序变为了 1>3>2
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton(){} public static Singleton getInstance(){ if(uniqueInstance == null){ // B线程检测到uniqueInstance不为空 synchronized(Singleton.class){ if(uniqueInstance == null){ uniqueInstance = new Singleton(); // A线程被指令重排了,刚好先赋值了;但还没执行完构造函数。 } } } return uniqueInstance;// 后面B线程执行时将引发:对象尚未初始化错误。 } } 复制代码
饿汉式-线程安全
线程不安全问题主要是由于 uniqueInstance 被实例化了多次,如果 uniqueInstance 采用直接实例化的话,就不会被实例化多次,也就不会产生线程不安全问题。但是直接实例化的方式也丢失了延迟实例化带来的节约资源的优势。
private static Singleton uniqueInstance = new Singleton(); 复制代码
静态内部类实现
当 Singleton 类加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance()
方法从而触发 SingletonHolder.INSTANCE
时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例。
这种方式不仅具有延迟初始化的好处,而且由虚拟机提供了对线程安全的支持。
public class Singleton { private Singleton() {} private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getUniqueInstance() { return SingletonHolder.INSTANCE; } } 复制代码
枚举实现
这是单例模式的最佳实践,它实现简单,并且在面对复杂的序列化或者反射攻击的时候,能够防止实例化多次。
public enum Singleton { uniqueInstance; } 复制代码
考虑以下单例模式的实现,该 Singleton 在每次序列化的时候都会创建一个新的实例,为了保证只创建一个实例,必须声明所有字段都是 transient,并且提供一个 readResolve() 方法。
public class Singleton implements Serializable { private static Singleton uniqueInstance; private Singleton() { } public static synchronized Singleton getUniqueInstance() { if (uniqueInstance == null) { uniqueInstance = new Singleton(); } return uniqueInstance; } } 复制代码
如果不使用枚举来实现单例模式,会出现反射攻击,因为通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象。如果要防止这种攻击,需要在构造函数中添加防止实例化第二个对象的代码。
从上面的讨论可以看出,解决序列化和反射攻击很麻烦,而枚举实现不会出现这两种问题,所以说枚举实现单例模式是最佳实践。
使用场景
- Logger Classes
- Configuration Classes
- Accesing resources in shared mode
- Factories implemented as Singletons
简单/静态工厂(Simple Factory)
定义
在创建一个对象时不向客户暴露内部细节,并提供一个创建对象的通用接口。
在简单工厂模式中,可以根据参数的不同返回不同类的实例。
简单工厂模式专门定义一个类来负责创建其他类的实例
结构
简单工厂模式包含如下角色:
- Factory:工厂角色
工厂角色负责实现创建所有实例的内部逻辑
- Product:抽象产品角色
抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口
- ConcreteProduct:具体产品角色
具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例。
实现
public class Test { public static void main(String[] args) { String loginType = "password"; String name = "name"; String password = "password"; Login login = LoginManager.factory(loginType); boolean bool = login.verify(name, password); if (bool) { /** * 业务逻辑 */ } else { /** * 业务逻辑 */ } } } 复制代码
优缺点
优点
构造容易,逻辑简单。
缺点
1.简单工厂模式中的if else判断非常多,完全是Hard Code,如果有一个新产品要加进来,就要同时添加一个新产品类,并且必须修改工厂类,再加入一个 else if 分支才可以, 这样就违背了 “开放-关闭原则“中的对修改关闭的准则了。
2.一个工厂类中集合了所有的类的实例创建逻辑,违反了高内聚的责任分配原则,将全部的创建逻辑都集中到了一个工厂类当中,所有的业务逻辑都在这个工厂类中实现。什么时候它不能工作了,整个系统都会受到影响。因此一般只在很简单的情况下应用,比如当工厂类负责创建的对象比较少时。
3.简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
适用环境
工厂类负责创建的对象比较少:由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
JDK
①JDK类库中广泛使用了简单工厂模式,如工具类java.text.DateFormat,它用于格式化一个本地日期或者时间。
public final static DateFormat getDateInstance(); public final static DateFormat getDateInstance(int style); public final static DateFormat getDateInstance(int style,Locale locale); 复制代码
②Java加密技术 获取不同加密算法的密钥生成器:
KeyGenerator keyGen=KeyGenerator.getInstance("DESede"); 复制代码
创建密码器:
Cipher cp = Cipher.getInstance("DESede"); 复制代码
工厂方法(Factory Method)
意图
又称为工厂模式/虚拟构造器(Virtual Constructor)模式/多态工厂(Polymorphic Factory)模式
即通过工厂子类来确定究竟应该实例化哪一个具体产品类。
使用动机
不再设计一个按钮工厂类来统一负责所有产品的创建,而是将具体按钮的创建过程交给专门的工厂子类去完成。
我们先定义一个抽象的按钮工厂类,再定义具体的工厂类来生成圆形按钮、矩形按钮、菱形按钮等,它们实现在抽象按钮工厂类中定义的方法。这种抽象化的结果使这种结构可以在不修改具体工厂类的情况下引进新的产品,如果出现新的按钮类型,只需要为这种新类型的按钮创建一个具体的工厂类就可以获得该新按钮的实例,这一特点无疑使得工厂方法模式具有超越简单工厂模式的优越性,更加符合“开闭原则”。
角色
- Product:抽象产品,工厂方法模式所创建的对象的超类,也就是所有产品类的共同父类或共同拥有的接口。在实际的系统中,这个角色也常常使用抽象类实现。
- ConcreteProduct:具体产品,这个角色实现了抽象产品(Product)所声明的接口,工厂方法模式所创建的每一个对象都是某个具体产品的实例。
- Factory:抽象工厂,担任这个角色的是工厂方法模式的核心,任何在模式中创建对象的工厂类必须实现这个接口。在实际的系统中,这个角色也常常使用抽象类实现。
- ConcreteFactory:具体工厂,担任这个角色的是实现了抽象工厂接口的具体Java类。具体工厂角色含有与业务密切相关的逻辑,并且受到使用者的调用以创建具体产品对象。
客户端调用
static void Main(string[] args) { //先给我来个灯泡 ICreator creator = new BulbCreator(); ILight light = creator.CreateLight(); light.TurnOn(); light.TurnOff(); //再来个灯管看看 creator = new TubeCreator(); light = creator.CreateLight(); light.TurnOn(); light.TurnOff(); } 复制代码
优缺点
优点
①在工厂方法模式中,工厂方法用来创建客户所需要的产品,同时还向客户隐藏了哪种具体产品类将被实例化这一细节,用户只需要关心所需产品对应的工厂,无须关心创建细节,甚至无须知道具体产品类的类名。
②工厂方法模式之所以又被称为多态工厂模式,是因为所有的具体工厂类都具有同一抽象父类。
③使用工厂方法模式的另一个优点是在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”,这点比简单工厂模式更优秀。
缺点
①在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。
②由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到DOM、反射等技术,增加了系统的实现难度。
JDK
- JDBC中的工厂方法:
Connection conn=DriverManager.getConnection("jdbc:microsoft:sqlserver://localhost:1433; DatabaseName=DB;user=sa;password="); Statement statement=conn.createStatement(); ResultSet rs=statement.executeQuery("select * from UserInfo"); 复制代码
- java.util.Calendar
- java.util.ResourceBundle
- java.text.NumberFormat
- java.nio.charset.Charset
- java.net.URLStreamHandlerFactory
- java.util.EnumSet
- javax.xml.bind.JAXBContext
抽象工厂(Abstract Factory)
定义
产品等级结构 :产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。
产品族 :在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中。
抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形态。
抽象工厂模式与工厂方法模式最大的区别
工厂方法模式针对的是一个产品等级结构,而抽象工厂模式则需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建 。
角色
抽象工厂模式包含如下角色:
- AbstractFactory:抽象工厂
- ConcreteFactory:具体工厂
- AbstractProduct:抽象产品
- Product:具体产品
实现
抽象产品: 苹果系列
public interface Apple { void AppleStyle(); } 复制代码
具体产品:iphone
public class iphone implements Apple { public void AppleStyle() { Console.WriteLine("Apple's style: iPhone!"); } } 复制代码
抽象工厂
public interface Factory { Apple createAppleProduct(); Sumsung createSumsungProduct(); } 复制代码
手机工厂
public class Factory_Phone implements Factory { public Apple createAppleProduct() { return new iphone(); } public Sumsung createSumsungProduct() { return new note2(); } } 复制代码
调用
public static void Main(string[] args) { //采购商要一台iPad和一台Tab Factory factory = new Factory_Pad(); Apple apple = factory.createAppleProduct(); apple.AppleStyle(); Sumsung sumsung = factory.createSumsungProduct(); sumsung.BangziStyle(); //采购商又要一台iPhone和一台Note2 factory = new Factory_Phone(); apple = factory.createAppleProduct(); apple.AppleStyle(); sumsung = factory.createSumsungProduct(); sumsung.BangziStyle(); } 复制代码
优缺点
优点
①应用抽象工厂模式可以实现高内聚低耦合的设计目的,因此抽象工厂模式得到了广泛的应用。
②增加新的具体工厂和产品族很方便,因为一个具体的工厂实现代表的是一个产品族,无须修改已有系统,符合“开闭原则”。
缺点
开闭原则的倾斜性(增加新的工厂和产品族容易,增加新的产品等级结构麻烦)
适用环境
在以下情况下可以使用抽象工厂模式:
①一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是重要的。
②系统中有多于一个的产品族,而每次只使用其中某一产品族。(与工厂方法模式的区别)
③属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。
④系统提供一个产品类的库,所有的产品以同样的接口出现,从而使客户端不依赖于具体实现。
JDK
- javax.xml.parsers.DocumentBuilderFactory
- javax.xml.transform.TransformerFactory
- javax.xml.xpath.XPathFactory
生成器(Builder)
意图
封装一个对象的构造过程,并允许按步骤构造。
实现
实现代码见参考链接
一水杯工厂要生产各式各样的水杯,无论杯子是神马造型,但都包括绳子,帽子和杯体。以此模型创建各种类型的杯子。
JDK
- java.lang.StringBuilder
- java.nio.ByteBuffer
- java.lang.StringBuffer
- java.lang.Appendable
- Apache Camel builders
原型模式(Prototype)
意图
通过一个已经存在的对象,复制出更多的具有与此对象具有相同类型的新的对象。
浅复制 clone():
我们知道,一个类的定义中包括属性和方法。属性用于表示对象的状态,方法用于表示对象所具有的行为。其中,属性既可以是Java中基本数据类型,也可以是引用类型。
Java中的浅复制通常使用 clone() 方式完成。
当进浅复制时,clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。同时,复制出来的对象具有与原对象一致的状态。
此处对象一致的状态是指:复制出的对象与原对象中的属性值完全相等==。
深复制 deepclone():
Java中的深复制一般是通过对象的序列化和反序列化得以实现。序列化时,需要实现Serializable接口。
从输出结果中可以看出,深复制不仅在堆内存上开辟了空间以存储复制出的对象,甚至连对象中的引用类型的属性所指向的对象也得以复制,重新开辟了堆空间存储。
JDK
- java.lang.Object#clone()
- deepclone()