本文最后更新于 2025年4月8日 晚上

设计模式(结构型)
结构型设计模式关注如何将现有的类或对象组织在一起形成更加强大的结构。并且根据我们前面学习的合成复用原则,我们该如何尽可能地使用关联关系来代替继承关系是我们本版块需要重点学习的内容。
类/对象适配器模式
在生活中,我们经常遇到这样的一个问题:笔记本太轻薄了,以至于没有RJ45网口和USB A口(比如Macbook为了轻薄甚至全是type-c形式的雷电口)但是现在我们因为工作需要,又得使用这些接口来连接线缆,这时我们想到的第一个解决方案,就是去买一个转接口(扩展坞),扩展坞可以将type-c口转换为其他类型的接口供我们使用,实际上这就是一种适配模式。

由于我们的电脑没有这些接口,但是提供了type-c类型的接口,虽然接口类型不一样,但是同样可以做其他接口能做的事情,比如USB文件传输、有线网络连接等,所以,这个时候,我们只需要添加一个中间人来帮我们转换一下接口形态即可。包括我们常用的充电头,为什么叫电源适配器呢?我们知道传统的供电是220V交流电,但是我们的手机可能只需要5V的电压进行充电,虽然现在有电,但是不能直接充,我们也不可能让电力公司专门为我们提供一个5V的直流电使用。这时电源适配器就开始发挥作用了,比如苹果的祖传5V1A充电头,实际上就是将220V交流电转换为5V的直流电进行传输,这样就相当于在220V交流电和我们的手机之前,做了一个适配器的角色。
在我们的Java程序中,也会经常遇到这样的问题,比如:
1 2 3 4 5 6
| public class TestSupplier {
public String doSupply(){ return "iPhone 14 Pro"; } }
|
1 2 3 4 5 6 7 8 9 10
| public class Main { public static void main(String[] args) { TestSupplier supplier = new TestSupplier(); test( ? ); }
public static void test(Target target){ System.out.println("成功得到:"+target.supply()); } }
|
1 2 3 4
| public interface Target {
String supply(); }
|
这个时候,我们就可以使用适配器模式了,适配器模式分为类适配器和对象适配器,我们首先来看看如何使用类适配器解决这种问题,我们直接创建一个适配器类:
1 2 3 4 5 6 7
| public class TestAdapter extends TestSupplier implements Target { @Override public String supply() { return super.doSupply(); } }
|
这样,我们就得到了一个Target类型的实现类,并且同时采用的是TestSupplier提供的实现。
1 2 3 4 5 6 7 8
| public static void main(String[] args) { TestAdapter adapter = new TestAdapter(); test(adapter); }
public static void test(Target target){ System.out.println("成功得到:"+target.supply()); }
|
不过,这种实现方式需要占用一个继承坑位,如果此时Target不是接口而是抽像类的话,由于Java不支持多继承,那么就无法实现了。同时根据合成复用原则,我们应该更多的通过合成的方式去实现功能,所以我们来看看第二种,也是用的比较多的一种模式,对象适配器:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class TestAdapter implements Target{
TestSupplier supplier; public TestAdapter(TestSupplier supplier){ this.supplier = supplier; } @Override public String supply() { return supplier.doSupply(); } }
|
现在,我们就将对象以组合的形式存放在TestAdapter中,依然是通过存放的对象调用具体实现。
桥接模式
相信各位都去奶茶店买过奶茶,在购买奶茶的时候,店员首先会问我们,您需要什么类型的奶茶,比如我们此时点了一杯啵啵芋圆奶茶,接着店员会直接问我们需要大杯、中杯还是小杯,最后还会询问我们需要加什么配料,比如椰果、珍珠等,最后才会给我们制作奶茶。

