likes
comments
collection
share

【一看就懂】常见的八种设计模式

作者站长头像
站长
· 阅读数 3

设计模式简介

设计模式可以大概分类三类,下面的简介可以混个眼熟,主要看后面的详情

  • 创建型模式
  • 结构性模式
  • 行为型模式
  1. 创建型模式
    • 单例模式:确保一个类只有一个实例,并提供全局访问点。
    • 抽象工厂模式:创建一组相关或相互依赖的对象。
    • 建造者模式:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
    • 工厂模式:定义一个用于创建对象的接口,由子类决定实例化哪个类。
    • 原型模式:通过复制现有对象来创建新对象。
  2. 结构型模式
    • 适配器模式:将一个类的接口转换成客户希望的另一个接口。
    • 桥接模式:将抽象部分与它的实现部分分离,使它们可以独立变化。
    • 装饰模式:动态地给一个对象添加一些额外的职责。
    • 组合模式:将对象组合成树形结构以表示“部分-整体”的层次结构。
    • 外观模式:为子系统中的一组接口提供一个统一的接口。
    • 享元模式:运用共享技术有效地支持大量细粒度的对象。
    • 代理模式:为其他对象提供一种代理以控制对这个对象的访问。
  3. 行为型模式
    • 模板方法模式:定义一个操作中的算法的骨架,将一些步骤延迟到子类中实现。
    • 命令模式:将请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化。
    • 迭代器模式:提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。
    • 观察者模式:定义对象之间的一对多依赖关系,使得一个对象的状态改变会通知其依赖者。
    • 中介者模式:用一个中介对象来封装一系列的对象交互。
    • 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
    • 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。
    • 状态模式:允许一个对象在其内部状态改变时改变它的行为。
    • 策略模式:定义一系列算法,将每个算法都封装起来,并使它们可以互换。
    • 责任链模式:将请求的发送者和接收者解耦,使多个对象都有机会处理这个请求。
    • 访问者模式:表示一个作用于某对象结构中的各元素的操作,它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

此篇为常见的面试常问到的设计模式篇,一些不常见的后续补

  • 单例模式
  • 工厂设计模式,包括工厂方法模式和抽象工厂模式
  • 建造者模式
  • 适配器设计模式,包括类适配器模式和对象适配器模式
  • 接口的适配器模式
  • 装饰模式
  • 策略模式
  • 代理模式

创建型模式

单例模式

单例模式想必是最熟悉不过了,学过 spring 的都知道,spring 就是默认是单例模式的。意思就是说,在单例模式bean 中,在整个上下文中获取对象都是同一个实例对象

单例模式关键特点:

  1. 只有一个实例:单例模式确保在整个应用程序中只有一个类的实例。
  2. 全局访问:通过单例,我们可以在应用程序的任何地方访问该实例。
  3. 延迟初始化:实例只在需要时才被创建,而不是在应用程序启动时立即创建。

单例模式有以下几个优点

  1. 节省资源:由于只有一个实例,单例模式减少了内存消耗和对象创建的开销。
  2. 全局共享状态的一致性:单例模式确保全局共享状态的一致性,避免了资源竞争和线程安全性问题。

单例模式的使用场景

  1. 共享资源管理:当多个对象需要共享某个资源(例如数据库连接、线程池、配置信息等)时,使用单例模式可以确保只有一个实例,避免资源浪费和冲突。
  2. 全局状态管理:某些应用程序状态需要在整个应用程序中共享,例如用户登录信息、缓存数据等。单例模式可以确保这些状态的一致性。
  3. 工具类:如果您有一个工具类,其中的方法不依赖于实例特定的状态,那么将其设计为单例可以避免不必要的实例化。
  4. 日志记录器:在应用程序中使用单例模式来管理日志记录器,以确保所有日志都写入同一个实例。
  5. 线程池:在多线程环境中,使用单例模式来管理线程池,以避免创建多个线程池实例。

这也是为什么 spring 默认bean 是单例的原因,例如数据库连接、线程池、配置信息等,必须整个程序保持一致,也防止了过多创建对象消耗内存

