设计模式-结构型模式
一、适配器模式
适配器模式(Adapter pattern)是一种结构型设计模式,通过创建一个适配器类将一个接口转换成客户端所期望的另一个接口。
换句话说,就是当两个东西原本接口不匹配,"但我们又想让它们协作时,就可以用个“适配器”做中间桥梁。
1.1 为什么要使用适配器模式
适配器模式的目的是为了应对不同类或系统间接口不兼容的问题。在开发过程中我们经常会遇到需要与已有系统或第三方库进行交互的情况。如果这些系统的接口和我们自己的系统不兼容,直接进行交互会很困难,这时就可以使用适配器模式。通过创建一个适配器,我们能够将第三方库或旧系统的接口转化为自己需要的接口,从而避免了大规模修改现有代码的复杂性和不必要的风险。适配器模式使得代码更加灵活,能够平滑过渡到不同的技术栈和系统,同时还可以在不改变客户端代码的前提下,扩展和引入新的功能。
为了让大家更好地感受到适配器模式的作用,以音频播放器为例,我们需要支持多种音频格式(MP3、VLC、MP4等)的播放。让我们来看看使用和不使用适配器模式的区别:

1.2 适配器模式的应用场景
举一些开发中典型的应用场景:
旧系统与新系统对接: 在企业级应用中,许多老旧系统的接口不符合现代需求(例如,返回的数据格式不同、方法签名不匹配等),通过适配器模式可以将新系统所需的接口转换成老系统能提供的格式,使新旧系统能够无缝对接。
不同数据库引擎访问: 在一些跨平台或跨数据库的应用中,可能会同时使用 MySQL、PostgreSQL、Oracle 等不同数据库。通过适配器模式,可以将不同数据库的查询接口统一,使得开发人员可以通过统一的 API访问不同的数据库。
不同格式的文件读取与处理: 例如系统需要处理不同格式的文件(如 CSV、Excel、JSON、XML等)。在没有统一接口的情况下,可以使用适配器模式为每种文件格式提供适配器,使得客户端可以通过统一的接口进行文件读取和数据解析。
音频播放器支持多种格式播放: 不同音频格式的播放实现通常来源于不同的解码库,接口风格也各不相同。可以为每种格式封装一个适配器,统实现一个播放器接口,让主播放器只关心“播放"这个动作,不关心具体格式和库。
1.3 适配器模式基本结构
目标接口(Target):客户端希望使用的接口。
适配器类(Adapter):实现了目标接口,并且通过委托的方式将请求转换为适配的接口调用。
适配者类(Adaptee):已有的、需要适配的类。
客户端(Client):通过目标接口与系统交互的代码。
1.4 适配器模式代码实现
以“多格式音频播放"为例,我们用适配器模式实现一个简单的播放器系统。
1.5 适配器模式的优缺点
优点:
解耦系统: 适配器模式通过将不兼容的接口适配为客户端所需的接口,能够有效地解耦系统中的不同模块,使得模块之间的依赖关系更加松散,增强系统的灵活性。
复用现有类: 通过适配器模式,可以复用已有的类或库,而无需修改原有代码。适配器将现有类的接口转换为需要的接口,从而使不兼容的类能够共同工作。
增强可扩展性: 适配器模式提供了一种灵活的方式来扩展现有系统,允许不同的系统组件无缝集成。可以通过添加新的适配器来支持新的接口和功能,而不影响现有系统的正常运行。
简化接口: 适配器模式可以将复杂的接口转换为简单的接口,简化客户端代码的使用,提升开发效率。
缺点:
增加系统复杂性: 适配器模式引入了额外的适配器类,尤其在适配多个接口时,可能导致系统中的类数量增加,从而增加了系统的复杂性和维护成本。
性能开销: 每次调用适配器时,都需要通过适配器进行额外的转化处理,这可能会引入一定的性能开销,尤其在频繁调用的场景下,性能损失可能不可忽视。
潜在的过度使用: 适配器模式的滥用可能导致设计过度复杂,特别是在接口之间差异较小时,可能不值得引入适配器,而直接通过调整接口即可解决问题。过多的适配器可能导致系统变得难以理解和维护。
不适合所有场景: 适配器模式主要适用于接口不兼容的情况,对于接口设计不合理的情况,可能并不适用。过度依赖适配器可能掩盖了系统设计的问题,造成长期维护上的困难。
二、代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,它的核心思想就是: 给某个对象提供一个代理对象,并由代理对象来控制对这个对象的访问。听起来像是在加一道”门”,任何人想找目标对象打交道,都必须先经过这个"门",而这个门就是我们说的代理。
2.1 为什么要使用代理模式
我相信很多初学者都会和我有一样的疑问,我们明明可以直接和目标对象打交道,为什么还要绕个弯子找代理呢?
核心原因是--控制复杂性,提升系统的灵活性和可维护性。有时候,我们不希望或不能直接访问某个对象,可能是因为这个对象太"贵"了,太复杂了,或者安全层面不允许别人直接访问。我们希望做一些“前置处理“或者”后置增强”,比如判断权限、打印日志、做缓存、懒加载这些行为。这个时候,如果把这些逻辑全塞进目标对象里,那目标对象的职责就变得很重,代码也不好维护。代理式的好处就是:可以在不改变原始类代码的前提下,对原始对象进行功能增强或者访问控制。
为了让大家更好地感受到代理模式的作用,以图片加载为例,我们需要在加载大图片时显示加载进度,并在图片加载完成后显示。让我们来看看使用和不使用代理模式的区别:

