✏️

软件设计

单例设计模式 Singleton Pattern

1. GoF

The Gang of Four (GoF) defines the Singleton Pattern as:
"Ensure a class only has one instance, and provide a global point of access to it."

2. 结构

  • 单例类,只能创建一个实例的类
  • 访问类,使用单例类

3. 实现

单例设计模式分类: 饿汉式:类加载会导致该单实例对象被创建 懒汉式:类加载不会导致,首次使用该对象时才创建
  1. 饿汉式-静态成员变量:
/**
 * @Descrption:
 *      饿汉式: 静态成员变量
 */
public class Singleton {
    //1. 私有构造方法
    private Singleton() {

    }

    //2. 在本类中创建该类的对象
    private static Singleton instance = new Singleton();

    //3. 提供一个公共的访问方式, 让外界获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}
  1. 饿汉式-静态代码块:
/**
 * @Descrption:
 *      饿汉式: 静态代码块
 */
public class Singleton {
    //私有构造方法
    private Singleton() {

    }

    //声明Singleton类型的变量
    private static Singleton instance;

    //在静态代码块中进行赋值
    static {
        instance = new Singleton();
    }

    //对外提供获取该类对象的方法
    public static Singleton getInstance() {
        return instance;
    }
}
说明:饿汉式浪费内存
  1. 懒汉式-线程不安全:
/**
 * @Description:
 * 懒汉式 线程不安全
 */
public class Singleton {
    // 私有构造方法
    private Singleton() {}

    //声明Singleton类型的变量
    private static Singleton instance;//只声明,未创建

    //对外提供访问方式
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 懒汉式-线程安全:
/**
 * @Description:
 * 懒汉式 线程不安全
 */
public class Singleton {
    // 私有构造方法
    private Singleton() {}

    //声明Singleton类型的变量
    private static Singleton instance;//只声明,未创建

    //对外提供访问方式
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 懒汉式-双重检查锁:
/**
 * @Description:
 * 懒汉式 双重检查锁
 */
public class Singleton {
    // 私有构造方法
    private Singleton() {}

    //声明Singleton类型的变量
    private static volatile Singleton instance;//只声明,未创建

    //对外提供访问方式
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
 
  1. 懒汉式-静态内部类方式:
/**
 * @Description:
 * 懒汉式 静态内部类
 */
public class Singleton {
    // 私有构造方法
    private Singleton() {}