实现方式

实现方式主要分为饿汉式懒汉式,但这又细分挺多种的,下面写一下常见的以及安全又高效的

  1. 饿汉式 (静态常量)

    • 在类加载时就创建实例,实现简单,但没有达到懒加载效果。
    • 可能造成内存浪费,因为实例在应用程序启动时就被创建了。
    • 示例代码如下:
    class Singleton {
        private static final Singleton instance = new Singleton();
        private Singleton() {}
        public static Singleton getInstance() {
            return instance;
        }
    }
    
  2. 懒汉式 (线程不安全)

  • 实现了懒加载,但只能在单线程下使用,因为多个线程同时进入条件判断时可能产生多个实例。
  • 实际开发中不推荐使用这种方式。
  • 示例代码如下:
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 懒汉式 (线程安全,同步方法)
  • 解决了线程不安全问题,但效率较低,因为每次访问方法都会同步。
  • 示例代码如下:
class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
  1. 双重检查
  • 通过两次判断实现懒加载,既保证线程安全又提高效率。
  • 示例代码如下:
class Singleton {
    private volatile static Singleton instance;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 静态内部类
  • 利用类加载机制实现懒加载,线程安全且效率高。
  • 示例代码如下:
class Singleton {
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

工厂模式

工厂模式是一种创建型设计模式,它提供了一种创建对象的最佳实践。在工厂模式中,我们在创建对象时不使用 new 关键字,而是通过调用工厂方法来创建对象。这种模式提供了一种抽象工厂,通过使用工厂方法来创建对象。

这个说法是不是很眼熟,就跟 springbean 一样,把创建对象交给 spring 处理,我们只管使用就是了。这就是创建与使用分离,工厂模式的核心思想

工厂模式分三种:简单工厂模式、工厂方法模式、抽象工厂模式

简单工厂模式

简单工厂模式是最基本的工厂模式。它通过一个工厂类来创建所有需要的对象。客户端通过调用工厂类的静态方法来创建对象。虽然简单,但它违背了开闭原则,当需要添加新的对象时,需要修改工厂类的代码。

// 抽象产品类
interface Product {
    void use();
}

// 具体产品类A
class ProductA implements Product {
    @Override
    public void use() {
        System.out.println("使用产品A");
    }
}

// 具体产品类B
class ProductB implements Product {
    @Override
    public void use() {
        System.out.println("使用产品B");
    }
}

// 工厂类
class Factory {
    public static Product createProduct(String type) {
        if (type.equals("A")) {
            return new ProductA();
        } else if (type.equals("B")) {
            return new ProductB();
        } else {
            return null;
        }
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        Product product = Factory.createProduct("A");
        product.use();
    }
}

工厂方法模式

工厂方法模式通过定义一个工厂接口和多个工厂类来解决简单工厂模式的缺点。每个工厂类负责创建一种类型的对象,客户端通过调用工厂类的工厂方法来创建对象。工厂方法模式通过多态来实现对象的创建。

// 抽象产品A
interface ProductA {
    void use();
}

// 具体产品A1
class ProductA1 implements ProductA {
    @Override
    public void use() {
        System.out.println("使用产品A1");
    }
}

// 具体产品A2
class ProductA2 implements ProductA {
    @Override
    public void use() {
        System.out.println("使用产品A2");
    }
}

// 抽象工厂
interface AbstractFactory {
    ProductA createProductA();
}

// 具体工厂1
class ConcreteFactory1 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ProductA1();
    }
}

// 具体工厂2
class ConcreteFactory2 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ProductA2();
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        AbstractFactory factory1 = new ConcreteFactory1();
        ProductA productA1 = factory1.createProductA();
        productA1.use();

        AbstractFactory factory2 = new ConcreteFactory2();
        ProductA productA2 = factory2.createProductA();
        productA2.use();
    }
}

抽象工厂模式