通过对比可以看出,不使用代理模式时,图片加载的逻辑直接写在 ImageLoader 类中,这导致了职责混乱、代码耦合度高、难以扩展等问题。当需要添加新的图片处理功能时,需要修改现有代码,增加了维护成本。
而使用代理模式后,我们通过 ImageProxy 类将图片加载的控制逻辑与实际图片加载分离,实现了关注点分离。代理模式通过提供一个代理对象来控制对实际对象的访问,使得代码结构更加清晰,职责划分更加明确。这种实现方式不仅提言了代码的可维护性,还使得添加新的图片处理功能变得更加简单,同时保持了代码的灵活性和可扩展性。
2.2 代理模式的应用场景
举一些开发中典型的应用场景:
权限代理: 在系统中有权限控制时,代理模式可用于控制对某些功能或资源的访问。例如,某些用户或角色只能访问特定的资源或方法,通过代理来拦截调用,验证用户权限后再决定是否允许执行操作。
日志记录代理: 在业务系统中,操作日志的记录是非常常见的需求。代理模式可以用于方法调用的拦截,在方法执行前后自动记录日志信息,而不需要修改原始业务逻辑。例如,在一个订单系统中,所有订单的创建、修改操作都通过代理来记录日志,追踪操作过程。
网络图片懒加载器: 在开发一些图片浏览应用或文档查看工具时,我们不希望一开始就把所有图片都加载到内存中,尤其是当图片很多、尺寸又大时。这时我们可以使用代理模式,让“代理图片”控制真实图片的加载时机,只有真正需要显示时再去加载资源,从而节省内存和加快响应。
2.3 代理模式的基本结构
代理模式具有的角色和职责:
抽象主题接口(Subject):定义了真实对象和代理对象都需要实现的方法。
真实主题(RealSubject):也就是目标对象,真正去执行核心业务逻辑的类。
代理类(Proxy):实现和目标对象相同的接口,在内部维护一个真实对象的引用。代理类会控制对真实对象的访问,可能会在调用前后加点特殊的处理逻辑。
2.4 代理模式的实现
下面就以“网络图片懒加载"为例,我用代理模式实现一个简单的图片加载系统。
具体代码请看:
2.5 代理模式的优缺点
优点:
控制对象访问: 通过代理类控制对目标对象的访问,有助于增强安全性、控制权限或延迟加载。
增强目标对象功能: 无需修改目标类代码即可添加额外功能,如缓存、日志、远程访问等,符合开闭原则。
隔离客户端与目标对象: 客户端通过代理与目标对象交互,有利于解耦,提高系统的可维护性。
支持多种代理形式: 支持静态代理、动态代理、远程代理等多种实现方式适应不同应用场景。
缺点:
增加系统复杂度: 引入代理类会增加类的数量和系统结构的复杂性,增加理解和维护成本。
可能引入性能开销: 特别是在动态代理或远程代理中,可能带来反射、网络传输等额外开销。
不适用于频繁变更的接口: 若目标接口频繁变更,代理类也需同步更新,降低了开发效率。
三、装饰模式
装饰模式(Decorator Pattern)是一种结构型设计模式,它的核心思想是:动态地给对象添加额外的功能,而不会影响到其他对象。
3.1 为什么要使用装饰模式
我们在实际开发中经常会遇到这样一种情况:某个类已经实现了基本功能,但过了-阵子,需求变了,需要在原有基础上加功能。比如要加日志、加缓存、加权限控制...这些都不是核心业务逻辑,但却是必需的。
这时候我们可能会想到继承。确实,继承是可以扩展功能。但一旦子类越来越多,继承层次就容易变得复杂,而且子类一旦继承了父类,就被绑死了,灵活性差。装饰式正好解决了这个问题:它通过组合的方式,把附加功能封装成一个个装饰类,灵活地套在目标对象外面,不影响原始类结构,也不会破坏开放-封闭原则。
为了让大家更好地感受到装饰模式的作用,以文本渲染为例,我们需要为文本添加不同的样式(加粗、颜色等)。让我们来看看使用和不使用装饰模式的区别:

通过对比可以看出,不使用装饰模式时,我们需要为每种文本样式的组合创建单独的方法,这导致了方法的数量过多、代码重复、难以维护等问题。每当需要添加新的样式或组合时,都需要创建新的方法,增加了开发成本。
而使用装饰模式后,我们通过 TextDecorator 类将样式功能动态地添加到文本上,实现了功能的灵活组合。装饰模式通过组合的方式动态地给对象添加新的职责,使得代码结构更加清晰,扩展性更强。这种实现方式不仅减少了类的数量,还使得添加新的样式变得更加简单。
3.2 装饰模式的应用场景
举一些开发中典型的应用场景:
动态增加功能或行为: 例如,在电商系统中,用户可能选择多个优惠(如打折、优惠券积分抵扣等)。这些优惠可以通过装饰模式动态地为订单增加不同的优惠功能,而不需要修改原始的订单类。每种优惠可以视为一个装饰类,独立实现不同的行为。
输入输出流处理: 在文件读取和写入操作中,经常使用装饰模式。例如,Java 的 I/O 流操作就使用了装饰模式。你可以使用BufferedReader装饰 FileReader ,为其增加缓冲功能,而不需要改变 FileReader 的原始实现。
日志系统功能增强:在日志记录系统中,通常有多种日志格式或输出方式。可以使用装饰模式为日志对象动态增加不同的功能,如输出到文件、控制台、远程服务器等。每种日志输出方式可以作为一个装饰器,透明地增加额外的功能。
文本渲染增强系统:在开发富文本编辑器或日志输出组件时,经常会需要在文本基础上增加不同的功能,比如加粗、加下划线、加颜色等等。如果直接在原有类中添加这些功能会导致类膨胀、职责混乱。用装饰模式可以在不改变原有类的基础上,为其动态添加功能,保持设计的灵活性和扩展性。
3.3 装饰模式的基本结构
装饰模式具有的角色和职责:
组件接口(Component):定义对象的接口,所有的具体组件和装饰器都实现这个接口。
具体组件(ConcreteComponent):就是原始对象,定义了核心的功能实现。
抽象装饰器(Decorator):实现组件接口,内部持有一个组件对象(也就是被装饰的对象)。
具体装饰器:(ConcreteDecorator):继承抽象装饰器,负责给组件对象添加新的功能。
3.4 装饰模式的实现
下面就以“文本渲染增强”为例,我们用装饰模式实现一个支持动态添加样式的文本组件系统。
详细代码请见:
3.5 装饰模式的优缺点
优点:
扩展功能更灵活: 装饰模式允许你在不修改原始类代码的前提下,动态地给对象增加额外的功能。比起继承,它更灵活,也更符合开闭原则,特别适合功能经常变动的场景。
避免类过多: 如果用继承来应对各种功能组合,很容易导致子类数量成倍增加,维护压力大。装饰模式通过“包装”的方式,把功能拆成一个个装饰类,组合使用,结构更清晰。
可以组合使用: 多个装饰类可以灵活善加,按需组合功能,像搭积木一样,自由度高,不用预先规划好所有情况,尤其适合处理功能可选、可加的业务逻辑。
缺点:
调试比较麻烦: 因为装饰模式是通过一层一层地包装对象的,有时候为了看清楚一个操作是在哪个环节做的,你可能得顺着一长串“包装链"去追踪,调试起来不太友好。
增加了系统复杂度: 虽然类过多的问题解决了,但类之间的关系变得更抽象了,理解和使用都需要一定的学习成本。特别是对于刚接触装饰模式的人来说,不太容易一下子理清谁在装饰谁。
可能影响性能: 如果装饰链太长,每次调用方法都要经过多层对象转发,虽然每层逻辑可能都很轻,但鲁加起来也可能对性能有些影响,尤其是在高频调用场景下。
四、外观模式
外观模式(Facade pattern)是一种结构型设计模式,它的核心思想是: 为复杂的子系统提供一个统一的对外接口,通过这个接口,我们就能更方便、更高效地访问这些子系统,而不需要关心它们内部的复杂逻辑。
4.1 为什么要使用外观模式
开发中我们经常会遇到这样一种情况:系统内部有很多块,比如数据库、缓存、第三方接口、日志系统等,而外部调用者(比如前端、接口调用者)根本没必要也不想知道这些模块是怎么运作的。我们如果直接暴露所有子系统接口,不仅使用麻烦,而且耦合度高,系统也不易维护。
这时候,我们通过引入外观模式,定义一个对外统一的访问入口,由这个入口来协调和管理名个子系统的调用流程。这样,外部调用者只需要跟这个“入口类”打交道,调用起来更方便,系统的结构也更加清晰、解耦。
为了让大家更好地感受到外观模式的作用,以用户注册为例,我们需要创建配置、数据库操作、发送欢迎邮件、日志记录等多个步骤。让我们来看看使用和不使用外观模式的区别:

通过对比可以看出,不使用外观模式时,客户端需要直接与多个子系统(数据访问、邮件服务、日志服务)交互,这导致了代码耦合度高、调用复杂、难以维护等问题。每当需要修改注册流程时,都需要修改多处代码,增加了开发成本。
而使用外观模式后,我们通过 UserserviceFacade 类将复杂的注册流程封装成一个简单的接口,实现了子系统与客户端的解耦。外观模式通过提供一个统一的接口来访问子系统,使得代码结构更加清晰,调用更加简单。这种实现方式不仅降低了代码耦合度,还使得修改注册流程变得更加容易,同时提高了代码的可维护性和可读性。
4.2 外观模式应用场景
举一些开发中典型的应用场景:
多模块复杂系统整合: 在一些大型的业务系统中,往往会有多个子模块(如订单、支付、物流等)。通过外观模式,可以为外部系统提供一个简单的接口,让外部系统通过外观类与各个模块进行交互,避免直接调用复杂的子系统接口,简化了系统的操作。例如,在电商系统中,创建一个”订单管理”外观类,将订单、支付、库存、配送等模块整合为一个简单接口,提供给前端调用。
数据库操作封装: 对于一些复杂的数据库操作,可能需要多个步骤才能完成(如数据查询、转换、保存等)。使用外观模式可以为数据库操作提供一个简单的外观类接口,隐藏复杂的查询细节和操作步骤,减少开发人员的认知负担。例如,设计一个数据访问外观类,它封装了常见的增删改查(CRUD)操作,让其他业务层无需关心数据库的具体实现。
复杂的第三方服务集成: 在项目中,如果需要集成多个第三方服务(如支付服务、短信服务、邮件服务等),使用外观模式可以统一对外提供一个简洁的接口,避免各个业务层与第三方服务的直接交互。例如,在一个电商系统中,可以创建一个“支付外观类”,统一处理支付宝、微信支付等支付方式的调用,使支付辑更加简单易用。
4.3 外观模式基本结构
外观模式具有的角色和职责:
外观类(Facade): 对外提供统一接口,封装子系统的调用。
子系统类(Subsystem): 负责自身的业务逻辑处理,通常不会对外暴露。
客户端(Client): 通过外观类来使用子系统的功能。
4.4 外观模式实现
具体实现代码请看:
4.5 外观模式的优缺点
优点:
简化调用逻辑: 外观模式最大的作用就是把系统的复杂度“挡在门外”。对于使用方来说,只需要跟一个外观类打交道,不用去理会底层有多少子系统、具体是怎么实现的调用起来简单又清爽。
降低耦合度: 外观类起到了一个中间人的作用,让调用者和内部子系统之间的联系变得松散。换句话说,你要是将来改了底层实现,只要外观接口不动,上层代码基本不用动,维护成本低。
有利于分层架构设计: 在一些分层结构的系统里,比如三层架构,外观类可以作为“门卫”,帮你管理每一层之间的依赖关系,让层与层之间解耦得更干净,结构也更清晰。
缺点:
功能扩展时不够灵活: 一旦客户端习惯了只跟外观类打交道,那要想用到子系统里一些比较细的功能,就得回头改外观类,或者绕过它。这时候,外观反而成了一种限制。
可能会变成”万能类”: 如果设计得不太合理,外观类很容易膨胀,什么事都往它身上堆,最后变成一个大杂烩,职责太多,反而不利于维护。
隐藏了部分系统细节: 虽然这本身是优点,但有时候也是把“双刃剑"。尤其是在调试或者排查问题时,如果所有东西都被外观包装了起来,开发者可能会对系统内部的真实结构缺乏了解,排错效率也可能受影响。
五、桥接模式
桥接模式(Bridge Pattern)是一种结构型设计模式,简单来说,就是一种把抽象和实现分离开来的方法,让它们可以各自独立变化。
在实际开发中,我们经常会遇到这样的问题:一个功能模块,既要支持多个平台,又要支持多种操作方式。比如做一个消息通知系统,你可能既要支持“邮件通知、短信通知、微信通知”,又要支持"普通用户、VIP 用户、企业用户”的各种业务逻辑。这两个维度一交叉,代码量直接炸裂,继承结构也一眼看不到头。
桥接模式的作用,就是把“消息类型”这条线和“用户类型”这条线拆开,不再死绑在一起,而是通过抽象接口桥接起来。这样我们想加一种新消息、新用户,只管在自己的维度扩展就行了,不用去动对方的代码,灵活又清晰。
5.1 为什么要使用桥接模式
当一个系统中,抽象和实现之间都可能频繁变化,而且两者都存在多种可能性组合的时候,直接通过继承关系硬连在一起会非常糟糕。继承会导致类数量过多,灵活性极差,任何一方变化都得连锁修改,出错率高。桥接模式通过把抽象和实现解耦,只用组合的方式关联,避免了继承导致的僵硬结构,系统的扩展性、维护性都会大大提高。特别是在面对多维度变化的复杂系统时,桥接模式几乎是必选的解决方案。
为了让大家更好地感受到桥接模式的作用,以消息推送系统为例,我们需要支持不同类型的消息(普通消息、紧急消息)通过不同的渠道(邮件、短信、微信)发送。让我们来看看使用和不使用桥接模式的区别:

通过对比可以看出,不使用桥接模式时,我们需要为每种消息类型和发送渠道的组合创建一个单独的类这导致了类的数量过多,代码重复度高,难以维护。每当需要添加新的消息类型或发送渠道时,都需要创建大量的新类。
而使用桥接模式后,我们将消息类型和发送渠道分离成两个独立的维度,通过组合的方式实现功能。这种设计使得代码结构更加清晰,扩展性更强。当需要添加新的消息类型或发送渠道时,只需要创建相应的新类,而不需要修改现有代码。
5.2 桥接模式的应用场景
举一些开发中典型的应用场景:
支付系统: 在电商平台中,不同的支付方式(如支付宝、微信支付、银行卡支付)通常具有不同的支付实现逻辑。通过桥接模式,可以将支付接口与支付方式分开,使得支付系统可以在不修改业务逻辑的情况下,灵活地添加新的支付方式,同时也能保持原有支付方式的稳定性。
多渠道广告系统: 在一个广告发布平台中,可能需要支持不同的广告投放渠道(如电视、互联网、户外广告等)。杯接模式可以将广告的具体展示与不同广告渠道的实现分开,使得增加或修改广告渠道时,系统的其他部分不受影响,提升系统的扩展性和维护性。
消息推送系统扩展: 在一个支持多平台(如邮件、短信、微信等)的消息推送系统中,不同的消息类型(如普通通知、告警通知)和发送渠道可能任意组合。使用桥接式,我们可以将“消息类型和“发送渠道”解耦,避免类数量过多,并实现任意组合的灵活扩展。
5.3 桥接模式的基本结构
抽象化(Abstraction): 定义抽象的接口,同时维护一个对实现化对象(lmplementor)的引用。
扩展抽象化(Refined Abstraction): 在抽象化基础上扩展,调用实现化对象的方法,增加新的功能。
实现化(lmplementor): 定义实现化角色的接口,它不需要与抽象化接口完全一样,但一般要提供基本操作。
具体实现化(Concrete lmplementor): 真正去实现实现化接口的类,完成具体的业务逻辑。
5.4 桥接模式的实现
下面就以“消息推送系统"为例,我们用桥接模式实现一个灵活扩展的消息系统。
具体代码请见:
5.5 桥接模式的优缺点
优点:
解耦抽象和实现: 桥接模式通过将抽象部分和实现部分分离开来,使得它们可以独立地变化。你可以在不改变抽象类的情况下,改变其实现类,反之亦然。这样减少了修改和扩展时的相互依赖。
提高系统的灵活性: 由于抽象和实现是分离的,你可以根据需要组合不同的抽象和实现,灵活性较高。这对于功能和实现不断变化的系统特别有用。
符合开闭原则: 通过使用桥接模式,系统可以在不修改现有代码的基础上,灵活地增加新的抽象层和实现层,符合开闭原则,便于扩展。
缺点:
增加了系统复杂度: 桥接模式需要引入多个类,增加了类的数量。这可能会使系统的结构更加复杂,理解和维护起来较为困难,尤其在初期设计阶段。
实现层的独立性问题: 虽然抽象和实现分离了,但有时候,过于独立的实现层可能会导致难以管理的依赖关系。如果设计不当,可能会造成几余代码或不必要的复杂性。
不适合简单场景: 对于那些抽象和实现之间变化不大的简单场景,使用桥接模式可能会显得过于复杂和冗余。此时,直接将抽象和实现合并在一起可能会更加高效和直观。
六、组合模式
组合模式(Composite Pattern)是一种结构型设计模式,它是一种让我们可以用统一的方式来处理单个对象和一组对象的设计思路。它的核心思想就是: 无论是一个元素,还是一组元素,我都用同一套接口、一致的方式去操作,不用每次都写一堆 if 判断去区分“你到底是个体还是整体”。
在开发中,我们经常会碰到“部分-整体”的结构,比如文件系统:一个文件夹里可以放文件,也可以放文件夹,后者又可以继续嵌套文件.…这时候如果每种情况都单独处理,会导致代码不够优雅。组合模式就让你像处理一个单独文件那样处理整个文件夹,递归也轻松搞定。
6.1 为什么要使用组合模式
系统开发中,随着功能的不断扩展,对象之间的关系往往变得复杂,有时候单个对象也能完成事情,有时候又需要一组对象配合才能完成。如果我们在代码里频繁去判断是单个对象还是一组对象,不仅逻辑繁琐,还容易出错。组合模式通过统一接口,把这两种情况统一起来,大大简化了客户端代码,让整体结构更加清晰,扩展起来也更加自然顺手。尤其是在涉及树形结构、递归处理的场景下,组合模式能帮我们优雅地管理这些复杂的对象关系。
为了让大家更好地感受到组合模式的作用,以文件系统管理为例,我们需要管理文件和文件夹的层级结构。让我们来看看使用和不使用组合模式的区别:

通过对比可以看出,不使用组合模式时,我们需要分别处理文件和文件夹两种不同的类型,这导致了代码的重复和复杂度的增加。每当需要添加新的操作时,都需要在文件和文件夹类中分别实现,而且客户端需要区分处理不同类型的节点。
而使用组合模式后,我们通过统一的接口将文件和文件夹抽象为相同的组件,使得客户端可以用一致的方式处理所有节点。这种设计不仅简化了代码结构,还提高了系统的可扩展性。当需要添加新的操作时,只需要在接口中定义并在各个实现类中实现即可,无需修改客户端代码。
6.2 组合模式的应用场景
举一些开发中典型的应用场景:
文件系统管理: 在一个文件管理系统中,文件和文件夹可以构成树形结构,文件夹可以包含多个文件或其他文件夹。组合模式可以将文件和文件夹抽象成统一的接口,使得在操作文件夹和文件时,可以使用相同的方法。比如,用户想删除一个文件夹,无论该文件夹中包含的是文件还是子文件夹,操作方式都是一样的。
企业组织结构: 在处理企业的组织结构时,可以将公司、部门、员工等抽象为组件。公司是一个组合对象,包含多个部门;部门可以包含多个员工。组合模式允许我们将公司、部门和员工看作一个统一的接口,方便在遍历和操作整个组织结构时使用相同的方式,无论操作的是公司、部门还是单个员工。
菜单管理系统: 在一个菜单管理系统中,菜单项可以是简单的单项(如一个按钮或链接)或者是复杂的菜单(如包含多个子菜单项的下拉菜单)。组合模式可以将菜单项和菜单容器统一处理,使得无论是普通菜单项还是包含子菜单的复合菜单,都能以相同的方式进行渲染、更新或删除。这样在实现菜单功能时,可以灵活地添加或修改菜单层级结构。
6.3 组合模式的基本结构
组合模式具有的角色和职责:
抽象组件(Component): 定义了所有组件(包括基本组件和容器组件)必须遵循的接口比如公共的方法,比如添加、删除、获取子组件等。
叶子节点(Leaf): 代表最基本的、不能再分的对象,比如普通员工。叶子节点实现了抽象组件定义的接口,但不会再包含子节点。
容器节点(Composite): 代表可以包含子组件的对象,比如部门经理。容器节点同样实现了抽象组件接口,同时内部维护着子节点集合,并实现对子节点的添加、删除等操作。
6.4 组合模式的实现
下面就以“文件系统管理”为例,我们用组合模式实现一个简单的文件结构系统
具体代码请见:
6.5 组合模式的优缺点
优点:
树形结构的表示: 组合模式能够将对象组织成树形结构,适用于表示“部分-整体”的层次结构,如文件系统、组织结构等。
统一性: 客户端可以统一处理单个对象和对象集合,简化了代码的复杂性。在操作时无需区分复合对象与叶子节点,可以统一使用相同的接口进行操作。
扩展性: 通过递归方式,容易为树形结构添加新的操作或新类型的对象,无需修改已有代码即可实现新功能。
灵活性: 组合模式使得对象的组合和解组合更加灵活,组合结构可以按需变化。
缺点:
过度泛化: 组合模式可能会导致设计过于泛化,叶子节点和复合节点使用统一接口,可能会引起不必要的复杂性。如果不加区分,复合节点和叶子节点的功能可能会被过度简化,导致某些操作不符合实际需求。
不适合复杂对象: 如果对象层次过于复杂,组合模式可能会导致大量的类和接口,增加系统的复杂性,降低维护性。
难以控制部分操作: 当组合对象非常复杂时,处理某些特定的部分操作可能会变得困难因为无法简单区分叶子节点和复合节点的行为。
七、享元模式
享元模式(Flyweight Pattern)是一种结构型设计模式,它的核心思想是: 通过共享对象,减少系统中对象的数量,从而节省内存,提高性能。在很多应用场景中,我们经常会遇到需要大量创建相似对象的情况,而这些对象之间其实有很多内容是可以共享的。享元模式就是专门针对这种情况提出的: 把可以共享的部分提取出来,放到一个公共对象里,只保留那些不可共享的部分,减少重复创建,从而节省资源开销。
7.1 为什么要使用享元模式
在系统开发中,有些对象的数量非常庞大,比如地图上的每一棵树、游戏中每一颗子弹、文档编辑器里的每一个字符。如果为每个元素都创建独立的对象,内存开销是巨大的,系统的响应速度也会下降。使用享元模式,我们可以把那些相同的数据抽取出来,做成可以共享的对象,多个使用场景共用同一份数据,只针对变化的部分做单独处理。这样既节省了内存,又能提高系统的整体性能。
为了让大家更好地感受到享元模式的作用,以字符缓存系统为例,我们需要高效地渲染大量重复的字符让我们来看看使用和不使用享元模式的区别:

通过对比可以看出,不使用享元模式时,每个字符都需要创建一个完整的对象实例,即使这些字符具有相同的符号和字体。这种方式会导致大量重复对象的创建,造成内存浪费,特别是在需要渲染大量重复字符的场景下。
而使用享元模式后,我们将字符的固有属性(符号和字体)作为内部状态,将变化的位置和颜色作为外部状态。通过享元工厂管理共享的字符对象,大大减少了内存占用。这种设计不仅提高了系统的性能,还使得代码结构更加清晰。当需要添加新的字符类型时,只需要在工厂中添加相应的创建逻辑即可,无需修改现有代码。
7.2 享元模式的应用场景
举一些开发中典型的应用场景:
文本编辑器中的字符缓存:在文本编辑器中,可能会有大量相同的字符(如重复出现的字母、符号等)。而每个字符的显示样式(如字体、颜色、大小)可能是相同的。通过享元模式,可以将共享的字符属性(如字形、字体)提取出来作为享元对象,只保存那些需要变化的属性(如位置、颜色),减少重复存储,提高系统效率。
网页中的图标缓存:在一个网页应用中,可能需要显示大量图标,很多图标是重复使用的(比如社交媒体图标、按钮图标等)。通过享元模式,可以将这些共享图标的显示细节(如图形、颜色、大小等)抽象为享元对象,而将特定位置、状态等动态变化的部分保存在其他地方,从而节省内存,提高响应速度。
7.3 享元模式的基本结构
享元模式具有的角色和职责:
抽象享元(Flyweight): 定义享元对象的接口,规定外部状态传入的标准方法。
具体享元(ConcreteFlyweight): 实现抽象享元接口,内部保存可以共享的状态。
享元工厂(FlyweightFactory): 负责创建和管理享元对象,确保相同的享元对象只被创建一次,并进行共享。
客户端(Client): 通过享元工厂获取享元对象,同时传入外部状态进行使用。
7.4 享元模式的实现
下面就以“字符缓存系统”为例,我们用享元模式实现一个高效的字符渲染机制。
具体实现请看:
7.5 享元模式的优缺点
优点:
节省内存: 享元模式通过共享对象来减少内存的使用。当多个对象拥有相同的状态时,可以通过共享这些对象来避免重复创建,尤其在需要大量相似对象时,节省了大量内存。
提高性能: 通过对象的共享,减少了对象的创建和销毁,尤其是在大量对象需要频繁创建的场景中,能够显著提高系统性能,减少系统开销。
支持大规模对象管理: 享元模式特别适用于需要管理大量对象的场景,如图形编辑、文字排版等。它能够通过共享内存,提高对象的使用效率,适应大规模的对象管理需求。
缺点:
增加了复杂性: 享元模式需要管理享元池(对象池)和分离内外部状态,设计和实现上可能比直接创建对象要复杂。需要仔细处理对象共享和状态管理,避免引入不必要的复杂性。
对象状态管理困难: 享元模式的共享对象往往是不可变的。若对象的状态是可变的,需要将内部状态和外部状态分开管理,这可能会增加代码的复杂度和维护难度。
可能导致资源竞争: 当多个对象共享同一个实例时,如果共享的对象没有正确同步,可能会导致资源竞争或线程安全问题,尤其在多线程环境下,必须格外小心。
