设计模式
文章目录
六个创建型模式
工厂模式的作用
为什么不直接使用new来创建对象
使用new创建对象时,创建对象和使用对象的职责耦合在一起。如果需要创建另一种对象,就需要修改创建对象的代码,违反了开闭原则。
如何解决
将创建职责分离出来,利用工厂来创建对象。
好处
- 在所有的工厂模式中,我们都强调一点:**两个类**A**和**B**之间的关系应该仅仅是**A**创建**B**或是*A*使用*B*,而不能两种关系都有。将对象的创建和使用分离,也使得系统更加符合“单一职责原则”,有利于对功能的复用和系统的维护。
- 此外,将对象的创建和使用分离还有一个好处:****防止用来实例化一个类的数据和代码在多个类中到处都是,可以将有关创建的知识搬移到一个工厂类中****
- 如果将对象的创建过程封装在工厂类中,我们****可以提供一系列名字完全不同的工厂方法,每一个工厂方法对应一个构造函数****,客户端可以以一种更加可读、易懂的方式来创建对象。
工厂模式的演进
简单工厂模式和工厂模式较为简单,初期一般使用这两种工厂模式,后期可以考虑使用抽象工厂模式进行替换。
1. 简单工厂模式(难度:⭐⭐,频率:⭐⭐⭐)
不属于Gof 23种设计模式中的一种,用于入门。
1.1 为什么需要简单工厂
eg:
class Chart {
private String type; //图表类型
public Chart(Object[][] data, String type) {
this.type = type;
if (type.equalsIgnoreCase("histogram")) {
//初始化柱状图
}
else if (type.equalsIgnoreCase("pie")) {
//初始化饼状图
}
else if (type.equalsIgnoreCase("line")) {
//初始化折线图
}
}
public void display() {
if (this.type.equalsIgnoreCase("histogram")) {
//显示柱状图
}
else if (this.type.equalsIgnoreCase("pie")) {
//显示饼状图
}
else if (this.type.equalsIgnoreCase("line")) {
//显示折线图
}
}
}
存在的问题:
- if /else块多,代码冗长,过多判断影响效率
- 类的职责过重,违背单一职责原则(既使用又创建)
- 添加新类需要修改条件语句,违法开闭原则
- 客户端只能通过new关键字创建对象,且需明确知道type类型,耦合度较高。
- 重复的初始化操作(如设置柱状图颜色,高度等)
1.2 简单工厂模式的描述
简单工厂模式(Simple Factory Pattern):定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因此简单工厂模式又被称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。
简单工厂模式的要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节。
class Factory {
//静态工厂方法
public static Product getProduct(String arg) {
Product product = null;
if (arg.equalsIgnoreCase("A")) {
product = new ConcreteProductA();
//初始化设置product
}
else if (arg.equalsIgnoreCase("B")) {
product = new ConcreteProductB();
//初始化设置product
}
return product;
}
}
1.3 问题解决
//抽象图表接口:抽象产品类
interface Chart {
public void display();
}
//柱状图类:具体产品类
class HistogramChart implements Chart {
public HistogramChart() {
System.out.println("创建柱状图!");
}
public void display() {
System.out.println("显示柱状图!");
}
}
//饼状图类:具体产品类
class PieChart implements Chart {
public PieChart() {
System.out.println("创建饼状图!");
}
public void display() {
System.out.println("显示饼状图!");
}
}
//折线图类:具体产品类
class LineChart implements Chart {
public LineChart() {
System.out.println("创建折线图!");
}
public void display() {
System.out.println("显示折线图!");
}
}
//图表工厂类:工厂类
class ChartFactory {
//静态工厂方法
public static Chart getChart(String type) {
Chart chart = null;
if (type.equalsIgnoreCase("histogram")) {
chart = new HistogramChart();
System.out.println("初始化设置柱状图!");
}
else if (type.equalsIgnoreCase("pie")) {
chart = new PieChart();
System.out.println("初始化设置饼状图!");
}
else if (type.equalsIgnoreCase("line")) {
chart = new LineChart();
System.out.println("初始化设置折线图!");
}
return chart;
}
}
1.4 小结
- 优化
- 为了避免频繁更改客户端参数,可以将参数存到配置文件中。
- 有时可以简化,将抽象产品类和工厂类合并
- 优点
- 客户端只需消费产品,实现了对象创建和使用的分离
- 客户端无需知道具体产品类的细节
- 可以通过引入配置文件的方式提高系统灵活性
- 缺点
- 工厂类集中了所有产品的创建逻辑,职责过重
- 增加系统类的个数,增加了复杂性和理解难度
- 扩展困难,需要频繁修改工厂逻辑
- 由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构
- 适用场景
- 工厂类需要创建的对象较少
- 客户端只知道传入工厂类的参数,对于如何创建对象并不关心
2. 工厂方法模式(难度:⭐⭐,频率:⭐⭐⭐⭐⭐)
2.1 为什么需要工厂方法模式
简单工厂模式的问题:
当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?
2.2 工厂方法模式描述
在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。
工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。
2.3 问题解决
//日志记录器接口:抽象产品
interface Logger {
public void writeLog();
}
//数据库日志记录器:具体产品
class DatabaseLogger implements Logger {
public void writeLog() {
System.out.println("数据库日志记录。");
}
}
//文件日志记录器:具体产品
class FileLogger implements Logger {
public void writeLog() {
System.out.println("文件日志记录。");
}
}
//日志记录器工厂接口:抽象工厂
interface LoggerFactory {
public Logger createLogger();
}
//数据库日志记录器工厂类:具体工厂
class DatabaseLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//连接数据库,代码省略
//创建数据库日志记录器对象
Logger logger = new DatabaseLogger();
//初始化数据库日志记录器,代码省略
return logger;
}
}
//文件日志记录器工厂类:具体工厂
class FileLoggerFactory implements LoggerFactory {
public Logger createLogger() {
//创建文件日志记录器对象
Logger logger = new FileLogger();
//创建文件,代码省略
return logger;
}
}
反射与配置文件
利用反射生成对象。
//通过类名生成实例对象并将其返回
Class c=Class.forName("String");
Object obj=c.newInstance();
return obj;
由于很多设计模式都关注系统的可扩展性和灵活性,因此都定义了抽象层,在抽象层中声明业务方法,而将业务方法的实现放在实现层中。
2.4 小结
工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足。工厂方法模式是使用频率最高的设计模式之一,是很多开源框架和API类库的核心模式。
- 扩展
- 工厂方法重载
- 工厂方法隐藏(对客户端隐藏)
- 优点
- 简单工厂模式的优点:只需关心工厂
- 多态性设计
- 扩展时无需修改原有代码
- 缺点
- 具体工厂类和具体产品类,增加了系统的复杂度和系统开销
- 客户端针对抽象编程,增加理解难度
- 反射,DOM的技术增加了系统的实现难度
- 适用场景
- 客户端不知道它所需要的对象的类。
- 需要频繁扩展
3. 抽象工厂模式(难度:⭐⭐⭐⭐,频率:⭐⭐⭐⭐⭐)
3.1 为什么需要抽象工厂模式
工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个==“产品族”==,由同一个工厂来统一生产,这就是我们本文将要学习的抽象工厂模式的基本思想。
- 同一种风格的组件通常是一起使用,为每个组件单独配置时较为复杂
3.2 抽象工厂模式的描述
*抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。*
抽象工厂模式为创建一组对象提供了一种解决方案。
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。
3.3 问题解决
//在本实例中我们对代码进行了大量简化,实际使用时,界面组件的初始化代码较为复杂,还需要使用JDK中一些已有类,为了突出核心代码,在此只提供框架代码和演示输出。
//按钮接口:抽象产品
interface Button {
public void display();
}
//Spring按钮类:具体产品
class SpringButton implements Button {
public void display() {
System.out.println("显示浅绿色按钮。");
}
}
//Summer按钮类:具体产品
class SummerButton implements Button {
public void display() {
System.out.println("显示浅蓝色按钮。");
}
}
//文本框接口:抽象产品
interface TextField {
public void display();
}
//Spring文本框类:具体产品
class SpringTextField implements TextField {
public void display() {
System.out.println("显示绿色边框文本框。");
}
}
//Summer文本框类:具体产品
class SummerTextField implements TextField {
public void display() {
System.out.println("显示蓝色边框文本框。");
}
}
//组合框接口:抽象产品
interface ComboBox {
public void display();
}
//Spring组合框类:具体产品
class SpringComboBox implements ComboBox {
public void display() {
System.out.println("显示绿色边框组合框。");
}
}
//Summer组合框类:具体产品
class SummerComboBox implements ComboBox {
public void display() {
System.out.println("显示蓝色边框组合框。");
}
}
//界面皮肤工厂接口:抽象工厂
interface SkinFactory {
public Button createButton();
public TextField createTextField();
public ComboBox createComboBox();
}
//Spring皮肤工厂:具体工厂
class SpringSkinFactory implements SkinFactory {
public Button createButton() {
return new SpringButton();
}
public TextField createTextField() {
return new SpringTextField();
}
public ComboBox createComboBox() {
return new SpringComboBox();
}
}
//Summer皮肤工厂:具体工厂
class SummerSkinFactory implements SkinFactory {
public Button createButton() {
return new SummerButton();
}
public TextField createTextField() {
return new SummerTextField();
}
public ComboBox createComboBox() {
return new SummerComboBox();
}
}
3.4 小结
在抽象工厂模式中,增加新的产品族很方便,但是增加新的产品等级结构很麻烦,抽象工厂模式的这种性质称为**“开闭原则”的倾斜性**。
抽象工厂模式是工厂方法模式的进一步延伸,由于它提供了功能更为强大的工厂类并且具备较好的可扩展性,在软件开发中得以广泛应用,尤其是在一些框架和API类库的设计中,例如在Java语言的AWT。
-
优点
- 工厂模式的共同优点
- 确保客户端只使用同一个产品族中的对象。
- 增加产品族很方便
-
缺点
增加新的产品等级结构麻烦,不符合开闭原则。
-
适用场景
- 将对象的创建和使用解耦
- 需要多个产品族
- 一个产品族的产品一起使用
- 产品等级结构稳定
4. 单例模式(难度:⭐,频率:⭐⭐⭐⭐)
4.1 为什么需要单例模式
需要确保系统中某个类只有一个唯一实例。
4.2 单例模式描述
*单例模式**(Singleton Pattern)**:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。*
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
4.3 问题解决
饿汉式单例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。 这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。
懒汉式(线程不安全)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉模式申明了一个静态对象,在用户第一次调用时初始化,虽然节约了资源,但第一次加载时需要实例化,反映稍慢一些,而且在多线程不能正常工作,存在线程不安全问题。
懒汉模式(线程安全)
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这种写法能够在多线程中很好的工作,但是每次调用getInstance方法时都需要进行同步,造成不必要的同步开销,而且大部分时候我们是用不到同步的,所以不建议用这种模式。
双重检查模式(DCL:double-check locking)
public class Singleton {
private volatile static Singleton instance;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
DCL虽然在一定程度解决了资源的消耗和多余的同步,线程安全等问题,但是他还是在某些情况会出现失效的问题,也就是DCL失效。建议用==静态内部类单例模式==来替代DCL。
由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
静态内部类
public class Singleton {
private Sinleton() {
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}
**Initialization Demand Holder (IoDH)**的技术。第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。缺点是不能防止利用反射来重复创建对象。
枚举
由于反射可以获取到类的构造函数,包括私有构造函数,因此反射可以生成新的对象。【如何解决:采用枚举实现】
在这种实现方式中,既可以避免多线程同步问题;还可以防止通过反射和反序列化来重新创建新的对象。
public enum Singleton {
INSTANCE;
public void doSomeThing() {
}
}
由于单例模式的枚举实现代码比较简单,而且又可以利用枚举的特性来解决线程安全和单一实例的问题,还可以防止反射和反序列化对单例的破坏,因此在很多书和文章中都强烈推荐将该方法作为单例模式的最佳实现方法。
4.4 小结
- 优点
- 唯一实例的受控访问
- 节约系统资源
- 缺点
- 扩展困难
- 单例类的职责过重
- 适用场景
- 系统只需要一个实例对象
- 客户只允许使用一个公共访问点。
5. 原型模式(难度:⭐⭐⭐,频率:⭐⭐⭐)
5.1 为什么需要原型模式
需要从一个对象克隆多个一样的对象
5.2 原型模式描述
原型模式(Prototype Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。
需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。
5.3 问题解决
重写clone方法
自己实现clone方法
//工作周报WeeklyLog:具体原型类,考虑到代码的可读性和易理解性,只列出部分与模式相关的核心代码
class WeeklyLog implements Cloneable
{
private String name;
private String date;
private String content;
public void setName(String name) {
this.name = name;
}
public void setDate(String date) {
this.date = date;
}
public void setContent(String content) {
this.content = content;
}
public String getName() {
return (this.name);
}
public String getDate() {
return (this.date);
}
public String getContent() {
return (this.content);
}
//克隆方法clone(),此处使用Java语言提供的克隆机制
public WeeklyLog clone()
{
Object obj = null;
try
{
obj = super.clone();
return (WeeklyLog)obj;
}
catch(CloneNotSupportedException e)
{
System.out.println("不支持复制!");
return null;
}
}
}
原型模式为工作流系统中任务单的快速生成提供了一种解决方案。
5.4 深拷贝与浅拷贝
深拷贝和浅拷贝的主要区别在于是否支持引用类型的成员变量的复制。
-
浅拷贝
在浅克隆中,如果原型对象的成员变量是值类型,将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。简单来说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。
在Java语言中,通过覆盖Object类的clone()方法可以实现浅克隆。
-
深拷贝
在深克隆中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象,深克隆将原型对象的所有引用对象也复制一份给克隆对象。简单来说,在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制。
在Java语言中,如果需要实现深克隆,可以通过==序列化(Serialization)==等方式来实现。
//使用序列化技术实现深克隆 public WeeklyLog deepClone() throws IOException, ClassNotFoundException, OptionalDataException { //将对象写入流中 ByteArrayOutputStream bao=new ByteArrayOutputStream(); ObjectOutputStream oos=new ObjectOutputStream(bao); oos.writeObject(this); //将对象从流中取出 ByteArrayInputStream bis=new ByteArrayInputStream(bao.toByteArray()); ObjectInputStream ois=new ObjectInputStream(bis); return (WeeklyLog)ois.readObject(); }
5.5 小结
-
原型管理器
专门负责克隆对象的工厂。
-
优点
- 简化对象创建过程
- 扩展性较好
- 无需工厂进行创建
- 可以使用深克隆保存对象的状态
-
缺点
- 每个类需要一个克隆方法,改造时需要修改,违反了开闭原则
- 深克隆实现比较麻烦
-
适用场景
- 创建新对象成本较大
- 需要保存对象的状态,而对象状态变化很小
6. 建造者模式(难度:⭐⭐⭐⭐,频率:⭐⭐)
6.1 为什么需要建造者模式
如何将这些部件组装成一辆完整的汽车并返回给用户,这是建造者模式需要解决的问题。建造者模式又称为生成器模式,它是一种较为复杂、使用频率也相对较低的创建型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。
6.2 建造者模式描述
建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。
建造者模式中需要一个director类,用于指导构造复杂对象的次序。
6.3 问题解决
6.4 小结
-
建造者模式与抽象工厂模式对比
建造者模式与抽象工厂模式有点相似,但是建造者模式返回一个完整的复杂产品,而抽象工厂模式返回一系列相关的产品;在抽象工厂模式中,客户端通过选择具体工厂来生成所需对象,而在建造者模式中,客户端通过指定具体建造者类型并指导Director类如何去生成对象,侧重于一步步构造一个复杂对象,然后将结果返回。如果将抽象工厂模式看成一个汽车配件生产厂,生成不同类型的汽车配件,那么建造者模式就是一个汽车组装厂,通过对配件进行组装返回一辆完整的汽车。
-
director的高级用法
- 省略director:加重了抽象建造者类的职责。
- 钩子方法的引入:根据条件更细粒度的控制产品的构建。
-
优点
- 将产品与创建过程解耦
- 更加精细地控制产品的创建过程
-
缺点
- 所创建的产品需要具有较多的共同点,组成部分相似
- 如果产品内部变化复杂,可能会增加系统的复杂性
-
适用场景
- 对象有复杂的内部结构
- 需要生成的对象的属性相互依赖,需要指定生成顺序
- 隔离复杂对象的创建和使用
小结
- 无论是类还是接口都应该注意单一职责原则,合理地对类和接口进行划分,减少耦合
- 面向抽象编程,为了良好的扩展性和维护性,在编程的过程中面向抽象编程,而不要面向具体的实现编程。
- 减少不必要的交互,进一步减少系统中的耦合。
小结
- 创建型模式主要用于创建对象,在需要创建对象时注意考虑对象之间的关系,以及具体的场景,从而选择正确的设计模式
- 对于设计模式需要进行合理的使用,滥用设计模式会导致系统变得臃肿且难以理解,对于设计模式的理解和运用需要不断在实际中加以学习体会
- 工厂模式主要是为了分离对象的创建和使用,且对象需要进行扩展
- 原型模式主要是为了减小创建对象的成本,且对象是相似的
- 建造者模式主要是为了减小创建对象的复杂度,即对象的组成部分相似,但具体细节不同