抽象工厂模式是工厂方法模式的升级版,它定义了多个工厂接口多个产品族接口。每个工厂接口负责创建一类产品族对象,每个产品族接口负责定义一类产品对象。具体工厂类实现工厂接口并负责创建一类产品族对象。

例如我们的产品分为手机跟路由器,那么这两个产品就组成了一个产品族,就得使用抽象工厂模式实现。如果使用工厂方法模式,则需分别定义手机抽象工厂与路由器抽象工厂,多少个产品定义多少个

// 抽象产品
interface Phone {
    void makeCall();
}

interface Router {
    void connect();
}

// 具体产品
class XiaomiPhone implements Phone {
    @Override
    public void makeCall() {
        System.out.println("Making a call with Xiaomi phone.");
    }
}

class HuaweiRouter implements Router {
    @Override
    public void connect() {
        System.out.println("Connecting to the internet with Huawei router.");
    }
}

// 抽象工厂
interface AbstractFactory {
    Phone createPhone();
    Router createRouter();
}

// 具体工厂
class XiaomiFactory implements AbstractFactory {
    @Override
    public Phone createPhone() {
        return new XiaomiPhone();
    }

    @Override
    public Router createRouter() {
        return null; // Xiaomi doesn't make routers
    }
}

class HuaweiFactory implements AbstractFactory {
    @Override
    public Phone createPhone() {
        return null; // Huawei doesn't make phones
    }

    @Override
    public Router createRouter() {
        return new HuaweiRouter();
    }
}

// 客户端
public class Client {
    public static void main(String[] args) {
        AbstractFactory factory = new XiaomiFactory();
        Phone phone = factory.createPhone();
        phone.makeCall();
    }
}

简单画个图理解一下

工厂方法模式,简单来说就是一个产品对应一个抽象方法工厂

【一看就懂】常见的八种设计模式

抽象工厂模式,简单来说就是过个产品都是通过此工厂来创建的,具体创建方式交由字类去实现。

【一看就懂】常见的八种设计模式

疑问?用了工厂模式反而更加麻烦了,我没添加一个产品,我自行去创建不就好了吗,何必多次一举呢?

  1. 解耦合
    • 分离对象的创建和使用可以降低代码的耦合度。客户端代码不需要知道具体的对象创建细节,只需调用工厂方法或抽象工厂来获取所需的对象。
    • 这使得代码更加灵活,容易维护和扩展。
  2. 隐藏实现细节
    • 工厂模式将对象的创建过程封装在工厂类中,客户端不需要知道具体的实现细节。
    • 这有助于隐藏对象的创建逻辑,使客户端代码更简洁。
  3. 易于替换和扩展
    • 如果需要更改对象的创建方式(例如,从单例模式切换到多例模式),只需修改工厂类,而不必修改客户端代码。
    • 同样,如果要添加新的产品或产品族,只需创建新的工厂类,而不必修改现有代码。
  4. 遵循开闭原则
    • 工厂模式遵循了开闭原则,即对扩展开放,对修改关闭。
    • 当需要添加新的产品时,只需创建新的工厂类,而不必修改现有的客户端代码。

建造者模式

建造者模式(也称为生成器模式)是一种对象创建型设计模式,它的目的是将复杂对象的构建过程与其表示分离,使得相同的构建过程可以创建不同的表示。这样,我们可以通过相同的构建步骤来创建不同类型的对象。

一般实现建造者模式有两种方式

  • 折叠构造函数模式:这种方式在构造函数中逐步添加参数,但会导致代码不够清晰易读
  • Javabean模式:这种方式通过setter方法逐步设置属性,但容易出现对象状态变化的问题

使用场景

建造者模式适用于以下情况:

  • 当一个类的构造函数参数超过4个,且其中一些参数是可选的时,可以考虑使用建造者模式。
  • 当需要创建一个复杂对象,且该对象的构建过程需要多个步骤时,建造者模式可以提高代码的可读性和可维护性。

折叠构造函数模式

例如我们项目中常用的就有通用的返回 vo,就是通过此方式实现的。这里就实现了创建过程跟表示分离。例如同样是使用 ok 这个方式创建,可以选择不同的传参,表示不同。

