# 23种设计模式 Uncompleted
这23种设计模式主要分为3种类型的模式:
- 创建型模式:对象实例化的模式,用于解耦对象的实例化过程
- 结构性模式:把类或对象结合在一起形成一个更大的结构
- 行为型模式:类和对象如何交互,划分责任和算法
——-
- 单例模式:某个类只能有一个实例,提供一个全局的访问点
- 简单工厂:一个工厂类根据传入的参数来决定是创建哪个产品的实例化(重载)
- 工厂方法:定义一个创建对象的接口,让子类去决定对哪个类进行实例化
- 抽象工厂:创建相关或者依赖对象的家族,但无需明确指定具体的类
- 建造者模式:封装一个复杂对象的构建过程,并且可以按照步骤构造
- 原型模式:通过复制现有的实例来创建新的实例
- 适配器模式:将一个类的方法接口转化成客户希望的另外一个接口
- 组合模式:将对象组合成树型结构,以表示“部分-整体”的层次结构
- 装饰模式:动态地给对象添加新的功能
- 代理模式:为其它对象提供一个代理,以便控制这个对象的访问
- 享元模式:通过共享技术来有效地支持大量细粒度的对象
- 外观模式:对外提供一个统一的方法,来访问子系统中的一群接口
- 桥接模式:将抽象的部分与它实现的部分分离,使它们都可以独立的变化
- 模板模式:定义一个算法结构,但是将一些步骤延迟到子类实现
- 解释器模式:给定一个语言,定义它的文法的一种表示,并且定义一个解释器
- 策略模式:定义一系列的算法,把它们封装起来,并且使它们可以相互替换
- 状态模式:允许一个对象在其对象内部改变时,就改变它的行为
- 观察者模式:对象间的一对多的依赖关系
- 备忘录模式:在不破坏封装的前提下,保持对象的内部状态
- 中介者模式:用一个中介对象来封装一系列的对象交互
- 命令模式:将命令请求封装为一个对象,使得可以用不同的请求来进行参数化
- 访问者模式:在不改变数据结构的前提下,增加作用于一组对象元素的新功能
- 责任链模式:将请求的发送者和接收者解耦,使得每个对象都有处理这个请求的机会
- 迭代器模式:一种遍历访问聚合对象中各个元素的方法,不暴露该对象的内部结构
# 单例模式
单例模式下,对应类只能生成一个实例。但是也有很多种不同的写法。
# 饿汉模式
public class Singleton { private static Singleton instance = new Singleton(); private Singleton() { } public static Singleton getInstance() { return instance; } }
Copied!
这种方式在类加载时就完成了初始化操作,所以类加载速度慢,但是获取对象的速度快。这种方式基于类加载机制,避免了多线程的同步问题,但是也不能确定有其它的方式或者其它的静态方法会导致类加载。此时的初始化instance
是没有达到懒加载的效果的。
# 懒汉模式(线程不安全)
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
Copied!
懒汉模式使用了懒加载的模式,申明了一个静态对象,只在用户第一次调用时需要初始化。虽然节约了资源,但是第一次加载时仍需要实例化,速度稍慢,而且在多线程下不能工作。因为当有多个线程并行调用getInstance()
的时候,就会创建多个实例了。
# 懒汉模式(线程安全)
public class Singleton { private static Singleton instance; private Singleton() { } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
Copied!
为了解决线程不安全的问题,就是将整个getInstance()
方法设置为同步(synchronized
)。这种写法能够在多线程中很好的工作,但是每次调用getInstance()
方法时都需要进行同步,会造成不必要的同步开销。因为在任何时候都只能有一个线程调用getInstance()
方法。但是大部分时候我们是用不到同步的,只需要在第一次调用时用到,所以不建议使用这种方式。
# 双重检验锁模式(double checked locking pattern)
public class Singleton { private static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
Copied!
DCL模式是一种使用同步块加锁的方法,在getSingleton()
方法中对instance
进行了两次判空,第一次是为了不必要的同步,第二次是在instance == null
的情况下才创建实例。之所以还要在同步块内再检验一次,就是因为可能会有多个线程一起进入同步块外的if
,如果不进行二次检验就会生成多个实例了。
但是上面这段代码仍然有问题。主要是instance = new Singleton()
这句代码,并非是一个原子操作。在JVM中,这句代码会执行下面三个操作:
- 给
instance
分配内存 - 调用
Singleton
的构造函数来初始化成员变量 - 将
instance
对象指向分配的内存空间(执行完这步后instance
就为非空了)
但是在JVM的即时编译器中存在指令重排序的优化,即第二步和第三步的顺序不能保证谁先执行。解决方法就是将instance
变量声明成volatile
就可以了。因为volatile
是可以禁止指令重排序优化。
public class Singleton { private volatile static Singleton instance; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized(Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
Copied!
使用volatile
的DCL模式的优点是资源利用率高,第一次执行getInstance()
时,单例对象才会被实例化,效率高。缺点是第一次加载时反应会慢一些。
# 静态内部类单例模式
public class Singleton() { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } }
Copied!
这种方法也是《Effect Java》推荐使用的。在第一次加载Singleton
类时并不会初始化sInstance
,只有第一次调用getInstance()
方法的时候虚拟机加载SingletonHolder
并初始化sInstance
。这样首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()
方法没有任何线程锁定,因此其性能不会造成任何影响。
由于SingletonHolder
是私有的,除了getInstance()
方法之外没有方法能够访问它,所以它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷,也不依赖JDK版本。
# 枚举单例
public enum Singleton { INSTANCE; }
Copied!
用枚举实现单例的方式十分简单。默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,而且能够防止反序列化导致创建新的对象。但是大部分应用开发很少使用到枚举,因为可读性不是很高,不建议使用。
# 枚举单例与饿汉模式的区别
# 使用容器实现单例模式
public class SingletonManager { private static Map<String, Object> objMap = new HashMap<String, Object>(); private Singleton() { } public static void registerService(String key, Objectinstance) { if (!objMap.containsKey(key)) { objMap.put(key, instance); } } public static ObjectgetService(String key) { return objMap.get(key); } }
Copied!
使用SingletonManager
将多种单例统一管理,使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现的细节,降低了耦合度。
# 简单工厂模式
简单工厂模式(Simple Factory Pattern),又称为静态工厂方法(Static Factory Method)模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。它专门定义了一个类来负责创建其它类的实例,被创建的实例通常都会具有共同的父类。
之所以要使用简单工厂模式,是因为要把“产品”和“生产”完全分开。用户只需要知道自己要使用哪些产品,以及如何使用产品即可,而产品的生产则由工厂来完成。因为不同的产品可能仅仅是因为某些参数的不同而作用于不同的场景,所以工厂只需要根据传进来的不同的参数去生产具体的产品给消费者使用。
简单工厂模式包含以下角色:
- Factory(工厂角色):工厂角色负责实现创建所有实例的内部逻辑
- Product(抽象产品角色):抽象产品角色是所创建的所有产品(实例)的父类,负责描述所有产品(实例)共有的公共接口
- ConcreteProduct(具体产品角色):具体产品角色是这个角色的具体类的实例,是抽象产品角色根据不同参数的具体实现
(或者让Product作为具体的产品角色,AbstractProduct作为抽象产品角色)。
简单工厂模式的优点:
- 简单工厂模式实现了对于责任的分割,提供了专门的工厂类用于创建对象,而客户端免除了直接创建产品对象的责任,而是仅仅消费产品;
- 客户端无需知道所创建的具体产品类的类名,只需要知道具体产品类对应的参数即可;
- 可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,一定程度上提高了系统的灵活性;
- 从产品的角度上符合“开闭原则”
简单工厂模式的缺点:
- 因为工厂类集中了所有的产品创建的逻辑,如果一旦不能正常工作,整个系统都会受到影响;
- 使用简单工厂模式会增加系统中类的个数,在一定程度上增加了系统的复杂度和理解难度;
- 系统扩展困难,一旦添加新的产品就要修改工厂类的逻辑,从工厂的角度上是不符合“开闭原则”的;
- 简单工厂模式由于使用了静态工厂方法,造成了工厂角色无法形成基于继承的结构;
适用场景:
- 工厂类负责创建的对象比较少的情况。因为创建的对象较少,就不会造成工厂方法中的业务逻辑太过复杂;
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心:客户端既不需要关心创建细节,甚至连类名都不需要记住,只需要知道类型所对应的参数。
# 工厂方法模式
工厂方法模式(Factory Method Pattern)又称为工厂模式,从名字就可以看出相对于简单工厂模式,工厂模式会更复杂一些。在该模式中,工厂父类负责定义创建产品对象的公共接口,而工厂实现类则负责生成具体的产品对象。抽象产品类和具体产品类的逻辑则与简单工厂模式一致。
工厂方法模式包含以下角色:
- Product(抽象产品):定义具体产品的公共接口;
- ConcreteProduct(具体产品):继承抽象产品类,定义生产的具体产品;
- Factory(抽象工厂):定义具体工厂的公共接口;
- ConcreteFactory(具体工厂):定义创建对应具体产品实例的方法;
工厂模式的优点:
- 更加符合“开闭原则”:当新增一种产品时,只需要增加响应的具体产品类和具体工厂类,抽象工厂和抽象产品无需修改;
- 符合“单一职责原则”:每个具体工厂类都只负责创建对应的产品;
- 不适用静态工厂方法:可以形成基于继承的结构
工厂模式可以说是简单工厂模式的一种抽象和拓展,在保留了简单工厂的封装有点的同时,让扩展变得更简单,实现了继承的结构,增加了多态性的体现。
工厂模式缺点:
- 添加新产品时,既要增加新的具体产品类,又要增加新的具体工厂类,系统类的个数成对增加,增加了系统的复杂度;
- 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到OOM、反射等技术,增加了实现难度;
- 一个具体工厂类只能对应一个产品;
- 如果需要更换产品而不是新增产品,就仍然需要修改实例化的具体工厂类;
适用场景:
- 当一个类希望通过子类来指定创建对象
- 当一个类不知道它所需要的对象的类
- 将创建对象的任务委托给多个具体工厂类的某一个,客户端在使用时可以无须关心是哪一个具体工厂类创建了该产品,需要时再动态指定即可,可以将具体工厂类的类名存储在配置文件或者数据库中
# 抽象工厂模式
抽象工厂模式(Abstract Factory Pattern),提供一个创建一系列相关或者相互依赖对象的接口,而无须指定它们具体的类;具体的工厂负责实现具体的产品实例。
因为工厂模式中,每个工厂只能创建一个类,十分浪费;抽象工厂就解决了这个问题。抽象工厂中每个工厂都可以创建多种类的产品。
抽象工厂模式包含以下角色:
- 抽象产品族:描述了抽象产品的公共接口;
- 抽象产品:描述了具体产品的公共接口;
- 具体产品:描述了具体产品;
- 抽象工厂:描述了具体工厂的公共接口;
- 具体工厂:描述了具体工厂;
具体代码如下所示:
// 创建抽象产品族类,定义抽象产品的公共接口 abstract class AbstractProduct { public abstract void show(); } // 创建抽象产品类,定义具体产品的公共接口 abstract class ContainerProduct extends AbstractProduct { @Override public abstract void show(); } // 创建抽象产品类,定义具体产品的公共接口 abstract class MouldProduct extends AbstractProduct { @Override public abstract void show(); } // 创建具体产品类,定义生产的具体产品 // 容器A类 class ContainerProduct A extends ContainerProduct { @Override public void show() { System.out.println("生产出了容器产品A"); } } // 容器B类 class ContainerProductB extends ContainerProduct{ @Override public void Show() { System.out.println("生产出了容器产品B"); } } // 模具A类 class MouldProductA extends MouldProduct{ @Override public void Show() { System.out.println("生产出了模具产品A"); } } // 模具B类 class MouldProductB extends MouldProduct{ @Override public void Show() { System.out.println("生产出了模具产品B"); } } // 创建抽象工厂类,定义具体工厂的公共接口 abstract class Factory { public abstract Product ManufactureContainer(); public abstract Product ManufactureMould(); } // 创建具体工厂类,定义工厂创建具体产品实例的方法 // 生产A产品的工厂 class FactoryA extends Factory { @Override public Product ManufactureContainer() { return new ContainerProductA(); } @Override public Product ManufactureMould() { return new MouldProductA(); } } // 生产B产品的工厂 class FactoryB extends Factory { @Override public Product ManufactureContainer() { return new ContainerProductB(); } @Override public Product ManufactureMould() { return new MouldProductB(); } } // 具体实现类(调用类) public class AbstractFactoryPattern { public static void main(String[] args) { FactoryA mFactoryA = new FactoryA(); FactoryB mFactoryB = new FactoryB(); //A厂当地客户需要容器产品A mFactoryA.ManufactureContainer().Show(); //A厂当地客户需要模具产品A mFactoryA.ManufactureMould().Show(); //B厂当地客户需要容器产品B mFactoryB.ManufactureContainer().Show(); //B厂当地客户需要模具产品B mFactoryB.ManufactureMould().Show(); } }
Copied!
抽象工厂模式的优点:
- 降低耦合:抽象工厂模式将具体产品的创建放在了具体工厂的子类中
- 更符合“开闭原则”:新增一种产品时,则需要增加新的具体产品类和相应的工厂子类即可。
抽象工厂模式的缺点:
- 很难支持产品的变化:因为抽象工厂接口中已经确定了可以被创建的产品集合,所以如果需要添加新产品的话,就必须去修改抽象工厂的接口,这样就违背了“开闭原则”。
# 建造者模式
建造者模式(Builder Pattern)是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
通俗的说话就是,建造者模式就是如何一步步构建一个包含多个组成部件的对象,相同的构建过程却可以创建出不同的产品。它允许用户只通过指定复杂对象的类型和内容就可以构建它们,而不要知道内部的具体构建细节。
建造者模式包含以下角色:
- Builder:抽象建造者。给出一个抽象接口,以规范产品对象的各个组成成分的建造。一般而言,此接口是独立于应用程序的商业逻辑的。它必须实现接口要求的两种方法。一个是建造方法
buildPart()
,一个是返回结构方法getResult()
。 - ConcreteBuilder:具体建造者。这个角色的任务在于实现抽象建造者Builder声明的接口,给出一步步完成创建产品实例的操作;在建造过程完成后,提供产品的实例。
- Director:导演者。这个角色负责调用较早者角色以创建产品对象。这个角色不了解任何关于产品的知识,只是负责调用。
- Product:产品。产品就是被建造的对象。
比如以下一个例子可以用于描述KFC如何创建套餐的。KFC套餐是一个复杂的对象,它一般包含主食(如汉堡、鸡翅等)和饮料(如果汁、可乐等)等组成部分,不同的套餐有不同的组成部分。那么服务员这个对象就要根据客户的需要,负责组装这些套餐返回给顾客。
在上面这个模型中,KFCWaiter就是导演者角色。它并不知道具体的主食、饮料等是如何做出来,只负责组装(调用Builder对象),然后与客户端交流。MealBuilder在这里就是抽象建造者对象,里面的getMeal()
方法就是从SubMealBuilder这个具体建造者对象得到建造好的产品。Meal是Product对象。
建造者模式的优点:
- 在建造者模式中,客户端是不知道产品内部组成的细节的,它只与Director交互。通过将产品与产品本身的创建过程解耦,使得相同的创建过程却可以创建出不同的产品对象;
- 同样的,每一个具体建造者也是相对独立的。用户可以使用不同的具体建造者建造出不同的产品对象;
- 可以更加精细地控制产品的创建过程;
- 增加新的具体建造者是无需修改原有类库的代码的,符合开闭原则。
建造者模式的缺点:
- 所创建的产品一般具有较多的共同点(如上面例子中饮料的可乐、果汁等)。如果产品之间差异性很大的话,就不适用于建造者模式(因为使用范围受到一定的限制);
- 如果产品的内部变化很复杂,就需要定义很多的具体建造类来实现这种变化,一个是不能满足的,这样系统会很庞大。
# 原型模式
原型模式是通过给定一个原型对象来指明所有创建的对象的类型,然后用这个原型对象提供的复制方法创建出更多同类型的对象。
即原型模式要求对象实现一个可以克隆自身的接口(类型)。这样一来,通过原型实例创建新的对象,就不需要关心这个实例本身的类型,只需要实现克隆自身的方法,也无需再去通过new
去创建。
原型对象主要就是用于对象的复制,其核心就是上图中的原型类Prototype。Prototype类需要具备以下两个条件:
- 实现
Cloneable
接口。它的作用是在运行时通知虚拟机可以安全地实现此接口的类上使用clone
方法。在JVM中,只有实现了这个接口的类才可以被拷贝; - 重写Object类中的
clone
方法。
总之,原型模式十分简单易理解,实现一个接口,重写一个方法就实现了原型模式。在实际应用中,原型模式很少单独出现,经常与其它模式混用,其原型类Prototype也常用抽象类来替代。
class Prototype implements Cloneable { public Prototype clone() { Prototype prototype = null; try { prototype = (Prototype) super.clone(); } catch (CloneNotSupportException e) { e.printStackTrace(); } return prototype; } } class ConcretePrototype extends Prototype { public void show() { System.out.println("原型模式实现类"); } } public class Client { public static void main(String[] args) { ConcretePrototype cp = new ConcretePrototype(); for (int i = 0; i < 5; i++) { ConcretePrototype clonecp = (ConcretePrototype) cp.clone(); clonecp.show(); } } }
Copied!
原型模式的优点:
- 简化对象的创建;
- 使用原型模式创建对象比直接
new
一个对象的性能要好得多,因为clone
方法是一个本地方法。
使用原型模式的注意事项:
- 使用原型模式复制对象不会调用类的构造方法
- 深拷贝与浅拷贝。Object类的
clone
方法只会拷贝对象中的基本数据类型,对于数组、容器对象、引用对象等都不会去拷贝,所以是浅拷贝。
# 外观模式
外观模式定义了一个高层接口,为子系统中的一组接口提供了统一的接口。
外观模式包含了如下两个角色:
- 外观角色(Facade):在客户端可以调用它的方法,在外观角色中可以知道相关的子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求都委派到相应的子系统中去,传递给相应的子系统对象去处理;
- 子系统角色(SubSystem):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不只是一个单独的类,而是一个类的集合,它实现了子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它要处理从外观类传过来的请求;子系统是不知道外观的存在的;
外观模式的本质是封装交互,简化调用。它的目的不是给予子系统添加新的功能接口,而是让外部减少与子系统内多个模块的交互,松散耦合,让外部更简单地使用子系统。
// 子系统角色,由若干个类组成 public class SubClass1 { public void method1() { System.out.println("这是子系统1中的方法1"); } public void method2() { System.out.println("这是子系统1中的方法2"); } } public class SubClass2 { public void method1() { System.out.println("这是子系统2中的方法1"); } public void method2() { System.out.println("这是子系统2中的方法2"); } } // 外观角色类 public class FacadeClass { // 把子系统中的几个方法汇总到了一起 public void FacadeMethod() { SubClass1 s1 = new SubClass1(); s1.method1(); SubClass2 s2 = new SubClass2(); s2.method2(); } } // 客户端主类 public class ClientClass { public static void main(String[] args) { FacadeClass fc = new FacadeClass(); fc.FacadeMethod(); } }
Copied!
- 外观模式为复杂子系统提供了一个简单的接口,并不为子系统添加新的功能和行为;
- 外观模式实现了子系统与客户端之间的松耦合的关系;
- 外观模式没有封装子系统的类,只是提供了简单的接口。如果有需要,客户其实也是能直接使用子系统的类的。
- 外观模式注重的是简化接口,它更多的时候是从架构的层次去看整个系统,而非单个类的层次。
# 适配器模式
适配器模式(Adapter Pattern),将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,别名为包装器(Wrapper)。
适配器模式包含以下角色:
- Target:目标抽象类
- Adapter:适配器类
- Adaptee:适配者类
- Client:客户类
有两种适配器模式,一种是类的适配器模式,一种是对象的适配器模式。它们都是把适配的类的API转换成目标类的API。
# 类的适配器模式
从上图可以看出,Target目标类想要调用Request
方法,但是适配者类Adaptee是没有这个方法的,那么就需要中间有一个适配器类Adapter,把Adaptee的API与Target的API衔接起来(通过继承Adaptee,实现Target接口)。
public interface Target { public void Request(); } public class Adaptee { public void SpecificRequest() { } } public class Adapter extends Adaptee implements Target { @Override public void Request() { this.SpecificRequest(); } } public class AdapterPattern { public static void main(String[] args) { Target mAdapter = new Adapter(); mAdapter.Request(); } }
Copied!
# 对象的适配器模式
对象的适配器模式就是把继承改在了Adapter的内部去实现。
public interface Target { public void Request(); } public class Adaptee { public void SpecificRequest() { } } public class Adapter implements Target { private Adaptee adaptee; public Adapter(Adaptee adaptee) { this.adaptee = adaptee; } @Override public void Request() { this.adaptee.SepcificRequest(); } } public class AdapterPattern { public static void main(String[] args) { Target mAdapter = new Adapter(new Adaptee()); mAdapter.Request(); } }
Copied!
适配器模式的优点:
- 将目标类Target与适配者类Adaptee解耦,中间通过一个适配器类Adapter完成;
- 增加了类的透明性和复用性;
- 灵活性和拓展性非常好
- 类适配器模式:由于适配器类Adater是适配者类Adaptee的子类,所以在适配器类Adapter还可以重写适配者Adaptee的一些方法,灵活性很强;
- 对象适配器模式:一个对象适配器可以把不同的适配者Adaptee适配到同一个目标,即把适配者类Adaptee和它的子类都适配到目标接口Target。
适配器模式的缺点:
因为Java等语言不支持多继承,所以一次最多只能适配一个适配者类Adaptee。
# 桥接模式
桥接模式(Bridge Pattern)的主要特点是把抽象(Abstraction)与实现(Implementation)分离开,从而保持各部分的独立性以及对应它们的功能扩展。
桥接模式的角色:
- Client:调用者
- Abstraction:抽象类接口。它的角色就是一个桥接类
- Refined Abstraction:是Abstraction类的子类
- Implementer:行为实现类的接口
- ConcreteImplementer:Implementer类的子类
// 首先定义Implementer接口,定义了一个operation()方法 public interface Implementer { public void operation(); } // 定义了Implementator接口的两个实现类 public class ConcreteImplementerA implements Implementer { @Override public void operation() { System.out.println("OperationA"); } } public class ConcreteImplementerB implements Implementer { @Override public void operation() { System.out.println("OperationB"); } } // 定义了桥接类Abstraction,这是一个抽象类,有对Implementer接口的引用 public abstract class Abstraction { private Implementer implementer; public Implementer getImplementer() { return implementer; } public void setImplementer(Implementer implementer) { this.implementer = implementer; } // 通过引用Implementer,调用其operation()方法 public void operation() { implementer.operation(); } } public class RefinedAbstraction extends Abstraction { @Override protected void operation() { super.getImplementer().operation(); } } // 测试类 public class BridgeTest { public static void main(String[] args) { Abstraction abstraction = new RefinedAbstraction(); abstraction.setImplementer(new ConcreteImplementerA()); abstraction.operation(); abstraction.setImplementer(new ConcreteImplementerB()); abstraction.operation(); } }
Copied!
桥接模式的优点:
- 实现了抽象和实现部分的分离,从而极大地增加了系统的灵活性。对于系统的高层部分,只需要知道抽象部分和实现部分的接口即可,不用知道具体的实现方法
- 更好的拓展性
- 可以动态的切换实现
桥接模式的缺点:
- 桥接模式的引入增加了系统的理解与设计难度。它的聚合关联联系建立在抽象层,要求开发者针对抽象进行设计和编程;
- 桥接模式要求正确识别出系统中的两个独立变化的维度,所以其使用范围是有一定局限性的。
# 代理模式
使用代理模式的目的是解决在直接访问对象时带来的问题。比如,要访问的对象在远程机器上,那么可能会因为创建对象开销很大或者其它原因,直接访问对象会给使用者或者系统结构带来很多麻烦,因此需要在访问对象时加上一个对此对象的访问层。
代理模式(Proxy Pattern):给定一个对象提供一个代理,并由代理对象控制对原对象的引用。
代理模式包含以下角色:
- Subject:抽象主题角色
- Proxy:代理主题角色
- RealSubject:真实主题角色
我们可以通过一个加载图片的过程来说明代理模式。我们创建了一个Image接口和实现了Image接口的实体类。ProxyImage是一个代理类,是用来减少RealImage对象加载的内存占用。
// 创建图片接口 public interface Image { void display(); } // 创建实现图片接口的类 public class RealImage implements Image { private Image fileName; public RealImage(String fileName) { this.fileName = fileName; loadFromDisk(fileName); } @Override public void display() { System.out.println("Displaying " + fileName); } private void loadFromDisk(String fileName) { System.out.println("Loading " + fileName + " from disk"); } } // 创建代理图片类 public class ProxyImage implements Image { private RealImage realImage; private String fileName; public ProxyImage(String fileName) { this.fileName = fileName; } @Override public void display() { if (realImage == null) { realImage = new RealImage(fileName); } readImage.display(); } } // 主测试类 public class ProxyTest { public static void main(String[] args) { Image image = new ProxyImage("test.jpg"); image.display(); } }
Copied!
从以上代码可以看出,通过创建一个ProxyImage的实例,调用其中的display()
方法,然后如果图片存在,则再创建一个RealImage的实例,去加载和显示图片。
所以通过代理类ProxyImage,就能够负责协调调用者和被调用者,在一定程度上降低了系统的耦合度。缺点就是可能会造成请求的速度变慢。
除此之外,还可以举出一些实例来证明代理模式。例如,猪八戒去高老庄找高翠兰。但是通过把高翠兰的外貌给抽离出来,而高翠兰本人和孙悟空都能实现这个外貌的接口。那么猪八戒就无法通过高翠兰的外貌识别这究竟是高翠兰还是孙悟空了。这就是代理模式。
几种常见的代理模式:
- 图片代理:比如对大图浏览的控制。用户通过浏览器访问网页时先不加载真实的大图,而是通过代理对象的方式进行处理。先在代理对象的方法中,先使用一个线程向客户端浏览器加载一个小图片,然后在后台使用另一个线程来调用大图片加载到客户端。当需要浏览大图片的时候,再将大图片在新网页中显示。如果用户在浏览大图时加载工作还未完成,可以再启动一个线程来显示相应的提示信息。通过代理技术结合多线程编程将真实图片的加载放到后台来操作,不影响前台图片的浏览。
- 远程代理:远程代理可以将网络的细节隐藏起来,使得客户端不必考虑网络的存在。客户完全可以认为被代理的远程业务对象是局域的而不是远程的,而远程代理对象则承担了大部分的网络通信操作。
- 虚拟代理:当一个对象的加载十分消耗资源的时候,虚拟代理的优势就非常明显地体现出来。虚拟代理模式是一种内存节省技术,那些占用大连内存或者处理复杂的对象将推迟到使用它的时候才创建。
# 装饰器模式
装饰器模式(Decorator Pattern)是动态地给一个对象增加一些额外的功能又不改变其结构。就增加对象功能而言,装饰器模式比生成子类的实现更加灵活。其别名也叫包装器(Wrapper)。
装饰器模式包含以下角色:
- Component:抽象构件。给出一个抽象的接口,以规范准备接受附加功能的对象。
- ConcreteComponent:具体构件。定义一个要接受附加功能的对象。
- Decorator:抽象装饰类。
- ConcreteDecorator:具体装饰类。
public interface Component { void operation(); } public class Decorator implements Component { private Component component; public Decorator(Component component) { super(); this.component = component; } public Decorator() { super(); } public void operation() { component.operation(); } } public class ConcreteComponent implements Component { public ConcreteComponent() { super(); } public void operation(); } public class ConcreteDecorator extends Decorator { public void operation() { super.operation(); } }
Copied!
根据上面的类图,可以通过“齐天大圣——孙悟空”的例子来掌握装饰器模式。
Component的角色就是“齐天大圣”,注意这里的“齐天大圣”是一个名号,是由孙悟空拥有;而ConcreteComponent就是孙悟空本人,它是“齐天大圣”这个名号的实际拥有者;Decorator是齐天大圣拥有的72变,而具体的ConcreteDecoratorA或者ConcreteDecoratorB就是72变可以变化的角色,例如花鸟鱼虫等等。
// 齐天大圣接口 public interface MonkeyKing { public void move(); } // 猴子类,是齐天大圣的本尊(具体实现) public class Monkey implements MonkeyKing { @Override public void move() { System.out.println("Monkey move"); } } // 72变类 public class Change implements MonkeyKing { private MonkeyKing king; public Change(MonkeyKing king) { this.king = king; } @Override public void move() { this.king.move(); } } // 具体的变化类,变成鱼 public class Fish extends Change { public Fish(MonkeyKing king) { super(king); } @Override public void move() { super.move(); System.out.println("Change fish"); } } // 具体的变化类,变成鸟 public class Bird extends Change { public Bird(MonkeyKing king) { super(king); } @Override public void move() { super.move(); System.out.println("Change bird"); } } public class Client { public static void main(String[] args) { MonkeyKing king = new Monkey(); MonkeyKing bird = new Bird(king); bird.move(); } }
Copied!
装饰模式是符合“开闭原则”的。再比如,定义一个抽象类Tea,它的功能是只能提供白开水。而通过装饰类BlackTea装饰之后就拓展了新功能,可以用白开水泡红茶,还可以选择加配料等等。
装饰器模式的使用场景:
- 当需要扩展一个类的功能或者给一个类增加附加功能时;
- 需要动态地给一个对象增加功能,而且这些功能还可以动态地撤销
装饰器模式的优点:
- 继承关系的目的都是拓展对象的功能,而装饰器模式可以提供更多的灵活性;
- 通过使用不同的具体修饰类以及这些修饰类的排列组合,可以创造出很多不同的组合;
装饰器模式的缺点:
- 使用装饰器模式就是会比继承关系产生更多的类,而且这些类看上去都比较接近。
# 享元模式
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。
在很多情况下,我们需要在系统中增加类和对象的个数,这样会付出很多代价,带来性能下降等问题。而享元模式就是来解决这一问题的。它通过共享技术来实现相同或相似对象的重用。
在享元模式中,可以共享的相同内容被称为内部状态(IntrinsicState),而那些需要外部环境来设置不能共享的内容称为外部状态(ExtrinsicState)。在享元模式中,通过会出现工厂模式,需要创建一个享元工厂来负责维护一个享元池(Flyweight Pool),用于存储具有相同内部状态的享元对象。
享元模式包含的角色:
- Flyweight(抽象享元类):通常是一个接口或者抽象类。在这里声明了具体享元类的公共方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- ConcreteFlyweight(具体享元类):它实现了抽象享元类中的具体方法,其实例就是享元对象。在具体享元类中为内部状态提供存储空间。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,所以与ConcreteFlyweight相反的就是不可被共享的子类,即非共享具体享元类。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各个类型的具体享元对象存储在一个享元池中(享元池一般设计成一个存储键值对的集合)。
// 享元工厂类(含有一个享元池) public class FlyweightFactory { // 定义一个HashMap用于存储享元对象,实现享元池 private HashMap flyweights = new HashMap(); public Flyweight getFlyweight(String key) { // 如果对象存在,则直接从享元池中获取 if (flyweights.containsKey(key)) { return (Flyweight)flyweights.get(key); } // 如果对象不存在,先创建一个新的对象添加到享元池中,然后返回它 else { Flyweight fw = new ConcreteFlyweight(); flyweights.put(key, fw); return fw; } } } // 享元类 public class Flyweight { // 内部状态intrinsicState作为成员变量,同一个享元对象其内部状态是一致的 private String intrinsicState; public Flyweight(String intrinsicState) { this.intrinsicState = intrinsicState; } // 如果是外部状态,则使用时由外部设置,不保存在享元对象中,即使是同一个对象 public void operation(String extrinsicState) { } }
Copied!
享元模式的优点:
- 可以极大地较少内存中对象的数量,使得相同的或者相似的对象在内存中只保留一份,从而可以节约系统资源,提高系统性能;
- 享元模式的外部状态相对独立,而且不会影响到内部状态,从而使得享元对象可以在不同的环境中被共享;
享元模式的缺点:
- 使得系统复杂化
- 读取外部状态将使得运行时间变长
# 职责链模式
# 命令模式
# 解释器模式
# 迭代器模式
# 中介者模式
# 备忘录模式
# 观察者模式
# 状态模式
# 策略模式
# 模板方法模式
# 访问者模式
← 线程