目录
前言
学习文档:
五、23 种设计模式
创建型模式
1、单例模式
1.1、单例模式的定义与特点
单例(Singleton)模式的定义:就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法(静态方法)。
例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。在计算机系统中,还有 Windows 的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序对象、打印机的后台处理服务、应用程序的日志对象、数据库的连接池、网站的计数器、Web 应用的配置对象、应用程序中的对话框、系统中的缓存等常常被设计成单例。
单例模式在现实生活中的应用也非常广泛,例如公司 CEO、部门经理等都属于单例模型。J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring 框架应用中的 ApplicationContext、数据库中的连接池、Hibernate的SessionFactory,它充当数据存储源的代理,并负责创建Session对象。SessionFactory并不是轻量级的,一般情况下,一个项目通常只需要一个SessionFactory就够了,这时就会使用到单例模式。等也都是单例模式。
单例模式有 3 个特点:
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
1.2、单例模式的优点和缺点
单例模式的优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
单例模式的缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
单例模式看起来非常简单,实现起来也非常简单。单例模式在面试中是一个高频面试题。希望大家能够认真学习,掌握单例模式,提升核心竞争力,给面试加分,顺利拿到 Offer。
1.3、单例模式的应用场景
对于 Java 来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC。
- 某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用。
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
- 频繁访问数据库或文件的对象。
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。
- 当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
1.4、单例模式的结构与实现
单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
单例模式有八种方式:
1. 饿汉式(静态常量);
2. 饿汉式(静态代码块);
3. 懒汉式(线程不安全);
4. 懒汉式(线程安全,同步方法);
5. 懒汉式(线程安全,同步代码块);
6. 双重检查;
7. 静态内部类;
8. 枚举
1.5、八种方式详解
1)、饿汉式(静态常量)
代码演示:
public class SingletonTest01 {
public static void main(String[] args) {
Singleton01 instance = Singleton01.getInstance();
Singleton01 instance2 = Singleton01.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest01-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest01-instance2.hashCode()="+instance2.hashCode());
}
}
// 饿汉式(静态常量)
class Singleton01 {
// 1、构造器私有化,外部不能new
private Singleton01() {
}
// 2、本类内部创建对象实例
private final static Singleton01 instance = new Singleton01();
// 3、提供一个公有的静态方法,返回实例对象
public static Singleton01 getInstance() {
return instance;
}
}
优缺点说明:
- 优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题;
- 缺点:在类装载的时候就完成实例化,没有达到懒加载的效果,如果从始至终从未使用过这个实例,则会造成内存的浪费;
- 这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,在单例模式中大多数都是调用getInstance方法,但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他静态方法)导致类加载,这时候初始化instance就没有达到懒加载的效果。
- 结论:这种单例模式可用,可能造成内存浪费。
2)、饿汉式(静态代码块)
代码演示:
public class SingletonTest02 {
public static void main(String[] args) {
Singleton02 instance = Singleton02.getInstance();
Singleton02 instance2 = Singleton02.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest02-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest02-instance2.hashCode()="+instance2.hashCode());
}
}
// 饿汉式(静态代码块)
class Singleton02 {
//1、构造器私有化,外部不能new
private Singleton02() {
}
// 2、静态代码块
private static Singleton02 instance;
static {
instance = new Singleton02();
}
// 3、提供一个公有的静态方法,返回实例对象
public static Singleton02 getInstance() {
return instance;
}
}
结果:
优缺点说明:
- 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码快中的代码,初始化类的实例。优缺点和上面是一样的。
- 结论:这种单例模式可用,但是可能造成内存浪费。
3)、懒汉式(线程不安全)
代码演示:
public class SingletonTest03 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest03-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest03-instance2.hashCode()="+instance2.hashCode());
}
}
// 懒汉式(线程不安全)
class Singleton {
// 定义变量添加final必须初始化,因为这里没有初始化,所以不需要添加final
private static Singleton instance;
private Singleton() {
}
// 提供一个静态的公有方法,当使用到该方法时,才去创建instance
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
结果:
优缺点说明:
- 起到了懒加载的效果,但是只能在单线程下使用;
- 如果在多线程下,一个线程进入了if(singleton==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
- 结论:在实际开发中,不要使用这种方式。
4)、懒汉式(线程安全,同步方法)
代码演示:
public class SingletonTest04 {
public static void main(String[] args) {
Singleton04 instance = Singleton04.getInstance();
Singleton04 instance2 = Singleton04.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest04-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest04-instance2.hashCode()="+instance2.hashCode());
}
}
// 懒汉式(线程安全,同步方法)
class Singleton04 {
private static Singleton04 instance;
private Singleton04() {
}
// 提供一个静态的公有方法,加入同步处理的代码(synchronized),解决线程安全问题
public static synchronized Singleton04 getInstance() {
if (instance == null) {
instance = new Singleton04();
}
return instance;
}
}
结果:
优缺点说明:
- 解决了线程安全问题;
- 效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低;
- 结论:在实际开发中,不推荐使用这种方式。
5)、懒汉式(线程安全,同步代码块)
代码演示:
public class SingletonTest05 {
public static void main(String[] args) {
Singleton05 instance = Singleton05.getInstance();
Singleton05 instance2 = Singleton05.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest05-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest05-instance2.hashCode()="+instance2.hashCode());
}
}
// 懒汉式(线程安全,同步代码块)
class Singleton05 {
private static Singleton05 instance;
private Singleton05() {
}
public static Singleton05 getInstance() {
if (instance == null) {
synchronized (Singleton05.class) {
instance = new Singleton05();
}
}
return instance;
}
}
结果:
优缺点说明:
- 这种方式,本意是想对第四种实现方式的改进,因为前面同步方法效率太低, 改为同步产生实例化的的代码块
- 但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一 致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行, 另一个线程也通过了这个判断语句,这时便会产生多个实例
- 结论:在实际开发中,不能使用这种方式
6)、双重检查
代码演示:
public class SingletonTest06 {
public static void main(String[] args) {
Singleton06 instance = Singleton06.getInstance();
Singleton06 instance2 = Singleton06.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest06-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest06-instance2.hashCode()="+instance2.hashCode());
}
}
//双重检查
class Singleton06 {
private static volatile Singleton06 instance;
private Singleton06() {
}
// 提供一个静态的公有方法,加入双重检查代码,解决线程安全问题,同时解决懒加载问题
public static synchronized Singleton06 getInstance() {
if (instance == null) {
synchronized (Singleton06.class) {
if (instance == null) {
instance = new Singleton06();
}
}
}
return instance;
}
}
结果:
优缺点说明:
- Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if(singleton==null)检查,这样就可以保证线程安全了。
- 这样,实例化代码只用执行一次,后面再次访问时,判断if(singleton==null),直接reeturn实例化对象,也避免反复进行方法同步;
- 线程安全,延迟加载,效率较高;
- 结论:在实际开发中,推荐使用这种单例设计模式。
7)、静态内部类
代码演示:
public class SingletonTest07 {
public static void main(String[] args) {
Singleton07 instance = Singleton07.getInstance();
Singleton07 instance2 = Singleton07.getInstance();
System.out.println(instance == instance2);
System.out.println("SingletonTest07-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest07-instance2.hashCode()="+instance2.hashCode());
}
}
// 静态内部类
class Singleton07 {
private Singleton07() {
}
// 写一个静态内部类,该类中有一个静态属性Singleton
private static class singleInstance {
private static final Singleton07 INSTANCE = new Singleton07();
}
//提供一个静态的公有方法,直接返回对象
public static synchronized Singleton07 getInstance() {
return singleInstance.INSTANCE;
}
}
结果:
优缺点说明:
- 静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,会装载SingletonInstance类,从而完成Singleton的实例化;
- 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的;
- 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高;
- 结论:推荐使用
8)、枚举
代码演示:
public class SingletonTest08 {
public static void main(String[] args) {
Singleton08 instance = Singleton08.INSTANCE;
Singleton08 instance2 = Singleton08.INSTANCE;
System.out.println(instance == instance2);
System.out.println("SingletonTest08-instance.hashcode()="+instance.hashCode());
System.out.println("SingletonTest08-instance2.hashCode()="+instance2.hashCode());
instance.sayOK();
}
}
// 使用枚举,可以实现单例
enum Singleton08 {
INSTANCE;//属性
public void sayOK() {
System.out.println("ok");
}
}
结果:
优缺点说明:
- 这借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
- 这种方式是Effective Java作者Josh Bloch 提倡的方式
- 结论:推荐使用
1.6、单例模式在JDK应用中的源码分析
1) 我们JDK中,java.lang.Runtime就是经典的单例模式(饿汉式)
2) 代码分析+Debug源码+代码说明
1.7、单例模式注意事项和细节说明
- 单例模式保证了系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
- 当想实例化一个单例类的时候,必须要记住使用相应的获得对象的方法,而不是使用new;
- 单例模式使用场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session工厂等。
2、工厂模式
2.1、简单工厂模式
现实生活中,原始社会自给自足(没有工厂),农耕社会小作坊(简单工厂,民间酒坊),工业革命流水线(工厂方法,自产自销),现代产业链代工厂(抽象工厂,富士康)。我们的项目代码同样是由简到繁一步一步迭代而来的,但对于调用者来说,却越来越简单。
在日常开发中,凡是需要生成复杂对象的地方,都可以尝试考虑使用工厂模式来代替。
注意:上述复杂对象指的是类的构造函数参数过多等对类的构造有影响的情况,因为类的构造过于复杂,如果直接在其他业务类内使用,则两者的耦合过重,后续业务更改,就需要在任何引用该类的源代码内进行更改,光是查找所有依赖就很消耗时间了,更别说要一个一个修改了。
工厂模式的定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
按实际业务场景划分,工厂模式有 3 种不同的实现方式,分别是简单工厂模式、工厂方法模式和抽象工厂模式。
我们把被创建的对象称为“产品”,把创建产品的对象称为“工厂”。如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。
在简单工厂模式中创建实例的方法通常为静态(static)方法,因此简单工厂模式(Simple Factory Pattern)又叫作静态工厂方法模式(Static Factory Method Pattern)。
简单来说,简单工厂模式有一个具体的工厂类,可以生成多个不同的产品,属于创建型设计模式。简单工厂模式不在 GoF 23 种设计模式之列。
简单工厂模式每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度,违背了“开闭原则”。
“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。
1)、优点和缺点
优点:
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确。
- 客户端无需知道所创建具体产品的类名,只需知道参数即可。
- 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类。
缺点:
- 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则。
- 使用简单工厂模式会增加系统中类的个数(引入新的工厂类),增加系统的复杂度和理解难度
- 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,在产品类型较多时,可能造成逻辑过于复杂
- 简单工厂模式使用了 static 工厂方法,造成工厂角色无法形成基于继承的等级结构。
应用场景
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
2)、模式的结构与实现
简单工厂模式的主要角色如下:
- 简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
- 抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
- 具体产品(ConcreteProduct):是简单工厂模式的创建目标。
其结构图如下图所示。
图 1 简单工厂模式的结构图
根据上图写出该模式的代码如下:
public class Client {
public static void main(String[] args) {
}
//抽象产品
public interface Product {
void show();
}
//具体产品:ProductA
static class ConcreteProduct1 implements Product {
public void show() {
System.out.println("具体产品1显示...");
}
}
//具体产品:ProductB
static class ConcreteProduct2 implements Product {
public void show() {
System.out.println("具体产品2显示...");
}
}
final class Const {
static final int PRODUCT_A = 0;
static final int PRODUCT_B = 1;
static final int PRODUCT_C = 2;
}
// 简单工厂
static class SimpleFactory {
public static Product makeProduct(int kind) {
switch (kind) {
case Const.PRODUCT_A:
return new ConcreteProduct1();
case Const.PRODUCT_B:
return new ConcreteProduct2();
}
return null;
}
}
}
3)、应用实例
看一个披萨的项目:要便于披萨种类的扩展,要便于维护。
- 披萨的种类很多(比如GreekPizza、CheesePizza等);
- 披萨的制作有prepare,bake,cut,box;
- 完成披萨店订购功能。
使用传统的方式完成
// 把Pizza类做成抽象类
public abstract class Pizza {
private String name;//名字
// 准备原材料,不同的披萨不一样,因此,我们做成抽象方法
public abstract void prepare();
public void bake() {
System.out.println(name + " baking");
}
public void cut() {
System.out.println(name + " cutting");
}
public void box() {
System.out.println(name + " boxing");
}
public void setName(String name) {
this.name = name;
}
}
public class GreekPizza extends Pizza {
@Override
public void prepare() {
System.out.println("给GreekPizza准备原材料");
}
}
public class CheesePizza extends Pizza {
@Override
public void prepare() {
System.out.println("给CheessPizza准备原材料");
}
}
public class OrderPizza {
// 构造方法
public OrderPizza() {
Pizza pizza = null;
String orderType;//订购披萨的类型
do {
orderType = getType();
if (orderType.equals("greek")) {
pizza = new GreekPizza();
pizza.setName("GreekPizza");
} else if (orderType.equals("cheese")) {
pizza = new CheesePizza();
pizza.setName("CheessPizza");
} else {
break;
}
// 输出pizza制作过程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} while (true);
}
// 写一个方法,可以获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type:");
String str = null;
str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
//相当于客户端
public class OrderStore {
public static void main(String[] args) {
OrderPizza orderPizza = new OrderPizza();
}
}
结果:
传统方式的优缺点:
- 优点是比较好理解,简单易操作;
- 缺点是违反了设计模式的ocp原则,即对扩展开放,对修改关闭。即当我们给类增加新功能的时候,尽量不修改代码,或者尽量少修改代码;
- 比如这时我们要新增加一个Pizza的种类(Pepper披萨),我们需要做的修改较大。
改进的思路分析:
- 分析:修改代码可以接受,但是如果我们在其它的地方也有创建Pizza的代码,就意味着,也需要修改,而创建Pizza的代码,往往有多处;
- 思路:把创建Pizza对象封装到一个类中,这样我们有新的Pizza种类时,只需要修改该类就可,其它有创建到Pizza对象的代码就不需要修改了(使用简单工厂模式)。
简单工厂模式介绍
简单工厂模式是属于创建型模式,是工厂模式的一种。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式。
简单工厂模式:定义了一个创建对象的类,由这个类来封装实例化对象的行为。
在软件开发中,当我们会用到大量的创建某种、某类或者谋批对象时,就会使用到工厂模式。
简单工厂模式的设计方案:定义一个可以实例化Pizza对象的类,封装创建对象的代码。
// 简单工厂类
// 简单工厂模式,也叫静态工厂模式,可以用static修饰该方法
public class SimpleFactory {
// 根据orderType返回对应的pizza对象。
public Pizza createPizza(String orderType) {
Pizza pizza = null;
if (orderType.equals("greek")) {
pizza = new GreekPizza();
pizza.setName("GreekPizza");
} else if (orderType.equals("cheese")) {
pizza = new CheesePizza();
pizza.setName("CheessPizza");
}
return pizza;
}
}
public class OrderPizza2 {
// 定义一个简单工厂对象
SimpleFactory simpleFactory;
// 构造器
public OrderPizza2(SimpleFactory simpleFactory) {
setFactory(simpleFactory);
}
Pizza pizza = null;
public void setFactory(SimpleFactory simpleFactory) {
String orderType = ""; //用户输入的
this.simpleFactory = simpleFactory; //设置简单工厂对象
do {
orderType = getType();
pizza = this.simpleFactory.createPizza(orderType);
// 输出pizza制作过程
if (pizza != null) { // 订购成功
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} else {
System.out.println("订购失败");
break;
}
} while (true);
}
// 写一个方法,可以获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type:");
String str = null;
str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
// 相当于客户端
public class PizzaStore2 {
public static void main(String[] args) {
OrderPizza2 orderPizza = new OrderPizza2(new SimpleFactory());
}
}
结果:
2.2、工厂方法模式
在现实生活中社会分工越来越细,越来越专业化。各种产品有专门的工厂生产,彻底告别了自给自足的小农经济时代,这大大缩短了产品的生产周期,提高了生产效率。同样,在软件开发中能否做到软件对象的生产和使用相分离呢?能否在满足“开闭原则”的前提下,客户随意增删或改变对软件相关对象的使用呢?这就是本节要讨论的问题。
在《简单工厂模式》一节我们介绍了简单工厂模式,提到了简单工厂模式违背了开闭原则,而“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。
优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
- 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
- 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。
缺点:
- 类的个数容易过多,增加复杂度
- 增加了系统的抽象性和理解难度
- 抽象产品只能生产一种产品,此弊端可使用抽象工厂模式解决。
应用场景:
- 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
- 客户不关心创建产品的细节,只关心产品的品牌
1)、模式的结构与实现
工厂方法模式由抽象工厂、具体工厂、抽象产品和具体产品等4个要素构成。本节来分析其基本结构和实现方法。
1. 模式的结构
工厂方法模式的主要角色如下。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
2.模式的实现
该模式的代码如下:
// 抽象产品角色
public interface Moveable {
void run();
}
// 具体产品角色
public class Plane implements Moveable {
@Override
public void run() {
System.out.println("plane....");
}
}
public class Broom implements Moveable {
@Override
public void run() {
System.out.println("broom.....");
}
}
// 抽象工厂
public abstract class VehicleFactory {
abstract Moveable create();
}
// 具体工厂
public class PlaneFactory extends VehicleFactory {
public Moveable create() {
return new Plane();
}
}
public class BroomFactory extends VehicleFactory {
public Moveable create() {
return new Broom();
}
}
// 测试类
public class Test {
public static void main(String[] args) {
VehicleFactory factory = new BroomFactory();
Moveable m = factory.create();
m.run();
}
}
程序运行结果如下:
broom.....
2)、应用实例
披萨项目新的需求:客户在点披萨时,可以点不同口味的披萨,比如“北京的奶酪披萨”、“北京的胡椒披萨”或者是“伦敦的奶酪披萨”、“伦敦的胡椒披萨”。
思路一:使用简单工厂模式,创建不同的简单工厂类,比如BJPizzaSimpleFactory、LDPizzzaSimpleFactory等等,从当前这个案例来说,也是可以的,但是考虑到项目的规模,以及软件的可维护性 、可扩展性并不是特别好。
思路二:使用工厂方法模式。将披萨项目的实例化功能抽象成抽象方法,在不同口味点餐子类中具体实现。工厂方法模式定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
披萨项目新的需求:客户在点披萨时,可以点不同口味的披萨,比如“北京的奶酪披萨”、“北京的胡椒披萨”或者是“伦敦的奶酪披萨”、“伦敦的胡椒披萨”。
思路一:使用简单工厂模式,创建不同的简单工厂类,比如BJPizzaSimpleFactory、LDPizzzaSimpleFactory等等,从当前这个案例来说,也是可以的,但是考虑到项目的规模,以及软件的可维护性 、可扩展性并不是特别好。
思路二:使用工厂方法模式。将披萨项目的实例化功能抽象成抽象方法,在不同口味点餐子类中具体实现。
工厂方法模式定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
1、抽象产品
// 抽象产品
//把Pizza类做成抽象类
public abstract class Pizza {
private String name;//名字
//准备原材料,不同的披萨不一样,因此,我们做成抽象方法
public abstract void prepare();
public void bake() {
System.out.println(name + " baking");
}
public void cut() {
System.out.println(name + " cutting");
}
public void box() {
System.out.println(name + " boxing");
}
public void setName(String name) {
this.name = name;
}
}
2、具体产品
// 具体产品
public class BJCheessPizza extends Pizza {
@Override
public void prepare() {
setName("北京的CheessPizza");
System.out.println(" 北京的CheessPizza 准备原材料");
}
}
// 具体产品
public class BJPepperPizza extends Pizza {
@Override
public void prepare() {
setName("北京的Pepper");
System.out.println(" 北京的Pepper 准备原材料");
}
}
// 具体产品
public class LDCheessPizza extends Pizza {
@Override
public void prepare() {
setName("伦敦的CheessPizza");
System.out.println(" 伦敦的CheessPizza 准备原材料");
}
}
// 具体产品
public class LDPepperPizza extends Pizza {
@Override
public void prepare() {
setName("伦敦的Pepper");
System.out.println(" 伦敦的Pepper 准备原材料");
}
}
3、抽象工厂
// 抽象工厂
public abstract class OrderPizza {
//构造方法
public OrderPizza() {
Pizza pizza = null;
String orderType;//订购披萨的类型
do {
orderType = getType();
//调用方法
pizza = creatPizza(orderType);
//输出pizza制作过程
pizza.prepare();
pizza.bake();
pizza.cut();
pizza.box();
} while (true);
}
//定义一个抽象方法,让各个工厂子类自己实现
abstract Pizza creatPizza(String orderType);
//写一个方法,可以获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type:");
String str = null;
str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
4、具体工厂
public class BJOrderPizza extends OrderPizza {
@Override
Pizza creatPizza(String orderType) {
Pizza pizza=null;
if (orderType.equals("Cheess")){
pizza=new BJCheessPizza();
}else if(orderType.equals("Pepper")){
pizza=new BJPepperPizza();
}
return pizza;
}
}
public class LDOrderPizza extends OrderPizza {
@Override
Pizza creatPizza(String orderType) {
Pizza pizza = null;
if (orderType.equals("Cheess")) {
pizza = new LDCheessPizza();
} else if (orderType.equals("Pepper")) {
pizza = new LDPepperPizza();
}
return pizza;
}
}
//披萨店
public class PizzaStore {
public static void main(String[] args) {
new BJOrderPizza();//创建北京各种口味的披萨
new LDOrderPizza();//创建伦敦各种口味的披萨
}
}
2.3、抽象工厂模式
抽象工厂模式:与工厂方法模式不同的是,工厂方法模式中的工厂只生产单一的产品,而抽象工厂模式中的工厂生产多个产品
同种类称为同等级,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如农场里既养动物又种植物,电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。
本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,图 1 所示的是海尔工厂和 TCL 工厂所生产的电视机与空调对应的关系图。
图1 电器工厂的产品等级与产品族
1)、模式的定义与特点
抽象工厂(AbstractFactory)模式的定义:是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
使用抽象工厂模式一般要满足以下条件。
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品。
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用。
抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。
- 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
- 当需要产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
- 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。
其缺点是:当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
2)、模式的结构与实现
抽象工厂模式同工厂方法模式一样,也是由抽象工厂、具体工厂、抽象产品和具体产品等 4 个要素构成,但抽象工厂中方法个数不同,抽象产品的个数也不同。现在我们来分析其基本结构和实现方法。
1. 模式的结构
抽象工厂模式的主要角色如下。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
抽象工厂模式的结构图如图 2 所示。
图2 抽象工厂模式的结构图
2. 模式的实现
从图 2 可以看出抽象工厂模式的结构同工厂方法模式的结构相似,不同的是其产品的种类不止一个,所以创建产品的方法也不止一个。下面给出抽象工厂和具体工厂的代码。
(1) 抽象工厂:提供了产品的生成方法。
interface AbstractFactory {
public Product1 newProduct1();
public Product2 newProduct2();
}
(2) 具体工厂:实现了产品的生成方法。
class ConcreteFactory1 implements AbstractFactory {
public Product1 newProduct1() {
System.out.println("具体工厂 1 生成-->具体产品 11...");
return new ConcreteProduct11();
}
public Product2 newProduct2() {
System.out.println("具体工厂 1 生成-->具体产品 21...");
return new ConcreteProduct21();
}
}
3)、模式的应用实例
【例1】用抽象工厂模式设计农场类。
分析:农场中除了像畜牧场一样可以养动物,还可以培养植物,如养马、养牛、种菜、种水果等,所以本实例比前面介绍的畜牧场类复杂,必须用抽象工厂模式来实现。
本例用抽象工厂模式来设计两个农场,一个是韶关农场用于养牛和种菜,一个是上饶农场用于养马和种水果,可以在以上两个农场中定义一个生成动物的方法 newAnimal() 和一个培养植物的方法 newPlant()。
对马类、牛类、蔬菜类和水果类等具体产品类,由于要显示它们的图像(点此下载图片),所以它们的构造函数中用到了 JPanel、JLabel 和 ImageIcon 等组件,并定义一个 show() 方法来显示它们。
客户端程序通过对象生成器类 ReadXML 读取 XML 配置文件中的数据来决定养什么动物和培养什么植物(点此下载 XML 文件)。其结构图如图 3 所示。
// 抽象产品角色
public interface Food {
void createFood();
}
public interface Vehicle {
void createVehicle();
}
public interface Weapon {
void createWeapon();
}
// 具体产品角色
public class Apple implements Food {
@Override
public void createFood() {
System.out.println("Apple....");
}
}
public class Car implements Vehicle {
@Override
public void createVehicle() {
System.out.println("Car ....");
}
}
public class AK47 implements Weapon {
@Override
public void createWeapon() {
System.out.println("AK47 ....");
}
}
//抽象工厂类
public abstract class AbstractFactory {
public abstract Vehicle createVehicle();
public abstract Weapon createWeapon();
public abstract Food createFood();
}
//具体工厂类,其中Food,Vehicle,Weapon是抽象类,
public class DefaultFactory extends AbstractFactory{
@Override
public Food createFood() {
return new Apple();
}
@Override
public Vehicle createVehicle() {
return new Car();
}
@Override
public Weapon createWeapon() {
return new AK47();
}
}
//测试类
public class Test {
public static void main(String[] args) {
AbstractFactory f = new DefaultFactory();
Vehicle v = f.createVehicle();
v.run();
Weapon w = f.createWeapon();
w.shoot();
Food a = f.createFood();
a.printName();
}
}
【例2】用抽象工厂模式设计订披萨。
1、抽象产品
//把Pizza类做成抽象类
public abstract class Pizza {
private String name;//名字
//准备原材料,不同的披萨不一样,因此,我们做成抽象方法
public abstract void prepare();
public void bake() {
System.out.println(name + " baking");
}
public void cut() {
System.out.println(name + " cutting");
}
public void box() {
System.out.println(name + " boxing");
}
public void setName(String name) {
this.name = name;
}
}
2、具体产品
public class BJCheessPizza extends Pizza {
@Override
public void prepare() {
setName("北京的CheessPizza");
System.out.println(" 北京的CheessPizza 准备原材料");
}
}
public class BJPepperPizza extends Pizza {
@Override
public void prepare() {
setName("北京的Pepper");
System.out.println(" 北京的Pepper 准备原材料");
}
}
public class LDCheessPizza extends Pizza {
@Override
public void prepare() {
setName("伦敦的CheessPizza");
System.out.println(" 伦敦的CheessPizza 准备原材料");
}
}
public class LDPepperPizza extends Pizza {
@Override
public void prepare() {
setName("伦敦的Pepper");
System.out.println(" 伦敦的Pepper 准备原材料");
}
}
3、抽象工厂
//一个抽象工厂模式的抽象层(接口)
public interface AbsFactory {
//让下面的工厂子类来具体实现
Pizza createPizza(String orderType);
}
4、具体工厂
//这是一个工厂子类
public class BJFactory implements AbsFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza = null;
if (orderType.equals("Cheess")) {
pizza = new BJCheessPizza();
}
if (orderType.equals("Pepper")) {
pizza = new BJPepperPizza();
}
return pizza;
}
}
public class LDFactory implements AbsFactory {
@Override
public Pizza createPizza(String orderType) {
Pizza pizza=null;
if (orderType.equals("Cheess")){
pizza=new LDCheessPizza();
}if (orderType.equals("Pepper")){
pizza=new LDPepperPizza();
}
return pizza;
}
}
public class OrderPizza {
AbsFactory factory;
//构造器
public OrderPizza(AbsFactory factory) {
setFactory(factory);
}
private void setFactory(AbsFactory factory) {
Pizza pizza = null;
String orderType = "";//用户输入
this.factory = factory;
do {
orderType = getType();
//factory可能是北京的工厂子类,也可能是伦敦的工厂子类
pizza = factory.createPizza(orderType);
if (pizza != null) {
pizza.prepare();
pizza.bake();
pizza.box();
pizza.cut();
} else {
System.out.println("订购失败");
break;
}
} while (true);
}
//写一个方法,可以获取客户希望订购的披萨种类
private String getType() {
try {
BufferedReader strin = new BufferedReader(new InputStreamReader(System.in));
System.out.println("input pizza type:");
String str = null;
str = strin.readLine();
return str;
} catch (IOException e) {
e.printStackTrace();
return "";
}
}
}
public class PizzaStore {
public static void main(String[] args) {
new OrderPizza(new BJFactory());
new OrderPizza(new LDFactory());
}
}
4)、工厂模式小结
工厂模式的意义:将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦,从而提高项目的扩展和维护性。
三种工厂模式:简单工厂模式、工厂方法模式、抽象工厂模式。
设计模式的依赖抽象原则:
- 创建对象实例时,不要直接new类,而是把这个new类的动作放在一个工厂的方法中,并返回。有的书上说,变量不要直接持有具体类的引用;
- 不要让类继承具体类,而是继承抽象类或者是实现interface(接口);
- 不要覆盖基类中已经实现的方法。
3、原型模式
3.1、原型模式的定义与特点
原型(Prototype)模式的定义如下:用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。在这里,原型实例指定了要创建的对象的种类。用这种方式创建对象非常高效,根本无须知道对象创建的细节。例如,Windows 操作系统的安装通常较耗时,如果复制就快了很多。在生活中复制的例子非常多,这里不一一列举了。
工作原理是:通过将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝它们自己来实施创建,即“对象.clone( )”。
原型模式的优点:
- Java 自带的原型模式基于内存二进制流的复制,在性能上比直接 new 一个对象更加优良。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
原型模式的缺点:
- 需要为每一个类都配置一个 clone 方法
- clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
- 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。
3.2、原型模式的结构与实现
由于 Java 提供了对象的 clone() 方法,所以用 Java 实现原型模式很简单。
1. 模式的结构
原型模式包含以下主要角色。
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
其结构图如图 1 所示。
2. 模式的实现
原型模式的克隆分为浅克隆和深克隆。
- 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。
- 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。
Java 中的 Object 类提供了浅克隆的 clone() 方法,具体原型类只要实现 Cloneable 接口就可实现对象的浅克隆,这里的 Cloneable 接口就是抽象原型类。其代码如下:
//具体原型类
class Realizetype implements Cloneable {
Realizetype() {
System.out.println("具体原型创建成功!");
}
public Object clone() throws CloneNotSupportedException {
System.out.println("具体原型复制成功!");
return (Realizetype) super.clone();
}
}
//原型模式的测试类
public class PrototypeTest {
public static void main(String[] args) throws CloneNotSupportedException {
Realizetype obj1 = new Realizetype();
Realizetype obj2 = (Realizetype) obj1.clone();
System.out.println("obj1==obj2?" + (obj1 == obj2));
}
程序的运行结果如下:
具体原型创建成功!
具体原型复制成功!
obj1==obj2?false
【例1】用原型模式生成“三好学生”奖状。
分析:同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,属于相似对象的复制,同样可以用原型模式创建,然后再做简单修改就可以了。图 4 所示是三好学生奖状生成器的结构图。
图4 奖状生成器的结构图
程序代码如下:
public class ProtoTypeCitation {
public static void main(String[] args) throws CloneNotSupportedException {
citation obj1 = new citation("张三", "同学:在2016学年第一学期中表现优秀,被评为三好学生。", "韶关学院");
obj1.display();
citation obj2 = (citation) obj1.clone();
obj2.setName("李四");
obj2.display();
}
}
//奖状类
class citation implements Cloneable {
String name;
String info;
String college;
citation(String name, String info, String college) {
this.name = name;
this.info = info;
this.college = college;
System.out.println("奖状创建成功!");
}
void setName(String name) {
this.name = name;
}
String getName() {
return (this.name);
}
void display() {
System.out.println(name + info + college);
}
public Object clone() throws CloneNotSupportedException {
System.out.println("奖状拷贝成功!");
return (citation) super.clone();
}
}
程序运行结果如下:
奖状创建成功!
张三同学:在2016学年第一学期中表现优秀,被评为三好学生。韶关学院
奖状拷贝成功!
李四同学:在2016学年第一学期中表现优秀,被评为三好学生。韶关学院
3.3、原型模式的应用场景
原型模式通常适用于以下场景。
- 对象之间相同或相似,即只是个别的几个属性不同的时候。
- 创建对象成本较大,例如初始化时间长,占用CPU太多,或者占用网络资源太多等,需要优化资源。
- 创建一个对象需要繁琐的数据准备或访问权限等,需要提高性能或者提高安全性。
- 系统中大量使用该类对象,且各个调用者都需要给它的属性重新赋值。
在 Spring 中,原型模式应用的非常广泛,例如 scope='prototype'、JSON.parseObject() 等都是原型模式的具体应用。
3.4、原型模式的扩展
原型模式可扩展为带原型管理器的原型模式,它在原型模式的基础上增加了一个原型管理器 PrototypeManager 类。该类用 HashMap 保存多个复制的原型,Client 类可以通过管理器的 get(String id) 方法从中获取复制的原型。其结构图如图 5 所示。
图5 带原型管理器的原型模式的结构图
【例2】用带原型管理器的原型模式来生成包含“圆”和“正方形”等图形的原型,并计算其面积。分析:本实例中由于存在不同的图形类,例如,“圆”和“正方形”,它们计算面积的方法不一样,所以需要用一个原型管理器来管理它们,图 6 所示是其结构图。
图6 图形生成器的结构图
程序代码如下:
import java.util.*;
interface Shape extends Cloneable {
public Object clone(); //拷贝
public void countArea(); //计算面积
}
class Circle implements Shape {
public Object clone() {
Circle w = null;
try {
w = (Circle) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("拷贝圆失败!");
}
return w;
}
public void countArea() {
int r = 0;
System.out.print("这是一个圆,请输入圆的半径:");
Scanner input = new Scanner(System.in);
r = input.nextInt();
System.out.println("该圆的面积=" + 3.1415 * r * r + "\n");
}
}
class Square implements Shape {
public Object clone() {
Square b = null;
try {
b = (Square) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println("拷贝正方形失败!");
}
return b;
}
public void countArea() {
int a = 0;
System.out.print("这是一个正方形,请输入它的边长:");
Scanner input = new Scanner(System.in);
a = input.nextInt();
System.out.println("该正方形的面积=" + a * a + "\n");
}
}
class ProtoTypeManager {
private HashMap<String, Shape> ht = new HashMap<String, Shape>();
public ProtoTypeManager() {
ht.put("Circle", new Circle());
ht.put("Square", new Square());
}
public void addshape(String key, Shape obj) {
ht.put(key, obj);
}
public Shape getShape(String key) {
Shape temp = ht.get(key);
return (Shape) temp.clone();
}
}
public class ProtoTypeShape {
public static void main(String[] args) {
ProtoTypeManager pm = new ProtoTypeManager();
Shape obj1 = (Circle) pm.getShape("Circle");
obj1.countArea();
Shape obj2 = (Shape) pm.getShape("Square");
obj2.countArea();
}
}
运行结果如下所示:
这是一个圆,请输入圆的半径:3
该圆的面积=28.2735
这是一个正方形,请输入它的边长:3
该正方形的面积=9
3.5、应用实例
1)、克隆羊问题
现在有一只羊,姓名为:tom,年龄为:1,颜色为:白色,请编写程序创建和tom羊属性完全相同的10只羊。
2)、传统方式解决克隆羊问题
传统的方式的优缺点:
- 优点是比较好理解,简单易操作;
- 在创建新的对象时,总是需要重新获取原始对象的属性,如果创建的对象比较复杂时,效率较低;
- 总是需要重新初始化对象,而不是动态地获得对象运行时的状态,不够灵活;
改进的思路分析:Java中Object类是所有类的根类,Object类提供了一个clone( )方法,该方法可以将一个Java对象复制一份,但是需要实现clone的Java类必须要实现一个接口Cloneable,该接口表示该类能够复制且具有复制的能力(原型模式)。
public class Sheep implements Cloneable{
private String name;
private int age;
private String color;
public Sheep(String name, int age, String color) {
this.name = name;
this.age = age;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Sheep{" +
"name='" + name + '\'' +
", age=" + age +
", color='" + color + '\'' +
'}';
}
//克隆该实例,使用默认的clone方法来完成
@Override
protected Object clone() {
Sheep sheep = null;
try {
sheep = (Sheep) super.clone();
} catch (CloneNotSupportedException e) {
System.out.println(e.getMessage());
}
return sheep;
}
}
public class Client {
public static void main(String[] args) {
Sheep sheep = new Sheep("tom", 1, "白色");
Sheep sheep2 = (Sheep) sheep.clone();
Sheep sheep3 = (Sheep)sheep.clone();
//......
System.out.println(sheep);
System.out.println(sheep2);
System.out.println(sheep3);
}
}
3.6、原型模式在Spring框架中源码分析
1) Spring中原型bean的创建,就是原型模式的应用
2) 代码分析+Debug源码
beans.xml
<bean id="id01" class="com.atguigu.spring.bean.Monster" scope="prototype"/>
Test.java
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
//获取monster[通过id获取monster]
Object bean1 = applicationContext.getBean("id01");
System.out.println("bean1" + bean2);
Object bean2 = applicationContext.getBean("id01");
System.out.println("bean1" + bean2);
System.out.println(bean1 = bean2); // FALSE
}
3.7、浅拷贝与深拷贝
1)、浅拷贝
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。
- 对于数据类型是引用类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响另一个对象的该成员变量值。
- 前面我们的克隆羊案例就是浅拷贝。
- 浅拷贝是使用默认的clone( )方法来实现。
2)、深拷贝
- 复制对象的所有基本数据类型的成员变量值。
- 为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象,也就是说,对象进行深拷贝要对整个对象(包括对象的引用类型)进行拷贝。
深拷贝的实现方式1:重写clone方法。
深拷贝实现方式2:通过对象序列化实现(推荐)。
public class DeepCloneAbleTarget implements Serializable, Cloneable {
private String cloneName;
private String cloneClass;
// 构造函数
public DeepCloneAbleTarget(String cloneName, String cloneClass) {
this.cloneName = cloneName;
this.cloneClass = cloneClass;
}
// 因为该类的属性,都是String,因此我们这里使用默认的clone方法即可
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class DeepProtoType implements Serializable, Cloneable {
public String name; // String 属性
public DeepCloneAbleTarget deepCloneAbleTarget; // 应用类型
public DeepProtoType() {
super();
}
// 深拷贝 - 方式一使用clone方法
@Override
protected Object clone() throws CloneNotSupportedException {
Object deep = null;
// 这里完成对基本类型(属性)和String的克隆
deep = super.clone();
// 对引用类型的属性单独处理
DeepProtoType deepProtoType = (DeepProtoType) deep;
deepProtoType.deepCloneAbleTarget = (DeepCloneAbleTarget) deepCloneAbleTarget.clone();
return deepProtoType;
}
// 方式二:通过对象的序列化实现深拷贝(推荐)
public Object deepClone(){
// 创建流对象
ByteArrayOutputStream bos = null;
ObjectOutputStream oos = null;
ByteArrayInputStream bis = null;
ObjectInputStream ois = null;
try {
// 序列化
bos = new ByteArrayOutputStream();
oos = new ObjectOutputStream(bos);
oos.writeObject(this); // 当前的这个对象以对象流的方式输出
// 反序列化
bis = new ByteArrayInputStream(bos.toByteArray());
ois = new ObjectInputStream(bis);
DeepProtoType copyObj = (DeepProtoType) ois.readObject();
return copyObj;
} catch (Exception e) {
e.printStackTrace();
return null;
}finally {
// 关闭流
try {
bos.close();
oos.close();
bis.close();
ois.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
public class Client {
public static void main(String[] args) throws Exception {
DeepProtoType p1 = new DeepProtoType();
p1.name = "宋江";
p1.deepCloneAbleTarget = new DeepCloneAbleTarget("大牛", "大黑牛");
// 方式一:完成深拷贝
DeepProtoType p2 = (DeepProtoType) p1.clone();
System.out.println("-----------------方式一:完成深拷贝-------------------");
System.out.println("d.name = " + p1.name + "d.deepCloneAbleTarget=" + p1.deepCloneAbleTarget.hashCode());
System.out.println("d2.name = " + p2.name + "d.deepCloneAbleTarget=" + p2.deepCloneAbleTarget.hashCode());
// 方式二:完成深拷贝
DeepProtoType p3 = (DeepProtoType) p1.deepClone();
System.out.println("-----------------方式二:完成深拷贝-------------------");
System.out.println("p2.name=" + p1.name + "p.deepCloneableTarget=" + p1.deepCloneAbleTarget.hashCode());
System.out.println("p2.name=" + p3.name + "p2.deepCloneableTarget=" + p3.deepCloneAbleTarget.hashCode());
}
}
运行结果
3.8、原型模式的注意事项的细节
- 创建新的对象比较复杂时,可以利用原型模式简化对象的创建过程,同时也能够提高效率;
- 不用重新初始化对象,而是动态地获得对象运行时的状态;
- 如果原始对象发生变化(增加或者减少属性),其它克隆对象的也会发生相应的变化,无需修改代码 ;
- 在实现深克隆的时候可能需要比较复杂的代码;
- 缺点:需要为每一个类配备一个克隆方法,这对全新的类来说不是很难,但对已有的类进行改造时,需要修改其源代码,违背了ocp原则,这点请注意。
4、建造者模式
在软件开发过程中有时需要创建一个复杂的对象,这个复杂对象通常由多个子部件按一定的步骤组合而成。例如,计算机是由 CPU、主板、内存、硬盘、显卡、机箱、显示器、键盘、鼠标等部件组装而成的,采购员不可能自己去组装计算机,而是将计算机的配置要求告诉计算机销售公司,计算机销售公司安排技术人员去组装计算机,然后再交给要买计算机的采购员。
生活中这样的例子很多,如游戏中的不同角色,其性别、个性、能力、脸型、体型、服装、发型等特性都有所差异;还有汽车中的方向盘、发动机、车架、轮胎等部件也多种多样;每封电子邮件的发件人、收件人、主题、内容、附件等内容也各不相同。
以上所有这些产品都是由多个部件构成的,各个部件可以灵活选择,但其创建步骤都大同小异。这类产品的创建无法用前面介绍的工厂模式描述,只有建造者模式可以很好地描述该类产品的创建。
4.1、模式的定义与特点
建造者(Builder)模式的定义:指将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
该模式的主要优点如下:
- 封装性好,构建和表示分离。
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦。
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
其缺点如下:
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
建造者(Builder)模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用。
4.2、模式的结构与实现
建造者(Builder)模式由产品、抽象建造者、具体建造者、指挥者等 4 个要素构成,现在我们来分析其基本结构和实现方法。
1. 模式的结构
建造者(Builder)模式的主要角色如下。
- 产品角色(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()。
- 具体建造者(Concrete Builder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。
- 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
4.3、盖房项目需求
需求描述:
- 需要建造房子:这一过程为:打桩、砌墙、封顶。
- 房子有各种各样的,比如普通房,高楼,别墅,各种房子的过程虽然一样,但是要求不要相同的。
- 请编写程序,完成需求。
传统方式解决盖房需求
public abstract class AbstractHouse {
public abstract void buildbasic();//打地基
public abstract void buildWalls();//砌墙
public abstract void roofed();//封顶
public void build() {
buildbasic();
buildWalls();
roofed();
}
}
public class CommonHouse extends AbstractHouse {
@Override
public void buildbasic() {
System.out.println("普通房子打地基");
}
@Override
public void buildWalls() {
System.out.println("普通房子砌墙");
}
@Override
public void roofed() {
System.out.println("普通房子封顶");
}
}
public class Client {
public static void main(String[] args) {
AbstractHouse commonHouse = new CommonHouse();
commonHouse.build();
}
}
传统方式问题分析:
- 优点是比较好理解,简单易操作。
- 设计的程序结构,过于简单,没有设计缓存层对象,程序的扩展和维护不好,也就是说,这种设计方案,把产品(房子)和创建产品的过程(建房子的流程)封装在一起,耦合性增强了。
- 解决方案:将产品和产品建造过程解耦,即使用建造者模式。
下面我们使用建造者模式来完成需求。
//产品-->Product
public class House {
private String baise;
private String wall;
private String roofed;
public String getBaise() {
return baise;
}
public void setBaise(String baise) {
this.baise = baise;
}
public String getWall() {
return wall;
}
public void setWall(String wall) {
this.wall = wall;
}
public String getRoofed() {
return roofed;
}
public void setRoofed(String roofed) {
this.roofed = roofed;
}
}
//抽象的建造者
public abstract class HouseBuilder {
protected House house = new House();
//将建造的流程写好,抽象的方法
public abstract void buildBasic();
public abstract void buildWalls();
public abstract void roofed();
//建造房子后,将产品(房子)返回
public House buildHouse() {
return house;
}
}
public class CommonHouse extends HouseBuilder {
@Override
public void buildBasic() {
System.out.println("普通房子打地基 5米");
}
@Override
public void buildWalls() {
System.out.println("普通房子砌墙10cm");
}
@Override
public void roofed() {
System.out.println("普通房子屋顶");
}
}
public class HighBuilding extends HouseBuilder {
@Override
public void buildBasic() {
System.out.println("高楼的打地基100米");
}
@Override
public void buildWalls() {
System.out.println("高楼的砌墙20cm");
}
@Override
public void roofed() {
System.out.println("高楼的透明屋顶");
}
}
//指挥者,指定制作流程
public class HouseDirector {
HouseBuilder houseBuilder = null;
//通过构造器传入houseBuilder
public HouseDirector(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
//或者通过set方法传入houseBuilder
public void setHouseBuilder(HouseBuilder houseBuilder) {
this.houseBuilder = houseBuilder;
}
//如何处理建造房子的流程?交给指挥者
public House constructHouse() {
houseBuilder.buildBasic();
houseBuilder.buildWalls();
houseBuilder.roofed();
return houseBuilder.buildHouse();
}
}
public class Client {
public static void main(String[] args) {
CommonHouse commonHouse = new CommonHouse();//盖普通房子
HouseDirector houseDirector = new HouseDirector(commonHouse);//创建房子的指挥者
House house = houseDirector.constructHouse();//完成盖房,返回产品(房子)
System.out.println("-------------------------------");
HighBuilding highBuilding = new HighBuilding();//盖高房子
houseDirector.setHouseBuilder(highBuilding);//重置建造者
houseDirector.constructHouse();
}
}
4.4、建造者模式在JDK中应用以及源码解析
- java.lang.StringBuilder中的建造者模式
- 代码说明+Debug源码
- 源码中建造者模式角色分析
Appendable 接口定义了多个append方法(抽象方法), 即Appendable 为抽象建 造者, 定义了抽象方法
AbstractStringBuilder 实现了 Appendable 接口方法,这里的 AbstractStringBuilder 已经是建造者,只是不能实例化
StringBuilder 即充当了指挥者角色,同时充当了具体的建造者,建造方法的 实现是由 AbstractStringBuilder 完成, 而StringBuilder 继承了AbstractStringBuilder
4.5、建造者模式的注意事项和细节
1)、客户端(使用程序)不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
2)、每一个具体建造者都相对独立,而与其他的具体建造者无关,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。
3)、可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
4)、增加新的具体建造者无需修改原有类库的代码,指挥者类针对抽象建造者类编程,系统扩展方便,符合“开闭原则”。
5)、建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
6)、如果产品的内部变化复杂,可能会导致需要定义很多具体建造者类来实现这种变化,导致系统变得很庞大,因此这种情况下,要考虑是否选择建造者模式。
7)、抽象工厂模式VS建造者模式:抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式不需要关心构建过程,只关心什么产品由什么工厂生产即可。而建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
结构型模式
5、适配器模式
在现实生活中,经常出现两个对象因接口不兼容而不能在一起工作的实例,这时需要第三者进行适配。例如,讲中文的人同讲英文的人对话时需要一个翻译,用直流电的笔记本电脑接交流电源时需要一个电源适配器,用计算机访问照相机的 SD 内存卡时需要一个读卡器等。
在软件设计中也可能出现:需要开发的具有某种业务功能的组件在现有的组件库中已经存在,但它们与当前系统的接口规范不兼容,如果重新开发这些组件成本又很高,这时用适配器模式能很好地解决这些问题。
5.1、模式的定义与特点
适配器模式(Adapter)的定义如下:将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
该模式的主要优点如下。
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
- 在很多业务场景中符合开闭原则。
其缺点是:
- 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。
5.2、模式的结构与实现
类适配器模式可采用多重继承方式实现,如 C++ 可定义一个适配器类来同时继承当前系统的业务接口和现有组件库中已经存在的组件接口;Java 不支持多继承,但可以定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。
对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。现在来介绍它们的基本结构。
1. 模式的结构
适配器模式(Adapter)包含以下主要角色。
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
5.3、类适配器模式
类适配器模式介绍
基本介绍:Adapter类,通过继承 src类,实现 dst 类接口,完成src->dst的适配。
类适配器模式应用实例
1)、以生活中充电器的例子来讲解适配器,充电器本身相当于Adapter,220V交流电 相当于src (即被适配者),我们的目dst(即 目标)是5V直流电
2)、思路分析(类图)
3)、代码实现
//被适配的类
public class Voltage220V {
//输出220V的电压
public int output220V(){
int src=220;
System.out.println("电源电压="+src+"伏");
return src;
}
}
//适配接口
public interface IVoltage5V {
int output5V();
}
public class VoltageAdapter extends Voltage220V implements IVoltage5V {
@Override
public int output5V() {
int srcV = output220V();//获取220V的电压
int dstV = srcV / 44;//进行处理
return dstV;
}
}
public class Phone {
//充电
public void charging(IVoltage5V iVoltage5V){
if (iVoltage5V.output5V()==5){
System.out.println("现在电压为5V,可以充电");
}else if (iVoltage5V.output5V()>5){
System.out.println("现在电压大于5V,可以充电");
}
}
}
public class Client {
public static void main(String[] args) {
Phone phone = new Phone();
phone.charging(new VoltageAdapter());
}
}
5.4、对象适配器模式
什么是对象适配器模式
基本思路和类的适配器模式相同,只是将Adapter类作修改,不是继承src类,而是持有src类的实例,以解决兼容性的问题。即:持有src类,实现dst类接口,完成src>dst的适配。
根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系。
对象适配器模式是适配器模式常用的一种。
思路分析
代码实现
//被适配的类
public class Voltage220V {
//输出220V的电压
public int output220V(){
int src=220;
System.out.println("电源电压="+src+"伏");
return src;
}
}
//适配接口
public interface IVoltage5V {
int output5V();
}
public class VoltageAdapter implements IVoltage5V {
private Voltage220V voltage220V;
//通过构造器传入
public VoltageAdapter(Voltage220V voltage220V) {
this.voltage220V = voltage220V;
}
@Override
public int output5V() {
int dst = 0;
if (null != voltage220V) {
int src = voltage220V.output220V();
dst = src / 44;
}
return dst;
}
}
public class Phone {
//充电
public void charging(IVoltage5V iVoltage5V){
if (iVoltage5V.output5V()==5){
System.out.println("现在电压为5V,可以充电");
}else if (iVoltage5V.output5V()>5){
System.out.println("现在电压大于5V,可以充电");
}
}
}
public class Client {
public static void main(String[] args) {
System.out.println("==============对象适配器模式================");
Phone phone = new Phone();
phone.charging(new VoltageAdapter(new Voltage220V()));
}
}
对象适配器注意事项和细节
对象适配器和类适配器其实算是同一种思想,只不过实现方式不同。根据合成复用原则,使用组合替代继承,所以它解决了类适配器必须继承src的局限性问题,也不再要求dst必须是接口。
使用成本更低更灵活。
5.5、接口适配器模式
什么是接口适配器模式
当不需要全部实现接口提供的方法时,可先设计一个抽象类实现接口,并为该接口中每个方法提供一个默认实现(空方法),那么该抽象类的子类可有选择地覆盖父类的某些方法来实现需求。
适用于一个接口不想使用其所有的方法的情况。
应用实例
思路分析
代码实现
public interface Interface4 {
void m1();
void m2();
void m3();
void m4();
}
//在AbsAdapter中我们将Interface4的方法进行默认实现
public abstract class AbsAdapter implements Interface4 {
@Override
public void m1() {
}
@Override
public void m2() {
}
@Override
public void m3() {
}
@Override
public void m4() {
}
}
public class Client {
public static void main(String[] args) {
AbsAdapter absAdapter= new AbsAdapter(){
@Override
public void m1() {
System.out.println("使用了m1的方法");
}
};
absAdapter.m1();
}
}
5.6、适配器在SpringMVC框架应用以及源码分析
1) SpringMvc中的HandlerAdapter, 就使用了适配器模式
2) SpringMVC处理请求的流程回顾
3) 使用HandlerAdapter 的原因分析:
可以看到处理器的类型不同,有多重实现方式,那么调用方式就不是确定的,如果需要直接调用 Controller方法,需要调用的时候就得不断是使用if else来进行判断是哪一种子类然后执行。那么如果后面要扩展Controller,就得修改原来的代码,这样违背了OCP原则。
4) 代码分析+Debug源码
5) 动手写SpringMVC通过适配器设计模式获取到对应的Controller的源码
说明:
• Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类
• 适配器代替controller执行相应的方法
• 扩展Controller 时,只需要增加一个适配器类就完成了SpringMVC的扩展了,
• 这就是设计模式的力量
//多种Controller实现
public interface Controller {
}
class HttpController implements Controller {
public void doHttpHandler() {
System.out.println("http...");
}
}
class SimpleController implements Controller {
public void doSimplerHandler() {
System.out.println("simple...");
}
}
class AnnotationController implements Controller {
public void doAnnotationHandler() {
System.out.println("annotation...");
}
}
//定义一个Adapter接口
public interface HandlerAdapter {
public boolean supports(Object handler);
public void handle(Object handler);
}
// 多种适配器类
class SimpleHandlerAdapter implements HandlerAdapter {
public void handle(Object handler) {
((SimpleController) handler).doSimplerHandler();
}
public boolean supports(Object handler) {
return (handler instanceof SimpleController);
}
}
class HttpHandlerAdapter implements HandlerAdapter {
public void handle(Object handler) {
((HttpController) handler).doHttpHandler();
}
public boolean supports(Object handler) {
return (handler instanceof HttpController);
}
}
class AnnotationHandlerAdapter implements HandlerAdapter {
public void handle(Object handler) {
((AnnotationController) handler).doAnnotationHandler();
}
public boolean supports(Object handler) {
return (handler instanceof AnnotationController);
}
}
import java.util.ArrayList;
import java.util.List;
public class DispatchServlet {
public static List<HandlerAdapter> handlerAdapters = new ArrayList<HandlerAdapter>();
public DispatchServlet() {
handlerAdapters.add(new AnnotationHandlerAdapter());
handlerAdapters.add(new HttpHandlerAdapter());
handlerAdapters.add(new SimpleHandlerAdapter());
}
public void doDispatch() {
// 此处模拟SpringMVC从request取handler的对象,
// 适配器可以获取到希望的Controller
HttpController controller = new HttpController();
// AnnotationController controller = new AnnotationController();
//SimpleController controller = new SimpleController();
// 得到对应适配器
HandlerAdapter adapter = getHandler(controller);
// 通过适配器执行对应的controller对应方法
adapter.handle(controller);
}
public HandlerAdapter getHandler(Controller controller) {
//遍历:根据得到的controller(handler), 返回对应适配器
for (HandlerAdapter adapter : this.handlerAdapters) {
if (adapter.supports(controller)) {
return adapter;
}
}
return null;
}
public static void main(String[] args) {
new DispatchServlet().doDispatch(); // http...
}
}
5.7、适配器模式的注意事项和细节
三种命名方式是根据src是以怎样的形式给到Adapter(在Adapter里的形式)来命名的。
- 类适配器:以类给到,在Adapter里,将src当做类,继承。
- 对象适配器:以对象给到,在Adapter里,将src作为一个对象,持有。
- 接口适配器:以接口给到,在Adapter里,将src作为一个接口,实现。
Adapter模式最大的作用还是将原本不兼容的接口融合到一起工作。
实际开发中,实现起来不拘泥于我们讲解的三种经典形式
6、桥接模式
在现实生活中,某些类具有两个或多个维度的变化,如图形既可按形状分,又可按颜色分。如何设计类似于 Photoshop 这样的软件,能画不同形状和不同颜色的图形呢?如果用继承方式,m 种形状和 n 种颜色的图形就有 m×n 种,不但对应的子类很多,而且扩展困难。
当然,这样的例子还有很多,如不同颜色和字体的文字、不同品牌和功率的汽车、不同性别和职业的男女、支持不同平台和不同文件格式的媒体播放器等。如果用桥接模式就能很好地解决这些问题。
6.1、桥接模式的定义与特点
桥接(Bridge)模式的定义如下:将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
通过上面的讲解,我们能很好的感觉到桥接模式遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开放。这里将桥接模式的优缺点总结如下。
桥接(Bridge)模式的优点是:
- 抽象与实现分离,扩展能力强
- 符合开闭原则
- 符合合成复用原则
- 其实现细节对客户透明
缺点是:由于聚合关系建立在抽象层,要求开发者针对抽象化进行设计与编程,能正确地识别出系统中两个独立变化的维度,这增加了系统的理解与设计难度。
将抽象和实现解耦,使得两者可以独立地变化。
是一种结构型设计模式。
6.2、桥接模式的结构与实现
可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
1. 模式的结构
桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
- Client类:桥接模式的调用者。
传统方式
案例
现在对不同手机类型的不同品牌实现编程操作,比如开机、关机、打电话等。
思路分析
问题分析
扩展性问题(类爆炸),如果我们再增加手机的样式(旋转式),就需要增加各个品牌手机的类,同样如果我们增加一个手机品牌,也要在各个手机样式类下增加。
违反了单一职责原则,当我们增加手机样式时,要同时增加所有品牌的手机,这样增加了代码维护成本。
解决方案:使用桥接模式
public interface Brand {
void open();
void close();
void call();
}
public class Vivo implements Brand {
@Override
public void open() {
System.out.println("Vivo手机开机");
}
@Override
public void close() {
System.out.println("Vivo手机关机");
}
@Override
public void call() {
System.out.println("Vivo手机打电话");
}
}
public class XiaoMi implements Brand {
@Override
public void open() {
System.out.println("小米手机开机了");
}
@Override
public void close() {
System.out.println("小米手机关机了");
}
@Override
public void call() {
System.out.println("小米手机打电话");
}
}
public abstract class Phone {
private Brand brand;//组合品牌
public Phone(Brand brand) {//构造器
this.brand = brand;
}
protected void open() {
this.brand.open();
}
protected void close() {
this.brand.close();
}
protected void call() {
this.brand.call();
}
}
public class FoldedPhone extends Phone {
public FoldedPhone(Brand brand) {
super(brand);
}
public void open(){
super.open();
System.out.println("折叠样式手机");
}
public void close(){
super.close();
System.out.println("折叠样式手机");
}
public void call(){
super.call();
System.out.println("折叠样式手机");
}
}
public class Client {
public static void main(String[] args) {
Phone phone1 = new FoldedPhone(new XiaoMi());
phone1.open();
phone1.call();
phone1.close();
System.out.println("=========================");
Phone phone2 = new FoldedPhone(new Vivo());
phone2.open();
phone2.call();
phone2.close();
}
}
6.3、桥接模式在 JDBC的源码剖析
1) Jdbc 的 Driver接口,如果从桥接模式来看,Driver就是一个接口,下面可以有 MySQL的Driver,Oracle的Driver,这些就可以当做实现接口类
2) 代码分析+Debug源码
6.4、注意事项
- 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。
- 对于系统的高层部分,只需知道抽象部分和实现部分的接口就可以了,其它的部分由具体业务来完成。
- 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
- 桥接模式的引入增加了系统的理解和设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计和编程。
- 桥接模式要求正确识别出系统中两个独立变化的维度(抽象、实现),因此其适用范围有一定的局限性,即需要有这样的应用场景。
对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用
6.5、桥接模式其他应用场景
1) 对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥 接模式尤为适用.
2) 常见的应用场景:
-JDBC驱动程序
-银行转账系统
转账分类: 网上转账,柜台转账,AMT转账
转账用户类型:普通用户,银卡用户,金卡用户..
-消息管理
消息类型:即时消息,延时消息
消息分类:手机短信,邮件消息,QQ消息...
7、装饰模式
上班族大多都有睡懒觉的习惯,每天早上上班时间都很紧张,于是很多人为了多睡一会,就会用方便的方式解决早餐问题。有些人早餐可能会吃煎饼,煎饼中可以加鸡蛋,也可以加香肠,但是不管怎么“加码”,都还是一个煎饼。在现实生活中,常常需要对现有产品增加新的功能或美化其外观,如房子装修、相片加相框等,都是装饰器模式。
在软件开发过程中,有时想用一些现存的组件。这些组件可能只是完成了一些核心功能。但在不改变其结构的情况下,可以动态地扩展其功能。所有这些都可以釆用装饰模式来实现。
7.1、装饰模式的定义与特点
装饰(Decorator)模式的定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
装饰(Decorator)模式的主要优点有:
- 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
- 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
- 装饰器模式完全遵守开闭原则
其主要缺点是:装饰模式会增加许多子类,过度使用会增加程序得复杂性。
7.2、装饰模式的结构与实现
通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。下面来分析其基本结构和实现方法。
1. 模式的结构
装饰模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
7.3、装饰者模式(Decorator)原理
1) 装饰者模式就像打包一个快递
主体:比如:陶瓷、衣服 (Component) // 被装饰者
包装:比如:报纸填充、塑料泡沫、纸板、木板(Decorator)
2) Component
主体:比如类似前面的Drink
3) ConcreteComponent和Decorator
ConcreteComponent:具体的主体,
比如前面的各个单品咖啡
Decorator: 装饰者,比如各调料.
4) 在如图的Component与ConcreteComponent之间,如果
ConcreteComponent类很多,还可以设计一个缓冲层,将共有的部分提取出来, 抽象层一个类。
传统方式
案例
星巴克咖啡订单项目:
- 咖啡种类/单品咖啡:Espresso(意大利浓咖啡)、ShortBlack、LongBlack(美式咖啡)、Decaf(无因咖啡);
- 调料:Milk、Soy(豆浆)、Chocolate;
- 要求在扩展新的咖啡种类时,具有良好的扩展性、改动方便、维护方便;
- 使用OO的来计算不同种类咖啡的费用:客户可以点单品咖啡,也可以单品咖啡+调料组合。
方案一分析:
- Drink是一个抽象类,表示饮料;
- des就是对咖啡的描述,比如咖啡的名字;
- cost()方法就是计算费用,Drink类中做成一个抽象方法;
- Decaf就是单品咖啡,继承Drink,并实现cost;
- Espress&&Milk就是单品咖啡+调料,这个组合很多。
- 问题:这样设计,会有很多类,当我们增加一个单品咖啡,或者一个新的调料,类的数量就会倍增,就会出现类爆炸。
方案二分析:
- 前面分析到方案一因为咖啡单品+调料组合会造成类的倍增,因此可以做改进,将调料内置到Drink类,这样就不会造成类数量过多,从而提高项目的维护性。
- 说明:milk,soy,chocolate可以设计为Boolean,表示是否要添加相应的调料。
- 方案二可以控制类的数量,不至于造成很多的类。
- 在增加或者删除调料种类时,代码的维护量很大。
- 考虑到用户可以添加多份调料时,可以将hasMilk返回一个对应int。
- 考虑使用装饰者模式。
装饰者模式解决星巴克咖啡订单
抽象类
public abstract class Drink {
public String des;//描述
private float price = 0.0f;
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public float getPrice() {
return price;
}
public void setPrice(float price) {
this.price = price;
}
//计算费用的抽象方法
public abstract float cost();
}
抽象构件(Component)角色
public class Coffee extends Drink {
@Override
public float cost() {
return super.getPrice();
}
}
具体构件(ConcreteComponent)角色
public class LongBlack extends Coffee {
public LongBlack() {
setDes("longBlack");
setPrice(5.0f);
}
}
public class Espresso extends Coffee {
public Espresso(){
setDes("意大利咖啡");
setPrice(6.0f);
}
}
public class ShortBlack extends Coffee {
public ShortBlack(){
setDes("shortBlack");
setPrice(4.0f);
}
}
抽象装饰(Decorator)角色
public class Decorator extends Drink {
private Drink obj;
public Decorator(Drink obj) {
this.obj = obj;
}
@Override
public float cost() {
return super.getPrice() + obj.cost();
}
@Override
public String getDes() {
return super.getDes() + " " + super.getPrice() + "&&" + obj.getDes();
}
}
具体装饰(ConcreteDecorator)角色(调味品)
public class Chocolate extends Decorator {
public Chocolate(Drink obj) {
super(obj);
setDes("巧克力");
setPrice(3.0f);
}
}
public class Milk extends Decorator {
public Milk(Drink obj) {
super(obj);
setDes("牛奶");
setPrice(2.0f);
}
}
public class Soy extends Decorator {
public Soy(Drink obj) {
super(obj);
setDes("豆浆");
setPrice(1.5f);
}
}
public class Client {
public static void main(String[] args) {
//装饰者模式下的订单:1份巧克力+1份牛奶的LongBlack
//1、点一份LongBlack
Drink order = new LongBlack();
System.out.println("费用="+order.cost());
System.out.println("描述="+order.getDes());
//2、加入一份牛奶
order = new Milk(order);
System.out.println("order 加入一份牛奶 费用="+order.cost());
System.out.println("order 加入一份牛奶 描述="+order.getDes());
//3、order加入一份巧克力
order=new Chocolate(order);
System.out.println("order 加入一份巧克力 费用="+order.cost());
System.out.println("order 加入一份巧克力 描述="+order.getDes());
}
}
7.4、装饰者模式在JDK中源码分析
Java的IO结构,FilterInputStream就是一个装饰者
//是一个抽象类,即Component
public abstract class InputStream implements Closeable{}
//是一个装饰者类Decorator
public class FilterInputStream extends InputStream {
//被装饰的对象
protected volatile InputStream in;
}
//FilterInputStream 子类,
class DataInputStream extends FilterInputStream implements DataInput { }
8、组合模式
在现实生活中,存在很多“部分-整体”的关系,例如,大学中的部门与学院、总公司中的部门与分公司、学习用品中的书与书包、生活用品中的衣服与衣柜、以及厨房中的锅碗瓢盆等。在软件开发中也是这样,例如,文件系统中的文件与文件夹、窗体程序中的简单控件与容器控件等。对这些简单对象与复合对象的处理,如果用组合模式来实现会很方便。
8.1、组合模式的定义与特点
组合(Composite Pattern)模式的定义:有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型设计模式。
组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,顶层的节点被称为根节点,根节点下面可以包含树枝节点和叶子节点,树枝节点下面又可以包含树枝节点和叶子节点,树形结构图如下。
由上图可以看出,其实根节点和树枝节点本质上属于同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于用一种类型。但是在组合模式中,会把树枝节点和叶子节点看作属于同一种数据类型(用统一接口定义),让它们具备一致行为。
这样,在组合模式中,整个树形结构中的对象都属于同一种类型,带来的好处就是用户不需要辨别是树枝节点还是叶子节点,可以直接进行操作,给用户的使用带来极大的便利。
组合模式的主要优点有:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
其主要缺点是:
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;
8.2、组合模式的结构与实现
组合模式的结构不是很复杂,下面对它的结构和实现进行分析。
1. 模式的结构
组合模式包含以下主要角色。
- 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
- 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
- 树枝构件(Composite)角色 / 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
传统方式
案例
编写程序展示一个学校院系结构,需求是这样的:要在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系。如图:
传统方案解决学校院系展示(类图)
分析:
- 将学院看作是学校的子类,系是学院的子类,这样实际上是站在组织大小来进行分层次的。
- 实际上我们的要求是:在一个页面中展示出学校的院系组成,一个学校有多个学院,一个学院有多个系,因此这种方案,不能很好地实现管理的操作,比如对学院、系的添加、删除、遍历等。
- 解决方案:把学校、院、系都看作是组织结构 ,他们之间没有继承的关系,而是一个树形结构,可以更好的实现管理操作(组合模式)
思路分析
代码实现
public abstract class OrganizationComponent {
private String name;//名字
private String des;//描述
public OrganizationComponent(String name, String des) {
this.name = name;
this.des = des;
}
protected void add(OrganizationComponent organizationComponent) {
//默认实现
throw new UnsupportedOperationException();
}
protected void remove(OrganizationComponent organizationComponent) {
//默认实现
throw new UnsupportedOperationException();
}
protected abstract void print();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
}
public class University extends OrganizationComponent {
List<OrganizationComponent> organizationComponents = new ArrayList<OrganizationComponent>();
public University(String name, String des) {
super(name, des);
}
@Override//重写
protected void add(OrganizationComponent organizationComponent) {
organizationComponents.add(organizationComponent);
}
@Override//重写
protected void remove(OrganizationComponent organizationComponent) {
organizationComponents.remove(organizationComponent);
}
@Override//重写
public String getName() {
return super.getName();
}
@Override//重写
public String getDes() {
return super.getDes();
}
@Override
protected void print() {
System.out.println("---------------" + getName() + "-------------------");
for (OrganizationComponent organizationComponent : organizationComponents) {
organizationComponent.print();
}
}
}
public class College extends OrganizationComponent {
//List 中 存放的Department
List<OrganizationComponent> organizationComponents = new ArrayList<OrganizationComponent>();
// 构造器
public College(String name, String des) {
super(name, des);
// TODO Auto-generated constructor stub
}
// 重写add
@Override
protected void add(OrganizationComponent organizationComponent) {
// TODO Auto-generated method stub
// 将来实际业务中,Colleage 的 add 和 University add 不一定完全一样
organizationComponents.add(organizationComponent);
}
// 重写remove
@Override
protected void remove(OrganizationComponent organizationComponent) {
// TODO Auto-generated method stub
organizationComponents.remove(organizationComponent);
}
@Override
public String getName() {
// TODO Auto-generated method stub
return super.getName();
}
@Override
public String getDes() {
// TODO Auto-generated method stub
return super.getDes();
}
// print方法,就是输出University 包含的学院
@Override
protected void print() {
// TODO Auto-generated method stub
System.out.println("--------------" + getName() + "--------------");
//遍历 organizationComponents
for (OrganizationComponent organizationComponent : organizationComponents) {
organizationComponent.print();
}
}
}
public class Department extends OrganizationComponent {
//没有集合
public Department(String name, String des) {
super(name, des);
// TODO Auto-generated constructor stub
}
//add , remove 就不用写了,因为他是叶子节点
@Override
public String getName() {
// TODO Auto-generated method stub
return super.getName();
}
@Override
public String getDes() {
// TODO Auto-generated method stub
return super.getDes();
}
@Override
protected void print() {
// TODO Auto-generated method stub
System.out.println(getName());
}
}
public class Client {
public static void main(String[] args) {
// TODO Auto-generated method stub
//从大到小创建对象 学校
OrganizationComponent university = new University("清华大学", " 中国顶级大学 ");
//创建 学院
OrganizationComponent computerCollege = new College("计算机学院", " 计算机学院 ");
OrganizationComponent infoEngineercollege = new College("信息工程学院", " 信息工程学院 ");
//创建各个学院下面的系(专业)
computerCollege.add(new Department("软件工程", " 软件工程不错 "));
computerCollege.add(new Department("网络工程", " 网络工程不错 "));
computerCollege.add(new Department("计算机科学与技术", " 计算机科学与技术是老牌的专业 "));
//
infoEngineercollege.add(new Department("通信工程", " 通信工程不好学 "));
infoEngineercollege.add(new Department("信息工程", " 信息工程好学 "));
//将学院加入到 学校
university.add(computerCollege);
university.add(infoEngineercollege);
university.print();
// infoEngineercollege.print();
}
}
8.3、组合模式在JDK集合的源码分析
1) Java的集合类-HashMap就使用了组合模式
2) 代码分析+Debug 源码
8.4、组合模式的注意细节和事项
1) 简化客户端操作。客户端只需要面对一致的对象而不用考虑整体部分或者节点叶子的问题。
2) 具有较强的扩展性。当我们要更改组合对象时,我们只需要调整内部的层次关系, 客户端不用做出任何改动.
3) 方便创建出复杂的层次结构。客户端不用理会组合里面的组成细节,容易添加节点或者叶子从而创建出复杂的树形结构
4) 需要遍历组织机构,或者处理的对象具有树形结构时, 非常适合使用组合模式.
5) 要求较高的抽象性,如果节点和叶子有很多差异性的话,比如很多方法和属性都不一样,不适合使用组合模式
9、外观模式
软件设计也是这样,当一个系统的功能越来越强,子系统会越来越多,客户对系统的访问也变得越来越复杂。这时如果系统内部发生改变,客户端也要跟着改变,这违背了“开闭原则”,也违背了“迪米特法则”,所以有必要为多个子系统提供一个统一的接口,从而降低系统的耦合度,这就是外观模式的目标。
图 1 给出了客户去当地房产局办理房产证过户要遇到的相关部门。
图1 办理房产证过户的相关部门
9.1、外观模式的定义与特点
外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
在日常编码工作中,我们都在有意无意的大量使用外观模式。只要是高层模块需要调度多个子系统(2个以上的类对象),我们都会自觉地创建一个新的类封装这些子系统,提供精简的接口,让高层模块可以更加容易地间接调用这些子系统的功能。尤其是现阶段各种第三方SDK、开源类库,很大概率都会使用外观模式。
外观(Facade)模式是“迪米特法则”的典型应用,它有以下主要优点。
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
外观(Facade)模式的主要缺点如下。
- 不能很好地限制客户使用子系统类,很容易带来未知风险。
- 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
9.2、外观模式的结构与实现
外观(Facade)模式的结构比较简单,主要是定义了一个高层接口。它包含了对各个子系统的引用,客户端可以通过它访问各个子系统的功能。现在来分析其基本结构和实现方法。
1. 模式的结构
外观(Facade)模式包含以下主要角色。
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
- 客户(Client)角色:通过一个外观角色访问各个子系统的功能。
其结构图如图 2 所示。
2. 模式的实现
外观模式的实现代码如下:
public class FacadePattern {
public static void main(String[] args) {
Facade f = new Facade();
f.method();
}
}
//外观角色
class Facade {
private SubSystem01 obj1 = new SubSystem01();
private SubSystem02 obj2 = new SubSystem02();
private SubSystem03 obj3 = new SubSystem03();
public void method() {
obj1.method1();
obj2.method2();
obj3.method3();
}
}
//子系统角色
class SubSystem01 {
public void method1() {
System.out.println("子系统01的method1()被调用!");
}
}
//子系统角色
class SubSystem02 {
public void method2() {
System.out.println("子系统02的method2()被调用!");
}
}
//子系统角色
class SubSystem03 {
public void method3() {
System.out.println("子系统03的method3()被调用!");
}
}
程序运行结果如下:
子系统01的method1()被调用!
子系统02的method2()被调用!
子系统03的method3()被调用!
传统方式
案例
影院管理项目:组建一个家庭影院:DVD播放器、投影仪、自动屏幕、环绕立体声、爆米花机,要求完成使用家庭影院的功能,其过程为使用遥控器统筹各设备开关。
思路分析
问题分析
在ClientTest的main方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程混乱,没有清晰的过程。
不利于在ClientTest中,去维护对子系统的操作。
解决思路:定义一个高层接口,给子系统中的一组接口提供一个一致的界面(比如在高层接口提供三个方法ready,play,end),用来访问子系统中的一群接口。
也就是说,就是通过定义一个一致的接口(界面类),用以屏蔽内部子系统的细节,使得调用端只需跟这个接口发生调用,而无需关心这个子系统的内部细节(外观模式)。
思路分析
代码实现
public class DVDPlayer {
//使用单例模式,使用饿汉式
private static DVDPlayer instance=new DVDPlayer();
private DVDPlayer(){//构造器私有化
}
public static DVDPlayer getInstance(){
return instance;
}
public void on(){
System.out.println("DVD打开");
}
public void off(){
System.out.println("DVD关闭");
}
public void play(){
System.out.println("DVD正在播放");
}
}
public class Popcorn {
private static Popcorn instance=new Popcorn();
private Popcorn(){
}
public static Popcorn getInstance(){
return instance;
}
public void on(){
System.out.println("爆米花机打开");
}
public void off(){
System.out.println("爆米花机关闭");
}
public void pop(){
System.out.println("爆米花机正在制作爆米花");
}
}
public class Projector {
private static Projector instance=new Projector();
private Projector(){
}
public static Projector getInstance(){
return instance;
}
public void on(){
System.out.println("投影仪打开");
}
public void off(){
System.out.println("投影仪关闭");
}
public void focus(){
System.out.println("投影仪正在聚焦");
}
}
public class Screen {
private static Screen instance=new Screen();
private Screen(){
}
public static Screen getInstance(){
return instance;
}
public void up(){
System.out.println("屏幕上升");
}
public void down(){
System.out.println("屏幕下降");
}
}
public class Stereo {
private static Stereo instance=new Stereo();
private Stereo(){
}
public static Stereo getInstance(){
return instance;
}
public void on(){
System.out.println("立体声打开");
}
public void off(){
System.out.println("立体声关闭");
}
}
public class TheaterLight {
private static TheaterLight instance=new TheaterLight();
private TheaterLight(){
}
public static TheaterLight getInstance(){
return instance;
}
public void on(){
System.out.println("灯光打开");
}
public void off(){
System.out.println("灯光关闭");
}
}
public class HomeTheaterFacade {
//定义各个子系统对象
private TheaterLight theaterLight;
private Popcorn popcorn;
private Stereo stereo;
private Projector projector;
private Screen screen;
private DVDPlayer dvdPlayer;
public HomeTheaterFacade() {
this.theaterLight = TheaterLight.getInstance();
this.popcorn = Popcorn.getInstance();
this.stereo = Stereo.getInstance();
this.projector = Projector.getInstance();
this.screen = Screen.getInstance();
this.dvdPlayer = DVDPlayer.getInstance();
}
//操作分为四步
public void ready(){
theaterLight.on();
projector.on();
popcorn.on();
stereo.on();
screen.up();
dvdPlayer.on();
}
public void play(){
dvdPlayer.play();
}
public void end(){
theaterLight.off();
projector.off();
popcorn.off();
stereo.off();
screen.down();
dvdPlayer.off();
}
}
9.3、外观模式在Mybatis框架应用的源码分析
1) MyBatis 中的Configuration 去创建MetaObject 对象使用到外观模式
2) 代码分析+Debug源码+示意图
9.4、外观模式的注意事项和细节
1) 外观模式对外屏蔽了子系统的细节,因此外观模式降低了客户端对子系统使用的复杂性
2) 外观模式对客户端与子系统的耦合关系,让子系统内部的模块更易维护和扩展
3) 通过合理的使用外观模式,可以帮我们更好的划分访问的层次
4) 当系统需要进行分层设计时,可以考虑使用Facade模式
5) 在维护一个遗留的大型系统时,可能这个系统已经变得非常难以维护和扩展,此时可以考虑为新系统开发一个Facade类,来提供遗留系统的比较清晰简单的接口, 让新系统与Facade类交互,提高复用性
6) 不能过多的或者不合理的使用外观模式,使用外观模式好,还是直接调用模块好。要以让系统有层次,利于维护为目的。
10、享元模式
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。
例如,围棋和五子棋中的黑白棋子,图像中的坐标点或颜色,局域网中的路由器、交换机和集线器,教室里的桌子和凳子等。这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式的产生背景。
10.1、享元模式的定义与特点
享元(Flyweight)模式的定义:运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
常用于系统底层开发,解决系统的性能问题。像数据库连接池,里面都是创建好的连接对象,在这些连接对象中有我们需要的则直接拿来用,避免重新创建,如果没有我们需要的,则创建一个。
享元模式能够解决重复对象的内存浪费的问题,当系统中有大量相似对象,需要缓冲池时,不需总是创建新对象,可以从缓冲池里拿,这样可以降低系统内存,同时提高效率。
享元模式经典的应用场景就是池技术了,String常量池、数据库连接池、缓冲池等等都是享元模式的应用,享元模式是池技术的重要实现方式。
享元模式的主要优点是:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
其主要缺点是:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。
内部状态和外部状态
比如围棋、五子棋、跳棋,它们都有大量的棋子对象,围棋和五子棋只有黑白两色,跳棋颜色多一点,所以棋子颜色就是棋子的内部状态;而各个棋子之间的差别就是位置的不同,当我们落子后,落子颜色是定的,但位置是变化的,所以棋子坐标就是棋子的外部状态。
享元模式提出了两个要求:细粒度和共享对象。
这里就涉及到内部状态和外部状态了,即对象的信息分为两个部分:
内部状态和外部状态。内部状态指对象共享出来的信息,存储在享元对象内部且不会随环境的改变而改变。
外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态。
10.2、享元模式的结构与实现
享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
- 内部状态指对象共享出来的信息,存储在享元信息内部,并且不回随环境的改变而改变;
- 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享。
比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而当每个连接要被回收利用时,我们需要将它标记为可用状态,这些为外部状态。
享元模式的本质是缓存共享对象,降低内存消耗。
1. 模式的结构
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
传统方式
案例
一个小型的外包项目,给客户A做一个产品展示网站,客户A的朋友感觉效果不错,也希望做这样的产品展示网站,但是要求有些不同:有客户要去以新闻的形式发布;有客户要求以博客的形式发布;有客户希望以微信公众号的形式发布。
思路分析
直接复制粘贴一份,然后根据客户不同要求,进行定制修改,然后给每个网站租用一个空间。
问题分析
需要的网站结构相似度很高,而且都不是高访问量网站,如果分成多个虚拟空间来处理,相当于一个相同网站的实例对象很多,造成服务器的资源浪费。
用享元模式来解决这个问题
思路分析
整合到一个网站中,共享其相关的代码和数据,对于硬盘、内存、CPU、数据库空间等服务器资源都可以达成共享,减少服务器资源。对于代码来说,由于是一份实例,维护和扩展都更加容易。即使用享元模式来解决。
代码实现
// 外部状态
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name) {
this.name = name;
}
}
public abstract class WebSite {
public abstract void use(User user);
}
public class ConcreteWebSite extends WebSite {
// 共享的部门,内部状态
private String type = "";//网站发布的形式
public ConcreteWebSite(String type) {
this.type = type;
}
@Override
public void use(User user) {
System.out.println("网站的发布形式为"+type+"," + user.getName() + "用户在使用");
}
}
public class WebSiteFactory {
//集合,充当池的作用
private HashMap<String, ConcreteWebSite> pool = new HashMap<>();
//根据网站的类型,返回一个网站,如果没有就创建一个网站,并放入到池中,并返回
public WebSite getWebSiteCategory(String type) {
if (!pool.containsKey(type)) {
pool.put(type, new ConcreteWebSite(type));
}
return (WebSite) pool.get(type);
}
//获取网站分类的总数
public int getWebSiteCount() {
return pool.size();
}
}
public class Client {
public static void main(String[] args) {
//创建一个工厂类
WebSiteFactory webSiteFactory = new WebSiteFactory();
//客户要一个以新闻形式发布的网站
WebSite webSite1 = webSiteFactory.getWebSiteCategory("新闻");
webSite1.use(new User("新浪"));
//客户要一个以博客形式发布的网站
WebSite webSite2 = webSiteFactory.getWebSiteCategory("博客");
webSite2.use(new User("谷歌"));
//客户要一个以博客形式发布的网站
WebSite webSite3 = webSiteFactory.getWebSiteCategory("博客");
webSite3.use(new User("百度"));
System.out.println("网站的分类共有:"+webSiteFactory.getWebSiteCount());
}
}
10.3、享元模式在JDK-Interger的应用源码分析
1) Integer中的享元模式
2) 代码分析+Debug源码+说明
如果Integer.valueOf(x) x 在 -128 --- 127 直接,就是使用享元模式返回,如果不在范围内,则仍然new
小结:
1、在valueOf()方法中,先判断值是否在IntegerCache中,如果不在,就创建新的Integer(new),否则,就直接从缓存池中直接返回
2、valueOf() 方法就使用到享元模式
3、如果valueOf()方法得到一个Integer实例,范围在-128 - 127,执行速度比new的快
public class SourceCode {
public static void main(String[] args) {
Integer x = Integer.valueOf(127);
Integer y = new Integer(127);
Integer z = Integer.valueOf(127);
Integer w = new Integer(127);
System.out.println(x.equals(y)); // ? true ----> 比较的内容
System.out.println(x == y ); // ? false ----> 比较的对象
System.out.println(x == z ); // ? true ----> 比较的对象
System.out.println(w == x ); // ? false ----> 比较的对象
System.out.println(w == y ); // ? false ----> 比较的对象
}
}
10.4、享元模式注意事项和细节
享元模式这样理解,“享”就表示共享,“元”表示对象。
系统中有大量对象,这些对象消耗大量内存,并且对象的状态大部分可以外部化时,我们就可以考虑选用享元模式。
用唯一标识吗判断,如果在内存中有,则返回这个唯一标识码所标识的对象,用HashMap/HashTable存储。
享元模式大大减少了对象的创建,降低了程序内存的占用,提高效率。
享元模式提高了系统的复杂度。需要分离出内部状态和外部状态,而外部状态具有固化特性,不应该随着内部状态的改变而改变,这是我们使用享元模式需要注意的地方。
使用享元模式时,注意划分内部状态和外部状态,并且需要有一个工厂类加以控制。
享元模式经典的应用场景是需要缓冲池的场景,比如String常量池、数据库连接池。
11、代理模式
在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。
在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。
11.1、代理模式的定义与特点
代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
代理模式的主要优点有:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;
- 代理对象可以扩展目标对象的功能;
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
其主要缺点是:
- 代理模式会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢;
- 增加了系统的复杂度;
那么如何解决以上提到的缺点呢?答案是可以使用动态代理方式
11.2、代理模式的结构与实现
代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构和实现方法。
1. 模式的结构
代理模式的主要角色如下。
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
11.3、静态代理
静态代理在使用时,需要定义接口或者父类,被代理对象(即目标对象)与代理对象一起实现相同的接口或者是继承相同父类。
应用实例
具体要求:
- 定义一个接口:ITeacherDao;
- 目标对象TeacherDao实现接口ITeacherDao;
- 使用静态代理方式,就需要在代理对象TeacherDaoProxy中也实现ITeacherDao;
- 调用的时候通过调用代理对象的方法来调用目标对象;
- 特别提醒:代理对象与目标对象要实现相同的接口,然后通过调用相同的方法来调用目标对象的方法。
思路分析
代码实现
public interface ITeacherDao {
void teach();//授课的方法
}
public class TeacherDao implements ITeacherDao {
@Override
public void teach() {
System.out.println("老师正在授课...");
}
}
public class TeacherDaoProxy implements ITeacherDao {
private ITeacherDao target; // 目标对象通过接口来聚合
// 构造器
public TeacherDaoProxy(ITeacherDao target) {
this.target = target;
}
@Override
public void teach() {
System.out.println("开始代理,完成某些操作。。。");
target.teach();
System.out.println("完成代理...............");
}
}
public class Client {
public static void main(String[] args) {
// 创建目标对象(被代理对象)
TeacherDao teacherDao = new TeacherDao();
// 创建代理对象,同时将被代理对象传递给代理对象
TeacherDaoProxy proxy = new TeacherDaoProxy(teacherDao);
// 通过被代理对象,调用到被代理对象的方法
// 即:执行的是代理对象的方法,代理对象再调用目标对象的方法
proxy.teach();
}
}
静态代理的优缺点:
1) 优点:在不修改目标对象的功能前提下, 能通过代理对象对目标功能扩展
2) 缺点:因为代理对象需要与目标对象实现一样的接口,所以会有很多代理类
3) 一旦接口增加方法,目标对象与代理对象都要维护
11.4、动态代理
代理对象不需要实现接口,但是目标对象要实现接口,否则不能用动态代理。
代理对象的生成,是利用JDK的API,动态的在内存中构建代理对象。
应用实例
将前面的静态代理改成JDK动态代理。
思路分析
代码实现
public interface ITeacherDao {
void teach();//授课的方法
}
public class TeacherDao implements ITeacherDao {
@Override
public void teach() {
System.out.println("老师正在授课...");
}
}
给目标对象生成一个代理对象
newProxyInstance方法参数说明
* 第一个参数:指定当前目标对象使用的类加载器;
* 第二个参数:目标对象实现的接口类型;
* 第三个参数:事件处理,执行目标对象的方式时,会触发事件处理器方法
public class ProxyFactory {
//目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
//给目标对象生成一个代理对象
public Object getProxyInstance() {
/**
* newProxyInstance方法参数说明
* 第一个参数:指定当前目标对象使用的类加载器;
* 第二个参数:目标对象实现的接口类型;
* 第三个参数:事件处理,执行目标对象的方式时,会触发事件处理器方法
*/
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("JDK代理开始...");
//反射机制调用目标对象的方法
Object invoke = method.invoke(target, args);
System.out.println("JDK代理提交...");
return invoke;
}
}
);
}
}
public class Client {
public static void main(String[] args) {
// 创建目标对象
ITeacherDao target = new TeacherDao();
// 给目标对象创建代理对象,可以转成ITeacherDao
ITeacherDao proxyInstance = (ITeacherDao) new ProxyFactory(target).getProxyInstance();
// org.springframework.cglib.proxy.Proxy$ProxyImpl$$EnhancerByCGLIB$$98299aad 内存中动态生成了代理对象
System.out.println("proxyInstance" + proxyInstance.getClass());
// 通过代理对象,调用目标对象的方法
proxyInstance.teach();
}
}
11.5、CGLIB代理
1)、静态代理和JDK代理都要求目标对象是实现一个接口,但是有时候目标对象只是一个单独的对象,并没有实现任何的接口,这个时候可使用目标对象子类来实现代理,这就是CGLIB代理。CGLIB包的底层是通过使用字节码处理框架ASM来转换字节码并生成新的类。
2)、CGLIB代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展。
3)、CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口,它广泛的被许多AOP的框架使用,例如Spring AOP,实现方法拦截。
4)、在AOP编程中如何选择代理模式:如果目标对象需要实现接口,用JDK代理;如果目标对象不需要实现接口,用CGLIB代理。
5)、Cglib包的底层是通过使用字节码处理框架ASM来转换字节码生成新的类
注意事项
- 需要引入cglib的jar文件。
- 在内存中动态构建子类,注意代理的类不能为final,否则报错。
- 目标对象的方法如果为final/static,那么就不会被拦截,即不会执行目标对象额外的业务方法。
思路分析
代码实现
public class TeacherDao {
public String teach(){
System.out.println("老师授课中...");
return "hello";
}
}
public class ProxyFactory implements MethodInterceptor {
// 维护一个目标对象
private Object target;
// 构造器,传入被代理对象
public ProxyFactory(Object target) {
this.target = target;
}
// 返回一个代理对象
public Object getProxyInstance(){
// 1、创建一个工具类
Enhancer enhancer = new Enhancer();
// 2、设置父类
enhancer.setSuperclass(target.getClass());
// 3、设置回调函数
enhancer.setCallback(this);
// 4、创建子类对象,即代理对象
return enhancer.create();
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib代理开始...");
Object invoke = method.invoke(target, args);
System.out.println("完成cglib代理");
return invoke;
}
}
public class Client {
public static void main(String[] args) {
//创建目标对象
TeacherDao teacherDao = new TeacherDao();
//创建代理对象,返回被代理的目标对象(必须强制类型转换)
TeacherDao proxyInstance = (TeacherDao) new ProxyFactory(teacherDao).getProxyInstance();
// 通过代理对象,调用目标对象的方法
String result = proxyInstance.teach();
System.out.println(result);
}
}
11.6、代理模式的变体
防火墙代理:内网通过代理穿透防火墙,实现对公网的访问。
缓存代理:比如,当请求图片文件等资源时,先到缓存代理取,如果取到资源则ok,如果取不到资源,再到公网或者数据库取,然后缓存。
远程代理:远程对象的本地代表,通过它可以把远程对象当本地对象来调用。远程代理通过网络和真正的远程对象沟通信息。
同步代理:主要使用在多线程编程中,完成多线程间同步工作。