public class ResponseVo<T> {
    /**
     * 数据对象
     */
    @ApiModelProperty(value = "数据对象", required = false)
    private T data;

    /**
     * 消息头meta 存放状态信息 code message
     */
    @ApiModelProperty(value = "消息头meta 存放状态信息 code message", required = true)
    private ResponseMeta meta;

    public ResponseMeta getMeta() {
        return meta;
    }

    public ResponseVo setMeta(ResponseMeta meta) {
        this.meta = meta;
        return this;
    }

    public T getData() {
        return data;
    }

    public ResponseVo setData(T data) {
        this.data = data;
        return this;
    }

    public static ResponseVo ok() {
        return ok("成功", null);
    }

    public static ResponseVo ok(Object data) {
        return ok("成功", data);
    }

    public static ResponseVo ok(String statusMsg, Object data) {
        ResponseVo responseVo = new ResponseVo();
        ResponseMeta meta = new ResponseMeta(Boolean.TRUE, 200, statusMsg, System.currentTimeMillis());
        responseVo.setMeta(meta);
        responseVo.setData(data);
        return responseVo;
    }

    public static ResponseVo error() {
        return error("失败");
    }

    public static ResponseVo error(String statusMsg) {
        return error(500, statusMsg);
    }

    public static ResponseVo error(int statusCode, String statusMsg) {
        ResponseVo responseVo = new ResponseVo();
        ResponseMeta meta = new ResponseMeta(Boolean.FALSE, statusCode, statusMsg, System.currentTimeMillis());
        responseVo.setMeta(meta);
        responseVo.setData(null);
        return responseVo;
    }
}

Javabean模式

假如我们使用建造者模式来构建不同类型的订单。以下是一个简化的 Java 示例:

  1. 首先,我们定义一个 Order 类,它包含共同的属性(例如订单号、购买者信息、总金额)。
  2. 然后,我们创建一个 OrderBuilder 类,它负责逐步构建订单对象。OrderBuilder 包含与订单属性相对应的方法,例如 setOrderNumbersetBuyerInfosetTotalAmountaddProduct 等。
  3. 最后,我们使用 OrderBuilder 来创建不同类型的订单,例如普通商品订单、礼品卡订单等。
// 订单类
class Order {
    private String orderNumber;
    private String buyerInfo;
    private double totalAmount;
    private List<String> products;

    public Order(String orderNumber) {
        this.orderNumber = orderNumber;
        this.products = new ArrayList<>();
    }

    public void setBuyerInfo(String buyerInfo) {
        this.buyerInfo = buyerInfo;
    }

    public void setTotalAmount(double totalAmount) {
        this.totalAmount = totalAmount;
    }

    public void addProduct(String product) {
        this.products.add(product);
    }

    // 其他 getter 和 setter 方法...
}

// 订单建造者类
class OrderBuilder {
    private Order order;

    public OrderBuilder(String orderNumber) {
        this.order = new Order(orderNumber);
    }

    public OrderBuilder setBuyerInfo(String buyerInfo) {
        order.setBuyerInfo(buyerInfo);
        return this;
    }

    public OrderBuilder setTotalAmount(double totalAmount) {
        order.setTotalAmount(totalAmount);
        return this;
    }

    public OrderBuilder addProduct(String product) {
        order.addProduct(product);
        return this;
    }

    public Order build() {
        return order;
    }
}

// 使用示例
public class Main {
    public static void main(String[] args) {
        OrderBuilder builder = new OrderBuilder("12345");
        Order order = builder
            .setBuyerInfo("John Doe")
            .setTotalAmount(100.0)
            .addProduct("Phone")
            .addProduct("Headphones")
            .build();

        System.out.println("订单号:" + order.getOrderNumber());
        System.out.println("购买者信息:" + order.getBuyerInfo());
        System.out.println("总金额:" + order.getTotalAmount());
        System.out.println("商品列表:" + order.getProducts());
    }
}

