揭秘23种设计模式的艺术与技巧之创建型

发布于:2025-09-05 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

23种设计模式

创建型模式(5)对象创建的艺术

*1.单例模式(Singleton Pattern)

五种实现方式:

①饿汉式SingletonDemo1.java

②懒汉式SingletonDemo2.java

③双重校验SingletonDemo3.java

④静态内部类SingletonDemo4.java

常见问题:为什么这种内部静态类的方式,是线程安全的?

⑤枚举类SingletonDemo5.java

结构型模式:优化软件结构的策略

行为型模式:优化软件行为的技巧

总结


接上篇大概讲完设计原则六大设计原则和23种设计模式,该篇主要详解设计模式

23种设计模式

创建型模式(5)对象创建的艺术

        主要关注点是“怎样创建对象”,它的特点是“将对象的创建与使用分离”。这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。        

*1.单例模式(Singleton Pattern)

        在软件系统中,有些资源是唯一的,比如数据库连接池、线程池等,只需要一个实例来管理,这时候单例模式就派上用场了。它确保一个类只有一个实例,并提供一个全局访问点。实现单例模式有多种方式,如饿汉式、懒汉式、双重检查锁等。饿汉式在类加载时就创建实例,优点是简单直接,线程安全;懒汉式则是在第一次使用时才创建实例,实现了延迟加载,但需要注意线程安全问题;双重检查锁方式既实现了延迟加载又保证了线程安全,是一种较为常用的实现方式。

定义:确保一个类只有一个实例,并提供一个全局访问点。

五种实现方式:
①饿汉式SingletonDemo1.java

/**
 * @description 单例模式——饿汉式(线程安全,调用效率高,但是不能延时加载)
 */
public class SingletonDemo1 {

    private static SingletonDemo1 instance = new SingletonDemo1();

    private SingletonDemo1(){}

    public static SingletonDemo1 getInstance(){
        return instance;
    }
}

②懒汉式SingletonDemo2.java

/**
 * @description 单例模式——懒汉式(线程安全,调用效率不高,但是能延时加载)
 */
public class SingletonDemo2 {
    // 类初始化时,不初始化这个对象(延时加载,真正用的时候再创建)
    private static SingletonDemo2 instance;

    // 构造器私有化
    private SingletonDemo2(){}

    // 方法同步,调用效率低
    public static synchronized SingletonDemo2 getInstance() {
        if(instance == null) {
            instance = new SingletonDemo2();
        }
        return instance;
    }
}

③双重校验SingletonDemo3.java
package com.muse.patterns.singleton;

/**
 * @description 单例模式——双重校验
 */
public class SingletonDemo3 {

    private volatile static SingletonDemo3 singletonDemo3;

    private SingletonDemo3() {}

    public static SingletonDemo3 newInstance() {
        if (singletonDemo3 == null) {
            synchronized (SingletonDemo3.class) {
                if (singletonDemo3 == null) {
                    singletonDemo3 = new SingletonDemo3();
                }
            }
        }
        return singletonDemo3;
    }
}

④静态内部类SingletonDemo4.java
常见问题:为什么这种内部静态类的方式,是线程安全的?

答:首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的<clinit>方法。那么什么是<clinit>方法?这不是由程序员写的程序、而是根据代码由ivac编译器生成的。它是由类里面所有的“静态成员的的赋值语句】和静态代码块】组成的。JVM内部会保证一个类的<clinit>方法在多线程环境下被正确的加锁同步、也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<ciinit>方法,其他的线程都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>方法。也就是说同一个加载器下,个类型只会初始化一次。

        那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>代码,当我们执行getInstance方法的时候,会导致SingletonclassInstance类的加载、类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以他只会有一个实例。


import java.util.Arrays;

/**
 * 问题:为什么这种内部静态类的方式,是线程安全的?
 * 答:首先要了解类加载过程中的最后一个阶段:即类的初始化,类的初始化阶本质就是执行类构造器的<clinit>方法。那么什么是<clinit>方法?
 * 这不是由程序员写的程序,而是根据代码由javac编译器生成的。它是由类里面所有的【静态成员的的赋值语句】和【静态代码块】组成的。JVM内部会保证一个类的
 * <clinit>方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的<clinit>方法,其他的线程
 * 都要阻塞等待,直到这个线程执行完<clinit>方法。然后执行完<clinit>方法后,其他线程唤醒,但是不会再进入<clinit>方法。也就是说同一个加载器下,
 * 一个类型只会初始化一次。
 *
 * 那么回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个<clinit>代码,当我们执行getInstance方法的时候,会导致SingletonClassInstance
 * 类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的<clinit>代码也只会被执行一次,所以他只会有一个实例。
 *
 * @description 单例模式——静态内部类(线程安全,调用效率高,可以延时加载)
 */
