设计模式
0、概论
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。
模式的经典定义:每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心。通过这种方式,我们可以无数次地重用那些已有的解决方案,无需再重复相同的工作。即模式是在特定环境中解决问题的一种方案。
设计模式使人们可以更加简单方便地复用成功的设计和体系结构。将已证实的技术表述成设计模式也会使新系统开发者更加容易理解其设计思路。
0.1 UML
依赖、聚合(关联)、继承等
-
继承:is-a关系,一种用于表示特殊与一般的关系。在UML术语中,描述继承的关系称为泛化。
-
实现:实现表示面向对象编程中类的接口实现;
-
依赖:uses-a关系,一个类的方法操纵另一个类的对象,即为一个类依赖于另一个类。开发中应该尽量减少类之间的依赖关系,即降低类之间的耦合度;
-
关联:表示两个实体之间的关系。有两种类型的关联:组合与聚合
- 聚合:has-a关系,聚合关系意味着一个类的对象含有另一个类的对象;
- 组合:一个类是另一个类的组成部分;
组合与聚合的区别:
聚合是一个类在逻辑上包含另一个类,但是所包含的类的实例是可以独立于第一个类在其上下文之外生存的,即它是可以被其他类引用的。如部门与员工
组合则是当主类不再存在时,依赖类也不再存在。如房子与房间
关系 | UML连接符 | 解释 |
---|---|---|
继承(泛化) | ![]() | 箭头指向父类 |
接口实现 | ![]() | 箭头指向接口 |
依赖 | ![]() | 箭头指向被包含的对象 |
关联 | ![]() | 箭头指向被关联的对象 |
聚合 | ![]() | 菱形指向整体 |
组合 | ![]() | 菱形指向整体 |
0.2 设计原则
0.2.1 开闭原则
在一个系统中,对于扩展是开放的,对于修改是关闭的,一个好的系统是在不修改源代码的情况下,可以扩展你的功能。
实现开闭原则的关键是抽象。通过扩展已有软件系统,可以提供新的行为,以满足对软件的新的需求,使软件有一定的适应性和灵活性。对于已有的软件模块,特别是最重要的抽象层模块不能被修改,这使变化中的软件系统有一定的稳定性和延续性。即**在"开-闭"原则中,不允许修改的是抽象的类或者接口,允许扩展的是具体的实现类。**
- 合理地抽象、分离出变化与不变化的部分,为变化的部分预留下可扩展的方式;要完全遵守开闭原则是不可能的,也没这个必要。适当的抽象可以提高系统的灵活性、使其可扩展、可维护;过度抽象,会大大增加系统的复杂程度;
- 当变化发生时,我们就创建抽象来隔离以后发生同类的变化;开闭原则是面向对象的核心所在;开发人员应该对程序中呈现出频繁变化的那部分做出抽象,拒绝对任何部分都刻意抽象及不成熟的抽象。
0.2.2 里氏代换原则
派生类必须完全可替代其基类。即任何基类出现的地方,子类也可以出现。
里氏替换原则是继承复用的基石,只有当派生类可以替换基类,软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。
里氏代换原则是对“开-闭”原则的补充。实现“开闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。
0.2.3 依赖倒转原则
抽象不应该依赖细节,细节应该依赖抽象。高级模块不应该依赖低级模块,两者都应该依赖抽象。
简单的说就是,写代码时用到具体类时,不与具体类交互,而与具体类的上层接口交互。原则表述:
(1)抽象不应当依赖于细节,细节应当依赖于抽象;
(2)要针对接口编程,不针对实现编程。
软件系统开发要求低耦合高内聚,耦合程度的高低取决于软件系统中各个模块彼此依赖的程度。依赖程度越低,维护和扩展系统就越容易。将高级逻辑与低级模块分开就是一个解耦的方法。
0.2.4 接口隔离原则
客户端不应该依赖于它所不需要的接口。
使用多个专门的接口比使用单一的总接口总要好。过于臃肿的接口是对接口的污染。不应该强迫客户依赖于它们不用的方法。在每个接口中不应该存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个总接口(多个接口方法集合到一个的接口)要好。
0.2.5 迪米特原则
迪米特法则又叫最少知识原则,一个对象应当对其它对象有尽可能少的了解。
系统中的类,尽量不要与其他类互相作用,减少类之间的耦合度。因为在你的系统中,扩展的时候,你可能需要修改这些类,而类与类之间的关系,决定了修改的复杂度,相互作用越多,则修改难度就越大;反之,如果相互作用的越小,则修改起来的难度就越小。
一个类对自己依赖的类知道的越少越好。无论被依赖的类多么复杂,都应该将逻辑封装在方法的内部,通过public方法提供给外部。这样当被依赖的类变化时,才能最小的影响该类。
0.2.6 合成复用原则
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分;新对象通过向这些对象的委派达到复用已有功能的目的。
要尽量使用合成/聚合原则,而不是继承关系达到软件复用的目的。
0.3 分类
-
根据其目的可分为创建型(Creational),结构型(Structural)和行为型(Behavioral)三种:
- 创建型模式主要用于创建对象
- 结构型模式主要用于处理类或对象的组合
- 行为型模式主要用于描述对类或对象怎样交互和怎样分配职责
-
根据范围,即模式主要是用于处理类之间关系还是处理对象之间的关系,可分为类模式和对象模式两种:
- 类模式: 处理类和子类之间的关系,这些关系通过继承建立,在编译时刻就被确定下来,是属于静态的
- 对象模式:处理对象间的关系,这些关系在运行时刻变化,更具动态性
![image-20200823113921842](https://i-blog.csdnimg.cn/blog_migrate/da662265e252db942991ee7950424f2d.png)
创建型模式
在软件工程中,创建型模式是处理对象创建的设计模式,试图根据实际情况使用合适的方式创建对象。基本的对象创建方式可能会导致设计上的问题,或增加设计的复杂度。创建型模式通过以某种方式控制对象的创建来解决问题。
创建型模式由两个主导思想构成。一是将系统使用的具体类封装起来,二是隐藏这些具体类的实例创建和结合的方式。
创建型模式又分为对象创建型模式和类创建型模式。对象创建型模式处理对象的创建,类创建型模式处理类的创建。详细地说,对象创建型模式把对象创建的一部分推迟到另一个对象中,而类创建型模式将它对象的创建推迟到子类中。
一、工厂模式
在面向对象编程中, 最通常的方法是一个new操作符产生一个对象实例,new操作符就是用来构造对象实例的。但是在一些情况下, new操作符直接生成对象会带来一些问题。如
Vehicle vehicle = new Car();
这段代码说明了Vehicle与Car两个类之间的依赖关系,这样的依赖关系使得代码紧密耦合,在不更改的情况下很难扩展。假设要用bike替换car,就必须要使用
Vehicle vehicle = new Bike();
这就存在两个问题:
- 类应该保持对扩展的开放与对修改的关闭(开闭原则)
- 每个类应该只有一个发生变化的原因(单一职责原则)
每增加新的类造成主要代码修改时会打破开闭原则,而主类除了其固有的功能之外还负责实例化对象,这种行为将会打破单一职责原则。此时可以使用简单工厂模式,来增加一个新类来负责实例化vehicle对象。
1.1 简单工厂模式
工厂模式用于实现逻辑的封装,并通过公共的接口提供对象的实例化服务,在添加新的类时只需做少量的修改即可。在创建一个对象时不向客户暴露内部细节,只提供一个创建对象的通用接口。
![image-20200824211019382](https://i-blog.csdnimg.cn/blog_migrate/ed6b5341d77752cd107a8c8c2ccda90a.png)
类SimpleFactory中包含实例化ConcreteProduct1与ConcreteProduct2的代码。当客户需要时,调SimpleFactory的createProduct()方法,并提供所需要的对象类型。SimpleFactory实例化相应的具体产品并返回,返回的产品对象被转换为基类类型。
// 简单工厂类
public class SimpleFactory {
public Product createProduct(int type) {
if (type == 1) {
return new ConcreteProduct1();
} else if (type == 2) {
return new ConcreteProduct2();
}
return new ConcreteProduct();
}
}
public class Client {
public static void main(String[] args) {
SimpleFactory simpleFactory = new SimpleFactory();
Product product1 = simpleFactory.createProduct(1);
// do something with the product
Product product2 = simpleFactory.createProduct(2);
}
}
简单工厂模式又称静态工厂方法模式。它存在的目的很简单:定义一个用于创建对象的接口。
- 工厂类角色:这是本模式的核心,含有一定的商业逻辑和判断逻辑,用来创建产品
- 抽象产品角色:它一般是具体产品继承的父类或者实现的接口
- 具体产品角色:工厂类所创建的对象就是此角色的实例。在java中由一个具体类实现。
但是很显然,该模式存在一个很大的缺陷,就是当每增加一个新的需求时,如要创建product3时,都要在工厂类中添加相应的业务逻辑,这显然是违背开闭原则的(即对修改开放了)。
于是工厂方法模式作为救世主出现了。 工厂类定义成了接口,而每新增的车种类型,就增加该车种类型对应工厂类的实现,这样工厂的设计就可以扩展了,而不必去修改原来的代码。
1.2 工厂方法模式
工厂方法模式是在静态工厂模式上的改进。工厂类被抽象化,用于实例化特定产品类的代码被转移到实现抽象方法的子类中。这样不需要修改就可以扩展工厂类。
![image-20200824213642260](https://i-blog.csdnimg.cn/blog_migrate/5dca75b3142d9bea660bd1fd0ee9e59f.png)
- 抽象工厂角色: 这是工厂方法模式的核心,它与应用程序无关。是具体工厂角色必须实现的接口或者必须继承的父类。在java中它由抽象类或者接口来实现
- 具体工厂角色:它含有和具体业务逻辑有关的代码。由应用程序调用以创建对应的具体产品的对象
- 抽象产品角色:它是具体产品继承的父类或者是实现的接口。在java中一般有抽象类或者接口来实现
- 具体产品角色:具体工厂角色所创建的对象就是此角色的实例。在java中由具体的类来实现
// 抽象产品类,也可定义为接口
abstract class BMW {
public BMW(){
}
}
// 具体产品类
public class BMW320 extends BMW {
public BMW320() {
System.out.println("制造-->BMW320");
}
}
public class BMW523 extends BMW{
public BMW523(){
System.out.println("制造-->BMW523");
}
}
// 抽象工厂类
interface FactoryBMW {
BMW createBMW();
}
// 具体工厂类
public class FactoryBMW320 implements FactoryBMW{
@Override
public BMW320 createBMW() {
return new BMW320();
}
}
public class FactoryBMW523 implements FactoryBMW {
@Override
public BMW523 createBMW() {
return new BMW523();
}
}
// 客户端
public class Customer {
public static void main(String[] args) {
FactoryBMW320 factoryBMW320 = new FactoryBMW320();
BMW320 bmw320 = factoryBMW320.createBMW();
FactoryBMW523 factoryBMW523 = new FactoryBMW523();
BMW523 bmw523 = factoryBMW523.createBMW();
}
}
继承自抽象工厂类的多个具体工厂类分担了工厂承受的压力。当有新的产品需求时,只需要创建一个新的继承抽象工厂类的具体工厂类即可,不需更改原先的工厂方法,符合了开闭原则。但是随之而来的是,当有非常多的新需求时,就会出现相应的具体工厂类对象,所以抽象工厂方法应运而生。
1.3 抽象工厂方法
抽象工厂方法是工厂方法模式的扩展版本。它不再是创建单一类型的对象,而是创建一系列相关联的对象。如果说工厂方法模式只包含一个抽象产品类,那么抽象工厂模式则包含多个抽象产品类。
工厂方法类中只有一个抽象方法,在不同的具体工厂类中分别实现抽象产品的实例化;而抽象工厂类中,每个抽象产品都有一个实例化方法。抽象工厂模式和工厂方法模式的区别就在于需要创建对象的复杂程度上。而且抽象工厂模式是三个里面最为抽象、最具一般性的。
![image-20200824220202129](https://i-blog.csdnimg.cn/blog_migrate/2433ac41417e87262a3165a4297cff01.png)
- 抽象工厂角色:是具体工厂角色必须实现的接口或者必须继承的父类。在java中它由抽象类或者接口来实现。用于声明创建不同类型产品的方法,它针对不同的抽象产品类都有对应的创建方法。
- 具体工厂角色:它含有和具体业务逻辑有关的代码,用于实现抽象工厂基类中声明的方法。由应用程序调用以创建对应的具体产品的对象,针对每个系列的产品都有一个对应的具体工厂类。
- 抽象产品角色:它是具体产品继承的父类或者是实现的接口。一簇相关的产品类来自不同层级的相似产品类组成。AI/A2来自子一个簇,由具体工厂类A实例化。
- 具体产品角色:具体工厂角色所创建的对象就是此角色的实例。
随着客户的要求越来越高,宝马车需要不同配置的空调和发动机等配件。于是这个工厂开始生产空调和发动机,用来组装汽车。这时候工厂有两个系列的产品:空调和发动机。宝马320系列配置A型号空调和A型号发动机,宝马230系列配置B型号空调和B型号发动机。
// 抽象产品-汽车
abstract class BWM{
}
// 具体产品
class BWM523 extends BWM {
}
class BWM320 extends BWM {
}
// 抽象产品-空调
abstract class aircondition{
}
// 具体产品
class airconditionBWM320 extends aircondition {
}
class airconditionBWM52 extends aircondition {
}
// 抽象工厂类-汽车
interface FactoryBMW {
function createBMW();
function createAirC();
}
// 具体工厂类-320
class FactoryBWM320 implements FactoryBMW {
function createBMW(){
return new BWM320();
}
function createAirC(){ //空调
return new airconditionBWM320();
}
}
// 具体工厂类-523
class FactoryBWM523 implements FactoryBMW {
function createBMW(){
return new BWM523();
}
function createAirC(){
return new airconditionBWM523();
}
}
// 客户端
public class Customer {
public static void main(String[] args){
//生产宝马320系列配件
FactoryBMW320 factoryBMW320 = new FactoryBMW320();
factoryBMW320.createEngine();
factoryBMW320.createAircondition();
//生产宝马523系列配件
FactoryBMW523 factoryBMW523 = new FactoryBMW523();
factoryBMW320.createEngine();
factoryBMW320.createAircondition();
}
}
二、单例模式
单例模式:用来保证一个对象只能创建一个实例,除此之外,他还提供了对实例的全局访问方法。
单例模式的实现非常简单,只由单个类组成。为确保单例实例的唯一性,所有的单例构造器都要被声明为私有的(private),再通过声明静态(static)方法实现全局访问获得该单例实例。
单例模式有以下特点:
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
**单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。**在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
2.1 懒汉式单例模式
// 懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
// 构造器被声明为private
private Singleton() {}
// Singleton为空,需要使用getInstance()来生成
private static Singleton single=null;
// 静态工厂方法,实现全局访问获得该单例实例
public static Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
}
Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。
在getInstance()方法中,需要判断实例是否为空。如果实例不为空,则表示该对象在之前已被创建;否则,用新的构造器创建它。
2.1.1 同步锁单例模式
但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例。在多线程应用中使用这种模式,如果实例为空,可能存在两个线程同时调用getInstance()方法的情况。此时,第一个线程会首先使用新构造器实例单例对象,同时第二个线程也会检查单例实例是否为空,由于第一个线程还没有完成实例化操作,所以第二个线程也会开始实例化单例对象。要解决这个问题,只需要创建一个代码块来检查实例是否空线程安全。
(1)同步方法
// 同步方法
public static synchronized Singleton getInstance() {
if (single == null) {
single = new Singleton();
}
return single;
}
(2)同步代码块
// 同步代码块
public static Singleton getInstance() {
// Singleton.class该对象提供锁
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
(3)双重检查校验锁机制
前面的实现方式能够保证线程安全,但同时带来了延迟。用来检查实例是否被创建的代码是线程同步的,也就是说此代码块在同一时刻只能被一个线程执行,但是同步锁只有在实例没有被创建的情况下才起作用。如果单例实例已经被创建了,那么任何线程都能用非同步的方式获取当前的实例。
只有在单例对象未实例化的情况下,才能在synchronized代码块前添加附加条件移动线程安全锁:
public static Singleton getInstance() {
// singleton == null被检查了两次
// 只有该单例对象没有被实例化时,才进行实例化
if (singleton == null) {
// 同步监视器为 类名.class
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
// 否则说明该单例对象已经被实例化,那么就直接返回已经实例化的单例对象
return singleton;
}
2.2 饿汉式单例模式
饿汉式在类创建的同时就已经创建好了一个静态的对象供系统使用,以后不再改变(final),所以天生线程是安全的。
//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton1 {
private Singleton1() {}
// 类中含有一个final的静态对象,天生安全
private static final Singleton1 single = new Singleton1();
//静态工厂方法
public static Singleton1 getInstance() {
// 直接返回该对象
return single;
}
}
此为单例模式的最佳实现形式,类只会被加载一次,通过在声明时直接实例化静态成员的方式来保证一个类只有一个实例。这种实现方式避免了使用同步锁机制和判断实例是否被创建的额外检查。
2.3 懒汉式与饿汉式的区别
-
类加载:
- 懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例
- 饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了
-
线程安全:
- 懒汉式本身是非线程安全的,为了实现线程安全要使用同步锁与判断实例是否存在的检查
- 饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题
线程安全:
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。
-
资源加载和性能:
- 懒汉式,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了
- 饿汉式,在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成
如果在应用开始时创建单例实例,就称为提前加载单例模式——饿汉式
如果在getInstance方法首次被调用时才调用单例构造器,则称为延迟加载单例模式——懒汉式
三、建造者模式
建造者模式:将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
在软件开发的过程中,当遇到一个“复杂的对象”的创建工作,该对象由一定各个部分的子对象用一定的算法构成,由于需求的变化,复杂对象的各个部分经常面临剧烈的变化,但将它们组合在一起的算法相对稳定。此时应使用建造者模式。
3.1 前言
3.1.1 肯德基取餐
典型的儿童餐包括一个主食,一个辅食,一杯饮料和一个玩具(例如汉堡、炸鸡、可乐和玩具车)。这些在不同的儿童餐中可以是不同的,但是组合成儿童餐的过程是相同的。
- 客户端:顾客,想去买一套套餐(这里面包括汉堡,可乐,薯条),可以有1号和2号两种套餐供顾客选择
- 指导者角色:收银员。知道顾客想要买什么样的套餐,并告诉餐馆员工去准备套餐。
- 建造者角色:餐馆员工。按照收银员的要求去准备具体的套餐,分别放入汉堡,可乐,薯条等。
- 产品角色:最后的套餐,所有的东西放在同一个盘子里面。
3.1.2 工资计算
工资的计算一般是:底薪+奖金-税。但底薪分为一级8000、二级6000、三级4000三个等级。根据岗位不同奖金的发放也不一样,管理及日常事务处理岗位(A类)每月根据领导及同事间的评议得分计算奖金,销售岗位(B类)则根据销售额发放提成。税金则根据奖金和底薪的数额进行计算。由此看出该工资的计算方式是比较稳定的构建算法,但对工资的每一部分都会根据不同的情况产生不同的算法,如何将客户端与变化巨烈的底薪、奖金和税金计算方式分离呢,这也比较适合用建造者模式。
3.2 概览
针对上述的问题,应使用一种封装机制来隔离复杂对象的各个组成部分,使用同样的构造过程按需构建稳定的复杂对象。
3.2.1 类图
当需要实例化一个复杂的类,以得到不同结构和不同内部状态的对象时,我们可以使用不同的类对它们的实例化操作逻辑分别进行封装,这些类就称为建造者。每当需要来自同一个类但具有不同结构的对象时们就可以通过构造另一个建造者来进行实例化。
建造者模式的目的是将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建出不同的表示,其核心是Director导演类。
-
抽象建造者角色(Builder):用于声明构建产品类的组成部分的抽象类或接口。为创建一个Product对象的各个部件指定抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此角色规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
-
具体建造者(ConcreteBuilder)
1)实现Builder的接口以构造和装配该产品的各个部件。即实现抽象建造者角色Builder的方法。
2)定义并明确它所创建的表示,即针对不同的商业逻辑,具体化复杂对象的各部分的创建
-
提供一个检索产品的接口getResult()
-
构造一个使用Builder接口的对象即在指导者的调用下创建产品实例
-
-
指导者(Director):调用具体建造者角色以创建产品对象的各个部分。指导者并没有涉及具体产品类的信息,真正拥有具体产品的信息是具体建造者对象。它只负责保证对象各部分完整创建或按某种顺序创建。
-
产品角色(Product):建造中的复杂对象。它要包含那些定义组件的类,包括将这些组件装配成产品的接口。
3.2.2 适用场景
-
当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时。
-
当构造过程必须允许被构造的对象有不同表示时。
3.3 实现
3.3.1 Builder
// 抽象建造者角色
public interface PersonBuilder {
// 声明构建产品类的组成部分
void buildHead();
void buildBody();
void buildFoot();
Person buildPerson();
}
// 具体建造者角色——男人建造者
public class ManBuilder implements PersonBuilder{
// 建造产品 Person 对象
Person person;
public ManBuilder() {
person = new Man();
}
// 实现抽象建造者类接口中声明的方法
// 当用不着实现某个方法时,可以不予实现,将其置空
public void buildFoot() {
person.setFoot("建造男人的脚");
}
public void buildHead() {
person.setHead("建造男人的头");
}
public void buildBody() {
person.setBody("建造男人的身体");
}
// getResult方法返回构建好的产品类
public Person buildPerson() {
return person;
}
}
// 具体建造者角色——女人建造者
public class WomanBuilder implements PersonBuilder {
Person person;
public WomanBuilder() {
person = new Woman();
}
@Override
public void buildHead() {
person.setHead("建造女人的头");
}
@Override
public void buildBody() {
person.setBody("建造女人的身体");
}
@Override
public void buildFoot() {
person.setFoot("建造女人的脚");
}
@Override
public Person buildPerson() {
return person;
}
}
3.3.2 Product
// 抽象产品类
public class Person {
private String head;
private String body;
private String foot;
public String getHead() {
return head;
}
public void setHead(String head) {
this.head = head;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getFoot() {
return foot;
}
public void setFoot(String foot) {
this.foot = foot;
}
}
// 具体产品类
public class Man extends Person{
public Man(){
System.out.println("开始建造男人");
}
}
// 具体产品类
public class Woman extends Person{
public Woman(){
System.out.println("开始建造女人");
}
}
3.3.3 Director
// 导演类:此为核心类
public class Director {
// 指导如何构建对象的类
// 对象构建的过程是一样的
public Person constructPerson(PersonBuilder pb) {
pb.buildHead();
pb.buildBody();
pb.buildFoot();
// 建造完毕,取回对象
return pb.buildPerson();
}
}
3.3.4 Client
public class Client {
public static void main(String[] args) {
Director pd = new Director();
Person manPerson = pd.constructPerson(new ManBuilder());
Person womanPerson = pd.constructPerson(new WomanBuilder());
System.out.println(womanPerson.getHead());
}
}
3.4 效果
Builder模式的主要效果:
-
它使你可以改变一个产品的内部表示
Builder对象提供给导向器一个构造产品的抽象接口。该接口使得生成器可以隐藏这个产品的表示和内部结构。它同时也隐藏了该产品是如何装配的。因为产品是通过抽象接口构造的,你在改变该产品的内部表示时所要做的只是定义一个新的生成器。
-
它将构造代码和表示代码分开
Builder模式通过封装一个复杂对象的创建和表示方式提高了对象的模块性。客户不需要知道定义产品内部结构的类的所有信息;这些类是不出现在Builder接口中的。每个ConcreteBuilder包含了创建和装配一个特定产品的所有代码。这些代码只需要写一次;然后不同的Director可以复用它以在相同部件集合的基础上构作不同的Product。
-
它使你可对构造过程进行更精细的控制
Builder模式与一下子就生成产品的创建型模式不同,它是在导向者的控制下一步一步构造产品的。仅当该产品完成时导向者才从生成器中取回它。因此Builder接口相比其他创建型模式能更好的反映产品的构造过程。这使你可以更精细的控制构建过程,从而能更精细的控制所得产品的内部结构。
3.5 优点
-
建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在导演类中对整体而言可以取得比较好的稳定性。
-
建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。
3.6 建造者模式与工厂模式的区别
我们可以看到,建造者模式与工厂模式是极为相似的,总体上,建造者模式仅仅只比工厂模式多了一个“导演类”的角色。在建造者模式的类图中,假如把这个导演类看做是最终调用的客户端,那么图中剩余的部分就可以看作是一个简单的工厂模式了。
与工厂模式相比,建造者模式一般用来创建更为复杂的对象,因为对象的创建过程更为复杂,因此将对象的创建过程独立出来组成一个新的类——导演类。也就是说,**工厂模式是将对象的全部创建过程封装在工厂类中,由工厂类向客户端提供最终的产品;而建造者模式中,建造者类一般只提供产品类中各个组件的建造,而将具体建造过程交付给导演类。**由导演类负责将各个组件按照特定的规则组建为产品,然后将组建好的产品交付给客户端。
建造者模式与工厂模式类似,他们都是建造者模式,适用的场景也很相似。一般来说,如果产品的建造很复杂,那么请用工厂模式;如果产品的建造更复杂,那么请用建造者模式。
没有复杂只有更复杂
四、原型模式
用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
4.1 前言
创建型模式一般是用来创建一个新的对象,然后我们使用这个对象完成一些对象的操作,我们通过原型模式可以快速的创建一个对象而不需要提供专门的new()操作就可以快速完成对象的创建,这无疑是一种非常有效的方式,快速的创建一个新的对象。
-
孙悟空拔下一嘬猴毛,轻轻一吹就会变出好多的孙悟空来。
-
寄快递
“给我寄个快递。”顾客说。
“寄往什么地方?寄给……?”你问。
“和上次差不多一样,只是邮寄给另外一个地址,这里是邮寄地址……”顾客一边说一边把写有邮寄地址的纸条给你。
“好!”你愉快地答应,因为你保存了用户的以前邮寄信息,只要复制这些数据,然后通过简单的修改就可以快速地创建新的快递数据了。
当对象的构造函数非常复杂,在生成新对象的时候会非常的耗时间、耗资源…
4.2 概览
可以通过复制(克隆、拷贝)一个指定类型的对象来创建更多同类型的对象。这个指定的对象可被称为“原型”对象,也就是通过复制原型对象来得到更多同类型的对象。即原型设计模式。
4.2.1 类图
![image-20200831095830556](https://i-blog.csdnimg.cn/blog_migrate/6898c899aa3d2b4d55bb1f379d3d9fb1.png)
在原型模式中,主要涉及两个类:
-
Prototype(抽象原型类):规定了具体原型对象必须实现的接口,声明了clone()方法的接口或基类,其中clone()方法必须由派生对象实现。
-
ConcretePrototype(具体原型类):从抽象原型派生而来,是客户程序使用的对象,即被复制的对象。此角色需要实现抽象原型角色所要求的接口。用于实现或扩展clone()方法的类。clone()方法必须要实现,因为它返回了类型的新实例。如果在基类中实现了clone()方法,却没有在具体原型类中实现,那么当我们在具体原型类的对象上调用该方法时,会返回一个基类的抽象原型对象。
可以在接口中声明clone()方法,因而必须在类的实现过程中实现clone()方法,这项操作会在编译阶段强制执行。但是,在多层继承层次中,如果父类实现了clone()方法,继承自它的子类将不会强制执行clone()方法。
4.2.2 适用场景
原型模式的主要思想是基于现有的对象克隆一个新的对象出来,一般是有对象的内部提供克隆的方法,通过该方法返回一个对象的副本,这种创建对象的方式,相比我们之前说的几类创建型模式还是有区别的,之前的讲述的工厂模式与抽象工厂都是通过工厂封装具体的new操作的过程,返回一个新的对象,有的时候我们通过这样的创建工厂创建对象不值得,特别是以下的几个场景的时候,可能使用原型模式更简单也效率更高。
-
当一个系统应该独立于它的产品创建、构成和表示时,要使用 Prototype模式
-
当要实例化的类是在运行时刻指定时,例如,通过动态装载
-
为了避免创建一个与产品类层次平行的工厂类层次时
-
当一个类的实例只能有几个不同状态组合中的一种时
建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。(也就是当我们在处理一些对象比较简单,并且对象之间的区别很小,可能只是很固定的几个属性不同的时候,使用原型模式更合适)。
4.3 实现
// 实现Cloneable接口
class Prototype implements Cloneable {
public Prototype clone(){
Prototype prototype = null;
try{
// 调用Object中的clone()
// 该方法直接在内存中复制数据,不会调用类中的构造方法
prototype = (Prototype)super.clone();
}catch(CloneNotSupportedException e){
e.printStackTrace();
}
return prototype;
}
}
class ConcretePrototype extends Prototype{
public void show(){
System.out.println("原型模式实现类");
}
}
public class Client {
public static void main(String[] args){
ConcretePrototype cp = new ConcretePrototype();
for(int i=0; i< 10; i++){
ConcretePrototype clonecp = (ConcretePrototype)cp.clone();
clonecp.show();
}
}
}
原型模式主要用于对象的复制,它的核心是就是类图中的原型类Prototype。Prototype类需要具备以下两个条件:
- 实现Cloneable接口。在java语言有一个Cloneable接口,它的作用只有一个,就是在运行时通知虚拟机可以安全地在实现了此接口的类上使用clone方法。在java虚拟机中,只有实现了这个接口的类才可以被拷贝,否则在运行时会抛出CloneNotSupportedException异常。
- **重写Object类中的clone方法。**Java中,所有类的父类都是Object类,Object类中有一个clone方法,作用是返回对象的一个拷贝,但是其作用域protected类型的,一般的类无法调用,因此,Prototype类需要将clone方法的作用域修改为public类型。
**注意:**使用原型模式复制对象不会调用类的构造方法。因为对象的复制是通过调用Object类的clone方法来完成的,它直接在内存中复制数据,因此不会调用到类的构造方法。不但构造方法中的代码不会执行,甚至连访问权限都对原型模式无效。单例模式中,只要将构造方法的访问权限设置为private型,就可以实现单例。但是clone方法直接无视构造方法的权限,所以,单例模式与原型模式是冲突的,在使用时要特别注意。
4.4 效果
Prototype模式有许多和Abstract Factory模式 和 Builder模式一样的效果:它对客户隐藏了具体的产品类,因此减少了客户知道的名字的数目。此外,这些模式使客户无需改变即可使用与特定应用相关的类。
-
运行时刻增加和删除产品
Prototype允许只通过客户注册原型实例就可以将一个新的具体产品类并入系统。它比其他创建型模式更为灵活,因为客户可以在运行时刻建立和删除原型。
-
改变值以指定新对象
高度动态的系统允许你通过对象复合定义新的行为。例如,通过为一个对象变量指定值,并且不定义新的类。你通过实例化已有类并且将这些实例注册为客户对象的原型,就可以有效定义新类别的对象。客户可以将职责代理给原型,从而表现出新的行为。这种设计使得用户无需编程即可定义新“类” 。实际上,克隆一个原型类似于实例化一个类。Prototype模式可以极大的减少系统所需要的类的数目。
-
改变结构以指定新对象
许多应用由部件和子部件来创建对象。
-
减少子类的构造
Factory Method 经常产生一个与产品类层次平行的 Creator类层次。Prototype模式使得你克隆一个原型而不是请求一个工厂方法去产生一个新的对象。因此你根本不需要Creator类层次。
-
用类动态配置应用
一些运行时刻环境允许你动态将类装载到应用中。在像 C + +这样的语言中,Prototype模式是利用这种功能的关键。一个希望创建动态载入类的实例的应用不能静态引用类的构造器。而应该由运行环境在载入时自动创建每个类的实例,并用原型管理器来注册这个实例 。这样应用就可以向原型管理器请求新装载的类的实例,这些类原本并没有和程序相连接。
Prototype的主要缺陷是每一个Prototype的子类都必须实现clone操作,这可能很困难。例如,当所考虑的类已经存在时就难以新增 clone操作。当内部包括一些不支持拷贝或有循环引用的对象时,实现克隆可能也会很困难的。
4.5 优点
使用原型模式创建对象比直接new一个对象在性能上要好的多,因为**Object类的clone方法是一个本地方法,它直接操作内存中的二进制流**,特别是复制大对象时,性能的差别非常明显。
使用原型模式的另一个好处是简化对象的创建,使得创建对象就像我们在编辑文档时的复制粘贴一样简单。
因为以上优点,所以**在需要重复地创建相似对象时可以考虑使用原型模式。比如需要在一个循环体内创建对象,假如对象创建过程比较复杂或者循环次数很多的话,使用原型模式不但可以简化创建过程,而且可以使系统的整体性能提高很多。**
4.6 new与clone的区别
clone顾名思义就是复制, 在Java语言中, clone方法被对象调用,所以会复制对象。所谓的复制对象,首先要分配一个和源对象同样大小的空间,再在这个空间中创建一个新的对象。在java语言中,有两种方式可以创建对象:
-
使用new操作符创建一个对象
-
使用clone方法复制一个对象
这两种方式有什么相同和不同呢?new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
4.6.1 引用复制
可以看出,打印的地址值是相同的,既然地址都是相同的,那么肯定是同一个对象。p和p1只是引用而已,他们都指向了一个相同的对象Person(23, “zhang”) 。 可以把这种现象叫做引用的复制。
4.6.2 对象复制(克隆)
可以看出两个对象的地址是不同的,也就是说此时创建了新的对象, 而不是把原对象的地址赋给了一个新的引用变量。
4.6.3 浅拷贝与深拷贝
上面的示例代码中,Person中有两个成员变量,分别是name和age, name是String类型, age是int类型。代码非常简单,如下所示:
private int age ;
private String name;
由于age是基本数据类型, 那么对它的拷贝没有什么疑议,直接将一个4字节的整数值拷贝过来就行。
但是name是String类型的, 它只是一个引用, 指向一个真正的String对象,那么对它的拷贝有两种方式:
- 直接将源对象中的name的引用值拷贝给新对象的name字段——浅拷贝
- 根据原Person对象中的name指向的字符串对象创建一个新的相同的字符串对象,将这个新字符串对象的引用赋给新拷贝的Person对象的name字段——深拷贝
![image-20200831112044901](https://i-blog.csdnimg.cn/blog_migrate/5989d2350b4f512477e6fb8beac840ff.png)
Object类的clone方法是浅拷贝。
只会拷贝对象中的基本的数据类型(byte,char,short,int,long,float,double,boolean),对于数组、容器对象、引用对象等都不会拷贝。如果要实现深拷贝,必须将原型模式中的数组、容器对象、引用对象等另行拷贝。为了要在clone对象时进行深拷贝, 那么就要Cloneable接口,覆盖并实现clone方法,除了调用父类中的clone方法得到新的对象, 还要将该类中的引用变量也clone出来。