来自于本地笔记的上传,本文时间较为久远
参考
[1] c.biancheng.net/view/1322.h…
[2] www.bilibili.com/video/BV1Np…
[3] refactoringguru.cn/design-patt…
设计原则
开闭原则
对扩展开放、对修改关闭。 可以对代码进行扩展,不可对代码进行修改
需要使用接口和抽象类。
当需要对程序进行扩展时,不需要修改源代码,只需要添加一个新的实现类
当我们为SougouInput设置新皮肤时,不需要修改具体的代码只需书写skin3并实现AbstractSkin并setSkin()即可(setSkin由外界调用)。
里氏代换原则
任何父类可以出现的地方,子类一定可以出现。即子类可以扩展父类的功能,但不能修改父类的功能,子类尽可能不重写父类的方法。子类重写父类方法会导致代码的可复用性降低。子类如必须重写父类方法,不应该影响父类方法的含义
当程序违背里氏代换原则时,即子类出现在父类的位置时,程序会发生错误。此时正确的修改方法为:取消它们之间的继承关系,重新设计它们之间的关系。
经典例子:“正方形不是长方形”、”几维鸟不是鸟“。
package principle;
public class LSPtest {
public static void main(String[] args) {
Bird bird1 = new Swallow();
Bird bird2 = new BrownKiwi();
bird1.setSpeed(120);
bird2.setSpeed(120);
System.out.println("如果飞行300公里:");
try {
System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");
System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。");
} catch (Exception err) {
System.out.println("发生错误了!");
}
}
}
//鸟类
class Bird {
double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子类
class Swallow extends Bird {
}
//几维鸟类
class BrownKiwi extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}
如果飞行300公里:
燕子将飞行2.5小时.
几维鸟将飞行Infinity小时。
原因在于 几维鸟 虽然也是鸟类,但在当前情况下,几维鸟并不属于鸟类(几维鸟并不会飞),此时如果我们将几维鸟继承于鸟类,则违背里氏代换原则
以下为改进
依赖倒置原则
高层模块不应该依赖底层模块,两者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。即要求我们面向抽象编程。类似于开闭原则的具体体现。
依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 使用继承时尽量遵循里氏替换原则。
接口隔离原则
接口所包含的功能应该小而精,而不应该大而全。
在具体应用接口隔离原则时,应该根据以下几个规则来衡量。
1. 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑。
1. 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法。
1. 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
1. 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。
单一职责原则
每个类应该专注于做一件或者一类事
例如:假如一个班级只有班长这一个管理人员,那么这个班长既需要管理学习、管理纪律也需要管理卫生。此时班长具有多重职责,任务多而杂,极易出错。
此时,我们可以设置学习委员承接班长管理学习的职责、设置纪律委员来承接班长管理纪律的职责、设置卫生委员来承接班长管理卫生的职责,而班长只需要负责管理各委员即可。此时各人物具有单一职责
接口隔离原则和单一职责十分相似,两者都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
- 单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离。
- 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建。
迪米特原则
又名:最少知识原则。如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
例1】明星与经纪人的关系实例。
分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则,其类图如图 1 所示。
package principle;
public class LoDtest {
public static void main(String[] args) {
Agent agent = new Agent();
agent.setStar(new Star("林心如"));
agent.setFans(new Fans("粉丝韩丞"));
agent.setCompany(new Company("中国传媒有限公司"));
agent.meeting();
agent.business();
}
}
//经纪人
class Agent {
private Star myStar;
private Fans myFans;
private Company myCompany;
public void setStar(Star myStar) {
this.myStar = myStar;
}
public void setFans(Fans myFans) {
this.myFans = myFans;
}
public void setCompany(Company myCompany) {
this.myCompany = myCompany;
}
public void meeting() {
System.out.println(myFans.getName() + "与明星" + myStar.getName() + "见面了。");
}
public void business() {
System.out.println(myCompany.getName() + "与明星" + myStar.getName() + "洽淡业务。");
}
}
//明星
class Star {
private String name;
Star(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//粉丝
class Fans {
private String name;
Fans(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
//媒体公司
class Company {
private String name;
Company(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
粉丝韩丞与明星林心如见面了。 中国传媒有限公司与明星林心如洽淡业务。
合成复合原则
合成复用原则又叫组合/聚合复用原则。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
可以降低耦合性
创建型设计模式
单例模式
简介: 令某一个类在程序中只存在一个实例
实例化方法
饿汉式:基本方式
在载入JVM虚拟机时直接实例化
我们知道,类加载的方式是按需加载,且加载一次。因此,在下述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
public class Demo1 {
public static void main(String[] args) {
for (int i = 1; i <= 999; ++i) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton instance = Singleton.getInstance();
System.out.println(instance); // 打印结果都一样
}
}).start();
}
}
}
懒汉式:基本方式
做到懒加载(懒汉式需要考虑线程安全问题)
优点:当需要时对对象进行实例化,节约了内存
缺点:存在线程同步问题,需要加锁,然而此种方法对getInstance()进行加锁,导致即使instance已经不为null,仅仅只单纯获取对象时依然受到锁的限制,效率过低。
class Singleton {
private static volatile Singleton instance; // volatile保证可见性
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式:双重校验锁
保留了基本方式的优点,也解决了基本方式的缺点
class Singleton {
private static volatile Singleton instance; // b
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
懒汉式:静态内部类
静态内部类单例模式由内部类创建,由于JVM在加载外部类时,不会加载静态内部类,只要当静态内部类被访问时才会进行加载,并初始化静态变量,静态变量被static修饰保证只会被实例化一次,切严格保证实例化顺序。
class Singleton {
private Singleton() {}
private static class SingletonHolder {
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
双重校验锁与静态内部类都是非常推荐的懒汉式单例模式
饿汉式:枚举
enum Singleton { // 枚举类 依然是 c
/**
* Singleton实例
*/
INSTANCE
}
public class Demo {
public static void main(String[] args) {
for (int i = 1; i <= 999; ++i) {
new Thread(new Runnable() {
@Override
public void run() {
Singleton instance = Singleton.INSTANCE;
System.out.println(instance.hashCode());
}
}).start();
}
}
}
总结
一般情况下,不建议使用懒汉式基本方式,建议使用饿汉式基本方式。只有在要明确实现 lazy loading 效果时,才会使用静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双重校验锁方式。
破坏单例模式
序列化与反序列化
序列化: 对象➡字节串;是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。
目的: 1、以某种存储形式使自定义对象持久化;2、将对象从一个地方传递到另一个地方;
反序列化: 字节串➡对象;将存储的对象信息重新转换为对象;
目的: 1、将持久化后的对象重新实例化;2、接收其他地方传递来的对象
class Singleton implements Serializable {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
public class Demo1 {
public static void main(String[] args) throws Exception {
writeSerializable();
Singleton singleton = readSerializable();
Singleton singleton1 = readSerializable();
System.out.println(singleton == singleton1);
}
public static void writeSerializable() throws Exception {
Singleton instance = Singleton.getInstance();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(
new FileOutputStream("C:\Users\80946\Desktop\temp.txt"));
objectOutputStream.writeObject(instance);
objectOutputStream.close();
}
public static Singleton readSerializable() throws Exception {
ObjectInputStream inputStream = new ObjectInputStream(
new FileInputStream("C:\Users\80946\Desktop\temp.txt"));
Singleton singleton = (Singleton) inputStream.readObject();
inputStream.close();
return singleton;
}
}
输出结果:
false
由此可见,Singleton在本程序中出现了两个实例,违背的单例模式。序列化与反序列化破坏了单例模式。
解决方案
在Singleton类中添加 readResolve()方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new除的对象
此解决方案与inputStream.readObject()的内部实现有关。
class Singleton implements Serializable {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public Object readResolve() {
return INSTANCE;
}
public static Singleton getInstance() {
return INSTANCE;
}
}
public class Demo1 {
public static void main(String[] args) {
writeSerializable();
Singleton singleton = readSerializable();
Singleton singleton1 = readSerializable();
System.out.println(singleton == singleton1);
}
public static void writeSerializable() {
Singleton instance = Singleton.getInstance();
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(
new FileOutputStream("C:\Users\80946\Desktop\temp.txt"))) {
objectOutputStream.writeObject(instance);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Singleton readSerializable() {
Singleton singleton = null;
try (ObjectInputStream inputStream = new ObjectInputStream(
new FileInputStream("C:\Users\80946\Desktop\temp.txt"))) {
singleton = (Singleton) inputStream.readObject();
} catch (Exception e) {
e.printStackTrace();
}
return singleton;
}
}
输出结果:
true
反序列化得到的时同一对象,遵守了单例模式
反射
Java反射:在运行状态中,程序可以拿到一个类或者一个对象的所有属性和方法,并对其进行修改。
class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
public class Demo1 {
public static void main(String[] args) throws Exception {
Class<Singleton> singletonClass = Singleton.class;
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Singleton singleton = declaredConstructor.newInstance();
Singleton singleton1 = declaredConstructor.newInstance();
System.out.println(singleton == singleton1);
}
}
输出结果:
false
由此可见通过反射可以改变构造函数的可见性,导致可以通过构造函数直接创建对象
使用Unsafe工具
难以解决
解决方案
饿汉式
class Singleton {
private final static Singleton INSTANCE = new Singleton();
/**
* 当单例对象已经被实现后,如果构造方法再次被调用,就会发生异常
*/
private Singleton() {
if (INSTANCE != null) {
throw new RuntimeException("不能创建多个对象");
}
}
public static Singleton getInstance() {
return INSTANCE;
}
}
懒汉式
懒汉式存在多线程问题,故处理方式和饿汉式不一样
// 静态内部类方式,其他的懒汉式也可以这样处理
class Singleton {
private volatile static boolean flag = false;
private Singleton() {
synchronized (Singleton.class) {
if (flag) {
throw new RuntimeException();
}
flag = true;
}
}
private static class SingletonHolder {
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public class Demo1 {
public static void main(String[] args) throws Exception {
for (int i = 0; i <= 99; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Class<Singleton> singletonClass = Singleton.class;
Singleton singleton = null;
Singleton singleton1 = null;
try {
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
singleton = declaredConstructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton);
}
}).start();
}
Singleton instance = Singleton.getInstance();
System.out.println(instance);
}
}
输出结果:
仅有一个输出不为null,故
Singleton只实例化了一次但经测试,此时类已被破坏,既即使不通过反射,通过正常方式也无法获取对象
原因分析
只能得到第一次反射创建的那个对象,但这个对象似乎无法存到
SingletonHolder.INSTANCE中。。导致使用正常方式获取单例对象时,就会执行Singleton INSTANCE = new Singleton(); 结果就又异常了,其他的懒汉式也有这种问题。。。。(我不会解决)
解决
// 仅限懒汉式:静态内部类法使用
// 其他方法使用会栈溢出
class Singleton {
private volatile static boolean flag = false;
private Singleton() {
synchronized (Singleton.class) {
Singleton instance = Singleton.getInstance();
if (flag) {
throw new RuntimeException();
}
flag = true;
}
}
private static class SingletonHolder {
private final static Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public class Demo1 {
public static void main(String[] args) throws Exception {
for (int i = 0; i <= 99; i++) {
new Thread(new Runnable() {
@Override
public void run() {
Class<Singleton> singletonClass = Singleton.class;
Singleton singleton = null;
Singleton singleton1 = null;
try {
Constructor<Singleton> declaredConstructor = singletonClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
singleton = declaredConstructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(singleton);
}
}).start();
}
Singleton instance = Singleton.getInstance();
System.out.println(instance);
}
}
简单工厂
简介: 一种最简单的工厂模式,并不属于标准设计模式的一部分,但十分符合人们的编程习惯,所有的产品皆由简单工厂生产
优点:
- 工厂类包含必要的逻辑判断,可以决定在什么时候创建哪一个产品的实例。客户端可以免除直接创建产品对象的职责,很方便的创建出相应的产品。工厂和产品的职责区分明确
- 客户端无需知道所创建具体产品的类名,只需知道参数即可
- 也可以引入配置文件,在不修改客户端代码的情况下更换和添加新的具体产品类
缺点:
- 简单工厂模式的工厂类单一,负责所有产品的创建,职责过重,一旦异常,整个系统将受影响。且工厂类代码会非常臃肿,违背高聚合原则
- 系统扩展困难,一旦增加新产品不得不修改工厂逻辑,违背开闭原则
应用场景
对于产品种类相对较少的情况,考虑使用简单工厂模式。使用简单工厂模式的客户端只需要传入工厂类的参数,不需要关心如何创建对象的逻辑,可以很方便地创建所需产品。
模式的结构与实现
简单工厂模式的主要角色如下:
- 简单工厂(SimpleFactory):是简单工厂模式的核心,负责实现创建所有实例的内部逻辑。工厂类的创建产品类的方法可以被外界直接调用,创建所需的产品对象。
- 抽象产品(Product):是简单工厂创建的所有对象的父类,负责描述所有实例共有的公共接口。
- 具体产品(ConcreteProduct):是简单工厂模式的创建目标。
UML类图
具体实现
interface Coffee {
void getName();
}
class AmericanCoffee implements Coffee {
@Override
public void getName() {
System.out.println(AmericanCoffee.class);
}
}
class LetterCoffee implements Coffee {
@Override
public void getName() {
System.out.println(LetterCoffee.class);
}
}
class SimpleFactory {
public static Coffee getInstance(String className) {
if ("AmericanCoffee".equals(className)) {
return new AmericanCoffee();
} else if ("LetterCoffee".equals(className)) {
return new LetterCoffee();
}
return null;
}
}
public class Demo {
public static void main(String[] args) {
Coffee letterCoffee = SimpleFactory.getInstance("LetterCoffee");
if (letterCoffee != null) {
letterCoffee.getName();
}
}
}
优化
使用反射创建对象,此时当我们需要添加新产品时,不需要修改代码,只需要修改配置文件,符合开闭原则。
interface Coffee {
void getName();
}
class AmericanCoffee implements Coffee {
@Override
public void getName() {
System.out.println(AmericanCoffee.class);
}
}
class LetterCoffee implements Coffee {
@Override
public void getName() {
System.out.println(LetterCoffee.class);
}
}
class SimpleFactory {
// 存储对象
private static Map<String, Coffee> map = new HashMap<>();
// 加载对象
static {
Properties properties = new Properties();
InputStream resource = SimpleFactory.class.getClassLoader().getResourceAsStream("bean.properties");
try {
properties.load(resource);
Set<String> strings = properties.stringPropertyNames();
for (String string: strings) {
String property = properties.getProperty(string);
Class<?> aClass = Class.forName(property);
Coffee coffee = (Coffee) aClass.newInstance();
map.put(string, coffee);
}
} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
public static Coffee getInstance(String className) {
return map.get(className);
}
}
public class Demo {
public static void main(String[] args) {
Coffee letter = SimpleFactory.getInstance("letter");
letter.getName();
Coffee american = SimpleFactory.getInstance("american");
american.getName();
}
}
工厂方法
简介: 简单工厂模式违背了开闭原则,而“工厂方法模式”是对简单工厂模式的进一步抽象化即将工厂也接口化,其好处是可以使系统在不修改原来代码的情况下引进新的产品,即满足开闭原则。一个工厂生产一个产品
优点:
- 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程。
- 灵活性增强,对于新产品的创建,只需多写一个相应的工厂类。
- 典型的解耦框架。高层模块只需要知道产品的抽象类,无须关心其他实现类,满足迪米特法则、依赖倒置原则和里氏替换原则。
缺点:
- 复杂度增加,系统中具有太多类
- 增加了系统的抽象性和理解难度
- 工厂只能生产一种产品,此弊端可使用抽象工厂模式解决。
应用场景:
- 客户只知道创建产品的工厂名,而不知道具体的产品名。
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
- 客户不关心创建产品的细节,只关心产品的品牌
模式的结构
工厂方法模式的主要角色如下。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
UML类图:
实现:
interface Coffee {
void getName();
}
class AmericanCoffee implements Coffee {
@Override
public void getName() {
System.out.println(AmericanCoffee.class);
}
}
class LetterCoffee implements Coffee {
@Override
public void getName() {
System.out.println(LetterCoffee.class);
}
}
interface AbstractFactory {
Coffee getInstance();
}
class AmericanCoffeeFactory implements AbstractFactory {
@Override
public Coffee getInstance() {
return new AmericanCoffee();
}
}
class LetterCoffeeFactory implements AbstractFactory {
@Override
public Coffee getInstance() {
return new LetterCoffee();
}
}
public class Demo {
public static void main(String[] args) {
AbstractFactory abstractFactory = new AmericanCoffeeFactory();
Coffee instance = abstractFactory.getInstance();
instance.getName();
AbstractFactory factory = new LetterCoffeeFactory();
Coffee coffee = factory.getInstance();
coffee.getName();
}
}
抽象工厂
简介: 在工厂方法中,单个工厂只能生产单种产品。但在实际生活中,许多工厂是综合型的工厂,通常能生产多种产品
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产异族同等级的产品(能生产咖啡,且是多种品牌的咖啡,但不能生产汉堡),而抽象工厂模式可生产多个等级的产品。
优点:
抽象工厂模式除了具有工厂方法模式的优点外,其他主要优点如下。
- 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理。
- 当需要同一个产品族时,抽象工厂可以保证客户端始终只使用同一个产品的产品组。
- 抽象工厂增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则。
缺点:
当需要增加一个新的产品等级时,所有的工厂类都需要进行修改。增加了系统的抽象性和理解难度。
应用场景:
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品(注意)
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用。
模式结构:
抽象工厂模式的主要角色如下。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
UML类图:
如果要添加一个新的产品族 (如 BKCCoffee 与 BKCHamburg)只需要添加一个新的工厂(BKCFactory)
但如果我们要添加一个新的产品等级(如 cookie),那麻烦就大了,我们必须修改所有的工厂
实现:
interface Coffee {
void getName();
}
class McDonalCoffee implements Coffee {
@Override
public void getName() {
System.out.println(McDonalCoffee.class);
}
}
class KFCCoffee implements Coffee {
@Override
public void getName() {
System.out.println(KFCCoffee.class);
}
}
class WallaceCoffee implements Coffee {
@Override
public void getName() {
System.out.println(WallaceCoffee.class);
}
}
interface Hamburg {
void getName();
}
class McDonalHamburg implements Hamburg {
@Override
public void getName() {
System.out.println(McDonalHamburg.class);
}
}
class KFCHamburg implements Hamburg {
@Override
public void getName() {
System.out.println(KFCHamburg.class);
}
}
class WallaceHamburg implements Hamburg {
@Override
public void getName() {
System.out.println(WallaceHamburg.class);
}
}
interface AbstractFactory {
Coffee getCoffee();
Hamburg getHamburg();
}
class McDonaldFactory implements AbstractFactory {
@Override
public Coffee getCoffee() {
return new McDonalCoffee();
}
@Override
public Hamburg getHamburg() {
return new McDonalHamburg();
}
}
class KFCFactory implements AbstractFactory {
@Override
public Coffee getCoffee() {
return new KFCCoffee();
}
@Override
public Hamburg getHamburg() {
return new KFCHamburg();
}
}
class WallaceFactory implements AbstractFactory {
@Override
public Coffee getCoffee() {
return new WallaceCoffee();
}
@Override
public Hamburg getHamburg() {
return new WallaceHamburg();
}
}
public class Demo {
public static void main(String[] args) {
AbstractFactory abstractFactory = new McDonaldFactory();
abstractFactory.getCoffee().getName();
abstractFactory.getHamburg().getName();
abstractFactory = new KFCFactory();
abstractFactory.getCoffee().getName();
abstractFactory.getHamburg().getName();
abstractFactory = new WallaceFactory();
abstractFactory.getCoffee().getName();
abstractFactory.getHamburg().getName();
}
}
输出结果:
class demo4.McDonalCoffee class demo4.McDonalHamburg class demo4.KFCCoffee class demo4.KFCHamburg class demo4.WallaceCoffee class demo4.WallaceHamburg
实际上我们也可以这样做
原型模式
简述:
用一个已经创建的实例作为原型,通过复制该原型来创建一个和原型相同的新实例。
在有些系统中,存在大量相同或相似对象的创建问题,如果用传统的构造函数来创建对象,会比较复杂且耗时耗资源,用原型模式生成对象就很高效
优点:
- 在性能上比直接 new 一个对象更加优良。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份,并将其状态保存起来,简化了创建对象的过程,以便在需要的时候使用(例如恢复到历史某一状态),可辅助实现撤销操作。
缺点:
- 需要为每一个类都配置一个 clone 方法
- clone 方法位于类的内部,当对已有类进行改造的时候,需要修改代码,违背了开闭原则。
- 当实现深克隆时,需要编写较为复杂的代码,而且当对象之间存在多重嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来会比较麻烦。因此,深克隆、浅克隆需要运用得当。
使用场景
- 对象的创建比较复杂,可以使用原型模式快速创建对象
- 性能和安全要求比较高
原型模式的结构
原型模式包含以下主要角色。
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。
UML类图:
代码实现:
浅克隆:创建一个新对象,新对象属性与原对象属性完全相同。对于引用数据类型,新对象与原对象的同一引用数据类型指向同一地址,即复制引用值,不复制引用对象
深克隆:创建一个新对象,新对象属性与原对象属性完全相同。对于引用数据类型,新对象会在堆上重新创建一个引用对象,即复制引用对象,不复制引用值
Java中的Object中自带clone()实现了浅克隆,在Java中只有implement Cloneable的类可以被克隆
浅拷贝
class Temp {
}
class Prototype implements Cloneable {
public int distance = 1;
public Temp temp = new Temp();
public void display() {
System.out.println(new Prototype().hashCode());
}
@Override
protected Prototype clone() throws CloneNotSupportedException {
return (Prototype) super.clone();
}
}
public class Demo {
public static void main(String[] args) throws CloneNotSupportedException {
Prototype prototype = new Prototype();
System.out.println(prototype.distance);
System.out.println(prototype.temp);
Prototype clone = prototype.clone();
System.out.println(clone.distance);
System.out.println(clone.temp);
System.out.println(prototype == clone);
}
}
// 1
// demo5.Temp@1540e19d
// 1
// demo5.Temp@1540e19d
// false
深克隆
class Temp implements Cloneable {
@Override
public Temp clone() throws CloneNotSupportedException {
return (Temp) super.clone();
}
}
class Prototype implements Cloneable {
public int distance = 1;
public Temp temp = new Temp();
public void display() {
System.out.println(new Prototype().hashCode());
}
@Override
public Prototype clone() throws CloneNotSupportedException {
Prototype clone = (Prototype) super.clone();
clone.temp = temp.clone();
return clone;
}
}
public class Demo {
public static void main(String[] args) throws CloneNotSupportedException {
Prototype prototype = new Prototype();
System.out.println(prototype.distance);
System.out.println(prototype.temp);
Prototype clone = prototype.clone();
System.out.println(clone.distance);
System.out.println(clone.temp);
System.out.println(prototype == clone);
}
}
// 1
// demo5.Temp@1540e19d
// 1
// demo5.Temp@677327b6
// false
通过序列化和反序列化也可以实现深拷贝
建造者模式
简介
将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的。
优点
- 封装性好,构建和装配(表示)分离;适用于创建对象比较复杂的情况
- 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险。
缺点
- 产品的组成部分必须相同,这限制了其使用范围。
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大。
结构
- 产品(Product):它是包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件。
- 抽象建造者(Builder):它是一个包含创建产品各个子部件的抽象方法的接口,这个接口规定要实现复杂对象的那些部分的创建,通常还包含一个返回复杂产品的方法
getResult();只定义规范,不涉及具体的创建。 - 具体建造者(Concrete Builder):实现
Builder接口,完成复杂产品的各个部件的具体创建方法。 - 指挥者(Director):它调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息。
UML类图
代码
@Data
class Bike {
private String frame;
private String seat;
}
abstract class Builder {
protected Bike mBike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
}
class MobikeBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("碳纤维骨架");
}
@Override
public void buildSeat() {
mBike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
class OfoBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("铝合金骨架");
}
@Override
public void buildSeat() {
mBike.setSeat("橡胶车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
public class Director {
private Builder builder;
public Director(Builder builder) {
this.builder = builder;
}
public Bike construct() {
builder.buildFrame();
builder.buildSeat();
return builder.createBike();
}
public static void main(String[] args) {
Director director = new Director(new MobikeBuilder());
Bike bike = director.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}
上面示例是Builder模式的常规用法,指挥者类Director在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合。
class Bike {
private String frame;
private String seat;
}
abstract class Builder {
protected Bike mBike = new Bike();
public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();
public Bike construct() {
this.buildFrame();
this.buildSeat();
return this.createBike();
}
}
class MobikeBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("碳纤维骨架");
}
@Override
public void buildSeat() {
mBike.setSeat("真皮车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
class OfoBuilder extends Builder {
@Override
public void buildFrame() {
mBike.setFrame("铝合金骨架");
}
@Override
public void buildSeat() {
mBike.setSeat("橡胶车座");
}
@Override
public Bike createBike() {
return mBike;
}
}
public class Director {
public static void main(String[] args) {
Builder builder = new MobikeBuilder();
Bike bike = builder.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}
这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct()过于复杂,建议还是封装到Director中。
扩展
建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误,此时就可以利用建造者模式进行重构。
@Builder // lombok
@ToString
class Bike {
private String frame;
private String seat;
}
public class Director {
public static void main(String[] args) {
Bike build = new Bike.BikeBuilder()
.seat("座椅")
.frame("frame")
.build();
System.out.println(build);
}
}
用于简化代码,从某种程度上可以提高开发效率,但对程序员要求高
模式的应用场景
建造者模式唯一区别于工厂模式的是针对复杂对象的创建。也就是说,如果创建简单对象,通常都是使用工厂模式进行创建,而如果创建复杂对象,就可以考虑使用建造者模式。
当需要创建的产品具备复杂创建过程时,可以抽取出共性创建过程,然后交由具体实现类自定义创建流程,使得同样的创建行为可以生产出不同的产品,分离了创建与表示,使创建产品的灵活性大大增加。
建造者模式主要适用于以下应用场景:
- 相同的方法,不同的执行顺序,产生不同的结果。
- 多个部件或零件,都可以装配到一个对象中,但是产生的结果又不相同。
- 产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
- 初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。
建造者模式和工厂模式的区别
- 建造者模式更加注重方法的调用顺序,工厂模式注重创建对象。
- 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成;工厂模式创建出来的对象都一样
- 关注重点不一样,工厂模式只需要把对象创建出来就可以了,而建造者模式不仅要创建出对象,还要知道对象由哪些部件组成。
- 建造者模式根据建造过程中的顺序不一样,最终对象部件组成也不一样。
结构性设计模式
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
代理模式
简介
为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
代理模式的定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
优点
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
- 代理对象可以扩展目标对象的功能
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
缺点
- 代理模式会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢
- 增加了系统的复杂度
结构
代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问
- 抽象主题
(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。 - 真实主题
(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。 - 代理
(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
静态代理
// 抽象主题类
interface SellTickets {
void sell();
}
// 具体主题类
class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理类
public class ProxyPoint implements SellTickets {
private TrainStation trainStation = new TrainStation();
@Override
public void sell() {
System.out.println("代售点收取服务费");
trainStation.sell();
System.out.println("代售点将票交给用户");
}
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
proxyPoint.sell();
}
}
JDK动态代理:基于接口
// 抽象主题类
interface SellTickets {
void sell();
}
// 具体主题类
class TrainStation implements SellTickets {
@Override
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理类
public class ProxyPoint {
// 声明目标对象
private TrainStation trainStation = new TrainStation();
/**
* ClassLoader loader: 类加载器, 用于加载代理类, 可以通过目标对象(具体主题类)获得类加载器
* Class<?>[] interfaces: 代理类实现的接口的字节码对象
* InvocationHandler h: 代理对象的调用处理程序
*/
public SellTickets getProxyObject() {
SellTickets proxyInstance = (SellTickets) Proxy.newProxyInstance(
TrainStation.class.getClassLoader(),
TrainStation.class.getInterfaces(),
new InvocationHandler() {
/**
* @param proxy 代理对象, 和proxyInstance是同一个对象, 在invoke中基本不用
* @param method 对接口中的方法进行封装的method方法
* @param args 方法的调用参数
* @return 方法的返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售点收取服务费");
// 调用目标对象的方法
Object invoke = method.invoke(trainStation, args);
System.out.println("代售点将票交给用户");
return invoke;
}
});
return proxyInstance;
}
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
SellTickets proxyObject = proxyPoint.getProxyObject();
proxyObject.sell();
}
}
通过arthas对动态生成的对象进行反编译;去除部分无关代码
final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
// 加载类,加载方法
m3 = Class.forName("demo7.SellTickets").getMethod("sell", new Class[0]);
}
public final void sell() {
// 增强方法,invoke为上面实现的方法
this.h.invoke(this, m3, null); // 这里的 h 为 Proxy 的成员变量 protected InvocationHandler h;
}
}
cglib动态代理:基于父类
// 主题类
class TrainStation {
public void sell() {
System.out.println("火车站卖票");
}
}
// 代理类
public class ProxyPoint {
// 声明目标对象
private TrainStation trainStation = new TrainStation();
public TrainStation getProxyObject() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(TrainStation.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("代售点收取服务费");
// 调用目标对象的方法
Object invoke = method.invoke(trainStation, args);
System.out.println("代售点将票交给用户");
return invoke;
}
});
return (TrainStation) enhancer.create();
}
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
TrainStation proxyObject = proxyPoint.getProxyObject();
proxyObject.sell();
}
}
三种代理的对比
使用场景
适配器模式
简介
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。适配器模式分为类结构型模式和对象结构型模式两种,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
优点
- 客户端通过适配器可以透明地调用目标接口。
- 复用了现存的类,程序员不需要修改原有代码而重用现有的适配者类。
- 将目标类和适配者类解耦,解决了目标类和适配者类接口不一致的问题。
- 在很多业务场景中符合开闭原则。
缺点
- 适配器编写过程需要结合业务场景全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。
结构
- 目标
(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。(圆口耳机) - 适配者
(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。(Type-C耳机口) - 适配器
(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。(转换线) - 谁是目标接口,谁是适配者依照情况来定
类适配器
实现方式:定义一个适配器类来实现当前系统的业务接口(目标接口),同时又继承现有组件库中已经存在的组件(适配者)
UML
代码
// 适配者接口
interface TFCard {
String readTF();
void writeTF();
}
// 适配者实现类
class TFCardImpl implements TFCard {
@Override
public String readTF() {
return "读TF卡的数据";
}
@Override
public void writeTF() {
System.out.println("向TF卡写数据");
}
}
// 目标接口
interface SDCard {
String readSD();
void writeSD();
}
// 目标接口实现类
class SDCardImpl implements SDCard {
@Override
public String readSD() {
return "读SD卡的数据";
}
@Override
public void writeSD() {
System.out.println("向SD卡写数据");
}
}
// 适配器类
class SDAdapterTF extends TFCardImpl implements SDCard {
@Override
public String readSD() {
return super.readTF();
}
@Override
public void writeSD() {
super.writeTF();
}
}
public class Computer {
public String readSD(@NotNull SDCard sdCard) {
return sdCard.readSD();
}
public void writeSD(@NotNull SDCard sdCard) {
sdCard.writeSD();
}
public static void main(String[] args) {
Computer computer = new Computer();
// 读取SD卡数据
System.out.println(computer.readSD(new SDCardImpl()));
// 写入SD卡数据
computer.writeSD(new SDCardImpl());
System.out.println("====================");
// 通过适配器读取TF卡数据
System.out.println(computer.readSD(new SDAdapterTF()));
// 通过适配器写入TF卡数据
computer.writeSD(new SDAdapterTF());
}
}
对象适配器
实现方式:对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类(聚合)中,该类同时实现当前系统的业务接口(目标接口)
UML
代码
// 适配者接口
interface TFCard {
String readTF();
void writeTF();
}
// 适配者实现类
class TFCardImpl implements TFCard {
@Override
public String readTF() {
return "读TF卡的数据";
}
@Override
public void writeTF() {
System.out.println("向TF卡写数据");
}
}
// 目标接口
interface SDCard {
String readSD();
void writeSD();
}
// 目标接口实现类
class SDCardImpl implements SDCard {
@Override
public String readSD() {
return "读SD卡的数据";
}
@Override
public void writeSD() {
System.out.println("向SD卡写数据");
}
}
// 适配器类
class SDAdapterTF implements SDCard {
// 声明适配者类
private TFCard tfCard;
public void setTfCard(TFCard tfCard) {
this.tfCard = tfCard;
}
@Override
public String readSD() {
return tfCard.readTF();
}
@Override
public void writeSD() {
tfCard.writeTF();
}
}
public class Computer {
public String readSD(@NotNull SDCard sdCard) {
return sdCard.readSD();
}
public void writeSD(@NotNull SDCard sdCard) {
sdCard.writeSD();
}
public static void main(String[] args) {
Computer computer = new Computer();
// 读取SD卡数据
System.out.println(computer.readSD(new SDCardImpl()));
// 写入SD卡数据
computer.writeSD(new SDCardImpl());
System.out.println("====================");
// 创建适配器对象
SDAdapterTF adapter = new SDAdapterTF();
// 设置适配者对象
adapter.setTfCard(new TFCardImpl());
// 通过适配器读取TF卡数据
System.out.println(computer.readSD(adapter));
// 通过适配器写入TF卡数据
computer.writeSD(adapter);
}
}
注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter,实现所有方法。而此时我们只需要继承该抽象类即可。
使用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同
装饰模式
简介
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式(比继承灵活),使用组合
优点
- 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态的给一个对象扩展功能,即插即用
- 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果
- 装饰器模式完全遵守开闭原则
缺点
- 装饰器模式会增加许多子类,过度使用会增加程序得复杂性。
结构
- 抽象构件
Component角色:定义一个抽象接口以规范准备接收附加责任的对象。 - 具体构件
ConcreteComponent角色:实现抽象构件,通过装饰角色为其添加一些职责。 - 抽象装饰
Decorator角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。 - 具体装饰
ConcreteDecorator角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
代码
// 一层包一层,实在不好理解
@Data
@NoArgsConstructor
@AllArgsConstructor
abstract class FastFood {
// 抽象构建角色
private String desc;
private Integer cost;
public abstract Integer cost();
}
class FastRice extends FastFood {
// 具体构建角色
public FastRice() {
super("炒饭", 10);
}
@Override
public Integer cost() {
return super.getCost();
}
}
@Setter
@Getter
@NoArgsConstructor
abstract class Garnish extends FastFood {
// 抽象装饰者角色
private FastFood fastFood; // 抽象构建角色
public Garnish(String desc, Integer cost, FastFood fastFood) {
super(desc, cost);
this.fastFood = fastFood;
}
}
class Egg extends Garnish {
// 具体装饰者角色
public Egg(FastFood fastFood) {
super("鸡蛋", 1, fastFood);
}
@Override
public Integer cost() {
return getCost() + getFastFood().cost();
}
@Override
public String getDesc() {
return super.getDesc() + getFastFood().getDesc();
}
}
public class Demo9 {
public static void main(String[] args) {
FastFood fastFood = new FastRice();
System.out.println(fastFood.getDesc() + ": " + fastFood.cost());
fastFood = new Egg(fastFood);
System.out.println(fastFood.getDesc() + ": " + fastFood.cost());
fastFood = new Egg(fastFood);
System.out.println(fastFood.getDesc() + ": " + fastFood.cost());
}
}
JDK中使用
IO流中的包装类 BufferedInputstream,Bufferedoutputstream,BufferedReader,BufferedWriter
代理模式和适配器模式的对比
桥接模式
简介
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度;可以将抽象化部分与实现化部分分开,取消二者的继承关系,改用组合关系。
结构
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现
代码
// 通过 “桥” 将系统和视频连接起来
interface VideoFile {
// 实现化角色
void decode(String filename);
}
class AviFile implements VideoFile {
// 具体实现化角色
@Override
public void decode(String filename) {
System.out.println("avi: " + filename);
}
}
class RmvbFile implements VideoFile{
// 具体实现化角色
@Override
public void decode(String filename) {
System.out.println("rmvb: " + filename);
}
}
abstract class OperationSystem {
// 抽象化角色
protected VideoFile videoFile;
public OperationSystem(VideoFile videoFile) {
this.videoFile = videoFile;
}
public abstract void play(String filename);
}
class Windows extends OperationSystem {
// 扩展抽象化角色
public Windows(VideoFile videoFile) {
super(videoFile);
}
@Override
public void play(String filename) {
videoFile.decode(filename);
}
}
class Mac extends OperationSystem {
// 扩展抽象化角色
public Mac(VideoFile videoFile) {
super(videoFile);
}
@Override
public void play(String filename) {
videoFile.decode(filename);
}
}
public class Demo10 {
public static void main(String[] args) {
AviFile aviFile = new AviFile();
OperationSystem mac = new Mac(aviFile);
mac.play("战狼");
}
}
与其他模式的关系
- 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。
- 桥接、 状态模式和策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 你可以将抽象工厂模式和桥接搭配使用。 如果由桥接定义的抽象只能与特定实现合作, 这一模式搭配就非常有用。 在这种情况下, 抽象工厂可以对这些关系进行封装, 并且对客户端代码隐藏其复杂性。
- 你可以结合使用生成器模式和桥接模式: 主管类负责抽象工作, 各种不同的生成器负责实现工作。
外观模式
简介
外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性 (统一接口)
结构
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它
- 客户(Client)角色:通过一个外观角色访问各个子系统的功能
代码
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:
// 子系统
class Tv {
public void on() {
System.out.println("打开电视");
}
public void off() {
System.out.println("关闭电视");
}
}
class Light {
public void on() {
System.out.println("打开电灯");
}
public void off() {
System.out.println("关闭电灯");
}
}
class AirCondition {
public void on() {
System.out.println("打开空调");
}
public void off() {
System.out.println("关闭空调");
}
}
// 外观类
class SmartApplicationFacade {
private Light light;
private Tv tv;
private AirCondition airCondition;
public SmartApplicationFacade() {
light = new Light();
tv = new Tv();
airCondition = new AirCondition();
}
public void say(String mag) {
if ("打开".equals(mag)) {
this.on();
} else if ("关闭".equals(mag)) {
this.off();
} else {
System.out.println("不明白");
}
}
private void on() {
this.light.on();
this.tv.on();
this.airCondition.on();
}
private void off() {
this.light.off();
this.tv.off();
this.airCondition.off();
}
}
public class Demo11 {
public static void main(String[] args) {
SmartApplicationFacade facade = new SmartApplicationFacade();
facade.say("打开");
System.out.println("===========");;
facade.say("关闭");
}
}
优缺点
优点
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
- 降低了大型软件系统中的编译依赖性,简化了系统在不同平台之间的移植过程,因为编译一个子系统不会影响其他的子系统,也不会影响外观对象。
缺点
- 不能很好地限制客户使用子系统类,很容易带来未知风险。
- 增加新的子系统可能需要修改外观类或客户端的源代码,违背了“开闭原则”。
外观模式的扩展
在外观模式中,当增加或移除子系统时需要修改外观类,这违背了“开闭原则”。如果引入抽象外观类,则在一定程度上解决了该问题。
组合模式
简介
组合(Composite Pattern)模式的定义:有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型设计模式。组合模式一般用来描述整体与部分的关系
模式的结构
组合模式包含以下主要角色。
- 抽象根节点(Component):它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
- 树枝节点(Composite)/ 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法
- 树叶节点(Leaf):是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件
例子
我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称
代码
// 抽象根节点
abstract class MenuComponent {
// 菜单组件名称
protected String name;
// 菜单组件层级
protected int level;
// 添加子菜单
public void add(MenuComponent menu) {
throw new UnsupportedOperationException();
}
// 移除子菜单
public void remove(MenuComponent menu) {
throw new UnsupportedOperationException();
}
// 获取指定子菜单
public MenuComponent getChild(int index) {
throw new UnsupportedOperationException();
}
// 获取菜单以及菜单项的名称
public String getName() {
return name;
}
// 打印菜单名称的方法
public abstract void print();
}
// 树枝节点
class Menu extends MenuComponent {
// 子菜单
private List<MenuComponent> menuList = new ArrayList<>();
// 构造方法
public Menu(String name, int level) {
this.level = level;
this.name = name;
}
@Override
public void add(MenuComponent menu) {
menuList.add(menu);
}
@Override
public void remove(MenuComponent menu) {
menuList.remove(menu);
}
@Override
public MenuComponent getChild(int index) {
return menuList.get(index);
}
@Override
public String getName() {
return super.getName();
}
@Override
public void print() {
// 打印菜单名称
for (int i = 0; i < this.level; i++) {
System.out.print("--");
}
System.out.println(this.name);
// 打印子菜单名称
menuList.forEach(MenuComponent::print);
}
}
// 树叶节点
class MenuItem extends MenuComponent {
public MenuItem(String name, int level) {
this.name = name;
this.level = level;
}
@Override
public void print() {
for (int i = 0; i < this.level; i++) {
System.out.print("--");
}
System.out.println(this.name);
}
}
public class Demo12 {
public static void main(String[] args) {
Menu menu = new Menu("菜单管理", 2);
menu.add(new MenuItem("页面访问", 3));
menu.add(new MenuItem("展开菜单", 3));
menu.add(new MenuItem("编辑菜单", 3));
menu.add(new MenuItem("删除菜单", 3));
menu.add(new MenuItem("新增菜单", 3));
Menu menu1 = new Menu("权限管理", 2);
menu1.add(new MenuItem("页面访问", 3));
menu1.add(new MenuItem("提交保存", 3));
Menu menu2 = new Menu("角色管理", 2);
menu2.add(new MenuItem("页面访问", 3));
menu2.add(new MenuItem("新增角色", 3));
menu2.add(new MenuItem("修改角色", 3));
Menu 系统管理 = new Menu("系统管理", 1);
系统管理.add(menu);
系统管理.add(menu1);
系统管理.add(menu2);
系统管理.print();
}
}
透明组合模式(默认方式)
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中Menucomponent声明了add、remove、getchild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供add ()、remove ()等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点Menu类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
优缺点
- 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
- 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码
- 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合"开闭原则"
- 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单
使用场景
组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的场景。比如文件目录显示,多级目录呈现等树形结构数据的操作
享元模式
定义
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率工
结构
享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
- 内部状态指对象共享出来的信息,存储在享元信息内部,并且不会随环境的改变而改变
- 外部状态指对象得以依赖的一个标记,随环境的改变而改变,不可共享
享元模式的主要角色
- 抽象享元(
Flyweight)角色:是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入 - 具体享元(
Concrete Flyweight)角色:实现抽象享元角色中所规定的接口 - 非享元(
Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中 - 享元工厂(
Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象
代码
// 抽象享元
abstract class AbstractBox {
// 获取图形的方法
public abstract String getShape();
// 显示图像与颜色
public void display(String color) { // 非享元
System.out.println("方块形状: " + this.getShape() + ", 颜色:" + color);
}
}
// 具体享元
class IBox extends AbstractBox {
@Override
public String getShape() {
return "I";
}
}
class LBox extends AbstractBox {
@Override
public String getShape() {
return "L";
}
}
class OBox extends AbstractBox {
@Override
public String getShape() {
return "O";
}
}
// 享元工厂(单例)
class BoxFactory {
private HashMap<String, AbstractBox> map;
private BoxFactory() {
map = new HashMap<>();
map.put("I", new IBox());
map.put("L", new LBox());
map.put("O", new OBox());
}
// 饿汉式
private static final BoxFactory boxFactory = new BoxFactory();
public static BoxFactory getInstance() {
return boxFactory;
}
public AbstractBox getShape(String s) {
return map.get(s);
}
}
// 不变的地方由工厂创建, 变化的地方再手动注入
public class Demo13 {
public static void main(String[] args) {
// 获得 I 图形对象
AbstractBox shape = BoxFactory.getInstance().getShape("I");
shape.display("蓝色");
// 获得 I 图形对象
AbstractBox shape2 = BoxFactory.getInstance().getShape("I");
System.out.println(shape == shape2);
}
}
优缺点
Integer使用了享元模式;可以看到Integer默认先创建并缓存[-128,127]之间数的Integer对象,当调用valueOf时如果参数在[-128,127]之间则计算下标并从缓存中返回,否则创建—个新的Integer对象(127可以改为大于127的数)
行为型设计模式
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性
模板方法模式(类行为型)
模板方法模式是一种行为设计模式, 它在超类中定义了一个算法的框架, 允许子类在不修改结构的情况下重写算法的特定步骤。
策略模式
策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。
当我们的方法涉及到多种算法,且经常需要在这些算法之间相互切换,可以使用策略模式
解释器模式(类行为型)
解释器模式提供了评估语言的语法或表达式的方式,它属于行为型模式。这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在 SQL 解析、符号处理引擎等。主要用于语法解析中
责任链模式
责任链模式是一种行为设计模式,允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。
Netty 的Channel与Spring Security就使用了责任链模式。Spring的拦截器链也是责任链模式
命令模式
命令模式是一种行为设计模式, 它可将请求转换为一个包含与请求相关的所有信息的独立对象。 该转换让你能根据不同的请求将方法参数化、 延迟请求执行或将其放入队列中, 且能实现可撤销操作。
观察者模式
观察者模式是一种行为设计模式, 允许你定义一种订阅机制, 可在对象事件发生时通知多个 “观察” 该对象的其他对象。
观察者模式可以使用发布订阅模式实现,当一个对象内的数据发生改变时,向交换机发布改变消息,交换机将其转发给订阅的观察(订阅)者
迭代器模式
迭代器模式是一种行为设计模式, 让你能在不暴露集合底层表现形式 (列表、 栈和树等) 的情况下遍历集合中所有的元素。在迭代器的帮助下, 客户端可以用一个迭代器接口以相似的方式遍历不同集合中的元素。
在Java中,Java集合都实现了迭代器模式,通过迭代器模式我们可以迭代List ,Set甚至Map;
中介者模式
中介者模式是一种行为设计模式, 能让你减少对象之间混乱无序的依赖关系。 该模式会限制对象之间的直接交互, 迫使它们通过一个中介者对象进行合作。
与其他模式的关系
-
责任链模式、 命令模式、 中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式:
- 责任链按照顺序将请求动态传递给一系列的潜在接收者, 直至其中一名接收者对请求进行处理。
- 命令在发送者和请求者之间建立单向连接。
- 中介者清除了发送者和请求者之间的直接连接, 强制它们通过一个中介对象进行间接沟通。
- 观察者允许接收者动态地订阅或取消接收请求。
-
外观模式和中介者的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
- 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。 子系统本身不会意识到外观的存在。 子系统中的对象可以直接进行交流。
- 中介者将系统中组件的沟通行为中心化。 各组件只知道中介者对象, 无法直接相互交流。
-
中介者和观察者之间的区别往往很难记住。 在大部分情况下, 你可以使用其中一种模式, 而有时可以同时使用。 让我们来看看如何做到这一点。
中介者的主要目标是消除一系列系统组件之间的相互依赖。 这些组件将依赖于同一个中介者对象。 观察者的目标是在对象之间建立动态的单向连接, 使得部分对象可作为其他对象的附属发挥作用。
有一种流行的中介者模式实现方式依赖于观察者。 中介者对象担当发布者的角色, 其他组件则作为订阅者, 可以订阅中介者的事件或取消订阅。 当中介者以这种方式实现时, 它可能看上去与观察者非常相似。比如kafka可以充当MySQL与Redis的中介。
当你感到疑惑时, 记住可以采用其他方式来实现中介者。 例如, 你可永久性地将所有组件链接到同一个中介者对象。 这种实现方式和观察者并不相同, 但这仍是一种中介者模式。
假设有一个程序, 其所有的组件都变成了发布者, 它们之间可以相互建立动态连接。 这样程序中就没有中心化的中介者对象, 而只有一些分布式的观察者。
备忘录模式
备忘录模式是一种行为设计模式, 允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。备忘录不会影响它所处理的对象的内部结构, 也不会影响快照中保存的数据。
CTRL+Z
状态模式
状态模式是一种行为设计模式, 让你能在一个对象的内部状态变化时改变其行为, 使其看上去就像改变了自身所属的类一样。
状态机,在不同的状态做不同的事情。
如果对象需要根据自身当前状态进行不同行为, 同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
模式建议你将所有特定于状态的代码抽取到一组独立的类中。 这样一来, 你可以在独立于其他状态的情况下添加新状态或修改已有状态, 从而减少维护成本。(有点像策略模式)
与其他模式的关系
- 桥接模式、 状态模式和策略模式 (在某种程度上包括适配器模式) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题。
- 状态可被视为策略的扩展。 两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。 策略使得这些对象相互之间完全独立, 它们不知道其他对象的存在。 但状态模式没有限制具体状态之间的依赖, 且允许它们自行改变在不同情景下的状态。
访问者模式
访问者模式是一种行为设计模式, 它能将算法与其所作用的对象隔离开来,允许你在不修改已有代码的情况下向已有类层次结构中增加新的行为。访问者不是常用的设计模式, 因为它不仅复杂, 应用范围也比较狭窄。