这里也很容易理解,通常我们的 set 方法是没有返回值的,这里返回了 this,也就实现了链式调用。不过这个方法,每次建一个实体类,都要建一个 builder,不会太麻烦了吗?我会这样问,当然会有解决的办法了。

lombok链式调用

lombok大家都不陌生吧,有了这个,就不用写 getset 方法以及构造方法了,大大简化了实体类,不过要想实现上面的链式调用,除了引用依赖之外还得加上一个配置文件

【一看就懂】常见的八种设计模式

config.stopBubbling = true
lombok.tostring.callsuper=CALL
lombok.equalsandhashcode.callsuper=CALL
lombok.accessors.chain=true

使用方式

@Data
@Builder
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Integer id;

    private String username;

    private String nickname;
}  

public class TestMain {
    public static void main(String[] args) {
        User user = new User().setId(1).setUsername("user");
        System.out.println(user);
    }

}

结构型模式

代理模式

代理模式它允许一个对象(代理对象)来代表另一个对象(真实对象)进行访问,从而可以在不改变真实对象的情况下增强或控制其行为。代理模式的主要目的是扩展目标对象的功能,同时隐藏其复杂性。

而代理模式又分为动态代理静态代理

  • 比如说一个电影播放电影,要在片头片尾插入广告,播放电影作为主事件,片头片尾插入广告作为增强时间,如果下次需要在片头放音乐呢?所以为了方便以后扩展,不改变原有的放电影的功能,写一个具有片头片尾插入广告和放电影的代理类。

静态代理

  1. 定义:
    • 静态代理是由程序员在编译期间创建或工具生成代理类的源码,然后编译代理类。
    • 代理类和委托类的关系在运行前就确定了。
  2. 特点:
    • 代理类和委托类必须实现同一个接口共同继承某个类
    • 代理类在应用程序执行前就已经存在,效率相对较高。
    • 静态代理代码可能会变得冗余,一旦需要修改接口,代理类和委托类都需要修改。

动态代理

  1. 定义:
    • 动态代理类的源码是在程序运行期间JVM 根据反射等机制动态生成的,不存在代理类的字节码文件。
    • 代理类和委托类不需要实现同一个接口
    • 委托类需要实现接口,否则无法创建动态代理。
  2. 特点:
    • 目标对象不固定,代理类在运行时动态生成。
    • 动态代理类的字节码由 JVM 在运行时生成,灵活性更高
    • 可以代理多个方法,满足生产需要且代码通用。

代码事例

  • 首先,创建一个接口 Movie,它代表电影播放的能力:
public interface Movie {
    void play();
}
  • 接下来,实现一个具体的电影类 CaptainAmericaMovie
public class CaptainAmericaMovie implements Movie {
    @Override
    public void play() {
        System.out.println("普通影厅正在播放的电影是《美国队长》");
    }
}

以上代码是原始类,接下来使用静态代理与动态代理分别创建他们的代理类

静态代理示例

  • 创建一个代理类 MovieStaticProxy,它会在电影播放前后插入广告:
public class MovieStaticProxy implements Movie {
    private Movie movie;

    public MovieStaticProxy(Movie movie) {
        this.movie = movie;
    }

    @Override
    public void play() {
        playStart();
        movie.play();
        playEnd();
    }

    private void playStart() {
        System.out.println("电影开始前正在播放广告");
    }

    private void playEnd() {
        System.out.println("电影结束了,接续播放广告");
    }
}
  • 最后,我们测试一下这个静态代理:
public class StaticProxyTest {
    public static void main(String[] args) {
        Movie captainAmericaMovie = new CaptainAmericaMovie();
        Movie movieStaticProxy = new MovieStaticProxy(captainAmericaMovie);
        movieStaticProxy.play();
    }
}
  • 运行结果:
电影开始前正在播放广告
正在播放的电影是《美国队长》
电影结束了,接续播放广告

静态代理可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。请注意,代理类和被代理类应该共同实现一个接口或共同继承某个类。

