设计模式
1.UML图
统一建模语言是用来设计软件的可视化建模语言。定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。
1.1类图
1.1.1类的表示方式
在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法
属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:
-
+:表示public
-
-:表示private
-
#:表示protected
属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]
方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]
1.1.2类与类之间关系的表示方式
关联关系:
对象之间的引用;表示一类对象与另一类对象之间的联系,比如老师和学生。然后也分为一般关联关系、聚合关系和组合关系。
一般关联关系分为单向关联,双向关联,自关联。
1,单向关联
单向关联用一个带箭头的实线表示。如图Customer类有Address的成员变量类,使得每个顾客都有一个地址。
2,双向关联
双向关联就是双方各自持有对方类型的成员变量。双向关联用一个不带箭头的直线表示,顾客可以有多个产品,每个产品又可以表示自己被那个顾客买走。
3,自关联
自关联在UML类图中用一个带有箭头且指向自身的线表示,也就是“自己包含自己”。
聚合关系
聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
聚合关系可以用带空心菱形的实线来表示,菱形指向整体。
组合关系
组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。
一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。
组合关系用带实心菱形的实线来表示,菱形指向整体。
依赖关系
它是对象之间耦合度最弱的一种关联方式,是临时性的关联。
类的方法里通过局部变量,方法参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。
依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。例如司机驾驶汽车,依赖汽车才能驾驶:
继承关系
继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。代码使用面对对象的继承机制实现即可,Student 类和 Teacher 类都是 Person 类的子类。
实现关系
接口与实现类之间的关系。类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。汽车和船实现了交通工具:
2 . 软件设计原则
提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性
2.1 开闭原则
对扩展开放,对修改关闭。即在程序需要进行拓展的时候,不能去修改原有的代码。使程序的扩展性好,易于维护和升级。
多使用接口和抽象类。抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。
2.2 里氏代换原则
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。简单理解就是:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
2.3 依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
例如:让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。
3.4 接口隔离原则
客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。
这种时候可以将接口,按照功能细分;然后依赖具体的功能。
3.5 迪米特法则
迪米特法则又叫最少知识原则。
其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
第三方:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
3.6 合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
通常类的复用分为继承复用和合成复用两种。
继承复用虽然有简单和易实现的优点,但它也存在以下缺点:
-
继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
-
子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
-
它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:
-
它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
-
对象间的耦合度低。可以在类的成员位置声明抽象。
-
复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
3 . 设计模式分类
-
创建型模式
用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。有单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。
-
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构。有代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
-
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。有模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
4 . 创建者模式
创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。
这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。
创建型模式分为:
-
单例模式
-
工厂方法模式
-
抽象工程模式
-
原型模式
-
建造者模式
这里主要讲常用的单例模式喝工厂方法模式
4.1 单例设计模式
它提供了一种创建对象的最佳方式。保证一个类仅有一个实例,并提供一个访问它的全局访问点。
这样做主要是利用全局访问点,在任何位置都可以访问到相同的实例,方便数据共享。确保一致性,避免竞态条件(多线程环境下,可以避免由于多个线程同时创建对象而导致的竞态条件。)
使用场景:
-
当一个类只应该有一个实例,且客户端应该能够从全局访问该实例时,可以考虑使用单例模式。
-
当需要控制资源的分配,限制实例的数量时,例如数据库连接池。
-
当希望避免频繁创建和销毁对象以提高性能时。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
实现:
1. 使用枚举(Enum)实现单例模式 (枚举方式属于恶汉式方式)
public enum Singleton {
INSTANCE;
// 在枚举中添加您需要的方法和属性
public void doSomething() {
System.out.println("Singleton instance is doing something.");
}
}
2. 懒汉式(静态内部类方式):
静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。静态属性由于被 static
修饰,保证只被实例化一次,并且严格保证实例化顺序。
/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
第一次加载Singleton类时不会去初始化INSTANCE,只有第一次调用getInstance,虚拟机加载SingletonHolder,并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。
单例模式有:饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式和懒汉式也有很多种方式,但是都存在着一些问题。详情可以看单例模式单独的介绍。
3. 懒汉式-方式3(双重检查锁):
再来讨论一下懒汉模式中加锁的问题,对于 getInstance()
方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为null
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile
关键字, volatile
关键字可以保证可见性和有序性。
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
小结:
添加 volatile
关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。
存在的问题:
使用序列化和反射可以破坏除枚举外的单例模式方法。
问题的解决:
序列化、反序列方式破坏单例模式的解决方法:
在Singleton类中添加readResolve()
方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新new出来的对象。
public class Singleton implements Serializable {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
/**
* 下面是为了解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
-
源码解析:
ObjectInputStream类
public final Object readObject() throws IOException, ClassNotFoundException{ ... // if nested read, passHandle contains handle of enclosing object int outerHandle = passHandle; try { Object obj = readObject0(false);//重点查看readObject0方法 ..... } private Object readObject0(boolean unshared) throws IOException { ... try { switch (tc) { ... case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));//重点查看readOrdinaryObject方法 ... } } finally { depth--; bin.setBlockDataMode(oldMode); } } private Object readOrdinaryObject(boolean unshared) throws IOException { ... //isInstantiable 返回true,执行 desc.newInstance(),通过反射创建新的单例类, obj = desc.isInstantiable() ? desc.newInstance() : null; ... // 在Singleton类中添加 readResolve 方法后 desc.hasReadResolveMethod() 方法执行结果为true if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { // 通过反射调用 Singleton 类中的 readResolve 方法,将返回值赋值给rep变量 // 这样多次调用ObjectInputStream类中的readObject方法,继而就会调用我们定义的readResolve方法,所以返回的是同一个对象。 Object rep = desc.invokeReadResolve(obj); ... } return obj; }
-
反射方式破解单例的解决方法:
public class Singleton {
//私有构造方法
private Singleton() {
/*
反射破解单例模式需要添加的代码
*/
if(instance != null) {
throw new RuntimeException();
}
}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
说明:
这种方式比较好理解。当通过反射方式调用构造方法进行创建创建时,直接抛异常。不运行此中操作。
经验之谈:一般情况下,因为懒汉式的其他方式存在但容易产生垃圾对象,线程不安全的问题。明确实现 lazy loading 效果时,可以使用静态内部类的方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。
4.2 工厂模式
其主要目的是封装对象的创建过程,使客户端代码和具体的对象实现解耦。这样子就不用每次都new对象,更换对象的话,所有new对象的地方也要修改,违背了开闭原则(对扩展开放,对修改关闭)。使用工厂来生产对象,更换对象也直接在工厂更换即可。
工厂模式的主要好处包括:
-
解耦合:工厂模式将对象的创建过程与客户端代码分离,客户端不需要知道具体的对象是如何创建的,只需要通过工厂方法获取对象即可,从而降低了代码之间的耦合度。
-
灵活性:由于工厂负责创建对象,客户端可以通过工厂方法获取不同的对象实例,而无需关心具体的实现细节,从而提高了系统的灵活性。
-
可扩展性:如果需要添加新的产品类型,只需在工厂中添加相应的产品创建逻辑,而不需要修改客户端代码,这样可以很方便地扩展系统的功能。
-
统一管理:工厂模式将对象的创建集中在一个地方,便于统一管理和维护,提高了代码的可维护性。
使用场景:
-
当一个系统需要创建多个类型的对象,并且这些对象之间存在着共同的接口时,可以考虑使用工厂模式。
-
当客户端不需要知道具体的对象是如何创建的,只需要获取对象实例时,可以使用工厂模式。
-
当系统需要动态地决定创建哪种类型的对象时,可以使用工厂模式。
工厂模式包含以下几个核心角色:
-
抽象产品(Abstract Product):定义了产品的共同接口或抽象类。它可以是具体产品类的父类或接口,规定了产品对象的共同方法。
-
具体产品(Concrete Product):实现了抽象产品接口,定义了具体产品的特定行为和属性。
-
抽象工厂(Abstract Factory):声明了创建产品的抽象方法,可以是接口或抽象类。它可以有多个方法用于创建不同类型的产品。
-
具体工厂(Concrete Factory):实现了抽象工厂接口,负责实际创建具体产品的对象。
这里介绍三种工厂:
-
简单工厂模式(不属于GOF的23种经典设计模式)
-
工厂方法模式
-
抽象工厂模式
4.2.1 简单工厂模式
简单工厂不是一种设计模式,反而比较像是一种编程习惯。
结构:
简单工厂包含如下角色:
-
抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
-
具体产品 :实现或者继承抽象产品的子类
-
具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。
使用场景:
-
当对象的创建逻辑相对简单,并且不需要频繁地进行变更时,可以考虑使用简单工厂模式。
-
在客户端只知道所需产品的名称或类型,而不需要关心产品的创建过程时,可以使用简单工厂模式。
实现思路:
// 抽象产品接口
interface Product {
void show();
}
// 具体产品类A
class ConcreteProductA implements Product {
@Override
public void show() {
System.out.println("This is product A.");
}
}
// 具体产品类B
class ConcreteProductB implements Product {
@Override
public void show() {
System.out.println("This is product B.");
}
}
// 简单工厂类
class SimpleFactory {
public static Product createProduct(String type) {
if ("A".equals(type)) {
return new ConcreteProductA();
} else if ("B".equals(type)) {
return new ConcreteProductB();
}
return null;
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Product productA = SimpleFactory.createProduct("A");
productA.show();
Product productB = SimpleFactory.createProduct("B");
productB.show();
}
}
上面的工厂类创建对象的功能定义为静态的,这个属于是静态工厂模式,当然你也可以不设置为静态的。
优缺点:
优点:
-
简单工厂模式中,客户端通过工厂类的静态方法来获取产品实例,而不需要直接实例化具体产品类。如果要实现新产品直接修改工厂类,而不需要在原代码中修改。
缺点:
-
工厂类负责创建所有产品,因此如果系统需要添加新的产品类型,需要修改工厂类,违反了开放封闭原则。
4.2.2 工厂方法模式
使用工厂方法模式可以完美的解决简单工厂模式的缺点,完全遵循开闭原则。
概念
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其工厂的子类。
结构
工厂方法模式的主要角色:
-
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
-
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
-
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
-
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
图例
使用工厂方法模式对上例进行改进:
工厂方法模式适用于需要创建一系列相关对象的情况
// 抽象产品接口
interface Product {
void show();
}
// 具体产品类A
class ConcreteProductA implements Product {
@Override
public void show() {
System.out.println("This is product A.");
}
}
// 具体产品类B
class ConcreteProductB implements Product {
@Override
public void show() {
System.out.println("This is product B.");
}
}
// 抽象工厂类
interface Factory {
Product createProduct();
}
// 具体工厂类A,负责创建产品A
class ConcreteFactoryA implements Factory {
@Override
public Product createProduct() {
return new ConcreteProductA();
}
}
// 具体工厂类B,负责创建产品B
class ConcreteFactoryB implements Factory {
@Override
public Product createProduct() {
return new ConcreteProductB();
}
}
// 客户端
public class Client {
public static void main(String[] args) {
Factory factoryA = new ConcreteFactoryA();
Product productA = factoryA.createProduct();
productA.show();
Factory factoryB = new ConcreteFactoryB();
Product productB = factoryB.createProduct();
productB.show();
}
}
于是乎要增加产品类时只要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
使用场景:
-
当需要创建的对象是一个具体的产品,但是不确定具体产品的类型时,可以使用工厂方法模式。
-
在工厂类中定义一个创建产品的抽象方法,由子类负责实现具体产品的创建过程,从而实现了产品的创建和客户端的解耦。
优缺点:
优点:
-
工厂方法模式中,客户端通过调用工厂类的方法来创建产品,具体产品的创建逻辑由子类实现,不同的产品由不同的工厂子类负责创建。
-
工厂方法模式符合开放封闭原则,因为客户端可以通过新增工厂子类来添加新的产品类型,而无需修改原有的代码。
缺点:
-
每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。
4.2.3 抽象工厂模式
抽象工厂模式通常涉及一族相关的产品,每个具体工厂类负责创建该族中的具体产品。
使用场景:
-
当一个系统需要创建一系列相互关联或相互依赖的产品对象时,可以考虑使用抽象工厂模式。
-
抽象工厂模式提供了一个创建一组相关或相互依赖对象的接口,客户端可以通过该接口来创建产品族中的不同产品,而不需要关心具体的产品实现。
所以由此也可看出,普通工厂模式,工厂方法模式都只是单一产品类的工厂;而很多时候我们需要综合性的,需要生产多等级产品的工厂。下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂:
结构
抽象工厂模式的主要角色如下:
-
抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
-
具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
-
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
-
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
代码案例:
// 抽象产品接口
interface Product {
void show();
}
// 具体产品类A
class ConcreteProductA implements Product {
@Override
public void show() {
System.out.println("This is product A.");
}
}
// 具体产品类B
class ConcreteProductB implements Product {
@Override
public void show() {
System.out.println("This is product B.");
}
}
// 抽象工厂接口
interface AbstractFactory {
Product createProductA();
Product createProductB();
}
// 具体工厂类,负责创建产品A和产品B
class ConcreteFactory implements AbstractFactory {
@Override
public Product createProductA() {
return new ConcreteProductA();
}
@Override
public Product createProductB() {
return new ConcreteProductB();
}
}
// 客户端
public class Client {
public static void main(String[] args) {
AbstractFactory factory = new ConcreteFactory();
Product productA = factory.createProductA();
productA.show();
Product productB = factory.createProductB();
productB.show();
}
}
-
使用场景:
-
当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。
-
系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
-
系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
-
优缺点
优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
模式扩展 (利用反射机制来创建对象)
简单工厂+配置文件解除耦合
可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。
通过使用配置文件,将创建对象的参数存储在外部配置文件中,可以在不修改客户端代码的情况下,通过修改配置文件来改变对象的创建方式。这样就可以实现对创建逻辑的解耦合,客户端不需要知道具体的创建方式,只需要从工厂类获取对象即可。
具体实现步骤如下:
-
在配置文件中配置需要创建的对象的类名或者类型。
-
在简单工厂类中读取配置文件,并根据配置的信息来创建对应的对象。
假设有一个配置文件 config.properties
,内容如下:
product.type=ConcreteProductA
创建简单工厂类 SimpleFactory.java
,用于读取配置文件并根据配置创建对象:
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public class SimpleFactory {
public static Product createProduct() {
Properties properties = new Properties();
try (InputStream inputStream = SimpleFactory.class.getResourceAsStream("config.properties")) {
properties.load(inputStream);
String productType = properties.getProperty("product.type");
if ("ConcreteProductA".equals(productType)) {
return new ConcreteProductA();
} else if ("ConcreteProductB".equals(productType)) {
return new ConcreteProductB();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
5. 结构型模式
结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。
由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。
结构型模式分为以下 7 种:
-
代理模式
-
适配器模式
-
装饰者模式
-
桥接模式
-
外观模式
-
组合模式
-
享元模式
日常业务主要用到适配器模式和装饰者模式,其他模式都是在特定情况下使用。
5.1 适配器模式
适配器模式是一种结构型设计模式,其主要作用是解决两个不兼容接口之间的兼容性问题。适配器模式通过引入一个适配器来将一个类的接口转换成客户端所期望的另一个接口,从而让原本由于接口不匹配而无法协同工作的类能够协同工作。
结构
适配器模式(Adapter)包含以下主要角色:
-
目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
-
适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
-
适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
图例:
AudioPlayer实现了 MediaPlayer 接口,只可以播放 mp3 。实现了 AdvancedMediaPlayer 接口的类则可以播放 vlc 和 mp4 格式的文件。可以创建一个实现了 MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 的实现类对象来播放所需的格式。AdapterPatternDemo 类则可以使用 AudioPlayer 类来播放各种格式的音频。
对象适配器模式代码案例:
// 目标接口
interface MediaPlayer {
void play(String audioType, String filename);
}
// 适配器接口
interface AdvancedMediaPlayer {
void playVlc(String filename);
void playMp4(String filename);
}
// 适配器类
class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMediaPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMediaPlayer.playVlc(filename);
} else if (audioType.equalsIgnoreCase("mp4")) {
advancedMediaPlayer.playMp4(filename);
}
}
}
// 具体实现类
class AudioPlayer implements MediaPlayer {
MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("mp3")) {
System.out.println("Playing mp3 file. Name: " + filename);
} else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, filename);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
class VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playVlc(String filename) {
System.out.println("Playing vlc file. Name: " + filename);
}
@Override
public void playMp4(String filename) {
// Do nothing
}
}
class Mp4Player implements AdvancedMediaPlayer {
@Override
public void playVlc(String filename) {
// Do nothing
}
@Override
public void playMp4(String filename) {
System.out.println("Playing mp4 file. Name: " + filename);
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
AudioPlayer audioPlayer = new AudioPlayer();
audioPlayer.play("mp3", "song.mp3");
audioPlayer.play("vlc", "movie.vlc");
audioPlayer.play("mp4", "video.mp4");
}
}
适配器模式有类适配器模式和对象适配器模式;这里使用对象适配器模式主要是类适配器模式违背了合成复用原则,它限制了适配器类只能适配一个具体的被适配者类。且Java 不支持多重继承,因此在 Java 中一般使用接口来实现类似的功能
比如下面类适配器,采用的是继承:
// 适配器类(类适配器)
class MediaAdapter extends Mp4Player implements MediaPlayer {
@Override
public void play(String audioType, String filename) {
if (audioType.equalsIgnoreCase("vlc")) {
playVlc(filename);
} else if (audioType.equalsIgnoreCase("mp4")) {
playMp4(filename);
}
}
}
当然,也有接口适配器模式,不过使用相对较少。当一个接口拥有许多方法,但实现类只需要实现其中一部分方法时,可以使用接口适配器模式,提供一个抽象适配器类实现该接口,并提供默认实现,从而避免实现类需要实现大量空方法。
使用场景:
-
当需要使用一个已经存在的类,但是它的接口不符合当前需求时,可以考虑使用适配器模式。
-
当需要复用一些已经存在的类,但是接口与其他类不兼容时,可以考虑使用适配器模式。
-
当需要创建一个可复用的类,该类可以与不相关或不可预见的类协同工作时,可以考虑使用适配器模式。
5.2 装饰器模式
它允许你在不改变对象结构的情况下,动态地将新功能附加到对象上。
结构:
-
抽象组件(Component):定义了原始对象和装饰器对象的公共接口或抽象类,可以是具体组件类的父类或接口。
-
具体组件(Concrete Component):是被装饰的原始对象,它定义了需要添加新功能的对象。
-
抽象装饰器(Decorator):继承自抽象组件,它包含了一个抽象组件对象,并定义了与抽象组件相同的接口,同时可以通过组合方式持有其他装饰器对象。
-
具体装饰器(Concrete Decorator):实现了抽象装饰器的接口,负责向抽象组件添加新的功能。具体装饰器通常会在调用原始对象的方法之前或之后执行自己的操作。
图例:
平常假如要加一个配料,都需要修改餐品的源代码,但是随着配料的增多, 类会变得越来越庞大,等下类爆炸了。
public class Main {
public static void main(String[] args) {
FriedNoodles friedNoodles = new FriedNoodles();
friedNoodles.addBacon();
friedNoodles.addEgg();
friedNoodles.addFish();
System.out.println("Cost: $" + friedNoodles.getCost());
}
}
案例:
假设有一个简单的咖啡店系统,其中有一个 Coffee
接口表示咖啡,它有一个方法 getCost()
来获取咖啡的价格。现在我们要给咖啡添加一些额外的配料,比如牛奶、摩卡和奶泡。
// 咖啡接口
interface Coffee {
double getCost();
}
// 具体咖啡类
class SimpleCoffee implements Coffee {
@Override
public double getCost() {
return 1.0;
}
}
// 装饰者抽象类
abstract class CoffeeDecorator implements Coffee {
protected final Coffee decoratedCoffee;
public CoffeeDecorator(Coffee decoratedCoffee) {
this.decoratedCoffee = decoratedCoffee;
}
public double getCost() {
return decoratedCoffee.getCost();
}
}
// 具体装饰者类
class Milk extends CoffeeDecorator {
public Milk(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
return super.getCost() + 0.5;
}
}
class Mocha extends CoffeeDecorator {
public Mocha(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
return super.getCost() + 1.0;
}
}
class Foam extends CoffeeDecorator {
public Foam(Coffee decoratedCoffee) {
super(decoratedCoffee);
}
@Override
public double getCost() {
return super.getCost() + 0.3;
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Coffee coffee = new SimpleCoffee();
coffee = new Milk(coffee);
coffee = new Mocha(coffee);
coffee = new Foam(coffee);
System.out.println("Cost: $" + coffee.getCost());
}
}
通过组合不同的装饰者,可以在不改变原有咖啡对象的情况下,动态地添加额外的功能和费用。
使用场景:
-
动态地给对象添加功能:当需要给对象动态地添加一些额外的功能,而且这些功能可以独立于该对象的创建。
-
当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
不能采用继承的情况主要有两类:
-
第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
-
第二类是因为类定义不能继承(如final类)
-
6. 行为型模式
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式分为:
-
模板方法模式
-
策略模式
-
命令模式
-
职责链模式
-
状态模式
-
观察者模式
-
中介者模式
-
迭代器模式
-
访问者模式
-
备忘录模式
-
解释器模式
常用的模式主要是策略模式(常用),观察者模式,模板方法模式。
6.1 模板方法模式
模板方法模式是一种行为型设计模式,它定义了一个算法的骨架,将一些步骤延迟到子类中实现。这种模式允许子类在不改变算法结构的情况下重新定义算法的某些步骤。
结构
-
抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。其中包含了一些基本操作的步骤,有些步骤由具体子类实现。
-
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
-
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
-
抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
-
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
-
钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
-
-
-
具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
案例:
你制作一个饮料,步骤是确定的,像烧水; 酿造;倒入杯中,添加调味品。烧水和倒杯是固定的基本操作,酿造和添加调味料这个则是通过具体的情况来定的。
代码实现:
// 抽象类
abstract class Beverage {
// 模板方法,定义了算法的骨架
public final void prepareBeverage() {
boilWater();
brew();
pourInCup();
addCondiments();
}
// 抽象方法,由子类实现
abstract void brew();
abstract void addCondiments();
// 公共方法,由父类实现
void boilWater() {
System.out.println("Boiling water");
}
void pourInCup() {
System.out.println("Pouring into cup");
}
}
// 具体类1
class Coffee extends Beverage {
@Override
void brew() {
System.out.println("Dripping coffee through filter");
}
@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}
// 具体类2
class Tea extends Beverage {
@Override
void brew() {
System.out.println("Steeping the tea");
}
@Override
void addCondiments() {
System.out.println("Adding lemon");
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
Beverage coffee = new Coffee();
coffee.prepareBeverage();
System.out.println();
Beverage tea = new Tea();
tea.prepareBeverage();
}
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。
使用场景:
-
当有一系列算法步骤,其中有一部分是固定的,但是另一部分需要在子类中具体实现时,可以考虑使用模板方法模式。
-
当需要在不同的子类中重用相同的算法框架时,可以使用模板方法模式。
以下是模板方法模式在开发后台管理系统中的使用场景示例:
-
权限管理: 在后台管理系统中,通常需要对不同用户或用户组的权限进行管理。模板方法模式可以定义一个权限管理的骨架,包括权限验证、权限分配等操作,而具体的权限验证和分配操作可以交由子类实现。
-
数据的增删改查: 后台管理系统通常需要对数据进行增加、删除、修改、查询等操作。可以使用模板方法模式定义一个数据操作的骨架,包括数据的验证、数据的持久化等步骤,而具体的数据操作可以由子类实现。
-
数据的导入导出: 后台管理系统可能需要支持数据的导入导出功能,例如从 Excel 文件中导入数据到数据库,或者将数据库中的数据导出为 Excel 文件。可以使用模板方法模式定义一个数据导入导出的骨架,包括数据格式的验证、数据的转换等步骤,而具体的导入导出操作可以由子类实现。
-
日志记录: 后台管理系统通常需要记录用户的操作日志,例如登录日志、操作日志等。可以使用模板方法模式定义一个日志记录的骨架,包括日志的格式化、日志的存储等步骤,而具体的日志记录操作可以由子类实现。
优缺点:
优点:
-
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
-
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
-
对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
-
父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
6.2 策略模式
策略模式是一种行为型设计模式,它定义了一系列算法,将每个算法封装到具有共同接口的独立类中,并且使它们可以相互替换。策略模式可以让算法的变化独立于使用算法的客户端。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
结构
策略模式的主要角色如下:
-
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。所有具体策略类都实现了该接口。
-
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
-
环境/上下文(Context)类:持有一个策略类的引用,负责将客户端的请求委派给具体的策略对象。
图例:
在 Java 中使用策略模式的写法:
-
定义策略接口:创建一个接口,用于定义所有具体策略类的公共行为。
-
创建具体策略类:实现策略接口,并提供具体的算法实现。
-
创建上下文类:维护一个对策略接口的引用,并提供方法来设置和切换不同的具体策略类。
-
客户端使用:在客户端代码中,创建上下文对象,并设置具体的策略类,然后调用上下文对象的方法来执行具体的算法。
// 1. 定义策略接口
interface PaymentStrategy {
void pay(double amount);
}
// 2. 创建具体策略类
class AliPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via AliPay.");
}
}
class WeChatPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via WeChatPay.");
}
}
// 3. 创建上下文类
class PaymentContext {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
public void makePayment(double amount) {
paymentStrategy.pay(amount);
}
}
// 4. 客户端使用
public class Main {
public static void main(String[] args) {
PaymentContext paymentContext = new PaymentContext();
// 使用支付宝支付
paymentContext.setPaymentStrategy(new AliPayStrategy());
paymentContext.makePayment(100.0);
// 使用微信支付
paymentContext.setPaymentStrategy(new WeChatPayStrategy());
paymentContext.makePayment(50.0);
}
}
代码中创建了策略接口 PaymentStrategy
和两个具体策略类 AliPayStrategy
和 WeChatPayStrategy
。然后,创建了上下文类 PaymentContext
,它维护了一个对策略接口的引用,并提供了设置和执行具体策略的方法。最后,在客户端 Main
类中,创建了 PaymentContext
的实例,并设置了具体的支付策略,然后进行支付操作。
使用场景:
-
当有多个相关的类只有行为或算法上稍有不同的情况下,可以考虑使用策略模式。它将算法的变化独立封装到各自的策略类中,易于扩展和维护。
-
一个系统需要动态地在几种算法中选择一种时,可以将这些行为封装成不同的策略类,并在需要时动态切换。
-
系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
-
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
混合模式:
混合模式是指在策略模式中引入了简单工厂模式或者享元模式等其他设计模式,来减少策略类的数量,简化系统的结构。
举例来说,假设一个系统有多种支付方式,除了支付宝支付和微信支付之外,还有银行卡支付、信用卡支付等多种支付方式。如果每种支付方式都对应一个具体的策略类,随着支付方式的增加,策略类的数量会急剧增加,导致类膨胀问题。为了解决这个问题,可以引入简单工厂模式,将支付方式的创建交给一个工厂类来完成;同时,如果某些支付方式具有相似的功能,可以使用享元模式来共享相同的部分,减少策略对象的数量。
代码案例:
// 1. 定义策略接口
interface PaymentStrategy {
void pay(double amount);
}
// 2. 创建具体策略类
class AliPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via AliPay.");
}
}
class WeChatPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via WeChatPay.");
}
}
class BankCardPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via BankCard.");
}
}
class CreditCardPayStrategy implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paid " + amount + " via CreditCard.");
}
}
// 3. 创建简单工厂类
class PaymentStrategyFactory {
private static final Map<String, PaymentStrategy> strategies = new HashMap<>();
static {
strategies.put("AliPay", new AliPayStrategy());
strategies.put("WeChatPay", new WeChatPayStrategy());
// 可以添加更多支付方式的策略对象
}
public static PaymentStrategy getPaymentStrategy(String type) {
return strategies.get(type);
}
}
// 4. 客户端使用
public class Main {
public static void main(String[] args) {
PaymentStrategy aliPayStrategy = PaymentStrategyFactory.getPaymentStrategy("AliPay");
aliPayStrategy.pay(100.0);
PaymentStrategy weChatPayStrategy = PaymentStrategyFactory.getPaymentStrategy("WeChatPay");
weChatPayStrategy.pay(50.0);
PaymentStrategy bankCardPayStrategy = PaymentStrategyFactory.getPaymentStrategy("BankCard");
bankCardPayStrategy.pay(80.0);
PaymentStrategy creditCardPayStrategy = PaymentStrategyFactory.getPaymentStrategy("CreditCard");
creditCardPayStrategy.pay(120.0);
}
}
6.3 观察者模式
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,使得当一个对象的状态发生变化时,其相关依赖对象都会得到通知并自动更新,如同发布-订阅模式。常见的情况如:公众号更新内容,所有的关注用户都能自动收到信息。
结构:
-
抽象主题(Subject):也称为被观察者,把所有观察者对象保存在一个集合里。主题可以提供接口去添加、删除和通知观察者的方法。
-
抽象观察者(Observer):抽象观察者是接收主题通知的对象。观察者需要实现一个更新方法,当收到主题的通知时,调用该方法进行更新操作。
-
具体主题(Concrete Subject):具体主题是主题的具体实现类。该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
-
具体观察者(Concrete Observer):具体观察者是抽象观察者的具体实现类。它实现了更新方法,定义了在收到主题通知时需要执行的具体操作。
观察者模式通过将主题和观察者解耦,实现了对象之间的松耦合。当主题的状态发生改变时,所有依赖于它的观察者都会收到通知并进行相应的更新。
图例:
代码案例:
import java.util.ArrayList;
import java.util.List;
// 主题接口(被观察者)
interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
// 具体主题(具体的被观察者)
class ConcreteSubject implements Subject {
private List<Observer> observers = new ArrayList<>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyObservers();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update();
}
}
}
// 观察者接口
interface Observer {
void update();
}
// 具体观察者
class ConcreteObserver implements Observer {
private ConcreteSubject subject;
public ConcreteObserver(ConcreteSubject subject) {
this.subject = subject;
this.subject.registerObserver(this);
}
@Override
public void update() {
System.out.println("State changed: " + subject.getState());
}
}
// 使用示例
public class Main {
public static void main(String[] args) {
ConcreteSubject subject = new ConcreteSubject();
ConcreteObserver observer1 = new ConcreteObserver(subject);
ConcreteObserver observer2 = new ConcreteObserver(subject);
// 改变主题状态
subject.setState(10);
subject.setState(20);
}
}
在这个代码中,Subject
是主题接口,定义了注册、移除和通知观察者的方法。ConcreteSubject
是具体的主题类,维护了观察者列表,并在状态改变时通知观察者。Observer
是观察者接口,定义了观察者需要实现的更新方法。ConcreteObserver
是具体的观察者类,实现了更新方法,并在构造函数中注册到主题对象中。在 Main
类中,创建了主题对象和两个观察者对象,然后改变主题的状态,观察者对象会收到通知并更新。
使用场景
-
对象间存在一对多关系,当一个对象的改变需要同时改变其他对象的时候。
-
当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。
优缺点
优点:
-
降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
-
被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
缺点:
-
如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
-
如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