深入解析Java设计模式:智慧演绎与代码工程化的基石

文章转载参考 1:智慧在进行演绎,并且无处不在。这是 CSDN 博客中关于 J2SE、Java 基础研究以及经验领域博主的内容。

深入解析Java设计模式:智慧演绎与代码工程化的基石

Java 中有一些常用的设计模式,这些设计模式在 Java 中被经常使用。Superme-CSDN 博客上有关于 Java 中常用设计模式的相关文章,文章的转载参考为 2。

目录

设计模式(Design Patterns)

一、什么是设计模式

还可保证代码的可靠性。毫无疑问,设计模式对自己、对他人以及对系统都是有益的,是多赢的。它让代码编制真正实现了工程化。设计模式是软件工程的重要基础,就如同大厦由一块块砖石构建而成一样。项目中合理运用设计模式能很好地解决诸多问题。每种模式在现实中都有相应原理与之对应。每一个模式都描述了一个在我们周围不断重复出现的问题,以及该问题的核心解决方案,这便是它能被广泛应用的原因。

二、设计模式的六大原则

1、开闭原则(Open Close Principle)

开闭原则意味着对扩展开放而对修改关闭。当程序需要拓展时,不可去修改原有的代码,以实现热插拔的效果。概括来说,就是为让程序具有良好的扩展性且易于维护和升级。要达到这样的效果,我们需使用接口和抽象类,后续具体设计中会提及这一点。

里氏代换原则指的是在软件系统中,子类对象能够替换父类对象并且保持程序的正确性和稳定性。也就是说,子类应该可以在不影响程序其他部分的情况下,替换父类的对象。如果子类不能满足这个条件,那么就可能会导致程序出现错误或者不稳定的情况。

里氏代换原则是面向对象设计的基本原则之一。在里氏代换原则中表明,只要是基类能够出现的地方,那么子类就一定能够在那里出现。LSP 是继承复用的重要基础。只有当衍生类能够替换基类,且软件单位的功能不会受到影响时,基类才能够真正被复用,同时衍生类也能够在基类的基础上添加新的行为。里氏代换原则对“开-闭”原则起到了补充作用,而实现“开-闭”原则的关键步骤是进行抽象化。基类与子类存在继承关系,这种继承关系就是抽象化的具体体现。因此,里氏代换原则对实现抽象化的具体步骤进行了规范。

依赖反转原则是指要依赖于抽象而不依赖于具体。具体来说,就是高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于具体,具体应该依赖于抽象。

这是开闭原则的基础。其具体内容为:要针对接口进行编程,并且依赖于抽象,而不能依赖于具体。

接口隔离原则是指将庞大复杂的接口拆分成多个粒度更小、更具体的接口,每个接口只负责完成特定的功能,而不是将所有的功能都集中在一个接口中,这样可以降低接口的复杂度,提高系统的可维护性和可扩展性。

这个原则表明:多个隔离的接口比单个接口更优。它还意味着降低类之间的耦合度。由此我们可以看出,设计模式实际上是一种软件设计思想,从大型软件架构的角度出发,是为了便于升级和维护。所以在上文中多次提及:降低依赖,降低耦合。

迪米特法则也被称作最少知道原则,其英文名为 Demeter Principle。

为什么叫最少知道原则呢?意思是:一个实体应当尽量减少与其他实体之间的相互作用,这样就能使系统的功能模块保持相对独立。

合成复用原则指的是 Composite Reuse Principle 这一原则。它强调在软件设计中,通过组合或聚合等方式来复用代码和行为,而不是通过继承。这样可以提高代码的灵活性和可维护性,避免继承带来的复杂性和脆弱性。同时,合成复用原则也有助于实现代码的高内聚和低耦合,使系统更加易于扩展和修改。

原则是尽量使用合成/聚合的方式,而不是使用继承。

三、设计模式的三大类

总体来说设计模式分为三大类:

创建型模式包含 5 种,分别是工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式有 7 种,分别是适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式包含 11 种,分别是:策略模式;模板方法模式;观察者模式;迭代子模式;责任链模式;命令模式;备忘录模式;状态模式;访问者模式;中介者模式;解释器模式。