动态代理示例

  • 使用动态代理来创建一个代理对象:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DynamicProxyExample {
    public static void main(String[] args) {
        CaptainAmericaMovie movie = new CaptainAmericaMovie();

        Movie movieProxy = (Movie) Proxy.newProxyInstance(
                Movie.class.getClassLoader(),
                new Class[]{Movie.class},
                new MovieInvocationHandler(movie)
        );

        movieProxy.play();
    }
}

class MovieInvocationHandler implements InvocationHandler {
    private Object target;

    public MovieInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("VIP影厅电影开始前正在播放广告");
        Object result = method.invoke(target, args);
        System.out.println("VIP影厅电影结束了,接续播放广告");
        return result;
    }
}
  • 运行结果:
VIP影厅电影开始前正在播放广告
VI影厅正在播放的电影是《钢铁侠》
VIP影厅电影结束了,接续播放广告

适配器模式

适配器模式是一种结构型设计模式,它的目的是将一个接口转换为客户端所期望的接口,从而使两个接口不兼容的类能够协同工作。这就像我们在现实生活中使用适配器来连接不同的电子设备一样。

使用场景

适配器模式适用于以下情况:

  1. 当需要使用一个现有的类,但它提供的接口与我们系统的接口不兼容,而我们又不能修改它时
  2. 当多个团队独立开发系统的各功能模块,然后组合在一起,但由于某些原因事先不能确定接口时。

举个例子

假设我们有一个老旧的日志系统,但它的接口很粗糙,不方便调试。现在我们想引入一个新的第三方日志库,但它的接口与现有系统不兼容。

  • 需要的目标接口Target
  • 需要转换成目标接口的Adaptee
  • 将Adaptee转成Target的适配器ObjectAdapter
  • 思路:在适配器ObjectAdapter中实现目标接口Target,将Adaptee作为成员变量引用,并在创建适配器时注入。然后在调用Target的request方法是调用原有类的方法。所以表面是掉了request,实则是调了specificRequest
// 目标接口
interface Target{
    public void request();
}

// 适配者
class Adaptee{
    public void specificRequest(){       
        System.out.println("适配者中的业务代码被调用!");
    }
}

// 适配器 将适配者Adaptee转成客户端需要的 Target
class ObjectAdapter implements Target{
    private Adaptee adaptee;
    public ObjectAdapter(Adaptee adaptee){
        this.adaptee=adaptee;
    }
  
  	@Override
    public void request(){
        adaptee.specificRequest();
    }
}
//客户端代码
public class ObjectAdapterTest{
    public static void main(String[] args){
        Adaptee adaptee = new Adaptee();
        Target target = new ObjectAdapter(adaptee);
        target.request();
    }
}

装饰模式

装饰模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。装饰模式通过将对象包装在装饰器类中,以便动态地修改其行为。这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

现有的对象添加新的功能乍一看,这玩意不是继承吗。请往下看。

装饰模式的主要优点包括:

  • 灵活性:装饰类和被装饰类可以独立发展,不会相互耦合。它是继承的一种替代模式,可以动态扩展一个实现类的功能。
  • 可代替继承:装饰模式避免了通过继承创建大量子类的问题。

然而,装饰模式也有一些缺点,例如多层装饰比较复杂。

装饰模式的核心角色:

  • 抽象组件(Component):定义了原始对象和装饰器对象的公共接口或抽象类,可以是具体组件类的父类或接口。
  • 具体组件(Concrete Component):被装饰的原始对象,定义了需要添加新功能的对象。
  • 抽象装饰器(Decorator):继承自抽象组件,包含了一个抽象组件对象,并定义了与抽象组件相同的接口,同时可以通过组合方式持有其他装饰器对象。
  • 具体装饰器(Concrete Decorator):实现了抽象装饰器的接口,负责向抽象组件添加新的功能。具体装饰器通常会在调用原始对象的方法之前或之后执行自己的操作。

举个例子

  • 假设我们有一个形状接口 Shape,以及实现了该接口的具体类 RectangleCircle
  • 我们还创建了一个抽象装饰类 ShapeDecorator,并将 Shape 对象作为它的实例变量。
  • 最后,我们实现了一个具体装饰器类 RedShapeDecorator,用于向形状添加红色边框。