    //声明Singleton类型的变量
    private static class SingletonHolder {
        //在内部类中声明并初始化外部变量
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供访问方式
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
  1. 枚举方式
/**
 * @Description:
 * 枚举方式
 */
public enum Singleton {
	INSTANCE;
}

4. 一些问题

Why are you making the class final? You have a private constructor that could prevent the inheritance. Is this correct?
  • Preventing Inheritance:
    • Declaring a class as final prevents it from being subclassed. In the context of the Singleton pattern, this is important because inheriting from a Singleton class could potentially introduce multiple instances if a subclass could be instantiated.
    • Even though a private constructor prevents direct instantiation from outside the class, a subclass could still be created (though it wouldn't be able to directly instantiate the Singleton class). By making the class final, you completely eliminate the possibility of subclassing, ensuring that the Singleton class cannot be extended.
  • Design Intent:
    • Making the class final clearly communicates that this class is not intended to be extended. This can make the code easier to understand and maintain because it enforces the design constraint of the Singleton pattern.
  • Security:
    • Preventing subclassing adds a layer of security to the Singleton pattern by ensuring that there is no risk of altering the Singleton behavior through subclassing. This ensures that the integrity of the Singleton pattern is maintained.

5. 破坏方法

  1. 序列化与反序列化
  1. 反射
 

原型设计模式 Prototype

1. GoF

Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

2. 结构

原型模式包含如下角色:
  • 抽象原型类:规定了具体原型对象必须实现的clone()方法
  • 具体原型类:实现抽象原型类的clone()方法,他是可被复制的对象
  • 访问类:使用具体原型类中的clone()方法来复制新的对象
 
notion image

3. 实现

原型模式的克隆分为浅克隆和深克隆:
浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型,仍指向原有属性所指向对象的内存地址。 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不在指向原有对象地址。
Java中的Object类中提供了clone()方法来实现浅克隆。Cloneable接口是上面的类图中的抽象原型类,接口的子实现类就是具体原型类。
  1. 浅克隆
Realizetype(具体原型类):
public class Realizetype implements Cloneable{

    public Realizetype() {
        System.out.println("具体原型类创建成功");
    }

    @Override
    public Realizetype clone() throws CloneNotSupportedException {
        System.out.println("具体原型类克隆成功");
        return (Realizetype) super.clone();
    }
}
PrototypeTest(访问类):
public class PrototypeTest {
    public static void main(String[] args) throws CloneNotSupportedException {
        Realizetype r1 = new Realizetype();
        Realizetype r2 = r1.clone();

        System.out.println("对象1和对象2是否是同一个对象?" + (r1 == r2)); //false
    }
}
  1. 深克隆
使用对象流实现。

4. 一些问题

What are the challenges associated with using prototype design patterns?
The Prototype design pattern has several challenges:
  1. Shallow vs. Deep Copy: Shallow copies may lead to shared state issues, while deep copying is complex, especially with nested objects.
  1. Inheritance Complexity: Cloning objects with complex inheritance hierarchies is tricky.
  1. Security Risks: Sensitive data might be exposed during cloning.
  1. Concurrency Issues: In multithreaded environments, cloning can lead to race conditions and inconsistent states.
When I copy an object in Java, I need to use the clone() method. Is this understanding correct?
While the clone() method can be used to copy objects in Java, it is typically for shallow copying, and its use comes with caveats that may require additional customization or alternatives for deep copying.
 

建造者模式 Builder

1. GoF

Separate the construction of a complex object from its representation so that the same construction processes can create different representations
将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。
 
  • 分离了的部件的构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
  • 由于实现了构建和装配的结耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
  • 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要制定复杂对象的类型就可以得到该对象,而无需知道其内部的具体构造细节。

2. 结构

建造者(Builder)模式包含如下角色:
  • 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的对象部件的创建。
  • 具体建造者类(Concrete Builder):实现Builder接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
  • 产品类(Product):要创建的复杂对象。
  • 指挥者类(Director):调用具体的建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
 
notion image

3. 实现

notion image
public class Bike {
    private String frame;
    private String seat;

    public void setFrame(String frame) {
        this.frame = frame;
    }

    public void setSeat(String seat) {
        this.seat = seat;
    }

    public String getFrame() {
        return frame;
    }

    public String getSeat() {
        return seat;
    }
}
public abstract class Builder {
    //声明Bike类型的变量 并进行赋值
    protected Bike bike = new Bike();

    public abstract void buildFrame();

    public abstract void buildSeat();

    public abstract Bike createBike();
}

public class MobileBuilder extends Builder{
    @Override
    public void buildFrame() {
        bike.setFrame("Mobile Frame");
    }

    @Override
    public void buildSeat() {
        bike.setSeat("Mobile seat");
    }

    @Override
    public Bike createBike() {
        return bike;
    }
}

public class OfoBuilder extends Builder{
    @Override
    public void buildFrame() {
        bike.setFrame("Ofo frame");
    }

    @Override
    public void buildSeat() {
        bike.setSeat("Ofo seat");
    }

    @Override
    public Bike createBike() {
        return bike;
    }
}
public class Director {
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    public Bike construct() {
        builder.buildFrame();
        builder.buildSeat();
        return builder.createBike();
    }
}
public class Client {
    public static void main(String[] args) {
        Director director = new Director(new MobileBuilder());
        Bike bike = director.construct();

        System.out.println(bike.getFrame()); //Mobile Frame
        System.out.println(bike.getSeat());  //Mobile seat
    }
}

4. 一些问题

优点:
  • 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
  • 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
  • 造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
How do I decide whether I should use an abstract class or an interface in an application?
  • 使用接口的情况:
  1. 契约导向:如果只需定义建造者应遵循的规范或蓝图,使用接口。
  1. 灵活性高:接口允许多重继承,适合需要继承其他类的情况。
  1. 没有默认实现:如果各个建造者的实现完全不同,使用接口更简洁。
  • 使用抽象类的情况:
  1. 共享功能:如果不同建造者有共享逻辑,抽象类可以避免代码重复。
  1. 部分实现:可以定义默认行为,子类可以继承或覆盖。
  1. 状态或字段:如果建造者需要维护内部状态,抽象类可以共享字段。

工厂方法模式 Factory Method Pattern

1. GoF

Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory method lets a class defer instantiation to subclasses.

2. 结构

工厂方法模式的主要角色:
  • 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。

3. 实现

使用工厂方法模式对上例进行改进,类图如下:
notion image
代码如下:
抽象工厂:
public interface CoffeeFactory {

    Coffee createCoffee();
}
具体工厂:
public class LatteCoffeeFactory implements CoffeeFactory {

    public Coffee createCoffee() {
        return new LatteCoffee();
    }
}

public class AmericanCoffeeFactory implements CoffeeFactory {

    public Coffee createCoffee() {
        return new AmericanCoffee();
    }
}
咖啡店类:
public class CoffeeStore {

    private CoffeeFactory factory;

    public CoffeeStore(CoffeeFactory factory) {
        this.factory = factory;
    }

    public Coffee orderCoffee(String type) {
        Coffee coffee = factory.createCoffee();
        coffee.addMilk();
        coffee.addsugar();
        return coffee;
    }
}
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。

4. 优缺点

优点:
  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;
缺点:
  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

5. 一些问题

  1. Why have you separated the CreateAnimal() method from client code?
在工厂方法模式中,将 CreateAnimal() 从客户端代码中分离可以降低耦合,客户端无需直接依赖具体类,增强了扩展性和可维护性。同时,职责分离让对象创建与业务逻辑独立,便于后续扩展新类型并遵循开闭原则。这样设计还提升了代码的灵活性和可测试性。
  1. I should always mark the factory method with an abstract keyword so that subclasses can complete them. Is this correct?
不一定必须将工厂方法标记为抽象,是否抽象取决于你是否需要强制子类提供自己的实现。如果你希望提供一个默认的工厂方法实现,则可以选择不使用 abstract

抽象工厂模式 Abstract Factory Pattern

1. GoF

Provide an interface for creating families of related or dependent objects without specifying their concrete classes.

2. 结构

工厂方法模式的主要角色:
  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。抽象工厂有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一关系。

3. 实现

notion image
代码如下:
抽象工厂:
public interface DessertFactory {
    Coffee createCoffee();

    Dessert createDessert();
}
具体工厂:
public class AmericanDessertFactory implements DessertFactory{
    @Override
    public Coffee createCoffee() {
        return new AmericanCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new MatchaMousse();
    }
}

public class ItalyDessertFactory implements DessertFactory{
    @Override
    public Coffee createCoffee() {
        return new LatteCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new Tiramisu();
    }
}

4. 一些问题

What are the challenges of using an abstract factory like this?
Challenges of using an abstract factory like this include increased code complexity, difficulty in extending or adding new product families, potential over-engineering for simple problems, and a steeper learning curve for developers unfamiliar with the pattern.
How can you distinguish a simple factory pattern from a factory method pattern or an abstract factory pattern?
区别总结
  • 工厂数量和层次
    • 简单工厂:只有一个具体工厂类。
    • 工厂方法:有一个抽象工厂接口和多个具体工厂类。
    • 抽象工厂:有多个抽象产品接口和多个具体工厂类,工厂类可以生产多个相关的产品。
  • 产品数量和复杂度
    • 简单工厂:产品种类少,结构简单。
    • 工厂方法:产品种类较多,但每个工厂只生产一种产品。
    • 抽象工厂:产品族和产品等级结构复杂,工厂生产一系列相关产品。
  • 扩展性
    • 简单工厂:增加新产品需要修改工厂代码,违反开闭原则。
    • 工厂方法:增加新产品只需添加新的具体工厂和产品类,符合开闭原则。
    • 抽象工厂:增加新的产品族需要修改工厂接口,增加新产品等级则需要修改所有工厂类,扩展性较差。
辨别方法
  • 如果只有一个工厂类,且通过参数决定创建哪种产品,是简单工厂模式
  • 如果有多个工厂类,每个工厂类创建一种产品,且工厂类可以被继承或实现,是工厂方法模式
  • 如果工厂类不仅创建产品,还创建一组相关或依赖的产品,是抽象工厂模式

代理模式 Proxy Pattern

1. GoF

Provide a surrogate or placeholder for another object to control access to it.
由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有JDK代理和CGLib代理两种。

2. 结构

代理(Proxy)模式分为三种角色:
  • 抽象主题(Subject)类: 通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类: 实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类 : 提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

3. 静态代理

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:
notion image
代码如下:
//卖票接口
//抽象主题类
public interface SellTickets {
    void sell();
}

//火车站  火车站具有卖票功能,所以需要实现SellTickets接口
//具体主题类
public class TrainStation implements SellTickets {

    public void sell() {
        System.out.println("火车站卖票");
    }
}

//代售点
//代理类
public class ProxyPoint implements SellTickets {

    private TrainStation station = new TrainStation();

    public void sell() {
        System.out.println("代理点收取一些服务费用");
        station.sell();
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        ProxyPoint pp = new ProxyPoint();
        pp.sell();
    }
}
从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。

4. JDK动态代理

Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。
代码如下:
//卖票接口
public interface SellTickets {
    void sell();
}

//火车站  火车站具有卖票功能,所以需要实现SellTickets接口
public class TrainStation implements SellTickets {

    public void sell() {
        System.out.println("火车站卖票");
    }
}

//代理工厂,用来创建代理对象
public class ProxyFactory {

    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
        //使用Proxy获取代理对象
        /*
            newProxyInstance()方法参数说明:
                ClassLoader loader:类加载器,用于加载代理类,使用真实对象的类加载器即可
                Class<?>[] interfaces:真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
                InvocationHandler h代理对象的调用处理程序
         */
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    /*
                        InvocationHandler中invoke方法参数说明:
                            proxy : 代理对象
                            method : 对应于在代理对象上调用的接口方法的 Method 实例
                            args : 代理对象调用接口方法时传递的实际参数
                     */
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
                        //执行真实对象
                        Object result = method.invoke(station, args);
                        return result;
                    }
                });
        return sellTickets;
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        //获取代理对象
        ProxyFactory factory = new ProxyFactory();

        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
    }
}

4. 一些问题

What are the different types of proxies?
代理模式分为以下几种常见类型:
  1. 远程代理(Remote Proxy):为位于不同地址空间的对象提供本地代表,用于控制对远程对象的访问,典型应用是远程方法调用(RMI)。
  1. 虚拟代理(Virtual Proxy):当创建一个开销很大的对象时,使用虚拟代理进行延迟加载,只有在真正需要时才去创建对象,如按需加载图片等资源。
  1. 保护代理(Protection Proxy):控制对对象的访问权限,通过代理限制不同用户对真实对象的访问,例如权限管理系统。
  1. 缓存代理(Cache Proxy):在代理中存储数据,减少对实际对象的重复请求,适用于需要频繁读取但不常更新的数据场景。
  1. 防火墙代理(Firewall Proxy):控制网络资源的访问权限,保护目标对象不受不良访问的影响。
  1. 智能引用代理(Smart Reference Proxy):代理不仅控制对象访问,还可以附加一些操作,例如计数、日志记录、资源释放等。
Can you give an example of a remote proxy?
// Interface for the file service
public interface FileService {
    String readFile(String fileName) throws RemoteException;
    void writeFile(String fileName, String data) throws RemoteException;
}

// The actual implementation on the server side
public class FileServiceImpl implements FileService {
    @Override
    public String readFile(String fileName) {
        // Simulate reading a file
        System.out.println("Reading file: " + fileName);
        return "File content for " + fileName;
    }