其实存在两类模式,分别是并发型模式和线程池模式。可以用一个图片来对其进行整体的描述:

根据作用范围来分

按照模式主要用于的方面来划分,这种方式可分为类模式和对象模式。一种是主要用于类上的模式,另一种是主要用于对象上的模式。

对象模式用于处理对象之间的关系,这些关系能够通过组合或聚合来达成,在运行时刻是可以发生变化的,具有更强的动态性。

类模式

工厂方法

(类)适配器

模板方法、解释器

对象模式

单例

原型

抽象工厂

建造者

代理

(对象)适配器

桥接

装饰

外观

享元

组合

策略

命令

职责链

状态

观察者

中介者

迭代器

访问者

备忘录

四、Java的二十三种设计模式

从这一块开始,我们详细介绍 Java 中 23 种设计模式的相关情况,包括概念以及应用场景等。并且结合这些设计模式的特点以及设计模式的原则来进行分析。

创建型模式有 5 种。它的作用是描述“怎样创建对象”。其主要特点是“将对象的创建与使用分离”。

1、单例模式(Singleton)

某个类只能生成一个实例,该类提供了一个全局访问点供外部获取这个实例,并且它的拓展是有限多例模式,这就是单例模式。

这样的模式有几个好处:

某些类创建较为频繁。对于一些大型对象而言,这会带来很大的系统开销。省去了 new 操作符,使得系统内存的使用频率降低,从而减轻了 GC 压力。像交易所的核心交易引擎这类类,控制着交易流程,倘若这类类可以创建多个,那么系统就会完全混乱。一个军队如果出现多个司令员同时指挥,那肯定会乱成一团。因此,只有采用单例模式,才能够确保核心交易服务器独立地控制整个流程。

优点:只有一个实例,节约了内存资源,提高了系统性能

缺点:

没有抽象层,不能扩展

职责过重,违背了单一性原则

首先我们写一个简单的单例类:

public class Singleton {
 
持有私有静态实例,以防止被引用。在此处将其赋值为 null,目的是实现延迟加载。
Singleton 的实例 instance 是私有的且为 null 。
 
	/* 私有构造方法,防止被实例化 */
	private Singleton() {
	}
 
	/* 静态工程方法,创建实例 */
如果 Singleton 实例不存在,就创建一个新的实例并返回;如果已经存在,则直接返回已有的实例。
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
 
如果该对象被用于序列化操作,那么能够确保该对象在序列化之前和之后保持一致。
	public Object readResolve() {
		return instance;
	}
}

这个类能够满足基本需求。然而,像此类毫无线程安全保护的类,如果将其置于多线程环境中,必然会出现问题。那么该如何解决呢?我们首先会想到给 getInstance 方法添加 synchronized 关键字,具体如下:

如果单例对象尚未创建,则创建单例对象;如果单例对象已经创建,则直接返回已创建的单例对象。获取单例对象的方法是静态同步的。
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}

但是,锁住的对象是通过 synchronized 关键字实现的。这种用法会导致性能下降,原因在于每次调用 getInstance()时都要对对象加锁。实际上,只有在首次创建对象时才需要加锁,之后就无需加锁了。因此,这个地方需要进行改进。我们可以改为以下方式:

静态的单例获取实例的方法是:如果实例为空,则创建一个新的实例并返回;如果实例已存在,则直接返回已有的实例。
		if (instance == null) {
			synchronized (instance) {
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}

解决了之前提及的问题,在内部加上了 synchronized 关键字。这意味着调用时无需加锁,仅在 instance 为 null 且创建对象时才需要加锁,性能有一定提升。但是,这种情况有可能存在问题,请看下面的情况:在 Java 指令里,创建对象与赋值操作是分别进行的。这意味着 instance = new Singleton(); 这条语句是分两步来执行的。但是 JVM 不能确保这两个操作的先后次序。这意味着,有可能 JVM 会先为新的 Singleton 实例分配空间,接着直接把该空间赋值给 instance 成员,随后才去初始化这个 Singleton 实例。如此一来就可能出现错误。我们以 A、B 两个线程为例:

①:A、B线程同时进入了第一个if判断

A 首先进入到 synchronized 块中。因为此时 instance 为 null,所以 A 会执行 instance = new Singleton(); 这个操作。

JVM内部有优化机制,它先画出了一些分配给 Singleton 实例的空白内存,接着将这些空白内存赋值给 instance 成员,不过在这个时候 JVM 还没有开始初始化这个实例。之后 A 离开了 synchronized 块。

B 进入到 synchronized 块中,因为此时 instance 不是 null 的,所以它立刻离开了 synchronized 块,接着将结果返回给调用该方法的程序。

此时 B 线程准备使用 Singleton 实例,然而发现该实例并未被初始化,所以错误就发生了。

所以程序存在发生错误的可能性。程序在运行过程较为复杂。从这一点能够看出,尤其是在编写多线程环境下的程序时,难度更大,更具挑战性。我们要对该程序进行进一步的优化。

同时也实现了延迟加载的效果。
Singleton 类创建了一个私有的静态实例 instance,这个实例是通过 new Singleton() 来初始化的。
    }         
如果 Singleton 实例尚未创建,那么创建一个新的 Singleton 实例;如果 Singleton 实例已经存在,直接返回该实例。
获取 SingletonFactory 的实例。
    } 

实际情况是,单例模式通过内部类来维持单例的实现。JVM 内部的机制具备这样的能力,即当一个类被加载时,这个类的加载过程会处于线程互斥的状态。当我们首次调用 getInstance 时,JVM 能确保 instance 仅被创建一次,还能保证给 instance 赋值的内存初始化完毕,如此我们就无需担忧上述问题。并且该方法仅在首次调用时使用互斥机制,从而解决了低性能问题。至此,我们暂且总结出一个完美的单例模式:

public class Singleton {
 
	/* 私有构造方法,防止被实例化 */
	private Singleton() {
	}
 
	/* 此处使用一个内部类来维护单例 */
这种设计模式在很多场景中都有广泛的应用。
静态的 Singleton 实例是通过 new Singleton()创建的。 这个实例被定义为私有的。 它是 Singleton 类的唯一实例。 这样可以确保在整个程序中只有一个 Singleton 实例存在。 当需要访问 Singleton 实例时,可以通过这个私有实例来进行操作。 这种方式实现了 Singleton 模式,使得 Singleton 类在程序中具有单例的特性。
	}
 
	/* 获取实例 */
	public static Singleton getInstance() {
返回 SingletonFactory 的实例。
	}
 
	/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
	public Object readResolve() {
		return getInstance();
	}
}

其实说它完美,这不一定。如果在构造函数中抛出异常,那么实例就永远无法得到创建,并且会出错。所以说,不存在十分完美的东西,我们只能依据实际情况,去选择最适合自己应用场景的实现方法。有人这样实现:我们只需在创建类的时候进行同步。只要把创建和 getInstance()分开,单独为创建加上 synchronized 关键字,也是可以的,因为这样就只需要在创建类时进行同步了。

public class SingletonTest {
 
静态的 SingletonTest 实例被初始化为 null 。
 
	private SingletonTest() {
	}
 
该方法是同步的。
		if (instance == null) {
创建了一个名为 instance 的对象,这个对象是 SingletonTest 类的实例。
		}
	}
 
通过该方法来获取 SingletonTest 的实例。
		if (instance == null) {
			syncInit();
		}
		return instance;
	}
}

如果考虑性能,整个程序只需创建一次实例,那么性能就不会受到影响。

补充:采用"影子实例"的办法为单例对象的属性同步更新

public class SingletonTest {
 