// 定义 Shape 接口 (抽象组件)
public interface Shape {
    void draw();
}

// 具体组件类 Rectangle (具体组件)
public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Shape: Rectangle");
    }
}

// 具体组件类 Circle (具体组件)
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Shape: Circle");
    }
}

// 抽象装饰类 ShapeDecorator (抽象装饰器)
public abstract class ShapeDecorator implements Shape {
    protected Shape decoratedShape;

    public ShapeDecorator(Shape decoratedShape) {
        this.decoratedShape = decoratedShape;
    }

    public void draw() {
        decoratedShape.draw();
    }
}

// 具体装饰器类 RedShapeDecorator (具体装饰器)
public class RedShapeDecorator extends ShapeDecorator {
    public RedShapeDecorator(Shape decoratedShape) {
        super(decoratedShape);
    }

    @Override
    public void draw() {
        decoratedShape.draw();
        setRedBorder(decoratedShape);
    }

    private void setRedBorder(Shape decoratedShape) {
        System.out.println("Border Color: Red");
    }
}

// 使用 RedShapeDecorator 来装饰 Shape 对象
public class DecoratorPatternDemo {
    public static void main(String[] args) {
        Shape circle = new Circle();
        ShapeDecorator redCircle = new RedShapeDecorator(new Circle());
        ShapeDecorator redRectangle = new RedShapeDecorator(new Rectangle());

        System.out.println("Circle with normal border");
        circle.draw();

        System.out.println("\nCircle of red border");
        redCircle.draw();

        System.out.println("\nRectangle of red border");
        redRectangle.draw();
    }

疑问?

为什么要搞得这么复杂呢?直接使用继承,然后设置一个边框颜色不就好了吗?为什么不用继承呢?存在即合理。

  1. 继承
    • 继承是一种静态的方式,它在编译时就确定了类的行为。
    • 通过继承,我们可以创建一个新的子类,该子类继承了父类的属性和方法。
    • 如果我们想要为现有类添加新功能,我们需要创建一个新的子类,并在其中重写父类的方法。
    • 继承的缺点之一是它会导致类层次结构变得复杂,特别是当我们需要多个不同组合的功能时
  2. 装饰模式
    • 装饰模式是一种动态的方式,它允许我们在运行时添加新功能。
    • 通过装饰模式,我们可以将新功能添加到现有的对象中,而无需修改其结构。
    • 装饰模式使用组合而不是继承,它将对象包装在装饰器类中,以便动态地修改其行为。
    • 装饰模式的优点之一是它避免了创建大量子类的问题,因为我们可以通过组合不同的装饰器来实现不同的功能。

让我们再次回顾一下装饰模式的示例。

  • 在我们的示例中,我们使用了装饰模式来向形状添加红色边框。如果我们使用继承,我们需要为每个形状创建一个新的子类,例如 RedCircleRedRectangle,这会导致类层次结构变得复杂。
  • 而装饰模式允许我们在运行时选择要添加的功能,而不会影响原始类的结构。那又说了:继承不用写那么多装饰器接口,装饰器实现这些呀,直接用一个字类继承就好了
  • 那如果我又需要黑色边框呢和白色呢,又创建好多子类,那我需要画矩形+原型+白色边框呢,阁下该如何面对
  • 所以装饰器也不是盲目使用,1,2 个字类能搞定的事情,也不用大费周章,如果需要多个组合起来的,用装饰器是很好的选择

行为型模式

策略模式

策略模式(也被称为政策模式)是一种行为型设计模式,它允许在运行时根据需要选择不同的算法,而无需修改客户端代码。这种模式将一组算法封装到独立的类中,使它们可以互相替换。通过使用策略模式,我们可以动态地选择不同的算法,而不必改变客户端代码。

核心角色包括:

  1. 环境(Context):维护对策略对象的引用,负责将客户端请求委派给具体的策略对象执行。环境类可以通过依赖注入、简单工厂等方式来获取具体策略对象。
  2. 抽象策略(Abstract Strategy):定义了策略对象的公共接口或抽象类,规定了具体策略类必须实现的方法。
  3. 具体策略(Concrete Strategy):实现了抽象策略定义的接口或抽象类,包含了具体的算法实现。

举个例子

让我们通过一个示例来理解策略模式。假设我们要设计一个计算器,可以根据用户选择的操作(加法、减法、乘法等)执行相应的算法。以下是策略模式的实现步骤:

  1. 首先,我们创建一个定义活动的 Strategy 接口,其中包含一个执行操作的方法。
  2. 然后,我们实现了三个具体的策略类:OperationAdd(加法)、OperationSubtract(减法)和OperationMultiply(乘法)。每个类都实现了 Strategy 接口的方法,分别执行对应的算法。
  3. 接下来,我们创建一个 Context 类,它维护一个对策略对象的引用。Context 类的 executeStrategy 方法根据用户选择的策略执行相应的操作。
  4. 最后,我们在演示类 StrategyPatternDemo 中使用 Context 和策略对象来展示不同算法的行为变化。

以下是示例代码:

// 创建一个接口 (抽象策略)
public interface Strategy {
    public int doOperation(int num1, int num2);
}

// 创建实现接口的实体类 (具体策略)
public class OperationAdd implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

public class OperationSubtract implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 - num2;
    }
}

public class OperationMultiply implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 * num2;
    }
}

// 创建 Context 类
public class Context {
    private Strategy strategy;

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

    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

// 演示类
public class StrategyPatternDemo {
    public static void main(String[] args) {
        Context context = new Context(new OperationAdd());
        System.out.println("10 + 5 = " + context.executeStrategy(10, 5));

        context = new Context(new OperationSubtract());
        System.out.println("10 - 5 = " + context.executeStrategy(10, 5));

        context = new Context(new OperationMultiply());
        System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
    }
}

疑问?

  • 这个计算器可以根据用户选择的操作(加法、减法、乘法等)?if(加法)elseif(减法)else if(乘法)?这个不更方便吗?还是那句话,存在即合理。

  • 我再加个除法、开根、平方等等呢,阁下如果应对?

  • 那还不简单,继续往下加 if else 呀。

  • 面试官:你的回答很好,回去等通知吧。

使用策略模式相对于直接使用 if-else 有一些优点,让我们来比较一下:

  1. 可维护性和扩展性

    • 策略模式将不同的算法封装到独立的类中,使得每个策略都可以单独修改、测试和维护,而不会影响其他策略。
    • 如果我们需要添加新的计价策略,只需创建一个新的策略类并实现接口,而不必修改现有的代码。
  2. 避免复杂的条件逻辑

    • 使用 if-else 可能会导致嵌套的条件逻辑,代码变得复杂且难以阅读。
    • 策略模式将不同的策略分离开来,使得代码更加清晰、简洁。
  3. 动态切换策略

    • 策略模式允许在运行时根据需要选择不同的算法,而不需要改变客户端代码。
    • 如果我们使用 if-else,则需要在客户端代码中硬编码所有的条件,不容易动态切换。
  4. 符合开闭原则

    • 开闭原则是面向对象设计中的一个基本原则,提出者是Bertrand Meyer。它强调软件实体(类、模块、函数等)应该对扩展开放对修改关闭

    • 换句话说,当需要添加新功能时,我们应该通过扩展现有代码来实现,而不是修改已有代码。

    • 策略模式正是为了满足开闭原则而设计的。

  • 虽然在某些情况下,if-else 可能更简单,但策略模式在处理复杂的业务逻辑、增加新的策略、以及保持代码的可维护性方面更具优势
  • 如果对原有的类进行修改,万一这一改,改出了一个 bug,则这整个计算器都不能用了
  • 使用策略模式新增策略,则不影响其他业务,即使新增的策略有 bug 也是这个策略不能用,其他的还是正常的
  • 因此,根据具体的需求和场景,选择合适的设计模式是很重要的。