第2章
创建型模式
本章主要介绍了创建型模式(Creational Pattern)。创建型模式主要用于处理对象的创建问题,本章主要介绍以下内容:
- 单例模式
- 工厂模式
- 建造者模式
- 原型模式
- 对象池模式
2.1 单例模式
自Java语言推广使用以来,单例模式(singleton pattern)就是最常用的设计模式,它具有易于理解、使用简便等特点。有时单例模式会过度使用或在不合适的场景下使用,造成弊大于利的后果,因此,单例模式有时被认为是一种反模式。但是很多情况下单例模式是不可或缺的。
单例模式,顾名思义,用来保证一个对象只能创建一个实例,除此之外,它还提供了对实例的全局访问方法。单例模式的实现方式如图2-1所示。
单例模式的实现非常简单,只由单个类组成。为确保单例实例的唯一性,所有的单例构造器都要被声明为私有的(private),再通过声明静态(static)方法实现全局访问获得该单例实例。实现代码如下所示:
当我们在代码中使用单例对象时,只需进行简单的调用,代码如下所示:
在getInstance方法中,需要判断实例是否为空。如果实例不为空,则表示该对象在之前已被创建;否则,用新的构造器创建它。经过这些操作,无论是哪种情况,实例都不再为空,可以返回实例对象。
2.1.1 同步锁单例模式
单例模式的实现代码简单且高效,但还需注意一种特殊情况,在多线程应用中使用这种模式,如果实例为空,可能存在两个线程同时调用getInstance方法的情况。如果发生这种情况,第一个线程会首先使用新构造器实例化单例对象,同时第二个线程也会检查单例实例是否为空,由于第一个线程还没完成单例对象的实例化操作,所以第二个线程会发现这个实例是空的,也会开始实例化单例对象。
上述场景看似发生概率很小,但在实例化单例对象需要较长时间的情况下,发生的可能性就足够高,这种情况往往不能忽视。
要解决这个问题很简单,我们只需要创建一个代码块来检查实例是否空线程安全。可以通过以下两种方式来实现。
- 向getInstance方法的声明中添加synchronized关键字以保证其线程安全:
- 用synchronized代码块包装if (instance == null)条件。在这一环境中使用synch-
ronized代码块时,需要指定一个对象来提供锁,Singleton.class对象就起这种作用。如以下代码片段所示:
2.1.2 拥有双重校验锁机制的同步锁单例模式
前面的实现方式能够保证线程安全,但同时带来了延迟。用来检查实例是否被创建的代码是线程同步的,也就是说此代码块在同一时刻只能被一个线程执行,但是同步锁(locking)只有在实例没被创建的情况下才起作用。如果单例实例已经被创建了,那么任何线程都能用非同步的方式获取当前的实例。
只有在单例对象未实例化的情况下,才能在synchronized代码块前添加附加条件移动线程安全锁:
注意到instance == null条件被检查了两次,因为我们需要保证在synchronized代码块中也要进行一次检查。
2.1.3 无锁的线程安全单例模式
Java中单例模式的最佳实现形式中,类只会加载一次,通过在声明时直接实例化静态成员的方式来保证一个类只有一个实例。这种实现方式避免了使用同步锁机制和判断实例是否被创建的额外检查:
2.1.4 提前加载和延迟加载
按照实例对象被创建的时机,可以将单例模式分为两类。如果在应用开始时创建单例实例,就称作提前加载单例模式;如果在getInstance方法首次被调用时才调用单例构造器,则称作延迟加载单例模式。
前面例子中描述的无锁线程安全单例模式在早期版本的Java中被认为是提前加载单例模式,但在最新版本的Java中,类只有在使用时候才会被加载,所以它也是一种延迟加载模式。另外,类加载的时机主要取决于JVM的实现机制,因而版本之间会有不同。所以进行设计时,要避免与JVM的实现机制进行绑定。
目前,Java语言并没有提供一种创建提前加载单例模式的可靠选项。如果确实需要提前实例化,可以在程序的开始通过调用getInstance方法强制执行,如下面代码所示:
2.2 工厂模式
正如前面章节所描述,在面向对象编程中,继承是一个基本概念,它与多态共同构成了类的父子继承关系(Is-A关系)。Car对象可以被当作Vehicle对象处理,Truck对象也可以被当作Vehicle对象处理。一方面,这种抽象方式使得同一段代码能为Car和Truck对象提供同样的处理操作,使代码更加简洁;另一方面,如果要扩展新的Vehicle对象类型,比如Bike或Van,不再需要修改代码,只需添加新的类即可。
在大多数情况下,最棘手的问题往往是对象的创建。在面向对象编程中,每个对象都使用特定类的构造器进行实例化操作,如下面代码所示:
这段代码说明了Vehicle和Car两个类之间的依赖关系。这样的依赖关系使代码紧密耦合,在不更改的情况下很难扩展。举例来说,假设要用Truck替换Car,就需要修改相应的代码:
这里存在两个问题:其一,类应该保持对扩展的开放和对修改的关闭(开闭原则);其二,每个类应该只有一个发生变化的原因(单一职责原则)。每增加新的类造成主要代码修改时会打破开闭原则,而主类除了其固有功能之外还负责实例化vehicle对象,这种行为将会打破单一职责原则。
在这种情况下就需要一种更好的设计方案。我们可以增加一个新类来负责实例化vehicle对象,称之为简单工厂模式。
2.2.1 简单工厂模式
工厂模式用于实现逻辑的封装,并通过公共的接口提供对象的实例化服务,在添加新的类时只需要做少量的修改。
简单工厂的实现描述如图2-2所示。
类SimpleFactory中包含实例化ConcreteProduct 1和ConcreteProduct 2的代码。当
客户需要对象时,调用SimpleFactory的createProduct()方法,并提供参数指明所需对象的类型。SimpleFactory实例化相应的具体产品并返回,返回的产品对象被转换为基类类型。因此,无论是ConcreteProduct 1还是ConcreteProduct 2,客户能以相同的方式处理。
1.静态工厂模式
下面我们写一个简单的工厂类用来创建Vehicle实例。我们创建一个抽象Vehicle类和继承自它的三个具体类:Bike、Car和Truck。工厂类(也叫静态工厂类)代码如下所示:
工厂类逻辑非常简单,只负责Vehicle类的实例化,符合单一职责原则;用户只调用Vehicle接口,这样做可以减少耦合,符合依赖倒置原则;但是当增加一个新的Vehicle类时,需要对VehicleFactory类进行修改,这样就打破了开闭原则。
我们可以改进这种简单工厂模式,使得注册的新类在使用时才被实例化,从而保证其对扩展开放,同时对修改闭合。
具体的实现方式有以下两种:
- 使用反射机制注册产品类对象和实例化。
- 注册产品对象并向每个产品添加newInstance方法,该方法返回与自身类型相同的新实例。
2.使用反射机制进行类注册的简单工厂模式
为此,我们需要使用map对象来保存产品ID及其对应的类:
然后,增加一个注册新Vehicle类的方法:
构造方法如下所示:
但在某些情况下,反射机制并不适用。比如,反射机制需要运行时权限,这在某些特定环境中是无法实现的。反射机制也会降低程序的运行效率,在对性能要求很高的场景下应该避免使用这种机制。
3.使用newInstance方法进行类注册的简单工厂模式
前面的代码中使用了反射机制来实现新Vehicle类的实例化。如果要避免使用反射机制,可以使用注册新Vehicle类的类似工厂类,不再将类添加到map对象中,而是将要注册的每种对象实例添加其中。每个产品类都能够创建自己的实例。
首先在Vehicle基类中添加一个抽象方法:
对于每种产品,基类中声明为抽象的方法都要实现:
在工厂类中,更改map用于保存对象的ID及其对应的Vehicle对象:
通过实例注册一种新的Vehicle类型:
也要相应地改变createVehicle方法:
2.2.2 工厂方法模式
工厂方法模式是在静态工厂模式上的改进。工厂类被抽象化,用于实例化特定产品类的代码被转移到实现抽象方法的子类中。这样不需要修改就可以扩展工厂类。工厂方法模式的实现如图2-3所示。
下面来看一些样例:假设有一个汽车工厂,目前只生产两种车型,小型跑车和大型家用车。在软件中,顾客可以自由决定买小型车或大型车。首先,我们需要创建一个Vehicle类和两个子类,子类分别为SportCar和SedanCar。
创建Vehicle类结构之后就可以创建抽象工厂类。要注意工厂类中并不包含任何创建新实例的代码:
为了增加汽车实例化的代码,我们创建了VehicleFactory的子类,即CarFactory类,并在CarFactory中实现从父类中调用的createVehicle抽象方法。实际上,Vehicle-
Factory类将Vehicle类的具体实例化操作委托给了它的子类:
在客户端,我们只需要创建工厂类并创建订单:
此时,我们意识到汽车工厂所带来的收益,是时候进一步拓展业务了。市场调查显示卡车的需求量很大,因此我们建一个卡车工厂(TruckFactory)。
我们使用如下代码来下订单:
匿名具体工厂模式
继续在前面的代码中添加一个BikeFactory,使得顾客可以选择购买小型或大型自行车。这里不用创建单独的类文件,只需直接在客户端代码中简单地创建一个匿名类来对VehicleFactory类进行扩展即可:
2.2.3 抽象工厂模式
抽象工厂模式是工厂方法模式的扩展版本。它不再是创建单一类型的对象,而是创建一系列相关联的对象。如果说工厂方法模式中只包含一个抽象产品类,那么抽象工厂模式则包含多个抽象产品类。
工厂方法类中只有一个抽象方法,在不同的具体工厂类中分别实现抽象产品的实例化,而抽象工厂类中,每个抽象产品都有一个实例化方法。
如果我们采用抽象工厂模式并将它应用于包含单个对象的簇,那么就得到了工厂方法模式。工厂方法模式只是抽象工厂模式的一种特例。
抽象工厂设计模式的实现如图2-4所示。
抽象工厂模式由以下类组成:
- AbstractFactory(抽象工厂类):抽象类,用于声明创建不同类型产品的方法。它针对不同的抽象产品类都有对应的创建方法。
- ConcreteFactory(具体工厂类):具体类,用于实现抽象工厂基类中声明的方法。针对每个系列的产品都有一个对应的具体工厂类。
- AbstracProduct(抽象产品类):对象所需的基本接口或类。一簇相关的产品类由来自不同层级的相似产品类组成。ProductA1和ProductB1来自第一个类簇,由ConcreteFactory1实例化。ProductA2和ProductB2来自第二个类簇,由ConcreteFactory2实例化。
2.2.4 简单工厂、工厂方法与抽象工厂模式之间的对比
之前我们阐述了实现工厂模式的三种不同方式,即简单工厂模式、工厂方法模式和抽象工厂模式。如果你目前对这三种实现方式还存在困惑,也无须自责,因为这些模式之间确实存在许多重叠的地方,况且,这些模式并不存在明确的定义,某些专家在如何实施这些模式上也存在着分歧。
本节的主旨是让读者理解工厂模式的核心概念。工厂模式的核心就是由工厂类来负责合适对象的创建。如果工厂类很复杂,比如同时服务于多种类型的对象或工厂,也可以根据前面内容相应的修改代码。
2.3 建造者模式
建造者模式与其他创建型模式一样服务于相同的目标,只不过它出于不同的原因,通过不同的方式实现。在开发复杂的应用程序时,代码往往会变得非常复杂。类会封装更多的功能,类的结构也会变得更加复杂。随着功能量的增加,就需要涵盖更多场景,从而需要构建更多不同的类。
当需要实例化一个复杂的类,以得到不同结构和不同内部状态的对象时,我们可以使用不同的类对它们的实例化操作逻辑分别进行封装,这些类就被称为建造者。每当需要来自同一个类但具有不同结构的对象时,就可以通过构造另一个建造者来进行实例化。
它的概念不仅可以用于不同表现形式的类,还可以用于由其他对象组成的复杂对象。构造建造者类来封装实例化复杂对象的逻辑,符合单一职责原则和开闭原则。实现实例化复杂对象的逻辑被放到了单独的建造者类中。当需要具有不同结构的对象时,我们可以添加新的建造者类,从而实现对修改的关闭和对扩展的开放,如图2-5所示。
建造者模式中包含以下类:
- Product(产品类):需要为其构建对象的类,是具有不同表现形式的复杂或复合对象。
- Builder(抽象建造者类):用于声明构建产品类的组成部分的抽象类或接口。它的作用是仅公开构建产品类的功能,隐藏产品类的其他功能;将产品类与构建产品类的更高级的类分离开。
- ConcreteBuilder(具体建造者类):用于实现抽象建造者类接口中声明的方法。除此之外,它还通过getResult方法返回构建好的产品类。
- Director(导演类):用于指导如何构建对象的类。在建造者模式的某些变体中,导演类已被移除,其角色被客户端或抽象建造者类所代替。
2.3.1 汽车建造者样例
在本节中,我们将在汽车软件中应用建造者模式。首先,存在一个Car类,需要为它创建实例。通过向汽车中添加不同的组件,我们分别可以制造轿车和跑车。当开始设计软件时,需要认识到以下几点:
- Car类非常复杂,创建类的对象也是一个复杂的操作。在Car类的构造函数中添加所有的实例化逻辑将使其变得体量庞大。
- 我们需要构建多种类型的汽车类。针对这种情况,我们通常会添加多个不同的构造函数,但直觉告诉我们这并非最好的解决方案。
- 将来我们可能需要构建多种不同类型的汽车对象。由于市场上对于半自动汽车的需求非常高涨,在不久的将来,我们应该做好准备进行代码扩展而不是重新修改代码。
为此,我们将创建以下如图2-6所示的类结构。
CarBuilder是建造者基类,它包含了四个抽象方法。我们创建了两个具体建造者类:ElectricCarBuilder和GasolineCarBuilder。每个建造者实现类都分别实现了CarBuilder
的所有抽象方法。那些类中不需要的方法(例如ElectricCarBuilder中的addGasTank方法)会被置空或抛出异常。ElectricCar类和GasolineCar类内部结构是不同的。
导演类使用抽象建造者类来创建新的汽车对象。buildElectricCar和buildGasolineCar
两个方法略有不同:
如果想要制造一辆既有电动又有汽油发动机的混合动力汽车:
2.3.2 简化的建造者模式
在建造者模式的某些实现方式中可以移除导演类。在类例子中,导演类封装的逻辑非常简单,在这种情况下可以不需要导演类。简化的构建器模式如图2-7所示。
我们只是将导演类中实现的代码移到了客户端,但是当抽象建造者类和产品类太过复杂,或者要使用建造者类从数据流中构建对象时,我们不建议这样修改。
2.3.3 拥有方法链的匿名建造者
如前所述,构建来自相同类但具有不同形式的对象的最直接方法就是构建多个构造函数,按照不同的场景进行不同的实例化操作。使用建造者模式避免这种情况是个不错的实践,在《Effective Java》一书中,Joshua Bloch建议使用内部建造者类和方法链来代替构建多个构造函数。
方法链是指通过特定方法返回当前对象(this)的一种技术。通过这种技术,可以以链的形式调用方法。例如:
在定义了更多类似上述方法之后,可以用方法链调用它们:
但在我们的例子中是将Car对象的建造者类构造为内部类。在需要增加新客户端时,可以执行以下操作:
2.4 原型模式
原型模式看似复杂,实际上它只是一种克隆对象的方法。现在实例化对象操作并不特别耗费性能,那么为什么还需要对象克隆呢?在以下几种情况下,确实需要克隆那些已经经过实例化的对象:
- 依赖于外部资源或硬件密集型操作进行新对象的创建的情况。
- 获取相同对象在相同状态的拷贝而无须进行重复获取状态操作的情况。
- 在不确定所属具体类时需要对象的实例的情况。
请看如图2-8所示的类图。
在原型模式中,主要涉及以下类:
- Prototype(抽象原型类):声明了clone()方法的接口或基类,其中clone()方法必须由派生对象实现。在简单的场景中,并不需要这种基类,只需要直接具体类就足够了。
- ConcretePrototype(具体原型类):用于实现或扩展clone()方法的类。clone()方法必须要实现,因为它返回了类型的新实例。如果只在基类中实现了clone()方法,却没有在具体原型类中实现,那么当我们在具体原型类的对象上调用该方法时,会返回一个基类的抽象原型对象。
可以在接口中声明clone()方法,因而必须在类的实现过程中实现clone()方法,这项操作会在编译阶段强制执行。但是,在多继承层次结构中,如果父类实现了clone()
方法,继承自它的子类将不会强制执行clone()方法。
浅拷贝和深拷贝
拷贝对象时,我们应该清楚拷贝的深度。当拷贝的对象只包含简单数据类型(如int和float)或不可变的对象(如字符串)时,就直接将这些字段复制到新对象中。但当拷贝对象中包含对其他对象的引用时,这样就会出现问题。例如,如果为具有引擎和四个轮子的Car类实现拷贝方法时,我们不仅要创建一个新的Car对象,还要创建一个新的Engine对象和四个新的Wheel对象。毕竟两辆车不能共用相同的发动机和车轮,这称为深拷贝。
浅拷贝是一种仅将本对象作为拷贝内容的方法。例如,如果我们要为Student对象实现拷贝方法,就不会拷贝它所指向的Course对象,因为多个Student对象会指向同一个Course对象。
在实践中,我们应根据具体情况来决定使用深拷贝、浅拷贝或混合拷贝。通常,浅拷贝对应于聚合关系,而深拷贝则对应于组合关系。
2.5 对象池模式
对象的实例化是最耗费性能的操作之一,这在过去是个大问题,现在不用再过分关注它。但当我们处理封装外部资源的对象(例如数据库连接)时,对象的创建操作则会耗费很多资源。
解决方案是重用和共享这些创建成本高昂的对象,这称为对象池模式,如图2-9所示,它具有以下结构。
对象池模式中使用的类如下所示:
- ResourcePool(资源池类):用于封装逻辑的类。用来保存和管理资源列表。
- Resource(资源类):用于封装特定资源的类。资源类通常被资源池类引用,因此只要资源池不重新分配,它们就永远不会被回收。
- Client(客户端类):使用资源的类。
当客户端需要新资源时,会向资源池类申请,资源池类检查后获取第一个可用资源并将其返回给客户端:
客户端使用完资源后会进行释放,资源会重新回到资源池以便重复使用。
资源池的典型用例是数据库连接池。通过维护数据库连接池,可以让代码使用池中的不同数据库连接。
2.6 总结
本章主要介绍了创建型设计模式。我们讨论了单例、工厂、建造者、原型和对象池等设计模式。这些模式都能够实现新对象的实例化,提高创建对象代码的灵活性和重用性。在下一章中,我们将介绍行为型模式。创建型模式有助于管理对象的创建操作,而行为型模式则提供了管理对象行为的简便方法。