那么现在让你来设计一下这种模式的Java类,该怎么做呢?首先我们要明确,一杯奶茶除了类型之外,还分大中小杯,甚至可能还分加什么配料,这个时候,如果我们按照接口实现的写法:
1 2 3
| public interface Tea { String getType(); }
|
1 2 3
| public interface Size { String getSize(); }
|
比如现在我们创建一个新的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public class LargeKissTea implements Tea, Size{
@Override public String getSize() { return "大杯"; }
@Override public String getType() { return "芋圆啵啵奶茶"; } }
|
虽然这样设计起来还挺合理的,但是如果现在我们的奶茶品种多起来了,并且每种奶茶都有大中小杯,现在一共有两个维度需要考虑,那么我们岂不是得一个一个去创建这些类?甚至如果还要考虑配料,那么光创建类就得创建不知道多少个了。显然这种设计不太好,我们得换个方式。
这时,就可以使用我们的桥接模式了,现在我们面临的问题是,维度太多,不可能各种类型各种尺寸的奶茶都去创建一个类,那么我们就还是单独对这些接口进行简单的扩展,单独对不同的维度进行控制,但是如何实现呢?我们不妨将奶茶的类型作为最基本的抽象类,然后对尺寸、配料等属性进行桥接:
1 2 3 4 5 6 7 8 9 10
| public abstract class AbstractTea { protected Size size; protected AbstractTea(Size size){ this.size = size; } public abstract String getType(); }
|
不过这个抽象类提供的方法还不全面,仅仅只有Tea的getType方法,我们还需要添加其他维度的方法,所以继续编写一个子类:
1 2 3 4 5 6 7 8 9
| public abstract class RefinedAbstractTea extends AbstractTea{ protected RefinedAbstractTea(Size size) { super(size); } public String getSize(){ return size.getSize(); } }
|
现在我们只需要单独为Size创建子类即可:
1 2 3 4 5 6 7
| public class Large implements Size{
@Override public String getSize() { return "大杯"; } }
|
现在我们如果需要一个大杯的啵啵芋圆奶茶,只需要:
1 2 3 4 5 6 7 8 9 10
| public class KissTea extends RefinedAbstractTea{ protected KissTea(Size size) { super(size); }
@Override public String getType() { return "啵啵芋圆奶茶"; } }
|
现在我们就将两个维度拆开,可以分别进行配置了:
1 2 3 4 5
| public static void main(String[] args) { KissTea tea = new KissTea(new Large()); System.out.println(tea.getType()); System.out.println(tea.getSize()); }
|
通过桥接模式,使得抽象和实现可以沿着各自的维度来进行变化,不再是固定的绑定关系。
组合模式
组合模式实际上就是将多个组件进行组合,让用户可以对它们进行一致性处理。比如我们的文件夹,一个文件夹中可以有很多个子文件夹或是文件:

它就像是一个树形结构一样,有分支有叶子,而组合模式则是可以对整个树形结构上的所有节点进行递归处理,比如我们现在希望将所有文件夹中的文件的名称前面都添加一个前缀,那么就可以使用组合模式。

