文章目录
- 设计模式分类
- 七大原则
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 接口隔离原则
- 依赖倒置原则
- 合成复用原则
- 迪米特法则
- 单例
- 懒汉式:
- 饿汉式:
- 线程安全版懒汉式
- 工厂模式
- 简单工厂模式:
- 工厂方法模式:
- 抽象工厂模式:
- 代理模式
- 静态代理:
- 动态代理:
- 虚拟代理:
- 装饰器模式
- 策略模式
- 观察者模式
- 责任链模式
本文主讲八种设计模式,分别是单例、工厂方法、抽象工厂、代理模式、装饰器模式、策略模式、观察者模式、责任链模式。
概念: 设计模式是一种解决软件设计中常见问题的经验总结,它是从实践中产生的并被广泛验证的软件开发最佳实践。设计模式提供了一种通用的解决方案,可以解决软件设计中常见的问题和困难,可以提高程序员的开发效率和代码的可重用性。设计模式是一种被抽象化和概括化的描述,它可以让一个系统或一个组件更加灵活、可扩展、易于维护和理解。设计模式不是一种能够直接使用的代码,而是一种思想和经验的体现。在软件开发中,设计模式是非常重要的,因为它可以帮助开发人员更加系统地理解和处理问题,提高软件的质量和可靠性,降低开发成本和风险。
设计模式分类
设计模式根据工作的目的,分为创建型模式、结构型模式和行为型模式三类。
创建型模式: 单例模式、工厂方法模式、抽象工厂模式、创建者模式、原型模式。
结构型模式: 适配器模式、代理模式、装饰器模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式: 策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
本文主讲八种设计模式,分别是单例、工厂方法、抽象工厂、代理模式、装饰器模式、策略模式、观察者模式、责任链模式。
七大原则
单一职责原则
单一职责原则指的是一个类或对象应该只有一个单一的功能,并且该功能被彻底封装在这个类的对象中。这样可以避免类或对象承担过多的职责,提高其内聚性和可维护性。
开闭原则
开闭原则指的是一个模块或者对象应该对扩展开放,对修改关闭。也就是说,在不修改原有代码的基础上通过扩展来实现新的功能,这样可以避免对原有代码产生影响,提高代码的可维护性和扩展性。
里氏替换原则
里氏替换原则指的是派生类(子类)对象应该能够替代其基类(父类)的对象,而程序仍能够正确地执行。也就是说,在设计时应该保证所有派生类兼容其基类,而不会破坏系统的一致性和稳定性。
接口隔离原则
接口隔离原则指的是一个类或对象应该只暴露其需要使用的接口,而不需要暴露所有的接口。这样可以避免不必要的依赖,提高代码的灵活性和可维护性。
依赖倒置原则
依赖倒置原则指的是高层模块不应该依赖于底层模块,它们都应该依赖于抽象接口。抽象接口应该定义在高层模块中,底层模块通过实现该接口来与高层模块进行交互。这样可以松耦合不同的模块,提高代码的可维护性和可扩展性。
合成复用原则
合成复用原则指的是在设计时应该优先使用对象组合而不是继承关系来实现代码的复用。这样可以避免继承关系的耦合性和局限性,提高代码的灵活性和可维护性。
迪米特法则
迪米特法则(最少知识原则)指的是一个对象应该有最小的依赖关系,只依赖于直接需要使用的类或对象。也就是说,如果一个对象要调用另一个对象的某个方法,应该尽量避免直接调用该方法,而是通过中间对象来调用。这样可以减少对象之间的耦合,提高代码的灵活性和可维护性。
单例
Java单例设计模式是一种创建对象的设计模式,确保在应用程序中只有一个实例。
优点:
确保一个类只有一个实例,避免了重复创建对象的开销。
提供全局访问点,方便在整个程序中访问该实例。
可以控制对象的数量和生命周期,确保对象的一致性和可靠性。
可以避免多线程冲突的问题,在多线程环境下保证对象的唯一性。
缺点:
单例模式可能导致紧耦合,使得程序的扩展性和可维护性变差。
单例模式不适合需要多个相互独立的实例的情况,违背了单一职责原则。
单例模式的实现需要考虑线程安全,增加了实现的复杂性。
单例设计模式主要分为两种,懒汉式(延迟加载) 与 饿汉式(启动加载)
懒汉式:
延迟加载,即只有在第一次使用时才会创建实例。 线程不安全,如果多个线程同时调用getInstance()方法,会出现多个实例的情况。
适合单线程或者在实例化时没有多线程环境的场景。
代码:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
饿汉式:
立即加载,即在类加载时就创建实例。 线程安全,由于实例是静态变量,所以在多线程环境下也只会创建一个实例。
适合多线程环境和要求唯一实例的场景。
代码:
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
线程安全版懒汉式
为了保证线程安全,可以通过以下几种方式实现懒汉式单例模式:
1.给getInstance方法加锁,使用synchronized保证同步。
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
2.使用双重锁定检查(double-checked locking),减少锁竞争的概率。
public class LazySingleton {
private volatile static LazySingleton instance;
private LazySingleton() {}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
3.使用静态内部类,可以保证线程安全以及延迟加载。
public class LazySingleton {
private LazySingleton() {}
private static class LazyHolder {
private static final LazySingleton INSTANCE = new LazySingleton();
}
public static LazySingleton getInstance() {
return LazyHolder.INSTANCE;
}
}
工厂模式
工厂方法模式(Factory Method Pattern)是一种创建类模式,它提供了一种创建对象的最佳方式。通常情况下,我们会使用new操作符来直接创建某个类的对象,但有时候我们希望在不暴露对象创建的细节的前提下实现对象的实例化。这个时候,工厂方法模式就可以派上用场了。
工厂方法模式定义了一个工厂接口,它负责定义工厂方法。具体的工厂类将实现这个接口,并根据不同的需求实现不同的工厂方法,用于创建不同的对象。
根据工厂方法的不同实现方式,我们可以将工厂方法分为三种:简单工厂模式、工厂方法模式、抽象工厂模式。
简单工厂模式:
简单工厂模式是一种比较基础的工厂方法实现方式。它通过一个具体的工厂类对多个不同的产品类进行实例化,并将实例化过程封装起来。
优点: 封装性好,降低客户端复杂度和维护成本,动态获取对象。
缺点: 扩展性差,添加新对象可能违反开闭原则,工厂类代码量过大。
为了方便理解,下面举个栗子
假设我们需要购买一双跑步鞋,我们可以到一家销售运动鞋的店面。这家店面就是一个工厂,它会提供不同的品牌和型号的跑步鞋,比如安踏、特步、鬼子宁等。我们只需要根据鞋子的品牌和型号告诉店员,店员就可以直接从工厂拿到相应的鞋子给我们。
实现:
public class TestSimpleFactoryPattern {
public static void main(String[] args) {
ShoeFactory factory = new ShoeFactory();
// 生产慢跑鞋
Shoe antaRunning = factory.createShoe("安踏", "慢跑鞋");
antaRunning.describe();
Shoe xtepRunning = factory.createShoe("特步", "慢跑鞋");
xtepRunning.describe();
// 生产篮球鞋
Shoe antaBasketball = factory.createShoe("安踏", "篮球鞋");
antaBasketball.describe();
Shoe xtepBasketball = factory.createShoe("特步", "篮球鞋");
xtepBasketball.describe();
}
}
// 定义鞋的工厂类
public class ShoeFactory {
public Shoe createShoe(String brand, String type) {
if (brand.equals("安踏")) {
if (type.equals("慢跑鞋")) {
return new AntaRunningShoe();
} else if (type.equals("篮球鞋")) {
return new AntaBasketballShoe();
}
} else if (brand.equals("特步")) {
if (type.equals("慢跑鞋")) {
return new XtepRunningShoe();
} else if (type.equals("篮球鞋")) {
return new XtepBasketballShoe();
}
}
return null;
}
}
// 鞋的接口
public interface Shoe {
void describe();
}
// 安踏跑步鞋
public class AntaRunningShoe implements Shoe {
public void describe() {
System.out.println("这是一双安踏跑步鞋。");
}
}
// 特步跑步鞋
public class XtepRunningShoe implements Shoe {
public void describe() {
System.out.println("这是一双特步跑步鞋。");
}
}
// 安踏篮球鞋
public class AntaBasketballShoe implements Shoe {
public void describe() {
System.out.println("这是一双安踏篮球鞋。");
}
}
// 特步篮球鞋
public class XtepBasketballShoe implements Shoe {
public void describe() {
System.out.println("这是一双特步篮球鞋。");
}
}
工厂方法模式:
工厂方法模式是指针对简单工厂模式的缺点进行的改进。定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂方法模式可以让一个类的实例化延迟到其子类中进行。这个模式就好比是一个工厂,它有多个制造产品的子工厂,每个子工厂可以制造多种类型的产品。
栗子:
还是需要购买一双跑步鞋。我们可以选择到安踏专卖店去购买。在安踏专卖店内,每一种型号的鞋子都对应一个工厂类,比如板鞋系列对应板鞋工厂类。我们告诉店员我们要买一双板鞋XXX鞋子,店员就会根据我们的需求,从板鞋工厂类中创建相应型号的板鞋XXX鞋子。
实现:
public class TestFactoryMethodPattern {
public static void main(String[] args) {
ShoeFactory antaRunningFactory = new AntaRunningShoeFactory();
Shoe antaRunningShoe = antaRunningFactory.createShoe();
antaRunningShoe.describe();
ShoeFactory xtepBasketballFactory = new XtepBasketballShoeFactory();
Shoe xtepBasketballShoe = xtepBasketballFactory.createShoe();
xtepBasketballShoe.describe();
}
}
// 鞋的工厂接口
public interface ShoeFactory {
Shoe createShoe();
}
// 安踏跑步鞋工厂
public class AntaRunningShoeFactory implements ShoeFactory {
public Shoe createShoe() {
return new AntaRunningShoe();
}
}
// 特步篮球鞋工厂
public class XtepBasketballShoeFactory implements ShoeFactory {
public Shoe createShoe() {
return new XtepBasketballShoe();
}
}
// 鞋的接口
public interface Shoe {
void describe();
}
// 安踏跑步鞋
public class AntaRunningShoe implements Shoe {
public void describe() {
System.out.println("这是一双安踏跑步鞋。");
}
}
// 特步篮球鞋
public class XtepBasketballShoe implements Shoe {
public void describe() {
System.out.println("这是一双特步篮球鞋。");
}
}
抽象工厂模式:
抽象工厂模式是一种对工厂方法模式进行进一步抽象和拓展的模式。在抽象工厂模式中,每个具体的工厂类不仅可以创建一种产品,而是可以创建一族产品(例如,某一个品牌的多种型号的手机)。抽象工厂模式保持了工厂方法模式的优点,同时通过提高代码抽象层级,实现了代码的可维护性和扩展性。
栗子:
继续需要购买鞋子,但这一次我们需要装备整套打篮球的鞋子。我们可以到一家提供篮球装备的运动鞋店购买。这家店提供一站式服务,包括高帮篮球鞋、宽扣篮球鞋、透气篮球鞋等多种类型的篮球鞋,还提供相应的配件,比如脚踝护具、前掌垫等。这家店是一个抽象工厂,每种类型的篮球鞋和对应的配件都属于一个产品族,而每个产品族都由一个具体的工厂类生产(比如高帮篮球鞋对应HighTopBasketballShoeFactory)。我们可以根据自己的需求选择一套合适的篮球装备。
实现:
public class TestAbstractFactoryPattern {
public static void main(String[] args) {
ShoeFactory highTopBasketballShoeFactory = new HighTopBasketballShoeFactory();
BasketballShoe highTopBasketballShoe = highTopBasketballShoeFactory.createBasketballShoe();
Accessories highTopBasketballAccessories = highTopBasketballShoeFactory.createAccessories();
highTopBasketballShoe.describe();
highTopBasketballAccessories.describe();
ShoeFactory lowTopBasketballShoeFactory = new LowTopBasketballShoeFactory();
BasketballShoe lowTopBasketballShoe = lowTopBasketballShoeFactory.createBasketballShoe();
Accessories lowTopBasketballAccessories = lowTopBasketballShoeFactory.createAccessories();
lowTopBasketballShoe.describe();
lowTopBasketballAccessories.describe();
}
}
// 鞋子工厂接口
public interface ShoeFactory {
BasketballShoe createBasketballShoe(); // 创建篮球鞋
Accessories createAccessories(); // 创建篮球配件
}
// 高帮篮球鞋工厂
public class HighTopBasketballShoeFactory implements ShoeFactory {
public BasketballShoe createBasketballShoe() {
return new HighTopBasketballShoe();
}
public Accessories createAccessories() {
return new HighTopBasketballAccessories();
}
}
// 低帮篮球鞋工厂
public class LowTopBasketballShoeFactory implements ShoeFactory {
public BasketballShoe createBasketballShoe() {
return new LowTopBasketballShoe();
}
public Accessories createAccessories() {
return new LowTopBasketballAccessories();
}
}
// 篮球鞋接口
public interface BasketballShoe {
void describe();
}
// 高帮篮球鞋
public class HighTopBasketballShoe implements BasketballShoe {
public void describe() {
System.out.println("这是一双高帮篮球鞋。");
}
}
// 低帮篮球鞋
public class LowTopBasketballShoe implements BasketballShoe {
public void describe() {
System.out.println("这是一双低帮篮球鞋。");
}
}
// 配件接口
public interface Accessories {
void describe();
}
// 高帮篮球鞋配件
public class HighTopBasketballAccessories implements Accessories {
public void describe() {
System.out.println("这是一套高帮篮球鞋配件。");
}
}
// 低帮篮球鞋配件
public class LowTopBasketballAccessories implements Accessories {
public void describe() {
System.out.println("这是一套低帮篮球鞋配件。");
}
}
代理模式
代理模式是一种结构型设计模式,它允许创建一个代理对象,代理对象可以控制对另一个对象的访问。代理对象起到一个中间层的作用,它拦截对实际对象的访问,并且可以在访问实际对象前后进行一些操作。
优点:
- 职责分离:代理对象可以处理与实际对象无关的事务,从而将业务逻辑分离出来,降低代码复杂度和耦合度。
- 访问控制:代理对象可以控制对实际对象的访问,从而保护实际对象的安全性。
- 延迟加载:代理对象可以延迟实际对象的加载,从而提高系统的性能和响应速度。
- 缓存管理:代理对象可以缓存实际对象,避免重复创建和销毁实际对象,从而降低系统开销。
缺点:
- 增加复杂度:在系统中引入了一个额外的代理层,增加了系统的复杂度和代码量。
- 性能损失:由于代理对象需要拦截访问,对系统的性能有一定的影响。
- 代码维护:增加了代理对象的维护工作,需要对代理对象和实际对象两个部分进行管理。
代理模式有三种常见实现方式,分别是静态代理、动态代理和虚拟代理。
静态代理:
也称为编译时代理,在编译时就确定代理对象和实际对象的关系。在静态代理中,代理对象和实际对象实现相同的接口,代理对象通过调用实际对象的方法来完成其操作。
优点: 实现简单,可以避免动态代理产生的运行时性能消耗。
缺点: 需要手动编写代理类,当实际对象的方法发生变化时,代理类也需要修改。
举例:
假设有一个计算器接口,有加、减、乘、除四个方法,实现加法和减法。我们可以创建一个代理类实现计算器接口,代理类中除了实现加减法之外还可以进行日志记录、参数验证等操作。使用静态代理时,我们需要手动编写代理类。
实现:
// 计算器接口
interface Calculator {
int add(int a, int b);
int sub(int a, int b);
}
// 计算器代理类
class CalculatorProxy implements Calculator {
private final Calculator calculator;
public CalculatorProxy(Calculator calculator) {
this.calculator = calculator;
}
@Override
public int add(int a, int b) {
// 记录计算日志
System.out.println("计算器正在进行加法运算...");
// 调用实际对象的方法
int result = calculator.add(a, b);
// 记录计算结果
System.out.printf("计算结果:%d\n", result);
return result;
}
@Override
public int sub(int a, int b) {
// 记录计算日志
System.out.println("计算器正在进行减法运算...");
// 调用实际对象的方法
int result = calculator.sub(a, b);
// 记录计算结果
System.out.printf("计算结果:%d\n", result);
return result;
}
}
// 实现计算器接口的类
class SimpleCalculator implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
}
public class StaticProxyDemo {
public static void main(String[] args) {
// 创建实际对象
Calculator calculator = new SimpleCalculator();
// 创建代理对象
Calculator proxy = new CalculatorProxy(calculator);
// 使用代理对象进行计算
proxy.add(1, 2); // 输出:计算器正在进行加法运算...,计算结果:3
proxy.sub(5, 3); // 输出:计算器正在进行减法运算...,计算结果:2
}
}
动态代理:
也称为运行时代理,在运行时根据需要动态生成代理对象。在动态代理中,代理对象不需要实现与实际对象相同的接口,而是通过Java中的反射机制在运行时生成代理对象,动态代理常常用于AOP(面向切面编程)。
优点: 无需手动编写代理类,可以根据运行时要求灵活地生成代理对象。
缺点: 生成代理对象的性能消耗较高,在运行时增加了系统的负担。
举例:
如果我们需要实现更多的操作,例如计算时间、返回结果加密等,手写代理类的工作量将变得很大。这时我们可以使用动态代理,使用Java的Proxy类和InvocationHandler接口,在运行时动态生成代理类。这样可以避免手动编写代理类的麻烦,也避免了编写多个代理类的代码笨重。
实现:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 计算器接口
interface Calculator {
int add(int a, int b);
int sub(int a, int b);
}
// 实现计算器接口的类
class SimpleCalculator implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
public int sub(int a, int b) {
return a - b;
}
}
public class DynamicProxyDemo {
public static void main(String[] args) {
// 创建实际对象
Calculator calculator = new SimpleCalculator();
// 创建动态代理对象
Calculator proxy = (Calculator) Proxy.newProxyInstance(
calculator.getClass().getClassLoader(), // 指定类加载器
new Class[]{Calculator.class}, // 指定接口
new CalculatorInvocationHandler(calculator)); // 指定处理器
// 使用代理对象进行计算
proxy.add(1, 2); // 输出:计算时间:0ms
proxy.sub(5, 3); // 输出:计算时间:0ms
}
}
// 计算器代理处理器
class CalculatorInvocationHandler implements InvocationHandler {
private final Calculator calculator;
public CalculatorInvocationHandler(Calculator calculator) {
this.calculator = calculator;
}
// 在调用代理对象的方法时会自动执行invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 记录计算时间
long start = System.currentTimeMillis();
Object result = method.invoke(calculator, args); // 调用实际对象的方法
long end = System.currentTimeMillis();
System.out.printf("计算时间:%dms\n", (end - start));
return result;
}
}
虚拟代理:
在需要时才创建实际对象。虚拟代理常用于管理大型、复杂对象的创建,如图片、音频、视频等,避免在加载时就创建全部对象,导致系统崩溃或性能低下。虚拟代理通过代理对象先加载占用较少资源的对象,待需要时再进行实际对象的加载,从而提升了系统的性能和响应速度。
优点: 可以优化系统的性能和资源占用,将实际对象的创建延迟到需要时再进行创建。
缺点: 需要额外的代理对象实现,会增加代码的复杂程度,并且代理对象需要实现与实际对象相同的接口,对于实现复杂的对象可能需要大量的开发工作。
举例:
假设我们需要加载一张非常大的图片,图片大小超过了我们内存的限制,直接加载会导致内存溢出。这时我们可以使用虚拟代理,将图片的加载推迟到需要时再进行。虚拟代理先加载小图片或者只加载部分图片,当用户需要查看大图时再加载完整的图片。这样既避免了内存溢出,也缩短了页面的加载时间,提高了用户体验。
实现:
import javax.swing.*;
import java.awt.*;
// 图片接口
interface Image {
void show();
}
// 实现图片接口的类
class BigImage implements Image {
private final String fileName;
public BigImage(String fileName) {
this.fileName = fileName;
// 加载大图片需要很长时间
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void show() {
// 显示大图片
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
ImageIcon icon = new ImageIcon(fileName);
JLabel label = new JLabel(icon);
frame.add(label, BorderLayout.CENTER);
frame.setVisible(true);
}
}
// 图片代理类
class ImageProxy implements Image {
private final String fileName;
private Image image;
public ImageProxy(String fileName) {
this.fileName = fileName;
}
@Override
public void show() {
if (image == null) {
// 只有当需要显示图片时才进行加载
image = new BigImage(fileName);
}
// 显示图片
image.show();
}
}
public class VirtualProxyDemo {
public static void main(String[] args) {
// 创建代理对象
Image image = new ImageProxy("big_image.jpg");
// 第一次显示图片,需要加载
image.show();
// 第二次显示图片,不需要加载
image.show();
}
}
装饰器模式
装饰器模式(Decorator Pattern)是一种结构型设计模式,它允许为一个对象动态地添加新的行为。该模式的意图是通过将对象包装在一层装饰器对象中来给对象添加新的行为和责任,而不需要对原有的类进行修改。
在装饰器模式中,有四个核心角色:
抽象组件(Component): 定义了被装饰的对象的接口,可以是一个抽象类或者接口,它声明了一些基本操作,具体的实现由具体组件类提供。
具体组件(ConcreteComponent): 实现了抽象组件的接口,即被包装的原始对象,它提供了基本操作的具体实现。
抽象装饰器(Decorator): 包含一个指向抽象组件对象的引用,它是所有装饰器的基类,它可以是一个抽象类或者接口。
具体装饰器(ConcreteDecorator): 实现抽象装饰器的接口,即具体的装饰器对象,通常会将抽象装饰器作为成员变量,并在自己的操作中调用抽象装饰器的方法,然后再进行自己的操作。
优点:
可以动态地给一个对象添加更多的责任,而不需要修改原有的类。
可以将多个装饰器对象进行嵌套,从而实现更复杂的功能。
增加装饰器对象和具体组件对象的独立性,客户端可以根据需要*地选择所需要的装饰器对象,而不需要关心具体组件对象的变化。
可以避免装饰器类和被装饰的类之间出现继承上的耦合关系,从而更好地实现了松耦合。
缺点:
对于装饰器对象和具体组件对象的接口有些限制,必须是同一个类型或接口。
装饰器模式增加了许多子类,如果过度使用会增加系统的复杂度。
举例:
假设有一家汽车制造商,他们生产出一种基础型号的汽车,它有基本的功能和特性,比如引擎、轮胎、座椅等等。然而,不同的客户都有不同的需求和喜好,他们想要为自己的汽车添加一些额外的功能或特性,比如更高的速度、更舒适的座椅、更好的音响、更个性化的外观等等。汽车制造商可以通过汽车定制服务来满足客户的这些需求。
如果客户想要一辆更快的车,汽车制造商可以为基础型号的汽车添加一个名为“SportsCar”的装饰器,这个装饰器可以为汽车添加更强大的引擎、更快的速度和更好的悬挂系统。如果客户想要一辆更豪华的车,我们可以为基础型号的汽车添加一个名为“LuxuryCar”的装饰器,这个装饰器可以为汽车添加更舒适的座椅、更好的音响和更高级的内饰。
因此,汽车定制服务使用装饰器模式可以帮助汽车制造商满足不同客户的需求,同时还能保持代码的简洁和易于维护。
代码实现:
// 汽车接口,定义了汽车的基本功能
public interface Car {
void assemble();
}
// 基础型号的汽车实现类
public class BasicCar implements Car {
@Override
public void assemble() {
System.out.println("Assembling basic car parts.");
}
}
// 汽车的装饰器
public abstract class CarDecorator implements Car {
// 持有原始汽车的引用
protected Car car;
// 通过构造器注入原始汽车的引用
public CarDecorator(Car car) {
this.car = car;
}
// 实现汽车接口的 assemble() 方法,调用原始汽车的 assemble() 方法
@Override
public void assemble() {
car.assemble();
}
}
// 为汽车添加运动功能的装饰器
public class SportsCar extends CarDecorator {
public SportsCar(Car car) {
super(car);
}
// 重写 assemble() 方法,为汽车添加运动功能
@Override
public void assemble() {
super.assemble();
System.out.println("Adding features of Sports Car.");
}
}
// 为汽车添加豪华功能的装饰器
public class LuxuryCar extends CarDecorator {
public LuxuryCar(Car car) {
super(car);
}
// 重写 assemble() 方法,为汽车添加豪华功能
@Override
public void assemble() {
super.assemble();
System.out.println("Adding features of Luxury Car.");
}
}
// 为汽车添加 SUV 功能的装饰器
public class SUV extends CarDecorator {
public SUV(Car car) {
super(car);
}
// 重写 assemble() 方法,为汽车添加 SUV 功能
@Override
public void assemble() {
super.assemble();
System.out.println("Adding features of SUV.");
}
}
// 客户端代码,使用已经装饰好的汽车
public class Client {
public static void main(String[] args) {
// 创建基础型号的汽车
Car car = new BasicCar();
// 为汽车添加运动功能
car = new SportsCar(car);
// 为汽车添加豪华功能
car = new LuxuryCar(car);
// 为汽车添加 SUV 功能
car = new SUV(car);
// 调用汽车的 assemble() 方法,输出装饰后的汽车特性和功能
car.assemble();
}
}
在这段代码中,我们定义了一个 Car 接口和一个 BasicCar 类,他们分别代表汽车的接口和基础型号的汽车。我们还定义了 CarDecorator 抽象类,以及具体的 SportsCar、LuxuryCar 和 SUV 装饰器类,它们继承了 CarDecorator 类,并重写了 assemble() 方法来添加特定的汽车功能。
在 Client 类中,我们使用基础型号的汽车分别添加了运动、豪华和 SUV 的功能,并调用了装饰后的汽车的 assemble() 方法来输出装饰后的汽车特性和功能。
策略模式
在策略模式中,我们定义了一系列的策略,每个策略都代表着一种算法。这些策略可以在运行时动态替换,从而允许我们在不同的情境下选择不同的算法来完成相同的任务。这种可替换性的特性使得策略模式非常适用于那些需要在多种算法中进行选择的情况。
策略模式通常由三部分构成:
客户端(Context): 客户端定义了算法策略的接口,并在运行时设置具体的策略实现对象。客户端通过策略接口与具体策略实现对象交互,调用相应的算法。
策略接口(Strategy): 策略接口定义了一系列算法策略的公共接口。它是所有具体策略类的基类。
具体策略(ConcreteStrategy): 具体策略类实现了策略接口,定义了一种具体的算法实现。客户端可以选择不同的具体策略类来完成相同的任务。
策略模式的优点在于,它提供了一种高度灵活的方式来处理不同的算法实现。由于策略接口定义了算法的公共接口,所以可以很容易地在不同的策略之间进行切换。这种可替换性的特性使得策略模式非常适用于那些需要动态改变算法的场景。此外,策略模式的实现通常使用组合而非继承实现,这样可以避免继承的种种问题,包括类层次结构的臃肿和复杂性。
举例:
假设你是一家酒店的经理,你想要提供给客人多种早餐选择。有些客人可能想要有营养的早餐,而有些客人则可能想要丰盛的早餐。为了满足不同客人的需求,你可以使用策略模式。具体的实现步骤如下:
首先,你需要定义一个早餐接口,该接口包含一个 prepare() 方法,用于准备早餐。
接着,你需要定义一些具体的早餐类,实现早餐接口并重写 prepare() 方法。例如,你可以实现一个豪华早餐类,其中包含肉类、鸡蛋、牛奶等丰富的食物。又或者你可以实现一个健康早餐类,其中包含水果、麦片、酸奶等营养丰富的食物。
最后,你需要在客户端代码(例如酒店前台)中创建一个早餐对象,并在运行时选择具体的早餐实现。例如,如果客人要求豪华早餐,你可以创建一个豪华早餐对象并设置为客人的早餐选择。
通过使用策略模式,你可以灵活地满足不同客人的需求,允许客人在多种早餐选择中进行选择,并在需要时轻松更改早餐实现。
代码实现:
@FunctionalInterface
public interface Breakfast {
void prepare();
}
public class BreakfastClient {
public static void main(String[] args) {
// 客人要求豪华早餐
Breakfast deluxeBreakfast = () -> System.out.println("豪华早餐:煎饼、面条、炒饭、牛奶、鸡蛋、培根等丰富的食物。");
deluxeBreakfast.prepare();
// 客人要求健康早餐
Breakfast healthyBreakfast = () -> System.out.println("健康早餐:水果沙拉、麦片、酸奶、全麦面包等营养丰富的食物。");
healthyBreakfast.prepare();
}
}
观察者模式
观察者模式的核心是,通过定义一个被观察者(Subject)对象,该对象负责维护一个确定数量的依赖于该对象的观察者(Observer),依赖于该被观察者对象的状态的变化,将会通知所有的观察者对象进行相应的更新。
该模式由以下角色组成:
被观察者(Subject): 也称为主题,它是一个抽象类或接口,具有添加、删除和通知观察者的方法。
具体被观察者(Concrete Subject): 该对象继承或实现了被观察者接口,它具有一个状态,当该状态发生改变时,会通知所有的观察者对象。
观察者(Observer): 定义一个更新接口,该接口可以被具体观察者实现。该接口定义了当被观察者状态发生变化时所要执行的操作。
具体观察者(Concrete Observer): 实现更新接口,以便在状态发生变化时更新自己的状态。
观察者模式的优点是减少了对象间的耦合,使得一个对象的状态变化可以让其他对象感知到并进行相应的操作。同时,被观察者对象不需要知道观察者对象的具体实现,可以方便地添加或删除观察者对象。缺点是当观察者对象较多时,被观察者对象进行通知的时间可能会变长,同时,观察者对象也应该避免相互之间的循环依赖。
来看一个通俗易懂的例子:报纸订阅。
报纸订阅的过程包含一个发布者(被观察者)和订阅者(观察者)。当新的一期报纸出版时,发布者会将消息发送给所有订阅该报纸的用户,因此,这里的观察者们(订阅者)可以在发布者发生变化时,自动获取信息。
具体实现如下:
首先,创建一个报纸发行者(被观察者)类 Publisher:
import java.util.ArrayList;
import java.util.List;
public class Publisher {
private List<Subscriber> subscribers = new ArrayList<>();
public void subscribe(Subscriber subscriber) {
subscribers.add(subscriber);
}
public void unsubscribe(Subscriber subscriber) {
subscribers.remove(subscriber);
}
public void notifySubscribers(String message) {
subscribers.forEach(subscriber -> subscriber.onUpdate(message));
}
}
在这个类中,我们实现了 subscribe 、unsubscribe 和 notifySubscribers 3个方法,分别用于增加/删除订阅者以及向订阅者发布新信息。
关于每个具体的订阅者(观察者) Subscriber 如下:
public interface Subscriber {
void onUpdate(String message);
}
public class NewspaperSubscriber implements Subscriber {
private String name;
public NewspaperSubscriber(String name) {
this.name = name;
}
public void subscribe(Publisher publisher) {
publisher.subscribe(this);
}
public void unsubscribe(Publisher publisher) {
publisher.unsubscribe(this);
}
@Override
public void onUpdate(String message) {
System.out.println(name + " receives message: " + message);
}
@Override
public String toString() {
return name;
}
}
每个订阅者拥有 subscribe 和 unsubscribe方法,并通过它们将自己注册到或移除 Publisher 中。
最后,我们可以在客户端代码中,创建被观察者对象和观察者对象,并建立它们之间的关系:
public class Client {
public static void main(String[] args) {
Publisher publisher = new Publisher();
NewspaperSubscriber subscriber1 = new NewspaperSubscriber("张三");
NewspaperSubscriber subscriber2 = new NewspaperSubscriber("李四");
NewspaperSubscriber subscriber3 = new NewspaperSubscriber("王五");
subscriber1.subscribe(publisher);
subscriber2.subscribe(publisher);
subscriber3.subscribe(publisher);
publisher.notifySubscribers("摆脱疫情影响,经济全速前行!");
// 打出以下信息:
// 张三 receives message: 摆脱疫情影响,经济全速前行!
// 李四 receives message: 摆脱疫情影响,经济全速前行!
// 王五 receives message: 摆脱疫情影响,经济全速前行!
subscriber2.unsubscribe(publisher);
publisher.notifySubscribers("机场扩建工程已经开始动工,于明年底完成");
// 打出以下信息:
// 张三 receives message: 机场扩建工程已经开始动工,于明年底完成
// 王五 receives message: 机场扩建工程已经开始动工,于明年底完成
}
}
在这个例子中,我们建立了三个订阅者,并将它们注册到 Publisher 中。当 Publisher 发送通知时,所有订阅的订阅者都会接收到信息。我们还可以通过移除某个订阅者来停止通知。
责任链模式
责任链模式是一种行为型设计模式,它通过创建一条具有顺序关系的处理对象链,将多个处理对象串联起来,并在这条链上传递请求,直到有一个处理对象可以处理请求为止。
在责任链模式中,请求发送者并不知道请求将会由哪个对象处理,每个处理对象都有自己的处理逻辑,如果当前对象能够处理请求,则直接处理;如果不能处理,则将请求传递给下一个处理对象。通过这种方式,责任链模式将请求发送者和接收者进行了解耦,从而实现了系统的灵活性和可维护性。
责任链模式的角色通常包括以下几种:
抽象处理对象(Handler): 定义了处理请求的接口,并维护一个对下一个处理对象的引用。
具体处理对象(Concrete Handler): 实现了抽象处理对象的接口,具体处理请求的逻辑。如果自己无法处理请求,则将请求转发给下一个处理对象。
客户端(Client): 创建并连接具体处理对象。
责任链模式主要解决了多个处理者共同处理同一个请求的场景。通常应用于需要进行请求多阶段处理的场合,例如工作流流程、权限校验等场景。
使用责任链模式时需要注意,处理请求的链需要被正确地组织起来,每个处理对象应该知道该请求是否由自己处理,否则将可能导致请求无法得到处理或者被重复处理。同时需要注意避免链太长,否则可能会降低系统的性能。
总体来说,责任链模式是一种非常实用的设计模式,可以帮助我们构建高内聚、低耦合的系统,从而提高系统的可维护性和灵活性。
举例:
请假审批。在这个例子中,有多个级别的审批人员,每个人员都可以进行审批,但是只有在当前审批人无法处理请求的情况下,才会将请求转发给下一个级别的审批人员。以下是相应的
/**
* 请假审批责任链模式
*/
// 定义抽象审批人员类
abstract class Approver {
protected Approver successor; // 下一个审批人员
public void setSuccessor(Approver successor) {
this.successor = successor;
}
public abstract void processRequest(LeaveRequest request);
}
// 定义具体审批人员类:员工、经理、总经理
class Employee extends Approver {
@Override
public void processRequest(LeaveRequest request) {
if (request.getDays() <= 5) {
System.out.println("员工可以审批该申请");
} else {
this.successor.processRequest(request);
}
}
}
class Manager extends Approver {
@Override
public void processRequest(LeaveRequest request) {
if (request.getDays() <= 10) {
System.out.println("经理可以审批该申请");
} else {
this.successor.processRequest(request);
}
}
}
class GeneralManager extends Approver {
@Override
public void processRequest(LeaveRequest request) {
if (request.getDays() <= 15) {
System.out.println("总经理可以审批该申请");
} else {
System.out.println("请假时间太长,无人能够处理该申请");
}
}
}
// 定义请假申请类
class LeaveRequest {
private String name;
private int days;
public LeaveRequest(String name, int days) {
this.name = name;
this.days = days;
}
public String getName() {
return name;
}
public int getDays() {
return days;
}
}
public class Main {
public static void main(String[] args) {
// 构造请假请求对象
LeaveRequest request = new LeaveRequest("小明", 7);
// 创建审批人员对象
Approver emp = new Employee();
Approver mgr = new Manager();
Approver gm = new GeneralManager();
// 设置责任链关系
emp.setSuccessor(mgr);
mgr.setSuccessor(gm);
// 将请假请求传递给处理链的第一个对象
emp.processRequest(request);
}
}
在这个例子中,定义了抽象的 Approver 类表示审批人员,在具体实现中分别定义了 Employee、Manager、GeneralManager 类表示员工、经理、总经理等不同级别的审批人员。这些审批人员都继承自 Approver 抽象类,并重写了 processRequest() 方法。在 processRequest() 方法中,如果当前审批人员可以处理该请求,就直接处理;否则将请求转发给下一个级别的审批人员。
在 Main 类中,首先构造请假请求对象 LeaveRequest,然后依次构造具体审批人员对象,最后将请求传递给处理链的第一个对象 Emp 进行处理。在代码运行时,会依次在控制台输出每个审批人员的处理结果。
============================================
以上就是常用的八种设计模式了,总之,设计模式是面向对象编程的基础,通过学习和应用设计模式,我们可以更好地掌握面向对象思想和技巧,更加精通软件开发。虽然设计模式并非一蹴而就,需要花费大量的精力和时间去学习和理解,但是这份努力将会为我们未来的编程生涯带来丰硕的成果。
如果你还想了解更多有关设计模式和面向对象编程方面的内容,推荐一些经典的参考书籍:
《设计模式:可复用面向对象软件的基础》(Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides)
《Head First 设计模式》(Eric Freeman, Elisabeth Robson, Bert Bates, Kathy Sierra)
《重构:改善既有代码的设计》(Martin Fowler)
感谢您的耐心阅读,如果您有任何问题或建议,请随时留言联系我。我会尽快回复您并进一步交流,谢谢!