	private static SingletonTest instance = null;
这个类中有一个名为 properties 的成员变量,它的类型是 Vector,并且目前这个变量的值为 null。
 
获取属性的方法是这个 public Vector。 这个方法用于获取属性。 它会返回一个 Vector 对象。 这个 Vector 对象包含了相关的属性。
		return properties;
	}
 
	private SingletonTest() {
	}
 
	private static synchronized void syncInit() {
		if (instance == null) {
			instance = new SingletonTest();
		}
	}
 
	public static SingletonTest getInstance() {
		if (instance == null) {
			syncInit();
		}
		return instance;
	}
 
这些操作可能涉及到获取当前属性值、修改属性值以及保存修改后的属性等步骤。
创建了一个 SingletonTest 的实例 shadow 。
shadow 获取了属性,这些属性被赋值给了 properties 。
	}
}

通过单例模式的学习告诉我们:

单例模式理解较为容易,然而具体实现却存在一定难度。synchronized 关键字所锁定的是对象,在使用时,务必在合适的位置进行使用(要留意需要使用锁的对象以及过程,有时并非整个对象以及整个过程都需要锁)。

到这儿,单例模式基本上讲完了。在结尾处,笔者忽然想到另一个问题,那就是用类的静态方法来实现单例模式的效果是可行的。那么在此处,这两者有什么不同呢?

首先,从类的角度来看,静态类可以实现接口。然而,这样做会破坏静态的特性。因为接口中不允许有被 static 修饰的方法,所以即使静态类实现了接口,实现的方法也不是静态的。

其次,单例能够被延迟进行初始化。静态类通常在第一次加载时进行初始化。之所以会延迟加载,是因为存在一些类规模较大的情况,而延迟加载有助于提升性能。

首先,单例类是可以被继承的。其次,单例类的方法是可以被覆写的。然而,静态类内部的方法都是 static 的,所以无法被覆写。

最后一点,单例类比较灵活。因为从实现角度来看,它只是一个普通的 Java 类。只要能满足单例的基本需求,你就可以在其中随意实现一些其他功能。然而,静态类是不行的。从这些概括中,能大致看出二者的区别。然而,从另一个角度来看,我们最后实现的那个单例模式,其内部是通过一个静态类来实现的。因此,二者有着很大的关联,只是我们看待问题的层面有所不同罢了。只有将两种思想结合起来,才能够造就出完美的解决方案。就如同 HashMap 采用数组加链表来实现那样,实际上在生活中的很多事情都是如此。单独使用不同的方法来处理问题,往往既有优点也有缺点。而最完美的方法是,把各个方法的优点结合起来,这样才能最有效地解决问题。

拓展:多例设计模式

单例设计模式仅留存一个类的一个实例化对象,多例设计模式则会定义出多个对象。比如:有一个用于表示星期的操作类,该类的对象只能有 7 个实例化对象,分别是星期一、星期二、星期三、星期四、星期五、星期六、星期日;还有一个用于表示性别的类,其对象只能有 2 个实例化对象,即男和女;另外有一个用于表示颜色的操作类,其对象只能有 3 个实例化对象,分别是红、绿、蓝。在这种情况下,这样的类不应该被用户无限制地创建实例化对象,而应该只使用有限的几个。这属于多例设计模式。无论是单例设计模式还是多例设计模式,都有一个核心不可动摇,那就是构造器方法私有化。

class Sex{
    private String title;
静态私有常量 MALE 是一个 Sex 类型的对象,其值为"男"。
- 静态的私有常量 Sex 被设定为 FEMALE,这个 FEMALE 是一个新创建的 Sex 对象,其值是“女”。
 
构造器被私有化,其参数为 String 类型的 title 。
        this.title = title;
    }
 
    public String toString(){
        return this.title;
    }
    
如果 int 类型的变量 ch 满足一定条件,就可以获取到对应的 Sex 实例。具体来说,需要根据 ch 的值来确定获取哪种 Sex 实例。如果 ch 符合某种特定情况,就会返回相应的 Sex 对象;如果 ch 不符合该情况,就可能返回 null 或者其他默认的 Sex 实例。
        switch(ch){
            case 1:
                return MALE;
            case 2:
                return FEMALE;
            default:
                return null;
        }
    }
}
 