组合模式的示例如下,这里我们就用文件和文件夹的例子来讲解:
1 2 3 4 5 6 7 8 9
|
public abstract class Component { public abstract void addComponent(Component component); public abstract void removeComponent(Component component); public abstract Component getChild(int index); public abstract void test(); }
|
接着我们来编写两种实现类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public class Directory extends Component{
List<Component> child = new ArrayList<>();
@Override public void addComponent(Component component) { child.add(component); }
@Override public void removeComponent(Component component) { child.remove(component); }
@Override public Component getChild(int index) { return child.get(index); }
@Override public void test() { child.forEach(Component::test); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class File extends Component{
@Override public void addComponent(Component component) { throw new UnsupportedOperationException(); }
@Override public void removeComponent(Component component) { throw new UnsupportedOperationException(); }
@Override public Component getChild(int index) { throw new UnsupportedOperationException(); }
@Override public void test() { System.out.println("文件名称修改成功!"+this); } }
|
最后,我们来测试一下:
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) { Directory outer = new Directory(); Directory inner = new Directory(); outer.addComponent(inner); outer.addComponent(new File()); inner.addComponent(new File()); inner.addComponent(new File()); outer.test(); }
|
可以看到我们对最外层目录进行操作后,会递归向下处理当前目录和子目录中所有的文件。
装饰模式
装饰模式就像其名字一样,为了对现有的类进行装饰。比如一张相片就一张纸,如果直接贴在墙上,总感觉少了点什么,但是我们给其添加一个好看的相框,就会变得非常对味。装饰模式的核心就在于不改变一个对象本身功能的基础上,给对象添加额外的行为,并且它是通过组合的形式完成的,而不是传统的继承关系。
比如我们现在有一个普通的功能类:
1 2 3
| public abstract class Base { public abstract void test(); }
|
1 2 3 4 5 6
| public class BaseImpl extends Base{ @Override public void test() { System.out.println("我是业务方法"); } }
|
不过现在的实现类太单调了,我们来添加一点装饰上去:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Decorator extends Base{
protected Base base;
public Decorator(Base base) { this.base = base; }
@Override public void test() { base.test(); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class DecoratorImpl extends Decorator{
public DecoratorImpl(Base base) { super(base); }
@Override public void test() { System.out.println("装饰方法:我是操作前逻辑"); super.test(); System.out.println("装饰方法:我是操作后逻辑"); } }
|
这样,我们就通过装饰模式对类的功能进行了扩展:
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) { Base base = new BaseImpl(); Decorator decorator = new DecoratorImpl(base); Decorator outer = new DecoratorImpl(decorator);
decorator.test();
outer.test(); }
|
这样我们就实现了装饰模式。
代理模式
代理模式和装饰模式很像,初学者很容易搞混,所以这里我们得紧接着来讲解一下。首先请记住,当无法直接访问某个对象或访问某个对象存在困难时,我们就可以通过一个代理对象来间接访问。
实际上代理在我们生活中处处都存在,比如手机厂商要去销售手机,但是手机厂商本身没有什么渠道可以大规模地进行售卖,很难与这些消费者进行对接,这时就得交给代理商去进行出售,比如Apple在中国的直营店很少,但是在中国的授权经销商却很多,手机厂商通过交给旗下代理商的形式来进行更大规模的出售。比如我们经常要访问Github,但是直接连接会发现很难连的上,这时我们加了一个代理就可以轻松访问,也是在体现代理的作用。

同时,代理类需要保证客户端使用的透明性,也就是说操作起来需要与原本的真实对象相同,比如我们访问Github只需要输入网址即可访问,而添加代理之后,也是使用同样的方式去访问Github,所以操作起来是一样的。包括Spring框架其实也是依靠代理模式去实现的AOP记录日志等。
比如现在有一个目标类,但是我们现在需要通过代理来使用它:
1 2 3
| public abstract class Subject { public abstract void test(); }
|
1 2 3 4 5 6 7
| public class SubjectImpl extends Subject{
@Override public void test() { System.out.println("我是测试方法!"); } }
|
现在我们为其建立一个代理类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class Proxy extends Subject{
Subject target;
public Proxy(Subject subject){ this.target = subject; }
@Override public void test() { System.out.println("代理前绕方法"); target.test(); System.out.println("代理后绕方法"); } }
|
乍一看,这不跟之前的装饰模式一模一样吗?
对装饰器模式来说,装饰者和被装饰者都实现同一个接口/抽象类。对代理模式来说,代理类和被代理的类都实现同一个接口/抽象类,在结构上确实没有啥区别。但是他们的作用不同,装饰器模式强调的是增强自身,在被装饰之后你能够在被增强的类上使用增强后的功能,增强后你还是你,只不过被强化了而已;代理模式强调要让别人帮你去做事情,以及添加一些本身与你业务没有太多关系的事情(记录日志、设置缓存等)重点在于让别人帮你做。
装饰模式和代理模式的不同之处在于思想。
当然实现代理模式除了我们上面所说的这种方式之外,我们还可以使用JDK为我们提供的动态代理机制,我们不再需要手动编写继承关系创建代理类,它能够在运行时通过反射机制为我们自动生成代理类:
1 2 3
| public interface Subject { void test(); }
|
1 2 3 4 5 6 7
| public class SubjectImpl implements Subject{
@Override public void test() { System.out.println("我是测试方法!"); } }
|
接着我们需要创建一个动态代理的处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class TestProxy implements InvocationHandler {
private final Object object;
public TestProxy(Object object) { this.object = object; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理的对象:"+proxy.getClass()); Object res = method.invoke(object, args); System.out.println("方法调用完成,返回值为:"+res); return res; } }
|
最后我们来看看如何创建一个代理类:
1 2 3 4 5 6 7 8 9
| public static void main(String[] args) { SubjectImpl subject = new SubjectImpl(); InvocationHandler handler = new TestProxy(subject); Subject proxy = (Subject) Proxy.newProxyInstance( subject.getClass().getClassLoader(), subject.getClass().getInterfaces(), handler); proxy.test(); }
|
运行一次,可以看到调用代理类的方法,最终会走到我们的invoke方法中进行:

根据接口,代理对象是com.sun.proxy.$Proxy0
类(看名字就知道不对劲),这个类是动态生成的,我们也找不到具体的源代码。
不过JDK提供的动态代理只能使用接口,如果换成我们一开始的抽象类,就没办法了,这时我们可以使用一些第三方框架来实现更多方式的动态代理,比如Spring都在使用的CGLib框架,Maven依赖如下:
1 2 3 4 5
| <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.1</version> </dependency>
|
由于CGlib底层使用ASM框架(JVM篇视频教程有介绍)进行字节码编辑,所以能够实现不仅仅局限于对接口的代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class TestProxy implements MethodInterceptor {
private final Object target;
public TestProxy(Object target) { this.target = target; }
@Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { System.out.println("现在是由CGLib进行代理操作!"+o.getClass()); return method.invoke(target, objects); } }
|
接着我们来创建一下代理类:
1 2 3 4 5 6 7 8 9 10 11
| public static void main(String[] args) { SubjectImpl subject = new SubjectImpl();
Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(SubjectImpl.class); enhancer.setCallback(new TestProxy(subject));
SubjectImpl proxy = (SubjectImpl) enhancer.create();
proxy.test(); }
|
可以看到,效果其实是差不多的:

可以看到代理类是包名.SubjectImpl$$EnhancerByCGLIB$$47f6ed3a
,也是动态生成的一个类,所以我们无法去查看源码,不过此类是继承自我们指定的类型的。
外观模式
你是否经历过类似的情况:今年计算机学院的奖学金评定工作开始了,由于你去年一不小心拿了个ACM的区域赛金牌,觉得自己又行了,于是也想参与到奖学金的争夺中,首先你的辅导员会通知你去打印你的获奖材料,然后你高高兴兴拿给辅导员之后,辅导员又给了你一张表,让你打印了之后填写一下,包括你的个人信息还有一些个人介绍,完成后,你本以为可以坐等发奖了,结果辅导员又跟你说我们评定还要去某某地方盖章,盖完章还要去找谁谁谁签字,最后还要参加一下答辩… 看着如此复杂的流程,你瞬间不想搞了。

实际上我们生活中很多时候都是这样,可能在办一件事情的时候,由于部门职能的不同,你得各个部门到处跑,你肯定会抱怨一句,就不能有个人来统一一下吗,就不能在一个地方一起把事情都办了吗?这时,我们就可以用到外观模式了。
外观模式充分体现了迪米特法则。可能我们的整个项目有很多个子系统,但是我们可以在这些子系统的上面加一个门面(Facade)当我们外部需要与各个子系统交互时,无需再去直接使用各个子系统,而是与门面进行交互,再由门面与后面的各个子系统操作,这样,我们以后需要办什么事情,就统一找门面就行了。这样的好处是,首先肯定方便了代码的编写,统一找门面就行,不需要去详细了解子系统,并且,当子系统需要修改时,也只需要修改门面中的逻辑,不需要大面积的变动,遵循迪米特法则尽可能少的交互。

比如现在我们设计了三个子系统,分别是排队、结婚、领证,正常情况下我们是需要分别去找这三个部门去完成的,但是现在我们通过门面统一来完成:
1 2 3 4 5
| public class SubSystemA { public void test1(){ System.out.println("排队"); } }
|
1 2 3 4 5
| public class SubSystemB { public void test2(){ System.out.println("结婚"); } }
|
1 2 3 4 5
| public class SubSystemC { public void test3(){ System.out.println("领证"); } }
|
现在三个系统太复杂了,我们添加一个门面:
1 2 3 4 5 6 7 8 9 10 11 12
| public class Facade {
SubSystemA a = new SubSystemA(); SubSystemB b = new SubSystemB(); SubSystemC c = new SubSystemC();
public void marry(){ a.test1(); b.test2(); c.test3(); } }
|
现在我们只需要一个门面就能直接把事情办完了:
1 2 3 4
| public static void main(String[] args) { Facade facade = new Facade(); facade.marry(); }
|
通过使用外观模式,我们就大大降低了类与类直接的关联程度,并且简化了流程。
享元模式
最后我们来看看享元模式(Flyweight),那么这个”享元”代表什么意思呢?我们先来看看下面的问题:
1 2 3 4
| public static void main(String[] args) { String str1 = "abcdefg"; String str2 = "abcd"; }
|
我们发现上面的例子中,两个字符串虽然长短不同,但是却包含了一段相同的部分,那么现在我们如果要对内存进行优化:
1 2 3 4 5
| public static void main(String[] args) { String str1 = "efg"; String str2 = "abcd"; System.out.println("str1 = "+str2+str1); }
|
而享元模式就是这个思想,我们可以将那些重复出现的内容作为共享部分取出,这样当我们拥有大量对象时,我们把其中共同的部分抽取出来,由于提取的部分是多个对象共享只有一份,那么就可以减轻内存的压力。包括我们的围棋,实际上我们只需要知道棋盘上的各个位置是黑棋还是白棋,实际上没有毕业创建很多个棋子对象,我们只需要去复用一个黑棋和一个白棋子对象即可。
比如现在我们有两个服务,但是他们都需要使用数据库工具类来操作,实际上这个工具类没必要创建多个,我们这时就可以使用享元模式,让数据库工具类作为享元类,通过享元工厂来提供一个共享的数据库工具类:
1 2 3 4 5
| public class DBUtil { public void selectDB(){ System.out.println("我是数据库操作..."); } }
|
1 2 3 4 5 6 7
| public class DBUtilFactory { private static final DBUtil UTIL = new DBUtil();
public static DBUtil getFlyweight(){ return UTIL; } }
|
最后当我们需要使用享元对象时,直接找享元工厂要就行了:
1 2 3 4 5 6 7
| public class UserService {
public void service(){ DBUtil util = DBUtilFactory.getFlyweight(); util.selectDB(); } }
|
当然,这只是简单的享元模式实现,实际上我们一开始举例的String类,也在使用享元模式进行优化,比如下面的代码:
1 2 3 4 5 6 7
| public static void main(String[] args) { String str1 = "abcd"; String str2 = "abcd"; String str3 = "ab" + "cd"; System.out.println(str1 == str2); System.out.println(str1 == str3); }
|
虽然我们这里定义了三个字符串,但是我们发现,这三个对象指向的都是同一个对象,这是为什么呢?实际上这正是Java语言实现了数据的共享,想要了解具体实现请前往JVM篇视频教程。