接上次博客:二十四种设计模式与六大设计原则(一):【策略模式、代理模式、单例模式、多例模式、工厂方法模式、抽象工厂模式】的定义、举例说明、核心思想、适用场景和优缺点-CSDN博客
目录
门面模式【Facade Pattern】
定义
举例说明
核心思想
适用环境
优缺点
适配器模式【Adapter Pattern】
定义
举例说明
核心思想
适用环境
优缺点
模板方法模式【Template Method Pattern】
定义
举例说明
核心思想
适用环境
优缺点
建造者模式【Builder Pattern】
定义
举例说明
核心思想
适用环境
优缺点
桥梁模式【Bridge Pattern】
定义
举例说明
核心思想
适用环境
优缺点
命令模式【Command Pattern】
定义
举例说明
核心思想
适用环境
优缺点
门面模式【Facade Pattern】
定义
当我们面对一个复杂系统时,通常会涉及多个子系统或模块,每个子系统都有自己的一组接口和实现。在这种情况下,直接让客户端与每个子系统进行交互会导致以下问题:
- 复杂性增加:客户端需要了解每个子系统的接口和实现细节,增加了系统的复杂性。
- 耦合度高:客户端与多个子系统之间存在紧密耦合,一旦子系统发生变化,可能会影响到客户端的代码。
- 不一致性:不同的客户端可能会以不同的方式与子系统进行交互,导致接口的不一致性。
为了解决这些问题,引入了门面模式。
门面模式(Facade Pattern)是一种结构型设计模式,门面模式通过引入一个门面类,将客户端与子系统的交互统一封装起来。它提供了一个统一的接口,用于访问一个系统中的一组接口。门面模式通过定义一个高层接口,隐藏了系统内部的复杂性,使得客户端可以更简单地与系统进行交互,而不需要了解系统内部的具体实现细节。
在门面模式中,门面(Facade)充当了客户端与系统之间的中间层。它封装了系统内部的一组子系统或接口,并提供了一个统一的接口给客户端使用。客户端只需要与门面进行交互,而不需要直接与系统内部的各个子系统进行通信。
门面模式通常用于简化复杂系统的接口,提供更简洁的接口给客户端使用。它可以帮助客户端隐藏系统的复杂性,降低了与系统的耦合度,同时提高了系统的可维护性和扩展性。
具体来说,门面模式包括以下几个角色:
- 门面(Facade):门面类是客户端与系统之间的接口,它封装了系统中一个或多个子系统的接口,提供了一个统一的、简化的接口给客户端使用。
- 子系统(Subsystem):子系统是组成系统的各个模块或组件,每个子系统都有自己的一组接口和实现。
- 客户端(Client):客户端是使用系统的外部实体,它通过门面类来与系统进行交互,而不直接与子系统进行通信。
门面模式的工作流程如下:
- 客户端通过门面类的统一接口来访问系统。
- 门面类接收到客户端的请求后,将请求转发给相应的子系统进行处理。
- 子系统根据请求执行相应的操作,并将结果返回给门面类。
- 门面类将结果返回给客户端,客户端无需了解具体的子系统实现细节。
举例说明
当我们设计一个游戏中的道具系统时,通常会涉及到多个子系统,比如背包系统、装备系统、战斗系统等。每个子系统都有自己的功能和操作,例如背包系统负责管理玩家的道具存储,装备系统负责管理玩家装备的道具,战斗系统负责处理玩家在战斗中使用道具的效果等。
在没有门面模式的情况下,玩家需要直接与每个子系统交互,才能完成对道具的操作。例如,如果玩家想要使用一瓶药水恢复生命值,他可能需要先与背包系统交互,从背包中找到并获取药水,然后再与战斗系统交互,告诉战斗系统要使用药水来恢复生命值。这样的交互过程需要玩家了解并且依赖于每个子系统的具体实现,使得代码复杂度增加,并且降低了代码的可维护性和扩展性。
为了简化玩家与道具系统的交互,我们引入了门面模式。门面模式提供了一个统一的接口,封装了与道具相关的各个子系统的操作。在我们的例子中,我们创建了一个名为"道具管理器"的门面类,它负责封装了与道具相关的操作,包括获得道具、使用道具、丢弃道具等。
玩家与道具系统的交互变得更加简单明了。例如,玩家想要使用一瓶药水恢复生命值,他只需要调用道具管理器的"useHealthPotion()"方法即可,而不需要了解具体的子系统细节。道具管理器内部会处理具体的逻辑,包括从背包中获取药水、检查当前是否处于战斗状态、恢复生命值等操作。
import java.util.HashMap;
import java.util.Map;// 定义道具接口
interface Item {void use();
}// 具体道具类:药水
class Potion implements Item {@Overridepublic void use() {System.out.println("使用药水,恢复生命值");}
}// 具体道具类:武器
class Weapon implements Item {@Overridepublic void use() {System.out.println("使用武器,增加攻击力");}
}// 背包系统:负责管理玩家的道具存储
class BackpackSystem {// 模拟玩家的背包,存放道具private Map<String, Item> backpack = new HashMap<>();// 初始化背包,加入一些初始道具public BackpackSystem() {// 假设背包中有一些初始道具backpack.put("Potion", new Potion());backpack.put("Weapon", new Weapon());}// 根据道具名称从背包中获取对应的道具public Item getItem(String itemName) {return backpack.get(itemName);}
}// 战斗系统:负责处理玩家在战斗中使用道具的效果
class BattleSystem {// 使用道具的方法public void useItem(Item item) {// 模拟处理战斗逻辑item.use();}
// 处理玩家在战斗中使用道具的效果。
// 它负责接收玩家使用的道具,并根据道具的效果进行相应的处理,
// 例如增加攻击力、恢复生命值等。在实际游戏中,
// 战斗系统可能会涉及到更多的战斗逻辑,包括敌人的行为、战斗结果的计算等,
// 但在这个简化的例子中,我们只是模拟了使用道具的效果。}// 道具管理器:门面类,封装了与道具相关的操作
class ItemManager {private BackpackSystem backpackSystem;private BattleSystem battleSystem;// 构造方法public ItemManager() {this.backpackSystem = new BackpackSystem();this.battleSystem = new BattleSystem();}// 获取道具并使用public void useItem(String itemName) {Item item = backpackSystem.getItem(itemName);if (item != null) {battleSystem.useItem(item);} else {System.out.println("道具不存在或不可用");}}
}// 游戏客户端
public class GameClient {public static void main(String[] args) {// 创建道具管理器ItemManager itemManager = new ItemManager();// 玩家想要使用药水恢复生命值System.out.println("玩家使用药水:");itemManager.useItem("Potion");// 玩家想要使用武器增加攻击力System.out.println("\n玩家使用武器:");itemManager.useItem("Weapon");// 玩家想要使用不存在的道具System.out.println("\n玩家使用不存在的道具:");itemManager.useItem("NonexistentItem");}
}
通过引入门面模式,我们使得道具系统的交互变得更加简单和直观,同时也提高了系统的灵活性和可维护性。如果以后需要扩展道具系统,比如添加新的道具类型或功能,只需要在道具管理器中进行修改和扩展,而不会影响到其他部分的代码。
这种设计方式还有一个好处是,它能够隐藏系统内部的复杂性,使得玩家无需关心具体的实现细节,只需要调用简单的方法即可完成操作。这样不仅降低了玩家学习和使用系统的难度,也减少了出错的可能性。
另外,使用门面模式后,对门面进行单元测试是一种良好的实践,可以确保门面类的行为符合预期,并提高代码的可靠性和可维护性。通过编写单元测试,可以验证门面类的各个方法在不同情况下的行为是否正确,以及处理异常情况的能力。这有助于捕获潜在的错误和边界情况,并在早期发现和修复问题。
单元测试还可以约束项目成员的代码质量。通过编写测试用例,可以明确门面类的功能和接口预期行为,这有助于团队成员在实现门面类时遵循统一的标准和规范。此外,单元测试还可以促使团队成员编写更 modul 和可测试的代码,从而提高整体项目质量。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;public class ItemManagerTest {// 测试道具管理器是否能正确使用药水@Testpublic void testUsePotion() {ItemManager itemManager = new ItemManager();String expectedOutput = "使用药水,恢复生命值";assertEquals(expectedOutput, getOutputString(() -> itemManager.useItem("Potion")));}// 测试道具管理器是否能正确使用武器@Testpublic void testUseWeapon() {ItemManager itemManager = new ItemManager();String expectedOutput = "使用武器,增加攻击力";assertEquals(expectedOutput, getOutputString(() -> itemManager.useItem("Weapon")));}// 测试道具管理器是否能正确处理不存在的道具@Testpublic void testUseNonexistentItem() {ItemManager itemManager = new ItemManager();String expectedOutput = "道具不存在或不可用";assertEquals(expectedOutput, getOutputString(() -> itemManager.useItem("NonexistentItem")));}// 辅助方法:获取方法输出的字符串private String getOutputString(Runnable runnable) {ByteArrayOutputStream outputStream = new ByteArrayOutputStream();PrintStream printStream = new PrintStream(outputStream);PrintStream originalOut = System.out;System.setOut(printStream);runnable.run();System.out.flush();System.setOut(originalOut);return outputStream.toString().trim();}
}
我们定义的一个辅助方法 getOutputString(),其作用是捕获方法执行过程中的输出内容,并将其转换为字符串返回。
让我解释一下这段代码的工作原理:
- 首先,我们创建一个 ByteArrayOutputStream 对象 outputStream,它是一个输出流,用于在内存中暂存方法的输出内容。
- 接着,我们创建一个 PrintStream 对象 printStream,它是一个打印流,将输出流连接到这个打印流上,这样通过 printStream 打印的内容将会被写入到 outputStream 中。
- 我们保存当前的标准输出流,即控制台输出流,为了以后还原回去。
- 然后,我们将标准输出流替换为 printStream,这样一旦有输出操作,内容就会被重定向到 outputStream。
- 接着,我们运行传入的 runnable 对象,也就是待测试的方法。因为我们已经替换了标准输出流,所以任何输出都会被重定向到 outputStream 中。
- 执行完 runnable 后,我们将标准输出流恢复为原来的标准输出流,以防止对后续代码的影响。
- 最后,我们调用 flush() 方法来刷新 outputStream,将其中的内容转换为字符串,并返回。
总的来说,这个辅助方法的作用就是捕获方法执行过程中的输出内容,使我们能够在测试中检查方法的输出是否符合预期。
核心思想
门面模式的核心思想是提供一个统一的接口(门面),隐藏系统内部复杂的实现细节,使得客户端可以通过这个统一的接口来访问系统,而无需直接与系统的各个组件进行交互。通过引入门面,将系统的复杂性封装起来,降低了客户端与系统之间的耦合度,同时提供了简化的接口,使得系统更易于使用和维护。
具体而言,门面模式的核心思想包括以下几个要点:
-
简化接口:门面类提供了一个简化的接口给客户端使用,隐藏了系统内部的复杂性。客户端只需要与门面类进行交互,而无需了解系统内部各个组件的具体实现。
-
封装实现细节:门面类封装了系统的各个组件之间的交互细节,使得客户端无需关心系统内部各个组件之间的通信和协调方式,降低了客户端的复杂度。
-
解耦合:通过引入门面,将客户端与系统的各个组件解耦合,使得系统更易于维护和扩展。客户端不需要知道系统的内部结构和实现细节,只需与门面类进行交互即可。
-
提高可维护性:由于客户端与系统之间的交互由门面类统一管理,系统的内部实现可以更灵活地调整和修改,提高了系统的可维护性。系统内部变化不会影响客户端的代码。
-
降低学习成本:门面模式可以将系统的复杂性隐藏起来,使得客户端更容易理解和使用系统,降低了学习成本。
总之,门面模式的核心思想是通过引入一个门面类来统一封装系统的复杂性,提供一个简化的接口给客户端使用,从而降低了客户端与系统之间的耦合度,提高了系统的可维护性和易用性。
适用环境
门面模式通常适用于以下环境:
-
简化复杂系统:当系统内部包含多个复杂的子系统,而客户端需要使用这些子系统的功能时,可以引入门面模式。门面类将系统的复杂性封装起来,为客户端提供一个简化的接口,使得客户端可以更轻松地使用系统的功能,而无需了解其复杂的内部结构和实现细节。
-
隐藏实现细节:当系统的内部实现发生变化时,为了避免对客户端造成影响,可以使用门面模式将系统的实现细节隐藏起来。客户端只需要与门面类进行交互,而无需关心系统的内部变化,从而降低了系统的耦合度。
-
提供统一接口:当客户端需要使用多个子系统的功能,并且这些功能需要按照特定顺序或条件进行调用时,可以使用门面模式提供一个统一的接口。门面类负责管理子系统之间的交互,并根据客户端的请求调用相应的子系统功能,使得客户端可以更方便地使用系统的功能。
-
降低复杂度:当系统的接口过于复杂,使得客户端难以理解和使用系统的功能时,可以引入门面模式来简化接口。门面类封装了系统的复杂性,为客户端提供了一个简单易用的接口,从而降低了客户端的复杂度和学习成本。
门面模式适用于需要简化复杂系统、隐藏实现细节、提供统一接口以及降低复杂度的情况。通过引入门面模式,可以使得系统更易于理解、使用和维护,提高了系统的灵活性和可维护性。
优缺点
门面模式的优点包括:
-
简化接口:门面模式通过提供一个统一的接口,隐藏了系统的复杂性,使得客户端可以更轻松地使用系统的功能,降低了系统的复杂度。
-
隐藏实现细节:门面模式将系统的内部实现细节封装起来,使得客户端无需关心系统的具体实现,从而降低了系统的耦合度,提高了系统的灵活性和可维护性。
-
提高可维护性:由于门面模式将系统的复杂性隐藏在门面类中,使得系统的修改和维护变得更加容易。如果系统的内部实现发生变化,只需要修改门面类即可,而不会影响到客户端。
-
提高安全性:门面模式可以限制客户端直接访问系统的内部功能,只提供特定的接口给客户端使用,从而提高了系统的安全性,防止客户端直接操作系统内部的敏感功能。
-
促进代码复用:门面模式可以将系统的公共功能封装在门面类中,不同的客户端可以共享同一个门面类,从而促进了代码的复用。
门面模式的缺点包括:
-
增加了系统的复杂性:引入门面模式会增加系统中的额外类和接口,可能会导致系统的复杂度增加,尤其是当系统本身已经比较简单时。
-
不符合开闭原则:如果系统的功能发生变化,可能会导致门面类的修改,违反了开闭原则。尽管门面模式隐藏了系统的内部实现细节,但如果系统的功能发生变化,仍然需要修改门面类,这可能会影响到客户端。
-
可能导致性能损失:在某些情况下,门面模式可能会导致性能损失,特别是当门面类需要频繁调用多个子系统的功能时,可能会影响系统的性能。
综上所述,门面模式可以简化系统的复杂性,提高系统的灵活性和可维护性,但也可能增加系统的复杂性,不符合开闭原则,并可能导致性能损失。因此,我们在使用门面模式时需要权衡利弊,根据具体的情况进行选择。
开闭原则(Open/Closed Principle,OCP)是面向对象设计中的一个重要原则,由勃兰特·梅耶(Bertrand Meyer)提出。该原则指出一个软件实体(如类、模块、函数等)应该对扩展开放,对修改关闭,即软件实体在不修改原有代码的情况下可以通过扩展来实现新的功能。
简单来说,开闭原则要求设计的软件组件应该可以被扩展,以便应对未来的变化,但是不应该被修改,以保持其稳定性。这意味着在软件设计中应该尽量避免修改已有的代码,而是通过增加新的代码来实现新的功能或变化。
遵循开闭原则可以提高软件的灵活性、可维护性和可扩展性,降低了软件的维护成本。同时,开闭原则也是设计模式的基础之一,许多设计模式都是为了实现开闭原则而提出的。
适配器模式【Adapter Pattern】
定义
在生活中,我们常常会遇到不同国家或地区使用不同电压标准的情况,比如中国和日本的电压标准就不同。如果我们带着中国生产的电子设备去日本,由于电压不兼容,设备无法直接使用。这时候,我们就需要使用一个电源适配器,它能够将日本的电压转换为中国的电压,从而让设备能够正常工作。
在软件开发中,适配器模式也能解决类似的问题。当我们设计软件系统时,经常会遇到需要将一个类的接口转换成另一个接口的情况。这可能是因为已有的类接口不符合当前需求,或者是为了与现有的代码进行集成而需要进行接口转换。适配器模式就是为了解决这类问题而设计的。适配器模式允许接口不兼容的类能够一起工作,其核心思想是创建一个包装类,即适配器,用于将原始类的接口转换成目标接口,从而让两个不兼容的类能够协同工作。
例如,当我们需要使用一个已有的类,但其接口与我们期望的接口不一致时,可以编写一个适配器类来转换这个接口,使其符合我们的需求。这样一来,我们就可以无缝地使用该类,而无需修改原有的代码或接口。适配器模式能够有效地降低系统的耦合度,提高代码的复用性和灵活性。
在适配器模式中,有三个核心角色:
-
目标接口(Target Interface):这是客户端期待的接口,也是适配器模式中的目标。客户端与适配器交互时使用的就是目标接口。
-
原始类(Adaptee):原始类是需要被适配的类,它的接口与目标接口不兼容。也就是说,原始类的接口不能直接满足客户端的需求。
-
适配器(Adapter):适配器是连接原始类和目标接口的桥梁。适配器实现了目标接口,并且内部持有原始类的实例。适配器通过调用原始类的方法来实现目标接口的方法,从而使得原始类能够与客户端一起工作。
在这个模式中,适配器起到了一个包装器(Wrapper)的作用,因为它包装了一个或多个被适配者对象,使其符合目标接口的形式。
举个简单的例子,假设我们有一个目标接口 Target,其中定义了一个名为 request() 的方法。另外,我们有一个被适配者接口 Adaptee,其中定义了一个名为 specificRequest() 的方法。由于客户端代码依赖于 Target 接口,但我们想要使用 Adaptee 接口提供的功能,我们需要编写一个适配器类 Adapter,它实现了 Target 接口,并在其 request() 方法中调用了 Adaptee 接口的 specificRequest() 方法。
通过适配器模式,客户端代码可以直接调用适配器的 request() 方法,而无需关心具体的实现细节,也不需要直接依赖于被适配者接口。适配器将被适配者接口的功能适配成了目标接口的形式,使得客户端代码能够方便地使用。
适配器模式的工作流程通常如下:
- 客户端与适配器交互,调用目标接口中的方法。
- 适配器内部调用原始类的方法,执行具体的逻辑。
- 原始类执行完逻辑后,将结果返回给适配器。
- 适配器将结果返回给客户端,完成整个调用过程。
举例说明
假设你是一位外星人,来到地球进行观察和研究。你发现地球上的人类使用的语言和你的母语完全不同。在尝试与地球人进行交流时,你遇到了巨大的困难,因为你无法理解地球上的语言。你感到很沮丧,因为你无法有效地与地球人交流,也无法深入了解他们的文化和生活方式。你想要学习地球上的语言,但是这需要时间和耐心。
就在你陷入困境之际,你发现了一种神秘的设备,名为“地球语言翻译器”。这个翻译器被称为地球人的“通用翻译器”,据说它能够将地球上各种不同的语言翻译成你所理解的语言,从而让你能够与地球人进行有效的沟通。
你兴奋地将“地球语言翻译器”拿起来,开始使用它。当你输入地球人的语言时,翻译器会立即将其转换成你所熟悉的语言,让你能够理解地球人所表达的意思。你发现这个翻译器非常有效,它不仅能够帮助你理解地球人的语言,还能够帮助你与他们进行流畅的交流。
通过使用这个神奇的翻译器,你开始与地球人进行深入的交流和探讨。你了解到地球人的文化、习俗和生活方式,与他们建立起了良好的关系。你开始感受到了地球的独特魅力,也逐渐融入了地球社会。
在这个过程中,你意识到适配器的重要性。正是这个地球语言翻译器的适配器,让你能够克服语言障碍,与地球人进行有效的沟通,从而更好地了解和融入地球文化。适配器模式的本质就是将不同的接口或系统进行适配,使它们能够协同工作,实现更大的目标。
在这个例子中:
-
你的母语就是目标接口(Target),它代表了你所理解的语言。
-
地球上的语言就是被适配者(Adaptee),它是你无法理解的语言。
-
“地球语言翻译器”就是适配器(Adapter),它能够接收地球上的语言,并将其转换成你能够理解的语言。
通过使用这个神奇的翻译器适配器,你可以轻松地理解地球人的交流内容,与他们进行沟通和交流,从而更好地了解地球文化和人类社会。
// 地球语言接口
interface EarthLanguage {String speak();
}// 具体的地球语言类
class EarthLanguageImpl implements EarthLanguage {@Overridepublic String speak() {return "你好,你能听懂我说的话吗?"; // 返回中文内容}
}// 外星语言接口
interface AlienLanguage {String understand(String input);
}// 具体的外星语言类
class AlienLanguageImpl implements AlienLanguage {@Overridepublic String understand(String input) {// 假设这里有一个复杂的算法来理解外星语言// 这里我们简单地返回一个假设的理解结果return "Understood: " + input; // 返回英文内容}
}// 地球与外星人的翻译器适配器
class EarthAlienTranslatorAdapter implements AlienLanguage {private EarthLanguage earthLanguage;public EarthAlienTranslatorAdapter(EarthLanguage earthLanguage) {this.earthLanguage = earthLanguage;}@Overridepublic String understand(String input) {// 地球语言转换成外星语言的逻辑// 这里我们简单地返回一个假设的翻译结果return "Translated: " + input; // 返回英文内容}
}// 地球人类
class Earthling {private EarthLanguage earthLanguage;public Earthling() {this.earthLanguage = new EarthLanguageImpl();}// 地球人使用翻译器进行交流public void communicateWithAlien(AlienLanguage translator) {System.out.println("Earthling: " + translator.understand(earthLanguage.speak()));}
}// 外星人类
class Alien {private AlienLanguage alienLanguage;public Alien(AlienLanguage alienLanguage) {this.alienLanguage = alienLanguage;}// 外星人使用自己的语言进行交流public void communicateWithEarthling(String input) {System.out.println("Alien: " + alienLanguage.understand(input));}
}// 地球与外星人的交流示例
public class CommunicationExample {public static void main(String[] args) {// 创建地球人和外星人对象Earthling earthling = new Earthling();AlienLanguage alienTranslator = new EarthAlienTranslatorAdapter(new EarthLanguageImpl());Alien alien = new Alien(alienTranslator);// 地球人与外星人进行交流earthling.communicateWithAlien(alienTranslator);alien.communicateWithEarthling("Hello, how are you?"); // 外星人说的内容}
}
我们首先定义了地球语言接口 EarthLanguage 和外星语言接口 AlienLanguage,分别用于实现地球人和外星人的语言。然后,我们实现了地球语言类 EarthLanguageImpl 和外星语言类 AlienLanguageImpl。
接着,我们创建了一个适配器类 EarthAlienTranslatorAdapter,它将地球语言转换成外星语言,从而让外星人能够理解地球人的语言。适配器实现了外星语言接口,但内部使用地球语言接口来实现翻译。地球人通过翻译器与外星人进行交流,地球人说地球语言,但翻译器将其翻译成了外星语言,使得外星人能够理解。外星人直接使用自己的语言与地球人交流,因为翻译器已经将地球人的话翻译成了外星人能够理解的外星语言。
最后,我们创建了地球人类 Earthling 和外星人类 Alien,它们分别使用了适配器和原始的外星语言类进行交流。通过适配器模式,外星人能够使用自己的语言与地球人进行交流,实现了语言的适配和沟通的目的。
简而言之,适配器的作用是让不兼容的接口能够协同工作。适配器让地球人和外星人能够用各自的语言进行交流,而不需要改变它们原本的方式。适配器内部处理了语言之间的转换,使得交流变得顺畅。
其实这里也可以使用门面模式:
// 地球语言翻译器接口
interface EarthLanguageTranslator {String translate(String input);
}// 具体的地球语言翻译器实现类
class UniversalTranslator implements EarthLanguageTranslator {@Overridepublic String translate(String input) {// 假设这里有一个复杂的算法来进行地球语言的翻译// 这里我们简单地返回一个假设的翻译结果return "Translated: " + input;}
}// 外星语言翻译器接口
interface AlienLanguageTranslator {String translate(String input);
}// 具体的外星语言翻译器实现类
class AlienTranslator implements AlienLanguageTranslator {@Overridepublic String translate(String input) {// 假设这里有一个复杂的算法来进行外星语言的翻译// 这里我们简单地返回一个假设的翻译结果return "Translated: " + input;}
}// 门面类:地球通用翻译器
class EarthUniversalTranslatorFacade {private EarthLanguageTranslator earthTranslator;private AlienLanguageTranslator alienTranslator;public EarthUniversalTranslatorFacade() {this.earthTranslator = new UniversalTranslator();this.alienTranslator = new AlienTranslator();}// 地球人使用通用翻译器进行翻译public String translateFromEarthLanguage(String input) {return earthTranslator.translate(input);}// 外星人使用通用翻译器进行翻译public String translateFromAlienLanguage(String input) {return alienTranslator.translate(input);}
}// 地球人类
class Earthling {private EarthUniversalTranslatorFacade translator;public Earthling() {this.translator = new EarthUniversalTranslatorFacade();}// 地球人使用通用翻译器进行翻译public void communicateWithAlien(String input) {System.out.println("Earthling: " + translator.translateFromEarthLanguage(input));}
}// 外星人类
class Alien {private EarthUniversalTranslatorFacade translator;public Alien() {this.translator = new EarthUniversalTranslatorFacade();}// 外星人使用通用翻译器进行翻译public void communicateWithEarthling(String input) {System.out.println("Alien: " + translator.translateFromAlienLanguage(input));}
}// 地球与外星人的交流示例
public class CommunicationExample {public static void main(String[] args) {Earthling earthling = new Earthling();Alien alien = new Alien();// 地球人与外星人进行交流earthling.communicateWithAlien("Hello, how are you?");alien.communicateWithEarthling("你好,你能听懂我说的话吗?");}
}
核心思想
适配器模式的核心思想是将一个类的接口转换成客户端期望的另一个接口,从而使得原本由于接口不兼容而无法一起工作的类能够协同工作。它允许原始类与客户端进行通信,同时隐藏了原始类的实现细节,使得客户端不需要知道原始类的具体实现。这种转换允许原本不兼容的接口进行协同工作,而无需修改其源代码。
具体而言,适配器模式通过创建一个适配器类,将原始类的接口转换成目标接口,以便客户端能够调用目标接口的方法。适配器内部持有一个原始类的实例,并通过调用原始类的方法来实现目标接口的方法。这样,客户端就可以直接与适配器进行交互,而不需要知道原始类的存在。
在适配器模式中,适配器充当一个中间者的角色,它将客户端的请求转发给原始类,并将原始类的响应转换成客户端所期待的形式,以完成接口之间的适配。
适配器模式的核心思想可以用以下几点来概括:
-
接口转换:适配器模式的主要任务是进行接口转换,将一个接口转换成另一个接口,使得原始类能够与客户端兼容。
-
保持封装性:适配器模式通过保持封装性,隐藏了原始类的实现细节,使得客户端无需了解原始类的具体实现即可使用适配器。
-
解耦合:适配器模式实现了客户端和原始类的解耦合,使得它们之间的依赖关系降低到最小程度。
-
灵活性:适配器模式提高了系统的灵活性,允许在不修改原始类的情况下,通过适配器来适应新的需求或新的环境。
适配器模式的核心思想在于解决不同接口之间的兼容性问题,使得原本无法一起工作的类能够协同工作,从而提高了系统的灵活性和可扩展性。
适用环境
适配器模式通常适用于以下几种情况:
-
已有接口不符合需求:当已有的接口与系统的需求不匹配,但又无法修改原始接口时,可以使用适配器模式。这种情况下,适配器可以将原始接口转换成客户端所期望的接口,从而使得原始接口能够在新的环境中被使用。
-
系统需要与多个不兼容的接口交互:当系统需要与多个不兼容的接口进行交互时,可以使用适配器模式来统一这些接口,使得系统能够与这些接口进行无缝集成。
-
封装已有类库:当需要使用某个已有类库,但其接口与系统的需求不符时,可以通过适配器模式来封装该类库,以满足系统的需求而不影响现有代码。
-
与外部系统集成:当系统需要与外部系统进行集成,但外部系统的接口与系统的内部接口不匹配时,可以使用适配器模式来进行接口转换,使得系统能够与外部系统无缝集成。
适配器模式适用于需要将不兼容的接口转换成兼容接口的场景,以实现系统的功能扩展、接口统一和系统集成等需求。
优缺点
适配器模式的优点包括:
-
解耦性强:适配器模式可以将客户端代码与被适配的类解耦,客户端无需了解被适配类的实现细节,只需要通过适配器进行调用。
-
复用性高:适配器模式可以重用现有的类,通过适配器将其接口转换为客户端所期望的接口,从而实现功能复用。
-
灵活性好:适配器模式可以在不修改现有代码的情况下引入新的功能,只需编写新的适配器类即可。
-
扩展性强:适配器模式可以灵活地添加新的适配器类来适配不同的类和接口,从而满足不同的需求。
适配器模式的缺点包括:
-
增加系统复杂性:引入适配器模式会增加代码的复杂性,特别是在系统中存在大量不兼容的接口时,可能需要编写大量的适配器类,导致系统变得复杂难以维护。
-
可能引入性能损耗:适配器模式在转换接口时可能会引入额外的性能开销,特别是在频繁调用的场景下,可能会影响系统的性能表现。
-
过度使用可能导致设计混乱:如果过度使用适配器模式,可能会导致系统设计变得混乱,增加代码的理解和维护难度,因此需要谨慎使用适配器模式。
模板方法模式【Template Method Pattern】
定义
模板方法模式是一种行为设计模式,它定义了一个算法的框架,将算法的具体步骤延迟到子类中实现。这种模式通过将算法的通用部分封装在一个抽象基类中,而将具体的实现细节交给子类来完成,从而提供了代码复用和灵活性。
在模板方法模式中,通常存在以下角色:
-
抽象基类(AbstractClass):也称为模板类,其中包含了算法的主要逻辑和一系列抽象方法或具体方法。这些方法组成了算法的各个步骤,而模板方法则定义了算法的框架。抽象基类可以包含一些具体方法,供模板方法调用。
-
具体子类(ConcreteClass):继承自抽象基类,并实现了其中定义的抽象方法,完成了算法中的具体步骤。具体子类可以根据需要重写模板方法中的具体步骤,以实现定制化的算法逻辑。
模板方法模式的核心思想是将算法的不变部分封装在抽象基类中,而将可变部分延迟到具体子类中去实现。这样做的好处在于,它能够保持算法的整体结构不变,同时又能够让子类根据需要来实现具体的细节,从而提高了代码的复用性和扩展性。
总的来说,模板方法模式可以帮助我们实现一个算法的框架,同时允许子类根据需要进行定制,从而提高了代码的可维护性和扩展性。
举例说明
假设你是一位披萨店老板,你经营的披萨店提供多种口味的披萨,包括意大利经典披萨、墨西哥风味披萨和美式经典披萨。每种披萨都有自己独特的制作流程,但整体上制作披萨的步骤大致相同。你希望使用模板方法模式来优化披萨制作流程,提高制作效率和品质保证。
现在让我们来看看披萨制作的流程:
-
准备原料:所有披萨制作都需要准备面团、番茄酱、奶酪和各种配料。
-
制作底盘:将面团擀成圆形薄饼作为披萨的底盘。
-
涂抹番茄酱:在底盘上均匀涂抹一层番茄酱。
-
撒上奶酪:将奶酪均匀撒在番茄酱上。
-
加入配料:根据披萨的种类加入不同的配料,如意大利披萨加入意大利香肠和橄榄,墨西哥披萨加入墨西哥辣椒和玉米粒,美式披萨加入培根和火腿。
-
烘烤披萨:将加好配料的披萨放入烤箱烘烤一段时间,直到奶酪融化且底部变脆。
-
完成:取出烤好的披萨,切片并上菜。
首先,我们定义了一个抽象基类 PizzaMaker,其中包含了披萨制作的整个流程。这个类里有一个模板方法 makePizza(),它定义了制作披萨的步骤:准备原料、制作面团、涂抹番茄酱、撒上奶酪、加入配料、烘烤披萨和完成披萨。除了模板方法外,抽象基类还包含了一些具体方法,如 prepareIngredients()、makeDough()、spreadTomatoSauce() 等,它们是构成制作流程的具体步骤,但可以在子类中根据需要进行重写。
然后,我们创建了三个具体的披萨类:ItalianPizza、MexicanPizza 和 AmericanPizza,它们分别表示意大利披萨、墨西哥披萨和美式披萨。每个具体披萨类都继承自抽象基类 PizzaMaker,并实现了其中的抽象方法 addToppings(),以完成特定种类披萨的特色配料加入。
最后,在客户端代码中,我们创建了不同种类的披萨对象,并调用它们的 makePizza() 方法来制作披萨。在制作过程中,模板方法 makePizza() 控制着制作流程的顺序和步骤,而具体披萨类则负责实现特定种类披萨的特色配料加入,从而形成了一个完整的披萨制作流程。
// 披萨制作抽象基类
abstract class PizzaMaker {// 制作披萨的模板方法,定义了披萨制作的流程public final void makePizza() {prepareIngredients();makeDough();spreadTomatoSauce();sprinkleCheese();addToppings();bakePizza();finishPizza();}// 准备原料protected abstract void prepareIngredients();// 制作面团protected abstract void makeDough();// 涂抹番茄酱protected abstract void spreadTomatoSauce();// 撒上奶酪protected abstract void sprinkleCheese();// 加入配料protected abstract void addToppings();// 烘烤披萨protected void bakePizza() {System.out.println("Baking the pizza...");}// 完成披萨protected void finishPizza() {System.out.println("Pizza is ready to be served!");}
}// 意大利披萨类
class ItalianPizza extends PizzaMaker {@Overrideprotected void prepareIngredients() {System.out.println("Preparing Italian pizza ingredients...");}@Overrideprotected void makeDough() {System.out.println("Making Italian pizza dough...");}@Overrideprotected void spreadTomatoSauce() {System.out.println("Spreading tomato sauce on Italian pizza...");}@Overrideprotected void sprinkleCheese() {System.out.println("Sprinkling cheese on Italian pizza...");}@Overrideprotected void addToppings() {System.out.println("Adding Italian pizza toppings: olives, mushrooms, tomatoes...");}
}// 墨西哥披萨类
class MexicanPizza extends PizzaMaker {@Overrideprotected void prepareIngredients() {System.out.println("Preparing Mexican pizza ingredients...");}@Overrideprotected void makeDough() {System.out.println("Making Mexican pizza dough...");}@Overrideprotected void spreadTomatoSauce() {System.out.println("Spreading tomato sauce on Mexican pizza...");}@Overrideprotected void sprinkleCheese() {System.out.println("Sprinkling cheese on Mexican pizza...");}@Overrideprotected void addToppings() {System.out.println("Adding Mexican pizza toppings: jalapenos, peppers, onions...");}
}// 美式披萨类
class AmericanPizza extends PizzaMaker {@Overrideprotected void prepareIngredients() {System.out.println("Preparing American pizza ingredients...");}@Overrideprotected void makeDough() {System.out.println("Making American pizza dough...");}@Overrideprotected void spreadTomatoSauce() {System.out.println("Spreading tomato sauce on American pizza...");}@Overrideprotected void sprinkleCheese() {System.out.println("Sprinkling cheese on American pizza...");}@Overrideprotected void addToppings() {System.out.println("Adding American pizza toppings: bacon, sausage, ham...");}
}// 客户端代码
public class PizzaFactory {public static void main(String[] args) {// 制作意大利披萨PizzaMaker italianPizza = new ItalianPizza();System.out.println("Making Italian Pizza:");italianPizza.makePizza();System.out.println();// 制作墨西哥披萨PizzaMaker mexicanPizza = new MexicanPizza();System.out.println("Making Mexican Pizza:");mexicanPizza.makePizza();System.out.println();// 制作美式披萨PizzaMaker americanPizza = new AmericanPizza();System.out.println("Making American Pizza:");americanPizza.makePizza();}
}
通过模板方法模式,我们成功地将披萨制作流程的公共部分封装在抽象基类中,而将特定种类披萨的个性化部分延迟到子类中实现,使得制作过程更加灵活和高效。
核心思想
模板方法模式的核心思想是将一个算法的骨架定义在一个抽象类中,而将具体的步骤延迟到子类中去实现。这个抽象类中通常包含一个模板方法,该方法定义了算法的框架,其中调用了一系列的抽象方法或具体方法,这些方法代表了算法的各个步骤。
具体来说,模板方法模式通常包含以下几个要素:
-
抽象类(AbstractClass): 抽象类定义了算法的框架,其中包含了一个或多个抽象方法或者具体方法,这些方法组成了算法的各个步骤。模板方法作为算法的骨架,定义了算法的整体流程,并调用了各个步骤的方法。
-
具体子类(ConcreteClass): 具体子类继承自抽象类,并实现了其中的抽象方法,完成了算法中的具体步骤。子类可以根据需要来重写模板方法中的具体步骤,从而实现定制化的算法逻辑。
-
模板方法(Template Method): 模板方法是定义在抽象类中的一个方法,它作为算法的骨架,定义了算法的整体流程,其中包含了一系列的步骤调用。这些步骤可以是抽象方法,由子类来实现,也可以是具体方法,提供了默认的实现。
通过使用模板方法模式,我们可以将算法的不变部分封装在抽象类中,而将可变部分交给子类来实现。这样可以确保算法的整体结构不会改变,但是具体的实现可以根据需要灵活变化。模板方法模式提供了一种在不改变算法整体结构的情况下定制算法细节的方法,从而提高了代码的可维护性和扩展性。
适用环境
模板方法模式通常在以下情况下使用:
-
算法的整体结构相同,但具体步骤可以在子类中灵活变化: 当存在一系列相似的算法,并且它们的整体结构是相同的,但其中一些步骤的具体实现可能会有所不同时,可以使用模板方法模式。这样可以将这些共同的行为封装在抽象基类中的模板方法中,而将具体步骤的实现延迟到子类中。
-
避免代码重复: 如果存在多个类似的算法,并且它们的某些部分具有相同的实现逻辑,可以将这些公共部分提取出来放在抽象基类中的模板方法中,从而避免重复编写相同的代码。
-
具体步骤需要定制化: 当需要在不同的情况下定制算法的具体步骤时,可以通过子类来实现这些具体步骤。模板方法模式允许在子类中重写模板方法的具体步骤,从而实现定制化的算法逻辑。
-
控制算法的流程: 模板方法模式可以在抽象基类中控制算法的整体流程,而具体的实现细节则由子类来决定。这样可以更好地管理算法的执行过程,提高了代码的可维护性和扩展性。
模板方法模式适用于需要定义算法的整体结构,并允许子类根据需要进行定制的情况。它能够提高代码的复用性,降低了代码的重复度,并且使算法的流程更加清晰易懂。
优缺点
模板方法模式具有以下优点和缺点:
优点:
-
提高代码复用性: 将共同的行为封装在抽象基类中的模板方法中,可以在多个子类中共享这些行为,从而提高了代码的复用性。
-
提高代码扩展性: 模板方法模式允许子类根据需要重写模板方法中的具体步骤,从而实现定制化的算法逻辑,使得算法的实现更加灵活和可扩展。
-
降低代码重复度: 将算法的共同部分抽象到抽象基类中,避免了在多个地方重复编写相同的代码,从而降低了代码的重复度,提高了代码的维护性和可读性。
-
符合开闭原则: 模板方法模式在抽象基类中定义了算法的框架,而具体步骤的实现延迟到子类中,使得算法的整体结构保持稳定,但具体实现可以灵活变化,符合开闭原则。
-
利于维护: 由于算法的整体结构都在抽象基类中定义,因此当需要修改算法时,只需要修改抽象基类中的模板方法即可,不需要修改所有的子类,使得系统更加易于维护。
缺点:
-
增加了类的数量: 使用模板方法模式会增加类的数量,因为每个具体算法都需要对应一个子类来实现具体步骤,可能会导致类的层次结构较深,增加了系统的复杂度。
-
限制了部分灵活性: 模板方法模式将算法的整体结构固定在抽象基类中,而具体步骤的实现则由子类来决定,这可能会限制一些特定场景下的灵活性,不够灵活。
-
对抽象方法的依赖: 抽象基类中的模板方法依赖于一系列抽象方法或者具体方法,这些方法必须由子类来实现,因此在使用模板方法模式时,需要设计良好的抽象接口,以保证子类能够正确实现这些方法。
总的来说,模板方法模式适用于需要定义算法的整体结构,并允许子类根据需要进行定制的情况,它能够提高代码的复用性和扩展性,但也会增加类的数量,限制部分灵活性,并且对抽象方法的设计要求较高。
建造者模式【Builder Pattern】
定义
建造者模式(Builder Pattern)是一种创建型设计模式,它允许你创建复杂对象的过程与其表示分离,从而可以使用相同的构建过程来创建不同的表示。
建造者模式是一种将一个复杂对象的构建过程与其表示分离的设计模式。通过将构建过程封装在一个独立的建造者对象中,客户端代码可以使用相同的构建过程来创建不同的表示。
在建造者模式中,通常包含以下几个关键角色:
-
产品(Product):表示被构建的复杂对象。产品类通常包含多个属性,并且可能包含其他对象作为其部分。
-
抽象建造者(Builder):定义了构建产品的抽象接口。抽象建造者通常包含多个方法,用于构建产品的不同部分,例如设置属性、添加部件等。
-
具体建造者(Concrete Builder):实现了抽象建造者接口,负责实际构建产品的具体步骤。每个具体建造者通常与特定类型的产品相关联,并实现了特定于该产品的构建逻辑。
-
指挥者(Director):负责使用建造者对象构建产品。指挥者通常包含一个构建方法,该方法接受一个建造者对象作为参数,并使用该建造者对象来构建产品。
使用建造者模式时,客户端通常通过以下步骤来创建产品:
-
创建一个具体建造者对象。
-
(可选)根据需要设置建造者对象的属性,例如调用方法来设置产品的属性或添加部件。
-
将建造者对象传递给指挥者对象。
-
指挥者对象使用建造者对象来构建产品。
-
客户端从指挥者对象中获取构建完成的产品。
通过建造者模式,可以将构建过程与具体产品的表示分离开来,使得客户端代码可以更加灵活地使用相同的构建过程来构建不同的产品。同时,建造者模式也可以避免在客户端代码中暴露产品的内部表示,从而提高了系统的封装性和安全性。
举例说明
假设你是一位寿司店的老板,你想要引入一种新的寿司套餐。当你决定引入新的寿司套餐时,你深知顾客的口味千差万别,有些人喜欢浓郁的三文鱼,而另一些人可能更喜欢鳗鱼的香味。因此,你希望设计一种寿司拼盘,能够满足不同顾客的口味需求,并且给他们带来愉悦的用餐体验。
你开始思考如何构建这样一款多样化的寿司拼盘。首先,你意识到寿司拼盘的构建过程是相当复杂的,需要考虑到各种不同种类的寿司、配料的搭配和摆盘的美观。为了简化这个过程,你决定采用建造者模式,将寿司的构建过程分解成多个简单的步骤,然后由具体的寿司建造者来负责执行这些步骤。
在实践中,你创建了一个名为“特色寿司拼盘”的寿司套餐,并定义了一个寿司建造者接口(SushiBuilder),其中包含了构建寿司的各个步骤,如准备米饭、选择海鲜、添加调味料等。接着,你设计了具体的寿司建造者类,例如SalmonSushiBuilder、EelSushiBuilder、ShrimpSushiBuilder等,每个建造者类负责构建特定类型的寿司。比如,SalmonSushiBuilder负责构建三文鱼寿司,EelSushiBuilder负责构建鳗鱼寿司,ShrimpSushiBuilder负责构建虾仁寿司。
然后,你在寿司店的客户端代码中,你创建了一个寿司店类,该类包含了一个构建寿司拼盘的方法,客户可以调用该方法并传入不同的寿司建造者对象来构建不同类型的寿司拼盘。通过使用建造者模式,你成功地实现了一个灵活、可定制的寿司拼盘套餐,使得顾客可以根据自己的喜好选择不同种类的寿司,从而提升了顾客的满意度和体验。
最终,你在寿司店的菜单中推出了“特色寿司拼盘”套餐,顾客可以根据自己的口味选择不同的寿司组合。如果顾客喜欢三文鱼,他们可以选择SalmonSushiBuilder构建的寿司,如果喜欢鳗鱼,他们可以选择EelSushiBuilder构建的寿司,以此类推。通过建造者模式,你成功地提供了一种灵活、多样化的寿司套餐,满足了不同顾客的口味需求,为他们带来了美味的用餐体验。
// 寿司建造者接口
interface SushiBuilder {void prepareRice();void chooseSeafood();void addSeasoning();void decorate();String getSushi();
}// 三文鱼寿司建造者
class SalmonSushiBuilder implements SushiBuilder {private StringBuilder sushi;public SalmonSushiBuilder() {this.sushi = new StringBuilder();}@Overridepublic void prepareRice() {sushi.append("Preparing rice...\n");}@Overridepublic void chooseSeafood() {sushi.append("Choosing fresh salmon...\n");}@Overridepublic void addSeasoning() {sushi.append("Adding soy sauce and wasabi...\n");}@Overridepublic void decorate() {sushi.append("Decorating with sesame seeds and cucumber slices...\n");}@Overridepublic String getSushi() {return sushi.toString();}
}// 鳗鱼寿司建造者
class EelSushiBuilder implements SushiBuilder {private StringBuilder sushi;public EelSushiBuilder() {this.sushi = new StringBuilder();}@Overridepublic void prepareRice() {sushi.append("Preparing rice...\n");}@Overridepublic void chooseSeafood() {sushi.append("Selecting fresh eel...\n");}@Overridepublic void addSeasoning() {sushi.append("Adding eel sauce and wasabi...\n");}@Overridepublic void decorate() {sushi.append("Garnishing with avocado slices and eel sauce drizzle...\n");}@Overridepublic String getSushi() {return sushi.toString();}
}// 虾仁寿司建造者
class ShrimpSushiBuilder implements SushiBuilder {private StringBuilder sushi;public ShrimpSushiBuilder() {this.sushi = new StringBuilder();}@Overridepublic void prepareRice() {sushi.append("Preparing rice...\n");}@Overridepublic void chooseSeafood() {sushi.append("Selecting fresh shrimp...\n");}@Overridepublic void addSeasoning() {sushi.append("Adding spicy mayo and wasabi...\n");}@Overridepublic void decorate() {sushi.append("Garnishing with avocado slices and tobiko...\n");}@Overridepublic String getSushi() {return sushi.toString();}
}// 寿司店类
class SushiShop {public void createSpecialSushi(SushiBuilder builder) {builder.prepareRice();builder.chooseSeafood();builder.addSeasoning();builder.decorate();System.out.println("Special Sushi Platter:\n" + builder.getSushi());}
}// 客户端代码
public class SushiShopMain {public static void main(String[] args) {SushiShop sushiShop = new SushiShop();// 构建三文鱼寿司SushiBuilder salmonSushiBuilder = new SalmonSushiBuilder();sushiShop.createSpecialSushi(salmonSushiBuilder);// 构建鳗鱼寿司SushiBuilder eelSushiBuilder = new EelSushiBuilder();sushiShop.createSpecialSushi(eelSushiBuilder);// 构建虾仁寿司SushiBuilder shrimpSushiBuilder = new ShrimpSushiBuilder();sushiShop.createSpecialSushi(shrimpSushiBuilder);}
}
在建造者模式中,指挥者的作用是负责组装产品的各个部件,但在某些情况下,我们也可以将指挥者的职责交给客户端类来处理,特别是当构建过程相对简单,不需要额外的指挥者时。
public class SushiShop {public static void main(String[] args) {// 创建特色寿司拼盘SushiBuilder salmonBuilder = new SalmonSushiBuilder();SushiBuilder eelBuilder = new EelSushiBuilder();SushiBuilder shrimpBuilder = new ShrimpSushiBuilder();// 构建三文鱼寿司prepareAndDisplay(salmonBuilder);// 构建鳗鱼寿司prepareAndDisplay(eelBuilder);// 构建虾仁寿司prepareAndDisplay(shrimpBuilder);}private static void prepareAndDisplay(SushiBuilder builder) {// 构建寿司拼盘builder.prepareRice();builder.chooseSeafood();builder.addSeasoning();builder.decorate();// 显示寿司拼盘内容System.out.println("Special Sushi Platter:");System.out.println(builder.getSushi());}
}
这个寿司店的例子就生动地展示了建造者模式的核心思想:将复杂对象的构建过程拆分成多个简单的步骤,并通过建造者类来封装这些步骤,从而实现了对象的灵活构建和定制化。
核心思想
具体来说,建造者模式包含以下关键思想:
-
分离构建过程与表示:建造者模式通过将一个复杂对象的构建过程封装在一个独立的建造者对象中,使得构建过程与最终的表示分离开来。这样可以使得构建过程可以灵活地复用,同时也使得客户端代码不需要了解具体的构建细节。
-
逐步构建:建造者模式通常采用逐步构建的方式来创建复杂对象,即通过一系列的步骤逐步完成对象的构建。每一步都由建造者对象负责完成,从而保证了构建过程的逐步完善。
-
产品与建造者的分离:在建造者模式中,通常将产品的表示与构建过程分离开来。产品类通常包含多个属性,而建造者类则负责构建产品的不同部分,并将这些部分组装成一个完整的产品。这样可以避免在客户端代码中直接操作产品对象,从而提高了系统的封装性和灵活性。
总的来说,建造者模式的核心思想是将一个复杂对象的构建过程封装在一个独立的建造者对象中,使得构建过程与表示分离,从而提高了系统的灵活性、可维护性和可扩展性。
适用环境
建造者模式通常在以下情况下适用:
-
创建复杂对象:当需要创建的对象具有复杂的内部结构,包含多个部分或者属性,并且这些部分之间的组合方式多种多样时,可以考虑使用建造者模式。通过建造者模式,可以将复杂对象的构建过程分解为多个简单的步骤,每个步骤由一个建造者负责,从而使得整个构建过程更加清晰和灵活。
-
构建过程需要灵活性:当构建过程中的每个步骤都可以灵活地配置或者组合时,可以考虑使用建造者模式。建造者模式允许客户端根据需要选择或者配置不同的建造者对象,从而实现灵活的构建过程,满足不同的需求。
-
避免构造器参数过多:当一个类的构造器参数过多,导致构造器方法参数列表过长或者难以理解时,可以考虑使用建造者模式。通过建造者模式,可以将复杂对象的构造过程拆分为多个方法调用,每个方法负责设置对象的一个属性或者部分属性,使得构造器方法的参数列表更加清晰简洁。
-
需要创建不同表示的对象:当需要使用相同的构建过程来创建不同表示的对象时,可以考虑使用建造者模式。建造者模式允许客户端根据需要选择或者配置不同的建造者对象,从而创建不同表示的对象,而无需修改构建过程的代码。
建造者模式适用于需要创建复杂对象,并且希望将构建过程与表示分离,同时需要灵活配置构建过程的情况。它能够使得对象的构建过程更加清晰、灵活和可维护,同时也提高了系统的可扩展性和复用性。
优缺点
建造者模式(Builder Pattern)的优点包括:
-
构建过程的灵活性:建造者模式可以通过组合不同的建造者对象来构建不同的产品,从而实现构建过程的灵活性。客户端可以根据需要选择或者配置不同的建造者对象,从而实现不同的构建过程。
-
隐藏复杂对象的构建过程:建造者模式将复杂对象的构建过程封装在具体的建造者类中,使得客户端无需了解对象的构建细节。客户端只需要与建造者接口交互,通过调用建造者的方法来构建对象,从而隐藏了对象构建过程的复杂性。
-
提高代码的可维护性:建造者模式将对象的构建过程拆分为多个步骤,并将每个步骤封装在单独的方法中,使得构建过程更加清晰和模块化。这样可以提高代码的可维护性,降低了代码的耦合度,使得修改和扩展更加容易。
-
创建复杂对象:建造者模式适用于创建复杂对象,包含多个部分或者属性,并且这些部分之间的组合方式多种多样的情况。通过建造者模式,可以将复杂对象的构建过程分解为多个简单的步骤,每个步骤由一个建造者负责,从而使得整个构建过程更加清晰和灵活。
建造者模式的缺点包括:
-
增加了代码量:建造者模式通常需要定义多个具体的建造者类,以及可能涉及到的指挥者类,这可能会增加代码的复杂度和数量。
-
对象构建过程较为静态:建造者模式一般将对象的构建过程封装在具体的建造者类中,而这些构建过程通常是静态的,难以动态地改变。因此,如果需要在运行时动态地改变对象的构建过程,可能不太适合使用建造者模式。
-
不适用于创建简单对象:如果对象的构建过程比较简单,或者对象的属性较少,可能使用建造者模式会显得过于繁琐,不太适合。建造者模式更适用于创建复杂对象的情况。
综上所述,建造者模式适用于创建复杂对象,并且需要灵活地配置构建过程的情况。它提供了灵活性、可维护性和封装性等优点,但也会增加代码量,并且不适用于创建简单对象的情况。
桥梁模式【Bridge Pattern】
定义
桥梁模式是一种结构型设计模式,它用于将抽象部分与实现部分分离,以便它们可以独立地变化。在桥梁模式中,通过将抽象部分和实现部分分别封装在不同的类中,并在它们之间建立一个桥梁(Bridge),使得抽象部分和实现部分可以独立地进行扩展、变化和组合。
具体来说,桥梁模式由以下几个关键组成部分:
-
抽象部分(Abstraction):定义了抽象部分的接口和方法,它通常是一个抽象类或接口。抽象部分包含了一个对实现部分的引用,即桥梁对象,通过桥梁对象调用实现部分的方法来完成其功能。
-
实现部分(Implementor):定义了实现部分的接口和方法,它也可以是一个抽象类或接口。实现部分提供了抽象部分定义的方法的具体实现。
-
抽象部分的实现(Refined Abstraction):是抽象部分的具体实现,它继承自抽象部分,并实现了抽象部分中定义的抽象方法。
-
实现部分的实现(Concrete Implementor):是实现部分的具体实现,它实现了实现部分中定义的方法。
-
桥梁(Bridge):连接了抽象部分和实现部分,使得它们可以独立地变化。桥梁通常是一个对象,它包含了对实现部分的引用,并将抽象部分的方法委托给实现部分来执行。
桥梁模式的核心思想是通过将抽象部分和实现部分分离开来,使得它们可以独立变化,从而提高了系统的灵活性和可扩展性。通过桥梁模式,可以将一个类的功能和实现分开,使得它们可以独立地进行修改、扩展和重用,同时也可以减少类之间的耦合度,提高系统的可维护性和可测试性。
我们可以举一个简单的例子,现在想象你有两个东西:形状(比如圆形、正方形)和颜色(比如红色、蓝色)。你想要创建不同形状的不同颜色的物体,比如红色圆形、蓝色正方形等等。
如果你直接把形状和颜色的信息硬编码在一起,就会导致代码重复和难以扩展。这时候,桥梁模式就派上用场了。
桥梁模式让你可以把形状和颜色分开处理,让它们可以独立变化。你可以有一个表示形状的类,比如 Circle 和 Square,还可以有一个表示颜色的类,比如 Red 和 Blue。然后你可以通过组合这两种类来创建各种不同的物体,比如 RedCircle、BlueSquare 等等。
这样一来,当你需要新增一种颜色或者形状时,你只需要添加一个新的类,而不需要改动已有的代码。这就使得你的代码更加灵活和易于扩展。
总的来说,桥梁模式的目标是将抽象部分和实现部分解耦,使得它们可以独立变化,从而更好地应对系统需求的变化和演化。
举例说明
我们这次用一个搭建桥梁的游戏来具体说明一下桥梁模式。
在这个游戏中,玩家将扮演一位桥梁建造师,任务是设计并搭建连接两个岛屿的桥梁,以便可爱的小动物们可以安全地通过。游戏的核心玩法是在有限的资源和预算下,选择适合的桥梁类型和材料来满足不同类型小动物的需求,以及应对不同地形和挑战。
游戏中包含以下关键角色和元素:
-
玩家:玩家扮演桥梁建造师的角色,负责设计和搭建桥梁,确保小动物们能够顺利通过。玩家需要考虑到各种因素,如小动物的种类、桥梁的长度、宽度、承载能力,以及地形的起伏和河流的宽度等。
-
小动物们:游戏中的主要角色之一,它们分为不同种类,如老鼠、兔子、猫、狗、大象等,每种小动物对桥梁的要求和能够通过的条件都不同。比如,老鼠可以通过简单的木头桥,而大象则需要更加坚固和稳固的桥梁。
-
桥梁类型:游戏中提供了多种类型的桥梁,包括木头桥、石头桥、钢筋混凝土桥等。每种类型的桥梁都有其特点和优缺点,玩家需要根据具体情况选择合适的桥梁类型。
-
桥梁材料:每种桥梁类型都由不同的材料构成,如木头、石头、钢筋混凝土等。桥梁的材料不仅影响着桥梁的外观和承载能力,还会影响到小动物通过的舒适度和安全性。
-
地形和挑战:游戏中的地形多种多样,有平坦的草地、起伏的山丘、湍急的河流等。玩家需要根据地形的不同来设计和搭建桥梁,并应对各种挑战,如风暴、洪水、地震等自然灾害。
具体来说,我们有以下几个角色:
-
抽象部分:桥梁抽象类(Abstraction)定义了搭建桥梁和检查桥梁安全性的方法。
-
实现部分:桥梁材料接口(Implementor)定义了具体桥梁材料的实现方法,如木头桥、石头桥和钢筋混凝土桥。
-
桥梁:连接桥梁抽象类和桥梁材料接口的桥梁。
-
扩展抽象部分:具体桥梁类继承桥梁抽象类,可以根据需要添加新的功能或修改已有功能,比如增加桥梁的长度或宽度。
-
扩展实现部分:具体桥梁材料类实现桥梁材料接口,提供了具体的桥梁材料和搭建方法。
通过使用桥梁模式,在游戏里面我们可以轻松地添加新的桥梁类型和材料,而不会影响到已有的游戏逻辑,从而为玩家带来更多的乐趣和挑战。
// 桥梁抽象类
abstract class Bridge {protected BridgeMaterial material;public Bridge(BridgeMaterial material) {this.material = material;}public abstract void buildBridge();public abstract void checkSafety();
}// 桥梁材料接口
interface BridgeMaterial {void useMaterial();
}// 桥梁材料实现类:木头桥
class WoodenBridge implements BridgeMaterial {@Overridepublic void useMaterial() {System.out.println("Using wood to build the bridge...");}
}// 桥梁材料实现类:石头桥
class StoneBridge implements BridgeMaterial {@Overridepublic void useMaterial() {System.out.println("Using stone to build the bridge...");}
}// 桥梁材料实现类:钢筋混凝土桥
class ConcreteBridge implements BridgeMaterial {@Overridepublic void useMaterial() {System.out.println("Using concrete to build the bridge...");}
}// 具体桥梁类:连接桥梁抽象类和桥梁材料接口
class ConcreteBridgeType extends Bridge {public ConcreteBridgeType(BridgeMaterial material) {super(material);}@Overridepublic void buildBridge() {System.out.println("Building a concrete bridge...");material.useMaterial();}@Overridepublic void checkSafety() {System.out.println("Checking the safety of the concrete bridge...");}
}// 客户端代码
public class BrigeMain {public static void main(String[] args) {// 创建桥梁材料BridgeMaterial wood = new WoodenBridge();BridgeMaterial stone = new StoneBridge();BridgeMaterial concrete = new ConcreteBridge();// 创建具体桥梁并使用不同材料搭建Bridge woodBridge = new ConcreteBridgeType(wood);woodBridge.buildBridge();woodBridge.checkSafety();Bridge stoneBridge = new ConcreteBridgeType(stone);stoneBridge.buildBridge();stoneBridge.checkSafety();Bridge concreteBridge = new ConcreteBridgeType(concrete);concreteBridge.buildBridge();concreteBridge.checkSafety();}
}
在这个例子中,桥梁模式将桥梁的建造和桥梁材料分开处理了。
- 桥梁抽象类 Bridge:定义了建造桥梁和检查桥梁安全性的方法,但并没有具体指定桥梁材料是什么,而是依赖于一个桥梁材料接口。
- 桥梁材料接口 BridgeMaterial:定义了使用材料的方法,每个具体的桥梁材料都需要实现这个接口。
- 具体桥梁类 ConcreteBridgeType:连接桥梁抽象类和桥梁材料接口,实现了建造桥梁和检查桥梁安全性的方法。在构造方法中,指定了使用的具体桥梁材料。
通过这种方式,桥梁的建造过程和使用的材料被分开处理,使得桥梁的建造和桥梁材料可以独立变化。这样一来,如果需要新增一种桥梁材料,只需要添加一个新的实现了 BridgeMaterial 接口的类,而不需要改动已有的桥梁类的代码。同样,如果需要新增一种类型的桥梁,也只需要添加一个新的具体桥梁类,而不需要改动已有的桥梁材料类的代码。
核心思想
桥梁模式的核心思想是将一个系统分解为两个独立的部分:抽象部分和实现部分,并通过一个桥梁将它们连接起来。这样做的目的是让它们可以独立地变化,从而提高系统的灵活性和可扩展性。
具体来说,桥梁模式包含以下几个关键点:
-
抽象部分(Abstraction):定义系统的高层结构,通常是一个抽象类或接口。抽象部分包含了客户端需要的接口或行为,并通过桥梁与实现部分连接。
-
实现部分(Implementor):定义了抽象部分的具体实现,通常是一个具体类或接口。实现部分负责实现抽象部分定义的接口或行为。
-
桥梁(Bridge):连接抽象部分和实现部分的接口,通常是一个抽象类或接口。桥梁允许抽象部分和实现部分可以独立地变化,而不会相互影响。
-
扩展抽象部分(Refined Abstraction):对抽象部分进行扩展,通常是一个具体类。扩展抽象部分可以添加新的功能或修改已有功能,而不影响实现部分。
-
扩展实现部分(Concrete Implementor):对实现部分进行扩展,通常是一个具体类。扩展实现部分可以添加新的方法或修改已有方法,而不影响抽象部分。
桥梁模式的核心思想是让抽象部分和实现部分可以独立地变化,从而使系统更加灵活和可扩展。通过将系统分解为抽象部分和实现部分,并通过桥梁将它们连接起来,可以有效地降低系统的耦合度,提高系统的可维护性和可扩展性。
适用环境
桥梁模式通常适用于以下情况:
-
当一个类存在两个独立变化的维度时,可以使用桥梁模式。例如,一个抽象类有多个实现类,且抽象类和实现类之间存在多对多的关系。
-
当需要在抽象部分和实现部分之间建立一个稳定的连接,并且希望它们可以独立地变化时,可以使用桥梁模式。这样可以避免在抽象部分中包含太多的实现细节,从而降低了系统的耦合度。
-
当希望在运行时可以动态地选择抽象部分和实现部分的具体实现时,可以使用桥梁模式。桥梁模式可以通过组合不同的抽象部分和实现部分来创建不同的对象,从而实现了灵活性和可定制性。
-
当一个类的实现可能存在多种维度的变化,且希望将这些变化分离开来,使得它们可以独立地进行扩展和变化时,可以使用桥梁模式。桥梁模式可以将不同维度的变化分别封装在抽象部分和实现部分中,从而提高了系统的灵活性和可扩展性。
总之,桥梁模式适用于那些具有多个独立变化维度并且需要将抽象部分和实现部分分离的情况。它提供了一种灵活的设计方式,使得系统能够更好地适应变化,并且降低了系统各部分之间的耦合度。
优缺点
桥梁模式(Bridge Pattern)的优点包括:
-
分离抽象和实现:桥梁模式通过将抽象部分和实现部分分离,使得它们可以独立地变化。这样一来,系统的各个部分之间的耦合度降低了,同时也更容易理解和维护。
-
提高扩展性:由于抽象部分和实现部分可以独立地变化,因此可以很容易地对系统进行扩展。添加新的抽象部分或实现部分都不会影响到其他部分的稳定性,从而提高了系统的可扩展性。
-
实现细节对客户端透明:桥梁模式将抽象部分和实现部分解耦,客户端只需要关注抽象部分即可,而无需关心实现部分的具体细节。这样可以降低客户端与实现部分之间的依赖关系,提高了系统的灵活性和可维护性。
-
支持动态切换实现:由于桥梁模式将抽象部分和实现部分分离,因此可以在运行时动态地切换实现部分,从而实现了更高的灵活性和可定制性。
桥梁模式的缺点包括:
-
增加了系统的复杂度:桥梁模式引入了额外的抽象层,可能会增加系统的复杂度,特别是在设计初期可能会增加一定的开发成本和时间。
-
需要正确识别抽象和实现:桥梁模式需要正确地识别系统中存在的抽象部分和实现部分,并将它们分离出来。如果识别不准确,可能会导致设计出错或不必要的复杂性。
-
增加了系统的维护成本:由于桥梁模式引入了额外的抽象层,因此可能会增加系统的维护成本。当系统发生变化时,需要同时修改抽象部分和实现部分,这可能会带来额外的工作量和风险。
命令模式【Command Pattern】
定义
命令模式(Command Pattern)是一种行为设计模式,它允许将请求封装成一个对象,从而使不同的请求可以参数化其他对象(发送者)并将请求排队、记录请求日志、撤销操作等。这种模式提供了解耦发送者和接收者的方式,使得发送者不需要知道接收者的具体操作,只需发送一个命令对象,由接收者来执行相应的操作。
具体来说,命令模式包含以下几个关键角色:
-
命令(Command):命令是一个接口或抽象类,其中声明了执行操作的方法 execute(),所有具体命令类都必须实现这个方法。命令通常包含了执行操作所需的参数,以及执行操作的接收者对象(可选)。
-
具体命令(Concrete Command):具体命令是命令接口的实现类,它负责执行具体的操作。每个具体命令类都封装了一个接收者对象,并调用接收者的方法来执行请求。
-
接收者(Receiver):接收者是执行操作的对象。命令对象通常会将请求委派给接收者来执行实际的操作。
-
发送者(Invoker):发送者是调用命令对象并要求执行请求的对象。发送者并不知道如何执行请求,它只需调用命令对象的 execute() 方法即可。
-
客户端(Client):客户端创建命令对象并将其与发送者绑定,然后将命令对象交给发送者执行。
通过使用命令模式,发送者和接收者之间的耦合度降低,发送者不需要知道接收者的具体实现细节,只需知道如何发送命令即可。这种解耦使得命令模式在需要将请求发送给多个接收者、以及需要支持撤销、重做等功能时非常有用。
举例说明
假设你正在玩一款模拟餐厅经营的游戏,你扮演餐厅的老板,需要管理厨房的各种订单和菜品制作。在这个游戏中,你可以使用命令模式来处理顾客的订单,并将其发送给厨师进行制作。
游戏中的角色和元素包括:
-
顾客: 游戏中的主要角色之一,他们来到你的餐厅点餐。每个顾客都会点一份或多份菜品,每份菜品都对应一个具体的命令。
-
命令接口(Command): 定义了执行命令的方法,例如执行订单或取消订单。
-
具体命令(Concrete Command): 实现了命令接口,代表了具体的订单。每个具体命令对应一个顾客的订单,包括菜品的种类、数量等信息。
-
厨师: 负责接收订单并根据订单制作菜品。厨师需要执行具体命令来完成制作工作。
-
命令调用者(Invoker): 在游戏中扮演餐厅的前台或点单系统,负责接收顾客的订单并将其转换为具体命令,然后发送给厨师进行处理。
// 命令接口
interface Command {void execute();
}// 具体命令:菜品订单
class OrderCommand implements Command {private String dishName;public OrderCommand(String dishName) {this.dishName = dishName;}public String getDishName() {return dishName;}@Overridepublic void execute() {System.out.println("制作菜品:" + dishName);// 在这里执行制作菜品的具体逻辑}
}// 具体命令:取消订单
class CancelOrderCommand implements Command {private Command orderCommand;public CancelOrderCommand(Command orderCommand) {this.orderCommand = orderCommand;}@Overridepublic void execute() {if (orderCommand instanceof OrderCommand) {System.out.println("取消订单:" + ((OrderCommand) orderCommand).getDishName());// 在这里执行取消订单的具体逻辑} else {System.out.println("无法取消订单:非法命令");}}
}// 厨师
class Chef {public void cook(Command command) {command.execute();}
}// 命令调用者:餐厅前台
class Waiter {private Chef chef;public Waiter(Chef chef) {this.chef = chef;}// 接收顾客的订单并发送给厨师public void takeOrder(Command command) {chef.cook(command);}// 撤销订单public void cancelOrder(Command orderCommand) {Command cancelCommand = new CancelOrderCommand(orderCommand);chef.cook(cancelCommand);}
}// 客户端代码
public class RestaurantSimulation {public static void main(String[] args) {Chef chef = new Chef();Waiter waiter = new Waiter(chef);// 顾客点了一个名为 "鱼香肉丝" 的菜品Command order1 = new OrderCommand("鱼香肉丝");// 顾客点了一个名为 "宫保鸡丁" 的菜品Command order2 = new OrderCommand("宫保鸡丁");// 将订单发送给厨师waiter.takeOrder(order1);waiter.takeOrder(order2);// 顾客取消了之前的订单waiter.cancelOrder(order1);}
}
核心思想
命令模式的核心思想是将请求封装成对象,以便于参数化其他对象,并使请求排队、记录日志、撤销操作等。它的主要目的是解耦发送者和接收者,让发送者不需要知道接收者的具体操作,只需发送一个命令对象,由接收者来执行相应的操作。
具体来说,命令模式的核心思想包括以下几个关键点:
-
将请求封装成对象:命令模式将一个请求封装成一个命令对象,包含了执行该请求所需的所有信息,如请求的操作、操作所需的参数等。
-
解耦发送者和接收者:发送者无需知道接收者的具体操作,只需将请求发送给命令对象,由命令对象负责调用接收者的方法执行请求。
-
支持请求排队和撤销操作:由于请求被封装成对象,可以轻松地将请求排队、记录请求日志,并且支持撤销和重做操作。
-
支持参数化其他对象:命令对象可以参数化其他对象,使得可以在不同的上下文中重用相同的命令对象,而不需要修改代码。
-
增强代码的灵活性和可扩展性:命令模式使得系统更具有灵活性和可扩展性,因为它将请求和执行操作的对象解耦,使得可以轻松地添加新的命令和接收者,而不会影响到已有的代码结构。
总的来说,命令模式的核心思想是将请求发送者和请求接收者解耦,通过命令对象来封装请求并进行传递,从而提高代码的灵活性、可维护性和可扩展性。
适用环境
命令模式通常适用于以下场景:
-
请求的发送者和接收者需要解耦:当请求的发送者和接收者之间存在紧耦合关系时,可以使用命令模式来解耦两者,使得它们之间相互独立,降低耦合度。
-
支持请求的排队和撤销操作:如果需要支持请求的排队执行、记录日志、撤销操作等功能,可以使用命令模式来实现。命令对象可以被保存在队列中,按照顺序执行,并且可以轻松地撤销和重做操作。
-
支持事务性操作:在需要支持事务性操作的场景中,命令模式也非常适用。命令对象可以将一系列操作封装在一起,使得它们要么全部执行成功,要么全部执行失败。
-
支持回调操作:命令模式可以通过命令对象来实现回调操作。命令对象可以在执行时调用接收者的方法,并传递所需的参数,从而实现灵活的回调机制。
-
支持日志记录和撤销操作:命令模式可以轻松地记录每个命令的执行日志,并支持撤销和重做操作。这对于需要实现可撤销的编辑操作或者事务性操作非常有用。
命令模式适用于需要将请求发送者和接收者解耦、支持请求的排队和撤销操作、支持事务性操作、支持回调操作以及支持日志记录和撤销操作等场景。通过使用命令模式,可以使系统更加灵活、可维护和可扩展。
优缺点
命令模式(Command Pattern)具有以下优点和缺点:
优点:
- 解耦请求发送者和接收者:命令模式将请求封装成一个对象,使得请求发送者和接收者之间解耦,可以灵活地组合命令和接收者,降低系统的耦合度。
- 容易扩展新命令:由于命令被封装成对象,因此新增命令类非常容易,无需修改现有的代码,符合开闭原则。
- 支持撤销和重做操作:命令对象可以保存历史状态,从而支持撤销和重做操作,提高系统的灵活性和可维护性。
- 支持事务性操作:命令模式可以将多个命令组合成一个复合命令,从而支持事务性操作,保证一系列操作要么全部执行成功,要么全部执行失败。
缺点:
- 类的数量增加:每个具体命令都需要一个单独的类来实现,可能会导致类的数量增加,增加系统的复杂度。
- 系统过度灵活:如果命令过多,可能会导致系统过度灵活,增加了理解和维护的难度。
- 可能引入额外的性能开销:由于命令需要封装成对象并保存历史状态,可能会引入额外的性能开销,特别是在需要频繁执行撤销和重做操作时。
综上所述,命令模式在解耦请求发送者和接收者、支持撤销和重做操作、支持事务性操作等方面具有显著优点,但也存在类的数量增加、系统过度灵活和可能引入额外的性能开销等缺点。因此,在使用命令模式时需要根据具体情况进行权衡和选择。