public class TestDemo{
公共静态 void 主函数(String 数组 args){
        Sex sex = Sex.getInstance(2);
        System.out.println(sex);
    }
}
 
 
==========程序执行结果=========
女

2、工厂方法模式(Factory Method)

工厂方法模式分为三种:

普通工厂模式是建立一个工厂类,用来创建实现了同一接口的一些类的实例。首先来看一下关系图:

举例如下:(我们举一个发送邮件和短信的例子)

首先,创建二者的共同接口:

public interface Sender {
	public void Send();
}

其次,创建实现类:

公共类 MailSender 实现了 Sender 接口。
	@Override
	public void Send() {
"this is mailsender!" 被 System.out 打印出来了 。
	}
}

公共类 SmsSender 实现了 Sender 接口。
    @Override
    public void Send() {
System 会输出 "this is sms sender!" 。
    }
}

最后,建工厂类:

public class SendFactory {
 
不同的类型会导致发送者生成不同的结果。
		if ("mail".equals(type)) {
			return new MailSender();
如果 type 等于 "sms",则执行以下操作。
			return new SmsSender();
		} else {
请输入正确的类型。
			return null;
		}
	}
}

我们来测试下:

public class FactoryTest {
 
公共静态 void 主函数(字符串数组 args){
创建了一个 SendFactory 类的实例 factory ,即 SendFactory factory = new SendFactory();
Sender 是通过 factory 生产出来的,生产的类型是 "sms"。
		sender.Send();
	}
}

输出:this is sms sender!

多个工厂方法模式是对普通工厂方法模式的一种改进。在普通工厂方法模式里,倘若传递的字符串出现错误,就无法正确地创建对象。然而,多个工厂方法模式会提供多个工厂方法,这些工厂方法分别用于创建对象。关系图如下:

对上面的代码进行修改,仅需改动 SendFactory 类,具体如下:

public class SendFactory {
	
	public Sender produceMail(){
		return new MailSender();
	}
	
	public Sender produceSms(){
		return new SmsSender();
	}
}

测试类如下:

public class FactoryTest {
 
	public static void main(String[] args) {
		SendFactory factory = new SendFactory();
		sender.Send();
	}
}

输出:this is mailsender!

静态工厂方法模式,就是把上述多个工厂方法模式中的那些方法设置为静态的。这样一来,就不需要去创建实例了,直接调用这些静态方法就可以了。

public class SendFactory {
	
静态的 Sender 能够产出邮件。
		return new MailSender();
	}
	
Sender 具备生成短信的功能(为静态的)。
		return new SmsSender();
	}
}

public class FactoryTest {
 
公共静态 void 主函数(字符串数组 args){
Sender 是通过 SendFactory 生成的邮件发送者,即 Sender sender = SendFactory.produceMail(); 表示创建了一个邮件发送者对象 sender 。
		sender.Send();
	}
}

输出:this is mailsender!

总体而言,工厂模式适用于以下情况:当有大量产品需要创建,并且这些产品具有共同接口时,能够通过工厂方法模式来进行创建。在上述的三种模式里,第一种模式如果传入的字符串出现错误,就无法正确创建对象。而第三种模式相较于第二种模式,不需要实例化工厂类。因此,在大多数情况下,我们会选择第三种模式,也就是静态工厂方法模式。

好处:客户端不需要创建对象,明确了各个类的职责

该工厂类的缺点在于负责创建所有实例。倘若有新的类加入进来,就必须不断地对工厂类进行修改,这样的情况不利于后期的维护工作。

3、抽象工厂模式

工厂方法模式存在一个问题,即类的创建依赖工厂类。这意味着,倘若要拓展程序,就必须对工厂类进行修改,而这违背了闭包原则。因此,从设计角度来看,它存在一定的问题。那么如何解决呢?这时就用到了抽象工厂模式,通过创建多个工厂类,一旦需要增加新的功能,只需直接增加新的工厂类即可,无需修改之前的代码。由于抽象工厂不太容易被理解,所以我们先来看图,接着再结合代码,这样就会比较容易理解了。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

26阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

26阅读