public class SingletonDemo4 {
    private SingletonDemo4(){
        System.out.println("SingletonDemo4");
    }

    /** 静态内部类 */
    private static class SingletonClassInstance {
        private static final SingletonDemo4 instance = new SingletonDemo4();
    }

    /** 只有在第一次调用时,才会被创建,可以认为是懒加载的升级版本 */
    public static SingletonDemo4 getInstance(){
        return SingletonClassInstance.instance;
    }

    public static void main(String[] args) {
        System.out.println("args = " + Arrays.deepToString(args));
        SingletonDemo4.getInstance();
        SingletonDemo4.getInstance();
    }
}

⑤枚举类SingletonDemo5.java


/**
 * @description 单例模式——枚举类
 * @author: muse
 */
public enum SingletonDemo5 {

    // 枚举元素本身就是单例
    INSTANCE;

    // 添加自己需要的操作,直接通过SingletonDemo5.INSTANCE.doSomething()的方式调用即可。
    public void doSomething() {
        System.out.println("doSomething");
    }
}

  • 工厂模式(Factory Pattern)

   工厂模式就像一个产品工厂,将对象的创建和使用分离。简单工厂模式是工厂模式的基础,它定义了一个工厂类,负责创建产品对象。例如,有一个汽车工厂类,根据传入的参数来创建不同品牌的汽车对象。工厂方法模式在简单工厂模式的基础上,将工厂方法抽象出来,由具体的子类实现,这样每个子类可以创建不同类型的产品。抽象工厂模式则更为复杂,它提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类,适用于创建对象家族的场景。

简单工厂( Simple  Factory )

  定义:简单工厂其实不是一个设计模式,反而比较像是一种编程习惯。

工厂方法模式 ( Factory  Method )

 定义:定义了一个创建对象的接口(类或接口中的方法),但由子类决定要实例化的类是哪一个。工厂方法把实例化推迟到子类。

  • 抽象工厂模式(Abstract Factory Pattern)
  • 继续以汽车生产为例,如果不仅要生产不同品牌的汽车,还要为这些汽车搭配不同品牌的轮胎、座椅等配件,这就需要抽象工厂模式。抽象工厂模式允许客户端通过一个抽象的工厂接口来创建一系列相关的产品对象,而具体的创建工作由具体的工厂子类完成。这样,当需要新增一种品牌的汽车及其配件时,只需要创建一个新的具体工厂子类,而不需要修改客户端代码,提高了系统的可维护性和扩展性。
  • 建造者模式(Builder Pattern)
  • 建造者模式关注的是对象的创建过程。比如建造一座房子,需要先打地基、然后建造框架、再进行内部装修等一系列步骤。建造者模式将复杂对象的构建过程和表示分离,使得同样的构建过程可以创建不同的表示。在代码实现中,有一个指挥者类来控制建造过程,多个具体建造者类负责具体的建造步骤,最后由指挥者调用具体建造者来完成对象的构建。
  • 原型模式(Prototype Pattern)

        原型模式就像使用复印机复印文件,通过复制一个已有的对象来创建新的对象。当创建新对象的过程比较复杂或者成本较高时,原型模式就非常有用。例如,在游戏开发中,创建一个复杂的角色对象可能涉及到很多属性的初始化。如果使用原型模式,只需要先创建一个原型角色对象,然后通过克隆这个原型来快速创建多个相似的角色对象,提高了对象创建的效率。

定义:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。也就是说,这种不通过new关键字来产生一个对象,而是通过对象复制(Java中的clone或反序列化)来实现的模式,就叫做原型模式。

性能优良:原型模式是在内存二进制流的拷贝,要比直接new一个对象性能好很多,特别是要在一个循环内产生大量的对象时,原型模式可能更好地体现其优点。

逃避构造函数的约束,直接在内存中拷贝,构造函数是不会执行的。(Prototype.java)

浅拷贝与深拷贝示例。(Prototype1.java) 使用场景:① 类的初始化需要消耗非常多的资源。② 通过new产生一个对象需要非常繁琐的数据准备或访问。③ 一个对象需要提供给其他对象访问,而且每个调用者可能都需要修改其值时。