    @Override
    public void writeFile(String fileName, String data) {
        // Simulate writing to a file
        System.out.println("Writing to file: " + fileName + " with data: " + data);
    }
}
import java.rmi.Naming;
import java.rmi.RemoteException;

// Proxy class to represent the remote FileService
public class FileServiceProxy implements FileService {
    private FileService remoteFileService;

    public FileServiceProxy() {
        try {
            // Lookup the remote object

					 	//主要是这里
            this.remoteFileService = (FileService) Naming.lookup("rmi://localhost/FileService");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public String readFile(String fileName) throws RemoteException {
        return remoteFileService.readFile(fileName);
    }

    @Override
    public void writeFile(String fileName, String data) throws RemoteException {
        remoteFileService.writeFile(fileName, data);
    }
}
public class Client {
    public static void main(String[] args) {
        FileService fileService = new FileServiceProxy();

        try {
            String content = fileService.readFile("example.txt");
            System.out.println("Content: " + content);

            fileService.writeFile("example.txt", "Updated content");
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}

装饰者模式 Decorator pattern

1. GoF

Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality.
指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。

2. 结构

  • 抽象构件(Component)角色 :定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色 :实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色 : 继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(Concrete Decorator)角色 :实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

3. 实现

notion image
代码如下:
//快餐接口 (抽象构件角色)
public abstract class FastFood {
    private float price;
    private String desc;

    public FastFood() {
    }

    public FastFood(float price, String desc) {
        this.price = price;
        this.desc = desc;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    public float getPrice() {
        return price;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public abstract float cost();  //获取价格
}

//炒饭 (具体构件角色)
public class FriedRice extends FastFood {

    public FriedRice() {
        super(10, "炒饭");
    }

    public float cost() {
        return getPrice();
    }
}

//炒面 (具体构件角色)
public class FriedNoodles extends FastFood {

    public FriedNoodles() {
        super(12, "炒面");
    }

    public float cost() {
        return getPrice();
    }
}

//配料类 (抽象装饰角色)
public abstract class Garnish extends FastFood {

    private FastFood fastFood;

    public FastFood getFastFood() {
        return fastFood;
    }

    public void setFastFood(FastFood fastFood) {
        this.fastFood = fastFood;
    }

    public Garnish(FastFood fastFood, float price, String desc) {
        super(price,desc);
        this.fastFood = fastFood;
    }
}

//鸡蛋配料 (具体装饰角色)
public class Egg extends Garnish {

    public Egg(FastFood fastFood) {
        super(fastFood,1,"鸡蛋");
    }

    public float cost() {
        return getPrice() + getFastFood().getPrice();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

//培根配料 (具体装饰角色)
public class Bacon extends Garnish {

    public Bacon(FastFood fastFood) {

        super(fastFood,2,"培根");
    }

    @Override
    public float cost() {
        return getPrice() + getFastFood().getPrice();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        //点一份炒饭
        FastFood food = new FriedRice();
        //花费的价格
        System.out.println(food.getDesc() + " " + food.cost() + "元");

        System.out.println("========");
        //点一份加鸡蛋的炒饭
        FastFood food1 = new FriedRice();

        food1 = new Egg(food1);
        //花费的价格
        System.out.println(food1.getDesc() + " " + food1.cost() + "元");

        System.out.println("========");
        //点一份加培根的炒面
        FastFood food2 = new FriedNoodles();
        food2 = new Bacon(food2);
        //花费的价格
        System.out.println(food2.getDesc() + " " + food2.cost() + "元");
    }
}
好处:
  • 饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

4. 使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
    • 不能采用继承的情况主要有两类:
    • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
    • 第二类是因为类定义不能继承(如final类)
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

适配器模式

1. GoF

Convert the interface of a class into another interface that clients expect. Adapter lets classes work together that could not otherwise because of incompatible interfaces.
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

2. 结构

适配器模式(Adapter)包含以下主要角色:
  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

3.1. 实现 (类适配器模式)

//SD卡的接口
public interface SDCard {
    //读取SD卡方法
    String readSD();
    //写入SD卡功能
    void writeSD(String msg);
}

//SD卡实现类
public class SDCardImpl implements SDCard {
    public String readSD() {
        String msg = "sd card read a msg :hello word SD";
        return msg;
    }

    public void writeSD(String msg) {
        System.out.println("sd card write msg : " + msg);
    }
}

//电脑类
public class Computer {

    public String readSD(SDCard sdCard) {
        if(sdCard == null) {
            throw new NullPointerException("sd card null");
        }
        return sdCard.readSD();
    }
}

//TF卡接口
public interface TFCard {
    //读取TF卡方法
    String readTF();
    //写入TF卡功能
    void writeTF(String msg);
}

//TF卡实现类
public class TFCardImpl implements TFCard {

    public String readTF() {
        String msg ="tf card read msg : hello word tf card";
        return msg;
    }

    public void writeTF(String msg) {
        System.out.println("tf card write a msg : " + msg);
    }
}

//定义适配器类
public class SDAdapterTF extends TFCardImpl implements SDCard {

    public String readSD() {
        System.out.println("adapter read tf card ");
        return readTF();
    }

    public void writeSD(String msg) {
        System.out.println("adapter write tf card");
        writeTF(msg);
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        SDCard sdCard = new SDCardImpl();
        System.out.println(computer.readSD(sdCard));

        System.out.println("------------");

        SDAdapterTF adapter = new SDAdapterTF();
        System.out.println(computer.readSD(adapter));
    }
}

3.2. 实现 (对象适配器模式)

//创建适配器对象(SD兼容TF)
public class SDAdapterTF  implements SDCard {

    private TFCard tfCard;

    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    public String readSD() {
        System.out.println("adapter read tf card ");
        return tfCard.readTF();
    }

    public void writeSD(String msg) {
        System.out.println("adapter write tf card");
        tfCard.writeTF(msg);
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        Computer computer = new Computer();
        SDCard sdCard = new SDCardImpl();
        System.out.println(computer.readSD(sdCard));

        System.out.println("------------");

        TFCard tfCard = new TFCardImpl();
        SDAdapterTF adapter = new SDAdapterTF(tfCard);
        System.out.println(computer.readSD(adapter));
    }
}

4. 一些问题

“Apart from this case, you will be blocked again when you notice that you need to adapt a method that is not specified in the interface.” What do you mean by this?
即使我们通过适配器模式成功解决了当前的接口不兼容问题(例如 ChineseSpeaker 通过 LanguageAdapterEnglishSpeaker 进行沟通),但在未来可能遇到更多适配问题,特别是当需要适配的某些方法没有在最初定义的接口中列出来时,适配器就无法帮助解决这些问题。
 
What are the drawbacks associated with this pattern?
  • 增加系统复杂性:引入了额外的适配器类,导致类的数量增多,代码更复杂。
  • 性能开销:适配器增加了额外的转换层,可能影响性能。
  • 接口局限性:只能适配现有的接口,无法应对未来的新方法需求。
  • 违背接口隔离原则:可能导致接口过于臃肿,依赖不必要的方法。
  • 掩盖设计缺陷:使用适配器可能是权宜之计,未解决系统设计的根本问题。
  • 难以维护:多个适配器类增加了维护和修改的复杂度。
 

外观模式 Facade Pattern

1. GoF

Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher level interface that makes the subsystem easier to use.
又名门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体的细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。
notion image

2. 结构

外观(Facade)模式包含以下主要角色:
  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

3. 实现

【例】智能家电控制
小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:
notion image
代码如下:
//灯类
public class Light {
    public void on() {
        System.out.println("打开了灯....");
    }public void off() {
        System.out.println("关闭了灯....");
    }
}//电视类
public class TV {
    public void on() {
        System.out.println("打开了电视....");
    }public void off() {
        System.out.println("关闭了电视....");
    }
}//控制类
public class AirCondition {
    public void on() {
        System.out.println("打开了空调....");
    }public void off() {
        System.out.println("关闭了空调....");
    }
}//智能音箱
public class SmartAppliancesFacade {private Light light;
    private TV tv;
    private AirCondition airCondition;public SmartAppliancesFacade() {
        light = new Light();
        tv = new TV();
        airCondition = new AirCondition();
    }public void say(String message) {
        if(message.contains("打开")) {
            on();
        } else if(message.contains("关闭")) {
            off();
        } else {
            System.out.println("我还听不懂你说的!!!");
        }
    }//起床后一键开电器
    private void on() {
        System.out.println("起床了");
        light.on();
        tv.on();
        airCondition.on();
    }//睡觉一键关电器
    private void off() {
        System.out.println("睡觉了");
        light.off();
        tv.off();
        airCondition.off();
    }
}//测试类
public class Client {
    public static void main(String[] args) {
        //创建外观对象
        SmartAppliancesFacade facade = new SmartAppliancesFacade();
        //客户端直接与外观对象进行交互
        facade.say("打开家电");
        facade.say("关闭家电");
    }
}
好处:
  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
  • 不符合开闭原则,修改很麻烦

4. 一些问题

How is it different from adapter design pattern?
Difference from Adapter Design Pattern:
  • The facade pattern simplifies and provides a unified interface to a set of interfaces in a subsystem, while the adapter pattern is used to convert the interface of an existing class into another interface that a client expects. Essentially, facades provide a higher-level interface, while adapters allow incompatible interfaces to work together.
What are the challenges associated with a facade pattern?
Challenges Associated with a Facade Pattern:
  • Over-Simplification: A facade can oversimplify interactions, potentially hiding important details that clients might need.
  • Single Point of Failure: If the facade becomes too complex, it can become a bottleneck, leading to issues if it fails.
  • Limited Flexibility: While it simplifies usage, it may also restrict access to specific subsystem functionalities, which could be a drawback for advanced users.
  • Maintenance Overhead: As the underlying subsystem evolves, maintaining the facade to ensure it remains relevant can require additional effort.

享元模式 Flyweight Pattern

1. GoF

Use sharing to support large numbers of fine-grained objects efficiently.
运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

2. 结构

享元(Flyweight)模式中存在以下两种状态:
  1. 内部状态,即不会随着环境的改变而改变的可共享部分。
  1. 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
  • 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
  • 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。

3. 实现

【例】俄罗斯方块
下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。
notion image
先来看类图:
notion image
代码如下:
俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。
public abstract class AbstractBox {
    public abstract String getShape();public void display(String color) {
        System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
    }
}
接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。
public class IBox extends AbstractBox {@Override
    public String getShape() {
        return "I";
    }
}public class LBox extends AbstractBox {@Override
    public String getShape() {
        return "L";
    }
}public class OBox extends AbstractBox {@Override
    public String getShape() {
        return "O";
    }
}
提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。
public class BoxFactory {

    private static HashMap<String, AbstractBox> map;

    private BoxFactory() {
        map = new HashMap<String, AbstractBox>();
        AbstractBox iBox = new IBox();
        AbstractBox lBox = new LBox();
        AbstractBox oBox = new OBox();
        map.put("I", iBox);
        map.put("L", lBox);
        map.put("O", oBox);
    }

    public static final BoxFactory getInstance() {
        return SingletonHolder.INSTANCE;
    }
 
    private static class SingletonHolder {
        private static final BoxFactory INSTANCE = new BoxFactory();
    }

    public AbstractBox getBox(String key) {
        return map.get(key);
    }
}

4. 一些问题

 

组合模式 Composite Pattern

1. GoF

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.
又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

2. 结构

组合模式主要包含三种角色:
  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

3. 实现

我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。
notion image
类图:
notion image
代码如下:
//菜单组件  不管是菜单还是菜单项,都应该继承该类
public abstract class MenuComponent {

    protected String name;
    protected int level;

    //添加菜单
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    //移除菜单
    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    //获取指定的子菜单
    public MenuComponent getChild(int i){
        throw new UnsupportedOperationException();
    }

    //获取菜单名称
    public String getName(){
        return name;
    }

    public void print(){
        throw new UnsupportedOperationException();
    }
}

public class Menu extends MenuComponent {

    private List<MenuComponent> menuComponentList;

    public Menu(String name,int level){
        this.level = level;
        this.name = name;
        menuComponentList = new ArrayList<MenuComponent>();
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponentList.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponentList.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponentList.get(i);
    }

    @Override
    public void print() {

        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
        for (MenuComponent menuComponent : menuComponentList) {
            menuComponent.print();
        }
    }
}

public class MenuItem extends MenuComponent {

    public MenuItem(String name,int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void print() {
        for (int i = 1; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
    }
}

4. 分类

在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。
  • 透明组合模式
    • 透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 addremovegetChild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
      透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)
  • 安全组合模式
    • 在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。

5. 优点

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

6. 使用场景

组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作。

桥接模式 Bridge Pattern

1. GoF

Decouple an abstraction from its implementation so that the two can vary independently.
notion image
在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。
定义:
将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

2. 结构

桥接(Bridge)模式包含以下主要角色:
  • 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
  • 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。

3. 实现

需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。
notion image
代码如下:
//视频文件
public interface VideoFile {
    void decode(String fileName);
}

//avi文件
public class AVIFile implements VideoFile {
    public void decode(String fileName) {
        System.out.println("avi视频文件:"+ fileName);
    }
}

//rmvb文件
public class REVBBFile implements VideoFile {

    public void decode(String fileName) {
        System.out.println("rmvb文件:" + fileName);
    }
}

//操作系统版本
public abstract class OperatingSystemVersion {

    protected VideoFile videoFile;

    public OperatingSystemVersion(VideoFile videoFile) {
        this.videoFile = videoFile;
    }

    public abstract void play(String fileName);
}

//Windows版本
public class Windows extends OperatingSystem {

    public Windows(VideoFile videoFile) {
        super(videoFile);
    }

    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}

//mac版本
public class Mac extends OperatingSystemVersion {

    public Mac(VideoFile videoFile) {
        super(videoFile);
    }

    public void play(String fileName) {
		videoFile.decode(fileName);
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        OperatingSystem os = new Windows(new AVIFile());
        os.play("战狼3");
    }
}

访问者模式 Visitor Pattern

1. GoF

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。

2. 结构

访问者模式包含以下主要角色:
  • 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
  • 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

3. 实现

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家
    • notion image
代码如下:
创建抽象访问者接口
public interface Person {
    void feed(Cat cat);void feed(Dog dog);
}
创建不同的具体访问者角色(主人和其他人),都需要实现 Person接口
public class Owner implements Person {@Override
    public void feed(Cat cat) {
        System.out.println("主人喂食猫");
    }@Override
    public void feed(Dog dog) {
        System.out.println("主人喂食狗");
    }
}public class Someone implements Person {
    @Override
    public void feed(Cat cat) {
        System.out.println("其他人喂食猫");
    }@Override
    public void feed(Dog dog) {
        System.out.println("其他人喂食狗");
    }
}
定义抽象节点 -- 宠物
public interface Animal {
    void accept(Person person);
}
定义实现Animal接口的 具体节点(元素)
public class Dog implements Animal {@Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,汪汪汪!!!");
    }
}public class Cat implements Animal {@Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,喵喵喵!!!");
    }
}
定义对象结构,此案例中就是主人的家
public class Home {
    private List<Animal> nodeList = new ArrayList<Animal>();public void action(Person person) {
        for (Animal node : nodeList) {
            node.accept(person);
        }
    }//添加操作
    public void add(Animal animal) {
        nodeList.add(animal);
    }
}
测试类
public class Client {
    public static void main(String[] args) {
        Home home = new Home();
        home.add(new Dog());
        home.add(new Cat());Owner owner = new Owner();
        home.action(owner);Someone someone = new Someone();
        home.action(someone);
    }
}

4. 优缺点

  1. 优点:
  • 扩展性好
    • 在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好
    • 通过访问者来定义整个对象结构通用的功能,从而提高复用程度。
  • 分离无关行为
    • 通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。
  1. 缺点:
  • 对象结构变化很困难
    • 在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  • 违反了依赖倒置原则
    • 访问者模式依赖了具体类,而没有依赖抽象类。

5. 使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

6. 扩展

访问者模式用到了一种双分派的技术。
  1. 分派:
变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。
静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。
动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。
  1. 动态分派:
通过方法的重写支持动态分派。
public class Animal {
    public void execute() {
        System.out.println("Animal");
    }
}public class Dog extends Animal {
    @Override
    public void execute() {
        System.out.println("dog");
    }
}public class Cat extends Animal {
     @Override
    public void execute() {
        System.out.println("cat");
    }
}public class Client {
    public static void main(String[] args) {
        Animal a = new Dog();
        a.execute();

        Animal a1 = new Cat();
        a1.execute();
    }
}
Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
  1. 静态分派:
通过方法重载支持静态分派。
public class Animal {
}public class Dog extends Animal {
}public class Cat extends Animal {
}public class Execute {
    public void execute(Animal a) {
        System.out.println("Animal");
    }public void execute(Dog d) {
        System.out.println("dog");
    }public void execute(Cat c) {
        System.out.println("cat");
    }
}public class Client {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal a1 = new Dog();
        Animal a2 = new Cat();Execute exe = new Execute();
        exe.execute(a);
        exe.execute(a1);
        exe.execute(a2);
    }
}
运行结果:
Animal
Animal
Animal
重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
  1. 双分派:
所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。
public class Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}public class Dog extends Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}public class Cat extends Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}public class Execute {
    public void execute(Animal a) {
        System.out.println("animal");
    }public void execute(Dog d) {
        System.out.println("dog");
    }public void execute(Cat c) {
        System.out.println("cat");
    }
}public class Client {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal d = new Dog();
        Animal c = new Cat();Execute exe = new Execute();
        a.accept(exe);
        d.accept(exe);
        c.accept(exe);
    }
}
在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。
运行结果如下:
animal
dog
cat
双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
 

观察者模式 Observer Pattern

1. GoF

Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

2. 结构

在观察者模式中有如下角色:
  • Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
  • ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
  • Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
  • ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。

3. 案例实现

在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。
类图如下:
notion image
代码如下:
定义抽象观察者类,里面定义一个更新的方法
public interface Observer {
    void update(String message);
}
定义具体观察者类,微信用户是观察者,里面实现了更新的方法
public class WeixinUser implements Observer {
    // 微信用户名
    private String name;public WeixinUser(String name) {
        this.name = name;
    }
    @Override
    public void update(String message) {
        System.out.println(name + "-" + message);
    }
}
定义抽象主题类,提供了attach、detach、notify三个方法
public interface Subject {
    //增加订阅者
    public void attach(Observer observer);//删除订阅者
    public void detach(Observer observer);

    //通知订阅者更新消息
    public void notify(String message);
}
微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法
public class SubscriptionSubject implements Subject {
    //储存订阅公众号的微信用户
    private List<Observer> weixinUserlist = new ArrayList<Observer>();@Override
    public void attach(Observer observer) {
        weixinUserlist.add(observer);
    }@Override
    public void detach(Observer observer) {
        weixinUserlist.remove(observer);
    }@Override
    public void notify(String message) {
        for (Observer observer : weixinUserlist) {
            observer.update(message);
        }
    }
}
客户端程序
public class Client {
    public static void main(String[] args) {
        SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
        //创建微信用户
        WeixinUser user1=new WeixinUser("孙悟空");
        WeixinUser user2=new WeixinUser("猪悟能");
        WeixinUser user3=new WeixinUser("沙悟净");
        //订阅公众号
        mSubscriptionSubject.attach(user1);
        mSubscriptionSubject.attach(user2);
        mSubscriptionSubject.attach(user3);
        //公众号更新发出消息给订阅的微信用户
        mSubscriptionSubject.notify("传智黑马的专栏更新了");
    }
}

4. 优缺点

  1. 优点:
  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
  • 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制】
  1. 缺点:
  • 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
  • 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃

5. 使用场景

  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。

策略模式 Strategy Pattern

1. GoF

定义:
该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

2. 结构

策略模式的主要角色如下:
  • 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

3. 实现

【例】促销活动
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:
代码如下:
定义百货公司所有促销活动的共同接口
public interface Strategy {
    void show();
}
定义具体策略角色(Concrete Strategy):每个节日具体的促销活动
//为春节准备的促销活动A
public class StrategyA implements Strategy {public void show() {
        System.out.println("买一送一");
    }
}//为中秋准备的促销活动B
public class StrategyB implements Strategy {public void show() {
        System.out.println("满200元减50元");
    }
}//为圣诞准备的促销活动C
public class StrategyC implements Strategy {public void show() {
        System.out.println("满1000元加一元换购任意200元以下商品");
    }
}
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
public class SalesMan {
    //持有抽象策略角色的引用
    private Strategy strategy;

    public SalesMan(Strategy strategy) {
        this.strategy = strategy;
    }

    //向客户展示促销活动
    public void salesManShow(){
        strategy.show();
    }
}

4. 优缺点

  1. 优点:
  • 策略类之间可以自由切换
    • 由于策略类都实现同一个接口,所以使它们之间可以自由切换。
  • 易于扩展
    • 增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。
  1. 缺点:
  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

5. 使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构。
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

模板方法模式 Template Method Pattern

1. GoF

定义:
定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。

2. 结构

模板方法(Template Method)模式包含以下主要角色:
  • 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
      • 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
      • 具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
      • 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。
        • 一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
  • 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

3. 案例实现

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
代码如下:
public abstract class AbstractClass {

    public final void cookProcess() {
        //第一步:倒油
        this.pourOil();
        //第二步:热油
        this.heatOil();
        //第三步:倒蔬菜
        this.pourVegetable();
        //第四步:倒调味料
        this.pourSauce();
        //第五步:翻炒
        this.fry();
    }public void pourOil() {
        System.out.println("倒油");
    }//第二步:热油是一样的,所以直接实现
    public void heatOil() {
        System.out.println("热油");
    }//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
    public abstract void pourVegetable();//第四步:倒调味料是不一样
    public abstract void pourSauce();
​
​
    //第五步:翻炒是一样的,所以直接实现
    public void fry(){
        System.out.println("炒啊炒啊炒到熟啊");
    }
}public class ConcreteClass_BaoCai extends AbstractClass {@Override
    public void pourVegetable() {
        System.out.println("下锅的蔬菜是包菜");
    }@Override
    public void pourSauce() {
        System.out.println("下锅的酱料是辣椒");
    }
}public class ConcreteClass_CaiXin extends AbstractClass {
    @Override
    public void pourVegetable() {
        System.out.println("下锅的蔬菜是菜心");
    }@Override
    public void pourSauce() {
        System.out.println("下锅的酱料是蒜蓉");
    }
}public class Client {
    public static void main(String[] args) {
        //炒手撕包菜
        ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
        baoCai.cookProcess();//炒蒜蓉菜心
        ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
        caiXin.cookProcess();
    }
}
注意:为防止恶意操作,一般模板方法都加上 final 关键词。

4. 优缺点

优点:
  • 提高代码复用性
    • 将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
  • 实现了反向控制
    • 通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

5. 适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。