概述
“设计模式”最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
1995年,由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides四位作者(GoF)合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了23个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。
软件设计模式的概念
是一套被反复使用,多数人知晓的,经过分类编目的,代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用。
学习设计模式的必要性
设计模式的本质是面向对象设计原则的实际运用,是对类的继承性,封装性和多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点:
可以提高程序员的思维能力,编程能力和设计能力
使程序设计更加标准化,代码编制更加工程化,是软件开发效率大大提高,从而缩短软件的开发周期
使设计的代码可重用性高,可读性强,可靠性高,灵活性好,可维护性强
设计模式的分类
创建型模式
用于描述“怎么创建对象”,它的主要特点是“将对象的创建与使用分离”。四人组书中提供了单例,原型,工厂方法,抽象工厂,建造者等5种创建型模式。
结构型模式
用于描述“如何将类和对象按照某种布局组成更大的结构”,书中提供了代理,适配器,桥接,装饰,外观,享元,组合等7种结构型模式。
行为型模式
用于描述“类或对象之间怎么互相协作共同完成单个对象无法单独完成的任务,以及怎么分配职责”,书中提供了模板方法,策略,命令,职责链,状态,观察者,中介者,迭代器,访问者,备忘录,解释器等11种行为型模式。
UML图
统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单,统一,图形化,能表达软件设计中的动态和静态信息。
UML从目标的不同角度出发,定义了用例图,类图,对象图,状态图,时序图,协作图,构件图,部署图等9种图。
类图概述
类图显示了模型的静态结构,特别是模型中存在的类,类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。
类图的作用
在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类的关系,可以简化人们对系统的理解。
类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。
类图表示法
类的表示方式
在UML类图中,类使用包含类名,属性(field)和方法(method)且带有分割线的举行表示,比如下图表示一个Empolyee类,它包含name,age和address这三个属性,以及work()方法。
属性或方法名称前加的加号和减号表示了它的可见性,UML类图中表示可见性的符号有三种:
+:表示public
-:表示private
#:表示protected
属性的完整表示方式是:可见性 名称 : 类型 [ = 缺省值 ]
方法的完整表示方式是: 可见性 名称(参数列表) [ :返回值类型 ]
注意:
中括号中的内容表示是可选的
也有将类型放在变量名前面,返回值类型放在方法名前面
例子:
上方Demo类定义了三个方法:
method()方法:修饰符为public。没有参数,没有返回值
method1()方法:修饰符为private,没有参数,返回值类型为String
method2()方法:修饰符为protected,接收两个参数,第一个参数为int,第二个参数类型为String,返回值类型是int。
类与类之间关系的表示
关联关系
关联关系是对象之间的一种引用关系,用于表示一类对象和另一类对象之间的关系,如老师和学生,师傅和徒弟,丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系,聚合关系和组合关系。先介绍一般关联。
关联又可以分为单向关联,双向关联,自关联。
单向关联
在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过Customer类持有一个类型为Address的成员变量类实现。
双向关联
所谓双向关联就是双方各自持有对方类型的成员变量。
从UML类图中,双向关联用一个不带箭头的直线表示。上图中在Custom类中维护一个List<Product>,表示一个顾客可以买多个商品,在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。
自关联
自关联在UML类图中用一个带箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是”自己包含自己“。
聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体而独立存在。例如学校和老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
在UML类图中,聚合关系可以用带空心菱形的实现来表示,菱形指向整体。
组合关系
组合表示类之间的整体和部分的关系,但它是一种更强烈的聚合关系。
在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能够脱离整体对象而存在。例如头和嘴的关系。
在UML类图中,组合关系用带实心菱形的实线来表示,菱形指向整体
依赖关系
依赖关系是一种使用关系,它是对象之间耦合度最脆弱的一种关联方式,是临时性的关系。在代码中,某个类的方法通过局部变量,方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
在UML类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。
继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
在UML类图中,泛化关系用带空心三角箭头的实现来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。
实现关系
实现关系时接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
在UML类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。
软件设计原则
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件开发效率,节约软件成本和维护成本。
开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热拔插的效果。简而言之,是为了是程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类。
因为抽象灵活性好,适用性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
例:输入法皮肤,这些皮肤都有共同的特点,可以为其定义一个抽象类,而每个具体的皮肤是其子类。用户可以根据需求增加新的主题,而不需要修改原代码。
里氏代换原则
里氏代换原则是面向对象设计的基本原则之一。
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解,子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
经典例子:正方形不是长方形
在数学领域,正方形毫无疑问是长方形,它是长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形
我们有一个resize方法如下
当我们传入的是长方形,则可正常执行,而如果我们的正方形类在继承长方形类后重写了set方法如下
那么此时这个正方形类的对象作为参数传入resize()就会一直执行,直到系统产生溢出错误。
因此,它们之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形
改进如下
依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
例子:组装电脑
可以看到此时电脑的个组件是固定了的,想换个牌子是不行的。
根据依赖倒转原则进行改进
接口隔离原则
客户端不应该被迫依赖于它不使用的方法:一个类对另一个类的依赖应该建立在最小的接口上。
例子:安全门
那么我们如果想要创建另一个安全门,它只有防盗,放火的功能,那么显然此时SafetyDoor接口就违反了接口隔离原则。
改进如下:
迪米特法则
又叫最少知识原则
只和你的直接朋友交谈,不和陌生人说话。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的互相调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的”朋友“是指:当前对象本身,当前对象的成员变量,当前对象所创建的对象,当前对象的方法参数等,这些对象同当前对象存在关联,聚合或组成的关系,可以直接访问这些对象的方法。
例子:明星和经纪人
合成复用原则
合成复用原则是指:尽量先使用组成或者聚合关系等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的服用分为继承复用和合成复用两种。
继承复用虽然有简单和易于实现的优点,但他也存在以下缺点:
继承复用破坏了类的封装性,因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为"白箱复用"。
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,不利于类的扩展和维护
它限制了复用的灵活性,从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,他有以下优点:
它维持了类的封装性,因为成分对象的内部细节是新对象看不见的,所以这种复用又称为黑箱复用
对象间的耦合度低,可以在类的成员位置声明抽象
复用的灵活度高。这种复用可以在运行时动态进行,新对象可以动态的引用与成分对象类型相同的对象。
例子:汽车分类管理
汽车按照”动力源“划分可以分为汽油汽车,电动汽车等,按颜色可以分为白色,黑色等。如果同时考虑着两种分类,其组合就有很多。
可以看出继承复用产生了很多子类,如果又有新的动力源或颜色,就需要定义新的类
改为聚合复用
创建型模式
创建型模式的主要关注点是”怎么创建对象“,它的主要特点是”将对象的创建与使用分离“。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建型模式分为:单例模式,工厂方法模式,抽象工厂模式,原型模式,建造者模式
单例设计模式
单例模式是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例设计模式的结构
单例模式主要有以下角色:
单例类:只能创建一个实例的类
访问类:使用单例类
单例模式的实现
单例设计模式分为两种:
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次该对象时才创建
饿汉式
方式一:静态变量方式
public class Singleton {
//私有化构造器
private Singleton() {
}
//在本类中创建本类对象
private static Singleton instance = new Singleton();
//提供一个公共的访问方式,让外界获取该对象
public static Singleton getInstance() {
return instance;
}
}
方式二:静态代码块方式
public class Singleton {
//声明Singleton类型的变量
private static Singleton instance;
//私有化构造器
private Singleton() {
}
static {
//在本类中创建本类对象
instance = new Singleton();
}
//提供一个公共的访问方式,让外界获取该对象
public static Singleton getInstance() {
return instance;
}
}
方式三:枚举
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分利用了枚举的这个特性来实现单例模式,写法非常简单,而且是单例实现中唯一一种不会被破坏的单例实现模式。
public enum Singleton {
INSTANCE;
}
饿汉式对象随着类的加载而创建,存在内存浪费问题,线程安全
懒汉式
方式一:线程不安全
public class Singleton {
//声明Singleton类型的变量
private static Singleton instance;
//私有化构造器
private Singleton() {
}
//提供一个公共的访问方式,让外界获取该对象
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
方式二:线程安全(双重检查锁)
对于getInstance()方法来说,绝大部分操作都是读操作(第一调用为instance赋值为写操作),读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
public class Singleton {
//声明Singleton类型的变量
private static Singleton instance;
//私有化构造器
private Singleton() {
}
//提供一个公共的访问方式,让外界获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为空则直接返回instance,不需要锁
if (instance == null) {
synchronized (Singleton.class) {
//第二次判断,如果instance不为空则不为它重新赋值
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
看似完美无缺,但是在多线程的情况下,可能会出现空指针的问题,原因是JVM在实例对象的时候会进行优化和指令重排序操作,要解决双重检查锁带来的空指针的问题,只需要使用volatile关键字,它可以保证可见性和有序性。
private static volatile Singleton instance;
方式三:静态内部类
静态内部类单例模式中实例由内部类创建,由于JVM在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性和方法被调用时才会被创建,并初始化其静态属性。静态属性由于被static关键字修饰,保证只被实例化一次,并且严格保证实例化顺序
public class Singleton {
//私有化构造器
private Singleton() {
}
//定义一个静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
只有第一次调用getIntance()方法,虚拟机才会加载SingletonHolder并初始化INSTANCE,这样不仅能确保线程安全,也能保证Singleton类的唯一性。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的单例模式,在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
单例模式存在的问题
通过序列化和反射可以是上面的单例类创建多个对象(枚举方式除外)
首先让单例类实现Serializable接口
使用序列化和反序列化的方式测试:
public class Client {
public static void main(String[] args) throws Exception {
//先使用这个方法将对象写入文件
// writeObject2File();
readObjectFromFile();
readObjectFromFile();
}
//从文件中读取数据(对象)
public static void readObjectFromFile() throws Exception {
//创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("a.txt"));
//读对象
Singleton instance = (Singleton) ois.readObject();
//输出对象
System.out.println(instance);
//释放资源
ois.close();
}
//像文件中写数据(对象)
public static void writeObject2File() throws Exception {
//获取Singleton对象
Singleton instance = Singleton.getInstance();
//创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("a.txt"));
//写对象
oos.writeObject(instance);
//释放资源
oos.close();
}
}
发现两次输出的地址不相同,创建了多个对象
通过反射破坏单例模式:
public class Client {
public static void main(String[] args) throws Exception {
Class clazz = Singleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance1 = (Singleton) constructor.newInstance();
Singleton instance2 = (Singleton) constructor.newInstance();
System.out.println(instance1 == instance2);
}
}
结果为false,也创建了多个对象
问题的解决
序列化,反序列化方式破坏单例的解决方法:在Singleton类中添加readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义则返回new出来的对象
解决序列化破解的单例模式:
观察ObjectInputStream的readObject()方法->readObject0()->readOrdinaryObject()的实现
可以看到它判断是否定义了readResolve方法,如果有则调用。
反射方式破解单例模式的解决方案
源码解析Runtime类
Runtime类就是使用的单例设计模式
创建本类对象并初始化,提供获取实例的方法,私有化构造器 饿汉式
测试Runtime类
public class RuntimeDemo {
public static void main(String[] args) throws IOException {
Runtime runtime = Runtime.getRuntime();
//调用runtime的方法exec,参数是一个命令
Process process = runtime.exec("ipconfig");
//调用Process对象的获取输入流的方法
InputStream is = process.getInputStream();
byte[] arr = new byte[1024 * 1024 * 1024];
int len = is.read(arr);
System.out.println(new String(arr, 0, len, "GBK"));
}
}
工厂模式
概述
例子:咖啡店点餐系统
设计一个咖啡类,定义其两个子类,再设计一个咖啡店类
在Java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,显然违反了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就之和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里面更换该对象即可,达到了与对象解耦的目的,所以说,工厂模式最大的优点就是解耦。
下面介绍三种工厂的使用:
简单工厂模式(不属于GOF的23种经典设计模式)
工厂方法模式
抽象工厂模式
简单工厂模式
不是一种设计模式,反而像是一种编程习惯。
结构包含如下角色:
抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
具体产品:实现或者继承抽象产品的子类
具体工厂:提供了创建产品的方法,调用者调用该方法来获取产品
实现:
代码
SimpleCoffeeFactory
public class SimpleCoffeeFactory {
public Coffee createCoffee(String type) {
Coffee coffee = null;
if ("american".equals(type)) {
coffee = new AmericanCoffee();
} else if ("latte".equals(type)) {
coffee = new LatteCoffee();
} else {
throw new RuntimeException("对不起,没有您想要的咖啡!");
}
return coffee;
}
}
CoffeeStore
public class CoffeeStore {
SimpleCoffeeFactory simpleCoffeeFactory = new SimpleCoffeeFactory();
public Coffee orderCoffee(String type) {
Coffee coffee = simpleCoffeeFactory.createCoffee(type);
coffee.addMilk();
coffee.addSugar();
return coffee;
}
}
简单工厂模式的优缺点
优点:封装了创建对象的过程,可以通过参数直接获取对象,把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新的产品直接修改工厂类。更容易扩展。
缺点:新增产品还是需要修改工厂类的代码,违背了“开闭原则”。
扩展:静态工厂
在开发中也有部分人将工厂类中创建对象的功能定义为静态的,这就是静态工厂。也不属于23种设计模式。
工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
结构包含如下角色:
抽象工厂:提供了创建产品的接口,调用者通过它访问具体的工厂的工厂方法来创建产品。
具体工厂:主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品:定义了产品的规范,描述产品的主要特性和功能
具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应
实现:
代码:
抽象工厂
public interface CoffeeFactory {
Coffee createCoffee();
}
具体工厂
public class AmericanCoffeeFactory implements CoffeeFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}
}
CoffeeStore
public class CoffeeStore {
private CoffeeFactory factory;
public void setFactory(CoffeeFactory factory) {
this.factory = factory;
}
public Coffee orderCoffee() {
Coffee coffee = factory.createCoffee();
coffee.addMilk();
coffee.addSugar();
return coffee;
}
}
工厂方法模式的优缺点
优点:用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体的创建过程。
在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则。
缺点:每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度
抽象工厂模式
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物,电视机厂只生产电视机
这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级的产品,如电器厂即生产电视机又生产洗衣机或空调。
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所产生的位于不同等级的一组产品称为一个产品族,下面所示横轴是产品等级,也就是一类产品,纵轴是产品族,也就是一个品牌的产品,同一品牌的产品产自同一个工厂。
概念
是一种为访问类提供一个创建一组相关或互相依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
抽象工厂模式的结构包含的对象:
抽象工厂:提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
具体工厂:主要是实现了抽象工厂中的多个抽象方法,完成具体产品的创建。
抽象产品:定义了产品的规范,描述产品的主要特性和功能,抽象工厂模式有多个抽象产品。
具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建。它同具体工厂之间的关系是多对一的关系。
实现:
代码:
抽象产品
public abstract class Dessert {
public abstract void show();
}
具体产品
public class MatchaMousse extends Dessert {
@Override
public void show() {
System.out.println("抹茶慕斯!");
}
}
抽象工厂
public interface DesertFactory {
Coffee createCoffee();
Dessert createDessert();
}
具体工厂
public class AmericanDessertFactory implements DesertFactory {
@Override
public Coffee createCoffee() {
return new AmericanCoffee();
}
@Override
public Dessert createDessert() {
return new MatchaMousse();
}
}
如果要添加一个产品族,只需添加一个具体工厂即可,不需要修改其他类。
抽象工厂模式的优缺点:
优点:当一个产品族的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族的对象
缺点:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
使用场景
当需要创建的对象是一系列相互关联或相互依赖的产品组时,如电器工厂中的电视机,洗衣机,空调等。
系统中有多个产品族,但每次只使用其中的某一族产品,如有人只喜欢穿一个品牌的衣服,鞋子
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构
如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。
模式扩展
简单工厂+配置文件解除耦合
配置文件
american=simple_factory.AmericanCoffee
latte=simple_factory.LatteCoffee
工厂类
public class CoffeeFactory {
private static Map<String, Coffee> map = new HashMap();
static {
Properties prop = new Properties();
InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
prop.load(is);
//遍历Properties集合对象
Set<Object> keys = prop.keySet();
for (Object key : keys) {
//根据键获取值
String className = prop.getProperty((String) key);
//获取字节码对象
Class clazz = Class.forName(className);
Coffee coffee = (Coffee) clazz.newInstance();
map.put((String) key, coffee);
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static Coffee createCoffee(String name) {
return map.get(name);
}
}
JDK源码解析-Collection.iterator方法
使用迭代器遍历集合
单列集合获取迭代器的方法就用到了工厂方法模式
Collection接口是抽象工厂
ArrayList是具体的工厂类
Iterator接口是抽象产品类
ArrayList$Itr是具体产品类
补充:DateFormat类中的getInstance()方法和Calender类的getInstance()方法使用的是工厂模式
原型模式
用一个已经创建的实例作为原型,通过复制该原型的对象来创建一个和原型对象相同的新对象
结构所包含的角色:
抽象原型类:规定了具体原型对象必须实现的clone()方法
具体原型类:实现抽象原型类的clone()方法,它是可被复制的对象
访问类:使用具体原型类中的clone()方法来复制新的对象
实现
原型模式的克隆分为浅克隆和深克隆
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的内存地址
深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址
Java中Object类中提供了clone()方法实现浅克隆,Cloneable接口是上面类图中的抽象原型类,而实现Clonable接口的子实现类就是具体的原型类。
具体原型类
public class RealizeType implements Cloneable {
public RealizeType() {
System.out.println("具体的原型对象创建完成");
}
@Override
public RealizeType clone() throws CloneNotSupportedException {
return (RealizeType) super.clone();
}
}
访问类
public class Client {
public static void main(String[] args) throws Exception {
//创建一个原型类对象
RealizeType realize = new RealizeType();
//使用clone()方法进行克隆
RealizeType realize2 = realize.clone();
System.out.println(realize2 == realize);
}
}
只执行了一次构造器中的输出语句,说明clone方法创建对象不是new出来的
判断结果为false,不是同一个对象,创建了新的对象
案例:生成奖状
具体原型类
public class Citation implements Cloneable {
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
protected Citation clone() throws CloneNotSupportedException {
return (Citation) super.clone();
}
}
访问类
public class Client {
public static void main(String[] args) throws Exception {
//创建一个原型类对象
Citation citation = new Citation();
//使用clone()方法进行克隆
Citation citation1 = citation.clone();
citation1.setName("GYQ");
Citation citation2 = citation.clone();
citation2.setName("GZZ");
}
}
这里虽然是String是引用类型,但是String被final修饰的不能被修改,当修改时会产生新的字符串变量,引用地址指向新的地址。所以即使是浅拷贝String类型也不会因为克隆出来的对象修改这个属性而改变原来对象原来的属性值。
使用场景
对象创建非常复杂,可以使用原型模式快捷的创建对象
性能和安全性要求比较高
扩展(深克隆)
将上面案例中name属性修改为Student类型的属性
测试
发现两个对象地址相同,如果修改其中一个Student对象的属性,另一个Citation对象中的也会改变,这就是浅克隆的效果,对具体原型类中的引用类型的属性进行引用的复制。这种情况需要使用深克隆来解决。
需要具体原型类和它包含的属性的类实现Serializable接口
public class Client {
public static void main(String[] args) throws Exception {
//创建一个原型类对象
Citation citation = new Citation();
Student student = new Student();
student.setName("GYQ");
citation.setStudent(student);
//创建对象输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.txt"));
//写对象
oos.writeObject(citation);
//关闭流
oos.close();
//创建对象输入流
//读对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.txt"));
Citation citation1 = (Citation) ois.readObject();
ois.close();
citation1.getStudent().setName("GYY");
System.out.println(citation.getStudent().getName());
}
}
原来的对象没有改变
建造者模式
将一个复杂对象的构建和表示分离,使得同样的构建过程可以创建不同的表示。
分离了部件的构造(由Builder来负责)和装配(由Director负责),从而可以构造出复杂的对象。
这个模式适用于:某个对象的构建过程复杂的情况。
由于实现了构建和装配的解耦。不同的构造器,相同的装配,也可以做出不同的对象;相同的构造器,不同的装配顺序也可以做出不同的对象,也就是实现了构建算法,装配算法的解耦,实现了更好的复用。
建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无需知道其内部的具体构造细节。
实现
建造者模式包含如下角色
抽象建造者类(Builder):这个接口规定要实现复杂对象的哪些部分进行的创建,并不涉及具体的对象部件的创建。
具体建造者类:实现Builder接口,完成复杂产品各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
产品类:要创建的复杂对象。
指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
案例:生产自行车
生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产,车架,车座又有各种材质。
抽象建造者
public abstract class Builder {
//声明Bike类型的变量,并进行赋值
protected Bike bike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
//构建自行车的方法
public abstract Bike createBike();
}
具体建造者
public class MobileBuilder extends Builder {
@Override
public void buildFrame() {
bike.setFrame("碳纤维车架");
}
@Override
public void buildSeat() {
bike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return bike;
}
}
指挥者
public class Director {
//声明Builder类型的变量
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
//组装自行车的功能
public Bike construct() {
builder.buildFrame();
builder.buildSeat();
return builder.createBike();
}
}
测试
上面是Builder模式的常规用法,指挥者类Director在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合。
这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不太符合单一职责原则,如果constructor()过于复杂,建议还是封装到Director中。
优缺点与使用场景
优点:
建造者模式的封装性很好。可以有效地封装变化(产品组件),在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此将主要的业务逻辑封装在指挥者类中对整体而言可以取得较好的稳定性。
在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建出不同的产品对象。
可以更加精细的控制产品的创建过程,将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
建造者模式很容易进行扩展,如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险,符合开闭原则。
缺点:
建造者模式所创建产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定限制。
使用场景
建造者模式创建的是复杂对象,其产品的每个部分经常面临剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它们通常在以下场合下使用。
创建的对象较复杂,有多个部件组成,各部件面临复杂的变化,但部件间的创建顺序是稳定的。
创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,及产品的构建过程和最终的表示是独立的
模式扩展
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且容易引入错误,此时就可以利用建造者模式进行重构。
重构前代码如下
重构:
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainBoard;
private Phone(Builder builder) {
this.cpu = builder.cpu;
this.screen = builder.screen;
this.memory = builder.memory;
this.mainBoard = builder.mainBoard;
}
public static final class Builder {
private String cpu;
private String screen;
private String memory;
private String mainBoard;
public Builder cpu(String cpu) {
this.cpu = cpu;
return this;
}
public Builder screen(String screen) {
this.screen = screen;
return this;
}
public Builder memory(String memory) {
this.memory = memory;
return this;
}
public Builder mainBoard(String mainBoard) {
this.mainBoard = mainBoard;
return this;
}
public Phone build() {
return new Phone(this);
}
}
@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainBoard='" + mainBoard + '\'' +
'}';
}
}
创建对象
重构后的代码在使用起来更加方便,某种程度可以提高开发效率。
创建型模式对比
工厂方法模式VS建造者模式
工厂方法模式注重的是整体对象的创建方式,而建造者模式注重的是部件构建的过程,意在一步一步的精确构造出一个复杂的对象。
我们举个简单例子说明两者的差异:如果创建一个超人,使用工厂方法模式,直接产生出来就是一个力大无穷,能够飞翔,内裤外穿的超人;而如果使用建造者模式,则需要组装头,手,脚,躯干等部分,最后把裤衩子给他套上,一个超人就诞生了。
抽象工厂模式VS建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心过程,只关心什么产品由什么工厂生产即可。
建造者模式则是按照指定的蓝图建造产品,它的主要目的是通过组装零件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装车间,通过对部分的组装可以返回一辆汽车。