结构型模式:优化软件结构的策略

  1. 代理模式(Proxy Pattern)
    代理模式就像一个经纪人,代表真实对象进行操作。比如,在网络访问中,我们可能会通过代理服务器来访问外部网站。在软件中,当一个对象由于某些原因(如访问权限限制、创建开销大等)不能直接访问另一个对象时,可以通过代理对象来间接访问。代理对象可以在访问真实对象前后添加一些额外的逻辑,如权限验证、缓存处理等。
  2. 适配器模式(Adapter Pattern)
    想象你有一个欧式插头的电器,但家里只有美式插座,这时候就需要一个转换插头(适配器)来让电器正常使用。在软件中,适配器模式用于将一个类的接口转换成客户希望的另一个接口。当我们需要使用一个现有的类,但它的接口与我们的需求不兼容时,就可以使用适配器模式。例如,有一个旧的支付接口,新的业务系统需要与之集成,但新系统使用的是不同的接口规范,这时候就可以创建一个适配器类,将旧支付接口适配成新系统可以使用的接口。
  3. 桥接模式(Bridge Pattern)
    桥接模式将抽象部分与实现部分分离,使它们可以独立变化。以手机为例,手机有不同的品牌(抽象部分),每个品牌又可以有不同的操作系统(实现部分)。如果不使用桥接模式,可能每个品牌的手机都要和每个操作系统进行组合,导致类的数量急剧增加。而使用桥接模式,将品牌和操作系统分离,通过一个桥接接口进行关联,这样当新增一个品牌或者一个操作系统时,只需要添加相应的类,而不需要修改其他类的代码,提高了系统的可维护性和扩展性。
  4. 装饰器模式(Decorator Pattern)
    装饰器模式就像给蛋糕添加不同的装饰,在不改变对象本身的基础上,动态地给对象添加新的功能。比如,一个简单的咖啡对象,我们可以通过装饰器为它添加牛奶、糖、奶油等不同的配料,从而得到不同口味的咖啡。在软件中,当需要在运行时给对象添加功能,而又不想通过继承的方式来扩展类时,装饰器模式是一个很好的选择。它通过创建一个装饰器类,将被装饰的对象作为成员变量,在装饰器类中可以调用被装饰对象的方法,并添加新的功能。
  5. 外观模式(Facade Pattern)
    外观模式提供了一个统一的接口,用来访问子系统中的一群接口。就像电脑的开机键,按下它就可以启动电脑的各个硬件组件,而用户不需要了解每个硬件组件是如何启动的。在软件中,当一个系统由多个复杂的子系统组成,客户端需要与这些子系统进行交互时,使用外观模式可以提供一个简单的接口,隐藏子系统的复杂性,降低客户端与子系统之间的耦合度。
  6. 享元模式(Flyweight Pattern)
    享元模式旨在复用对象,减少对象的创建数量,以提高系统的性能。比如在一个围棋游戏中,棋盘上有大量的棋子,每个棋子的颜色、形状等属性是相同的,只是位置不同。我们可以将这些相同属性的棋子共享,只创建少量的棋子对象,通过改变它们的位置来表示不同的棋子。在软件中,当有大量相似对象存在,且这些对象的创建和销毁开销较大时,享元模式可以有效地减少内存占用,提高系统的运行效率。
  7. 组合模式(Composite Pattern)
    组合模式用于将对象组合成树形结构,以表示 “部分 - 整体” 的层次关系。例如,在一个文件系统中,文件夹可以包含文件和子文件夹,文件和文件夹都可以看作是节点,通过组合模式可以方便地对整个文件系统进行遍历、操作等。组合模式使得客户端可以统一对待单个对象和组合对象,简化了代码的实现。

行为型模式:优化软件行为的技巧

  1. 策略模式(Strategy Pattern)
    策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。比如,在一个电商系统中,计算商品运费可以有不同的算法,如按重量计算、按距离计算、按固定金额计算等。通过策略模式,将这些算法封装成不同的策略类,客户端可以根据不同的需求选择不同的运费计算策略,使得系统更加灵活,易于扩展新的算法。
  2. 模板方法模式(Template Method Pattern)
    模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中实现。比如,制作咖啡和制作茶都有一些相似的步骤,如烧水、冲泡等,但具体的冲泡方式不同。可以在一个抽象类中定义一个制作饮品的模板方法,包含烧水、冲泡等步骤,其中冲泡步骤由具体的咖啡类和茶类子类来实现,这样既复用了相同的代码逻辑,又能根据不同的需求进行定制。
  3. 观察者模式(Observer Pattern)
    观察者模式定义了一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。例如,在一个新闻发布系统中,当有新的新闻发布时,订阅该新闻频道的所有用户都会收到通知。在软件中,被观察的对象(主题)维护一个观察者列表,当主题状态改变时,遍历列表通知所有观察者。
  4. 迭代器模式(Iterator Pattern)
    迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示。比如,在一个集合类中,我们可以通过迭代器来遍历集合中的元素,而不需要了解集合的具体实现是数组还是链表。迭代器模式使得集合的遍历操作更加统一,并且可以在不改变集合结构的情况下增加新的遍历方式。
  5. 责任链模式(Chain of Responsibility Pattern)
    责任链模式将请求的发送者和接收者解耦,使多个对象都有机会处理这个请求。这些对象连成一条链,请求沿着链传递,直到有一个对象处理它为止。例如,在一个请假审批系统中,请假请求可能先由小组长审批,然后如果超过一定天数,再由部门经理审批,最后由总经理审批。通过责任链模式,将不同的审批者连接成一条链,请求在链上传递,每个审批者根据自己的职责决定是否处理该请求。
  6. 命令模式(Command Pattern)
    命令模式将一个请求封装为一个对象,从而使我们可以用不同的请求对客户进行参数化,对请求排队或者记录请求日志,以及支持可撤销的操作。比如在一个游戏中,玩家的操作(如攻击、移动等)可以封装成命令对象,这样可以方便地对这些操作进行管理,如记录操作日志、实现撤销功能等。
  7. 备忘录模式(Memento Pattern)
    备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。例如,在一个文本编辑器中,我们可以通过备忘录模式保存文本的某个历史版本,当需要撤销操作时,就可以从备忘录中恢复到之前的状态。
  8. 状态模式(State Pattern)
    状态模式允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。比如,一个手机有不同的状态,如开机、关机、静音等,在不同的状态下,手机对相同的操作(如来电)会有不同的反应。通过状态模式,将每个状态封装成一个状态类,手机对象根据当前的状态来决定如何响应操作,使得代码更加清晰,易于维护和扩展新的状态。
  9. 访问者模式(Visitor Pattern)
    访问者模式将数据结构与数据操作分离。当一个数据结构包含多种类型的元素,且需要对这些元素进行多种不同的操作时,使用访问者模式可以将这些操作封装到不同的访问者类中,而不需要在数据结构的类中添加大量的操作方法。例如,在一个图形系统中,有圆形、方形、三角形等不同的图形对象,现在需要对这些图形进行计算面积、计算周长等不同的操作。通过访问者模式,可以创建计算面积的访问者类和计算周长的访问者类,分别对不同的图形进行相应的操作。
  10. 中介者模式(Mediator Pattern)
    中介者模式用一个中介对象来封装一系列的对象交互。对象之间不再直接交互,而是通过中介者进行交互,这样可以降低对象之间的耦合度。比如在一个聊天系统中,多个用户之间的聊天消息不是直接发送给对方,而是通过服务器(中介者)来转发。在软件中,当多个对象之间存在复杂的交互关系,导致代码难以维护时,使用中介者模式可以将这些交互逻辑集中到中介者对象中,简化对象之间的关系。
  11. 解释器模式(Interpreter Pattern)
    解释器模式给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。例如,在一个简单的数学表达式解析系统中,我们可以定义一个文法来表示数学表达式,然后创建一个解释器来解析和计算这些表达式的值。解释器模式适用于一些特定的领域,当需要处理一些简单的语言解析任务时,可以使用这种模式。

总结

        六大设计原则和 23 种设计模式构成了软件设计的核心知识体系。六大设计原则为我们提供了高层次的指导方针,帮助我们从宏观角度构建合理、可维护的软件架构。而 23 种设计模式则是具体的实践工具,针对不同的场景和问题,提供了经过验证的解决方案。

        在实际的软件开发过程中,我们需要根据具体的业务需求和场景,灵活运用这些原则和模式。它们不是孤立存在的,而是相互关联、相互补充的。例如,在使用单例模式确保系统中某些资源唯一的同时,可能会遵循单一职责原则,让单例类专注于一项核心功能;在构建复杂系统结构时,结构型模式与行为型模式可能会协同工作,以优化系统的结构和行为。

        深入理解并熟练掌握这些原则和模式,不仅能够提高我们的代码质量和开发效率,更能培养我们良好的软件设计思维。随着技术的不断发展和业务需求的日益复杂,软件设计的艺术也在不断演进,但六大设计原则和


网站公告

今日签到

点亮在社区的每一天
去签到