目录
[策略模式【Strategy Pattern】](#策略模式【Strategy Pattern】)
[代理模式【Proxy Pattern】](#代理模式【Proxy Pattern】)
[单例模式【Singleton Pattern】](#单例模式【Singleton Pattern】)
[多例模式【Multition Pattern】](#多例模式【Multition Pattern】)
[工厂方法模式【Factory Method Pattern]](#工厂方法模式【Factory Method Pattern])
[抽象工厂模式【Abstract Factory Pattern】](#抽象工厂模式【Abstract Factory Pattern】)
策略模式【Strategy Pattern】
定义
策略模式(Strategy Pattern)是一种经典的软件设计模式,属于行为型模式的一种。其核心思想是定义一系列算法,将每个算法封装成独立的类,并使它们可以相互替换。这种设计模式使得算法可以独立于使用它的客户端而变化。
在策略模式中,通常有三个关键角色:
-
策略接口(Strategy Interface):定义了一个公共接口,用于所有支持的算法。通常由一个接口或抽象类来表示,确保所有具体策略类都实现了相同的方法。
-
具体策略(Concrete Strategies):实现了策略接口,提供了具体的算法实现。每个具体策略类都实现了策略接口中定义的方法,以便在上下文中被调用。
-
上下文(Context):包含一个成员变量指向策略接口,用于调用其中的算法。上下文允许客户端通过设置不同的策略对象来改变其行为,而无需了解具体算法的实现细节。
具体来说,在策略模式中,上下文对象(Context)持有一个策略接口的引用,并在需要的时候调用具体策略对象(Concrete Strategies)的方法来完成特定的任务。这种设计使得算法的选择和使用与上下文对象解耦,客户端可以根据需要灵活地切换不同的策略,而不必关心具体算法的实现细节。
另外,通过使用策略模式,我们可以将算法的实现封装在各个具体策略类中,这样一来,如果需要新增、修改或删除某个算法,只需添加、修改或删除相应的具体策略类,而不会影响到上下文对象或其他具体策略类。这种高内聚、低耦合的设计使得系统更加灵活和易于维护。
所以,策略模式是一种简单而又强大的设计模式,能够有效地管理和切换算法,提高代码的灵活性和可维护性,是在需要根据不同情况选择不同算法的场景中非常实用的解决方案。
举例说明
假设你是一名游戏玩家,正在玩一款战斗类游戏。在游戏中,你扮演的角色需要选择不同的战斗策略来应对不同的敌人。
游戏中的要素如下:
-
角色:你所控制的游戏角色是一名勇敢的骑士。作为骑士,你具有一定的基础能力,比如攻击力、防御力等。你的任务是与游戏中的各种敌人战斗,并取得胜利。
-
敌人:游戏中有多种敌人,包括哥布林、巨魔、龙等。每种敌人都有自己独特的特点和能力,比如哥布林速度快但攻击力弱,巨魔力大无穷但移动缓慢,龙拥有强大的火焰攻击等。不同的敌人需要采用不同的战斗策略来对付。
-
战斗策略:为了应对不同的敌人,你需要选择合适的战斗策略。战斗策略包括以下几种:
-
近战攻击:你使用剑或者其他近战武器,直接接近敌人进行攻击。这种战斗策略适用于速度较慢或者防御较弱的敌人。
-
远程攻击:你使用弓箭或者投掷武器,远距离攻击敌人。这种战斗策略适用于速度较快或者攻击范围广的敌人。
-
魔法攻击:你释放魔法咒语或者施展魔法技能,对敌人造成魔法伤害。这种战斗策略适用于具有特殊能力或者防御力较高的敌人。
-
-
策略选择器:在战斗过程中,你需要根据当前遇到的敌人情况选择合适的战斗策略。策略选择器负责根据敌人的特点和你的当前状态,自动选择最合适的战斗策略。比如,当你遇到速度较快的哥布林时,策略选择器可能会选择使用远程攻击来保持距离;而当你遇到力大无穷的巨魔时,可能会选择近战攻击来快速解决。
在这个例子中,策略模式的体现如下:
-
策略接口:定义了一个战斗策略接口,包含了执行战斗的方法。
-
具体策略:实现了战斗策略接口,包括近战、远程攻击、魔法攻击等不同的具体战斗策略。
-
角色:持有一个对战斗策略接口的引用,并根据当前遇到的敌人,选择合适的具体战斗策略。
首先,我们定义了一个名为"战斗策略"的接口,其中包含了一个执行战斗的方法,比如 fight()。这个接口规定了所有具体战斗策略类必须实现的方法。
java
public interface FightStrategy {
void fight();
}
然后,我们创建了具体的战斗策略类,比如 MeleeStrategy(近战策略)、RangedStrategy(远程攻击策略)、MagicStrategy(魔法攻击策略)等,它们实现了战斗策略接口中的方法,分别表示不同的战斗方式。
java
// 近战攻击策略
class MeleeStrategy implements FightStrategy {
@Override
public void fight() {
System.out.println("使用剑进行近战攻击!");
}
}
// 远程攻击策略
class RangedStrategy implements FightStrategy {
@Override
public void fight() {
System.out.println("使用弓箭进行远程攻击!");
}
}
// 魔法攻击策略
class MagicStrategy implements FightStrategy {
@Override
public void fight() {
System.out.println("施放火球术进行魔法攻击!");
}
}
游戏中的角色持有一个对战斗策略接口的引用,并在遇到敌人时根据敌人的类型选择合适的战斗策略。比如,当遇到巨魔时,选择近战策略;遇到哥布林时,选择远程攻击策略;遇到龙时,选择魔法攻击策略。
java
// 角色类
public class Player {
private FightStrategy fightStrategy; // 持有一个战斗策略接口的引用
// 设置战斗策略
public void setFightStrategy(FightStrategy fightStrategy) {
this.fightStrategy = fightStrategy;
}
// 执行战斗
public void performFight() {
if (fightStrategy != null) {
fightStrategy.fight();
} else {
System.out.println("未选择战斗策略!");
}
}
public static void main(String[] args) {
// 创建角色
Player player = new Player();
System.out.println("糟糕!!!遇到巨魔袭击!准备战斗!");
// 设置战斗策略
player.setFightStrategy(new MeleeStrategy());
player.performFight(); // 输出:使用剑进行近战攻击!
System.out.println("西边冲出一群哥布林!正在快速移动!");
player.setFightStrategy(new RangedStrategy());
player.performFight(); // 输出:使用弓箭进行远程攻击!
System.out.println("我的真神呐!来了一头龙!为了骑士的荣誉,和它拼了!");
player.setFightStrategy(new MagicStrategy());
player.performFight(); // 输出:施放火球术进行魔法攻击!
}
}
通过这种设计,游戏中的角色可以根据不同的敌人选择不同的战斗策略,而不需要在角色类中编写大量的条件语句来判断敌人类型。这样的设计使得系统更加灵活和易于扩展,符合策略模式的核心思想:将算法的定义和使用分离开来,使得算法可以独立于使用它的客户端而变化。
核心思想
策略模式的核心思想是将算法的定义和使用分离开来,以实现算法的独立演化。这种分离使得算法可以独立于使用它的客户端而变化,而客户端仅需关注如何使用这些算法而不必关心其具体实现细节。这样的设计使得系统更加灵活、可维护,并且方便进行单元测试。
策略模式包含以下关键点(也就是上面我们提到的三个关键角色):
-
定义算法接口:首先,我们定义一个算法接口或抽象类,其中声明了算法的方法。这个接口或抽象类通常称为策略接口。
-
具体算法实现:针对算法接口,我们可以有多个具体的算法实现类,每个实现类都实现了策略接口中定义的方法,并提供了自己的算法逻辑。
-
上下文:上下文是使用算法的类或对象,它持有一个指向策略接口的引用。在需要执行算法的时候,上下文对象调用策略接口中的方法来委托给具体的算法实现类。
通过这种设计,我们实现了算法的独立性,使得算法可以自由地变化而不影响到使用它的客户端。当需要新增、修改或删除算法时,我们只需添加、修改或删除相应的具体算法实现类,而不需要修改上下文对象或其他代码。这样的设计使得系统更加灵活,能够适应变化。
另外,由于算法的定义和使用被分离开来,我们可以更方便地进行单元测试。我们可以针对每个具体的算法实现类编写单元测试,验证其逻辑的正确性。同时,在使用算法的客户端中,我们也可以更容易地进行集成测试,保证整体系统的正确性。
综上所述,策略模式通过将算法的定义和使用分离开来,使得系统更加灵活、可维护,并且方便进行单元测试。这种设计思想在软件开发中被广泛应用,特别是在需要根据不同情况选择不同算法的场景中,策略模式能够为我们提供一种简洁而有效的解决方案。
适用场景
策略模式适用于以下场景:
-
多种算法选择: 当系统中有多种算法可供选择,并且需要在运行时动态地选择其中一种算法时,策略模式非常适用。例如,排序算法、计算税收算法等场景。
-
避免条件语句: 当系统中存在大量的条件语句,根据不同条件执行不同的行为时,可以考虑使用策略模式来避免条件语句的臃肿。策略模式将不同的行为封装到不同的策略类中,使得代码更加清晰和易于维护。
-
算法的独立性: 当需要将算法的实现与上下文对象解耦,使得它们可以独立演化时,策略模式是一个很好的选择。每个具体策略类都封装了一个特定的算法,使得算法的变化不会影响到上下文对象或其他具体策略类。
-
动态替换算法: 当需要在运行时动态地选择、切换算法时,策略模式非常有用。上下文对象可以持有一个对策略接口的引用,在需要时可以随时替换为另一个具体策略对象,从而改变其行为。
-
单一职责原则: 当需要保持每个类的职责单一,使得每个类只负责一种特定的功能或算法时,策略模式可以帮助实现这一目标。每个具体策略类都只负责一个算法的实现,使得代码更加模块化和可维护。
总之,策略模式适用于需要根据不同情况选择不同算法的场景,以及需要将算法的实现与使用者解耦的情况。通过合理地使用策略模式,我们可以使系统更加灵活、可扩展,同时也更易于理解和维护。
优缺点
策略模式的优点和缺点总结如下:
优点:
-
提高代码的可维护性和扩展性:策略模式将不同的算法封装到不同的策略类中,使得每个类职责清晰,易于理解和维护。当需要新增、修改或删除算法时,只需操作相应的具体策略类,而不会影响到其他部分的代码。
-
降低算法的依赖性:策略模式使得算法与客户端解耦,客户端只需要知道如何使用策略接口,而不需要关心具体的算法实现细节。这样一来,算法的变化不会影响到客户端的代码,提高了系统的灵活性和可维护性。
-
简洁清晰的组织方式:策略模式提供了一种简洁、清晰的方式来组织和管理多个算法。每个具体策略类都封装了一个特定的算法,使得代码结构更加清晰易懂。
缺点:
-
增加了类和对象的数量:在策略模式中,每个具体策略类都对应一个具体的算法实现,可能会导致类和对象的数量增加。如果系统中有大量的算法需要支持,可能会导致类的数量增加,增加系统的复杂度。
-
需要客户端了解各个策略的区别:虽然策略模式将算法的选择和使用解耦,但客户端仍需要了解各个策略的区别,以便在运行时选择合适的策略。如果策略之间的差异不大,可能会增加客户端的理解和维护成本。
虽然策略模式具有诸多优点,但我们在使用时还是需要权衡其优缺点,确保能够在系统设计中选择合适的模式。通常情况下,策略模式适用于需要根据不同情况选择不同算法的场景,以及需要将算法的实现与使用者解耦的情况。
代理模式【Proxy Pattern】
定义
代理模式(Proxy Pattern)是一种结构型设计模式,它允许我们提供一个代理类来控制对其他对象的访问。代理类通常充当客户端与实际目标对象之间的中介,用于控制对目标对象的访问。
在代理模式中,有三个关键角色:
-
抽象主题(Subject):定义了代理类和真实主题的共同接口。这个接口可以是一个抽象类或接口,它声明了客户端可以使用的方法。
-
真实主题(Real Subject):定义了代理类所代表的真实对象。也就是说,它是客户端最终想要访问的对象。真实主题类实现了抽象主题接口,提供了具体的业务逻辑。
-
代理(Proxy):代理持有对真实主题的引用,并提供与真实主题相同的接口,以便于客户端访问。代理类可以在客户端访问真实主题之前或之后执行额外的操作,例如记录日志、控制访问权限等。代理类与真实主题之间的关系通常是关联关系,即代理类中含有一个真实主题对象的引用。
总之,代理模式允许我们通过引入代理类来间接访问目标对象,从而控制访问、增强功能或隐藏复杂性。代理模式的核心在于代理对象充当客户端与实际目标对象之间的中介,管理对目标对象的访问。
举例说明
假设你是一位大学生,正在准备参加一场重要的学术会议。为了备战这场会议,你需要阅读大量的学术论文和专业书籍。然而,由于你每天的课程安排非常紧张,没有足够的时间去书店购买所需的书籍。于是,你决定雇佣一位图书代理人来帮助你购买这些书籍。
在这个场景中,代理模式的角色如下:
-
你(委托人):你代表了需要购买书籍的一方,即大学生。由于时间有限,无法亲自前往书店购买书籍。
-
图书代理人:代表了你雇佣的帮你购买书籍的人。图书代理人是一位专业的购书员,拥有丰富的购书经验和良好的书店资源。他知道去哪里购买书籍,如何挑选最适合你的书籍,并且能够以更快的速度完成购书任务。
-
书店:代表了实际的目标对象,即需要购买的书籍。在这个例子中,书店提供了各种学术论文和专业书籍供你选择。然而,由于你没有时间亲自前去购买,因此需要通过图书代理人来代替你完成购书任务。
图书代理人充当了你和书店之间的中介,负责代表你去书店购买书籍。虽然图书代理人实际上执行购买书籍的任务,但整个过程对你来说是透明的,你只需告诉代理人你需要哪些书籍,然后等待书籍送达即可。这样,代理模式通过引入代理人,实现了你和书店之间的解耦,同时提供了更便捷的购书服务。
抽象主题接口文件,定义了购买书籍的方法 purchaseBooks:
java
// 抽象主题接口
interface BookPurchase {
void purchaseBooks(String[] bookTitles);
}
真实主题类文件,实现了 BookPurchase 接口,表示书店类,负责实际购买书籍的操作:
java
// 真实主题类,即书店
public class BookStore implements BookPurchase {
@Override
public void purchaseBooks(String[] bookTitles) {
System.out.println("购买以下书籍:");
for (String title : bookTitles) {
System.out.println("- " + title);
}
System.out.println("购书完成!");
}
}
代理类文件,同样实现了 BookPurchase 接口,表示图书代理人类,负责代替客户购买书籍:
java
// 代理类,即图书代理人
public class BookProxy implements BookPurchase {
private BookStore bookStore; // 代理持有对真实主题的引用
public BookProxy() {
this.bookStore = new BookStore(); // 实例化真实主题
}
@Override
public void purchaseBooks(String[] bookTitles) {
System.out.println("代理人帮您购买书籍:");
bookStore.purchaseBooks(bookTitles); // 通过真实主题完成购书任务
System.out.println("书籍购买完成!");
}
}
上述,被代理类是 BookStore,即真实主题类,而代理类是 BookProxy,即图书代理人。
客户端类文件,包含了 main 方法,主要用于演示如何使用代理类来购买书籍:
java
// 客户端代码
public class Client {
public static void main(String[] args) {
// 实例化图书代理人
BookPurchase bookProxy = new BookProxy();
// 客户端委托代理人购买书籍
String[] booksToPurchase = {"Java编程思想", "设计模式", "计算机网络"};
bookProxy.purchaseBooks(booksToPurchase);
}
}
-
被代理类(真实主题类):BookStore
- 被代理类是实际执行任务的类,即真正负责购买书籍的对象。在这个例子中,BookStore 类代表了真实的书店,它实现了 BookPurchase 接口,提供了具体的购买书籍的功能。
- BookStore 类的方法 purchaseBooks 实际执行了购买书籍的操作,包括打印购买书籍的信息和完成购书的提示。
-
代理类:BookProxy
- 代理类是客户端和被代理类之间的中介,负责控制和管理对被代理类的访问。在这个例子中,BookProxy 类充当了图书代理人的角色。
- BookProxy 类也实现了 BookPurchase 接口,这样它就能与被代理类拥有相同的方法,使得客户端无需知道具体是哪个类在执行购书操作。
- 在 BookProxy 类的 purchaseBooks 方法中,它首先打印了代理人帮您购买书籍的提示,然后调用了被代理类 BookStore 的 purchaseBooks 方法,完成了实际的购书任务,最后输出了书籍购买完成的提示。
-
客户端代码:Client
- 客户端代码是使用代理模式的地方,它实例化了代理类 BookProxy 对象,并通过代理类来间接访问真实主题类 BookStore 的功能。
- 在客户端代码中,创建了代理对象 bookProxy,然后调用代理对象的 purchaseBooks 方法来委托代理人购买书籍。这样,客户端通过代理类来与真实主题类进行交互,无需直接操作真实主题类。
上面的这个例子清晰地展示了代理模式的核心概念:代理类充当了客户端和真实主题类之间的中介,使得客户端可以通过代理类来间接访问真实主题类的功能。代理类能够控制和管理对真实主题类的访问,从而实现了更灵活、更安全、更可控的系统设计。
核心思想
代理模式的核心思想是引入一个代理对象来控制对目标对象的访问,从而实现间接访问目标对象。代理模式允许代理对象充当客户端与实际目标对象之间的中介,可以在客户端访问目标对象之前或之后执行额外的操作。
具体来说,代理模式的核心思想包括以下几个方面:
-
控制访问:代理模式允许代理对象控制对目标对象的访问。代理对象可以限制客户端对目标对象的访问,例如通过验证客户端的身份、检查权限等。
-
增强功能:代理对象可以在客户端访问目标对象之前或之后执行额外的操作,从而增强目标对象的功能。例如,代理对象可以记录日志、缓存数据、实现懒加载等。
-
解耦合:代理模式可以将客户端与目标对象解耦合,客户端无需直接与目标对象交互,而是通过代理对象进行间接访问。这样可以降低系统的耦合度,提高系统的灵活性和可维护性。
-
隐藏复杂性:代理模式可以隐藏目标对象的具体实现细节,使客户端无需关心目标对象的内部实现,只需与代理对象进行交互。这样可以降低客户端的复杂性,提高系统的易用性。
代理模式的核心思想是通过引入代理对象来控制对目标对象的访问,并在必要时增强目标对象的功能,从而实现对目标对象的间接访问和管理。
代理模式主要利用了Java的多态特性。在代理模式中,被代理类(真实主题)和代理类实现了相同的接口或继承了相同的父类,这样客户端对于代理类和被代理类就是一种透明的操作,无需关心具体是哪个类在执行。
被代理类实际上是干活的,而代理类则主要是接活、做一些额外的操作或者控制访问。代理类收到客户端的请求后,可以选择性地将任务交给幕后的被代理类去执行。
在代理模式中,通过接口或者继承的方式,确保了代理类和被代理类具有相同的方法签名,这样客户端对于两者的使用方式是一致的。通过这种方式,代理类能够代理任何实现了相同接口的类,而不需要知道被代理类的具体类型,实现了解耦和灵活性。
因此,通过利用Java的多态特性,代理模式能够在保持灵活性的同时,实现对被代理类的控制和增强。
适用场景
代理模式适用于许多不同的场景,特别是在需要控制对对象的访问、增强对象功能或者隐藏对象复杂性时:
-
远程代理(Remote Proxy):用于控制对位于不同地址空间的对象的访问。远程代理允许客户端通过代理对象访问远程对象,而不需要了解远程对象的具体实现细节。
-
虚拟代理(Virtual Proxy):用于控制对创建开销较大的对象的访问。虚拟代理延迟加载目标对象,直到客户端真正需要使用目标对象时才创建,从而节省系统资源。
-
保护代理(Protection Proxy):用于控制对对象的访问权限。保护代理允许代理对象验证客户端的身份、检查权限等,然后决定是否允许客户端访问目标对象。
-
缓存代理(Cache Proxy):用于缓存访问过的对象,提高系统性能。缓存代理在客户端访问目标对象时,首先检查缓存中是否已经存在目标对象的副本,如果存在则直接返回缓存的副本,否则再从真实主题获取对象并缓存起来。
-
日志记录代理(Logging Proxy):用于记录客户端对目标对象的访问日志。日志记录代理在客户端访问目标对象时,记录访问时间、访问者信息等相关日志信息,以便后续分析和监控。
-
动态代理(Dynamic Proxy):用于在运行时动态地创建代理对象。动态代理允许在不修改源代码的情况下为目标对象创建代理,通过反射机制实现代理类的动态生成。
总的来说,代理模式适用于需要控制对对象访问、增强对象功能、隐藏对象复杂性或者提高系统性能的场景。通过合理地使用代理模式,可以提高系统的灵活性、可维护性和可扩展性。
优缺点
代理模式是一种常用的设计模式,它具有许多优点和一些缺点:
优点:
-
增强对象功能:代理模式允许代理对象在客户端访问目标对象之前或之后执行额外的操作,从而增强目标对象的功能。这样可以在不改变目标对象的前提下,扩展其功能或行为。
-
控制访问:代理模式可以通过代理对象来控制对目标对象的访问。代理对象可以检查客户端的请求,验证客户端的身份、权限等,然后决定是否允许客户端访问目标对象。
-
隐藏对象复杂性:代理模式可以隐藏目标对象的具体实现细节,使客户端无需了解目标对象的内部实现。这样可以降低客户端的复杂性,提高系统的可维护性和可扩展性。
-
远程访问:代理模式可以实现远程代理,允许客户端通过代理对象访问位于不同地址空间的目标对象。这样可以实现分布式系统中的远程调用,提高系统的灵活性和可扩展性。
-
性能优化:代理模式可以实现缓存代理,将访问过的对象缓存起来,避免重复创建对象。这样可以提高系统的性能,减少资源消耗。
缺点:
-
增加复杂性:引入代理对象会增加系统的复杂性,因为需要额外的代理类来管理对目标对象的访问。这样会增加代码量和维护成本。
-
可能降低性能:在某些情况下,代理模式可能会降低系统的性能。例如,远程代理或虚拟代理可能会引入额外的网络通信或延迟加载,从而影响系统的响应速度。
-
过多的代理类:如果系统中存在过多的代理类,会增加系统的复杂性,使代码难以理解和维护。因此,在使用代理模式时需要谨慎设计代理类的数量和结构。
综上所述,代理模式具有许多优点,例如增强对象功能、控制访问、隐藏对象复杂性等,但也存在一些缺点,例如增加复杂性、可能降低性能等。因此,在实际应用中需要根据具体情况权衡利弊,合理使用代理模式来提高系统的灵活性、可维护性和性能。
单例模式【Singleton Pattern】
定义
单例模式(Singleton Pattern)是一种创建型设计模式,它确保类只有一个实例,并提供了一个全局访问点以访问该实例。
在单例模式中,类会通过私有的构造函数来限制该类的实例化,然后提供一个静态方法来允许客户端获取该类的唯一实例。如果该类已经有一个实例存在,那么这个静态方法会返回这个实例;如果没有,它将创建一个新的实例并返回。
单例模式的关键点是:
- 私有构造函数:确保其他类无法直接通过构造函数实例化该类的对象,从而避免了多个实例的创建。
- 静态方法:提供一个静态方法来获取该类的唯一实例。这个静态方法通常被称为"getInstance()"方法,它负责在需要时创建实例或返回现有实例。
- 静态实例变量:保存类的唯一实例,通常是私有的静态变量。
- 线程安全:考虑多线程环境下的安全性,确保在多线程环境下也能正确地创建唯一实例。
单例模式经常被用于管理资源、配置信息、日志记录器等一些需要全局访问且只需要一个实例的场景。
举例说明
当我们谈论单例模式时,我们指的是一种设计模式,其目的是确保类只有一个实例,并提供一个全局访问点来访问该实例。这种模式常用于需要在系统中全局访问某个对象的情况下。
让我们以公司中的 HR 经理为例,更详细地解释单例模式。
在一个公司中,HR 经理是负责管理员工入职手续和档案的人员。由于公司可能有成千上万名员工,因此需要一个统一的管理者来处理所有入职事务。这就是 HR 经理的角色。HR 经理不仅负责执行公司的入职流程,还管理着员工的档案信息,确保公司的人力资源管理顺利运作。单例模式确保只有一个 HR 经理的实例存在,并且所有员工的入职手续都由这个实例来处理。
在单例模式中,HR 经理类会有一个私有的静态成员变量来保存唯一的实例,并且会提供一个公共的静态方法来获取这个实例。这个静态方法会检查实例是否已经存在,如果不存在,则创建一个新的实例并返回,如果已经存在,则直接返回已经存在的实例。
这种设计确保了无论何时何地,只要有需要,都可以通过调用 HR 经理类的静态方法来获取 HR 经理的实例。这样就不会出现多个不同的 HR 经理实例存在的情况,保证了系统中只有一个唯一的 HR 经理。
作为单例类,HR 经理的存在确保了入职流程的一致性和数据的一致性。无论员工是在哪个部门,哪个岗位,都会由同一个 HR 经理来处理入职手续,避免了因为多个 HR 经理之间信息不同步导致的混乱和错误。这种单例设计模式确保了公司的人力资源管理高效而准确。
- HRManager 类只有一个私有的构造函数,这意味着外部不能直接实例化这个类。
- getInstance() 方法是获取 HRManager 实例的静态方法。在该方法中,首先检查实例是否已经存在,如果不存在则创建一个新实例并返回,如果已经存在则直接返回已经存在的实例。
- processHiring() 和 manageEmployeeFiles() 方法用于处理员工的入职手续和管理员工的档案信息。这些方法可以在任何地方通过获取 HRManager 实例后调用,确保了入职流程的一致性和数据的一致性。
java
// HRManager.java - 单例模式的 HR 经理类
public class HRManager {
// 静态变量 instance 用于保存唯一的实例
private static HRManager instance;
// 私有构造函数,防止外部实例化
private HRManager() {
// 这里可以放一些初始化逻辑,比如连接数据库等
System.out.println("HR 经理实例被创建!");
}
// 公共静态方法,用于获取 HRManager 实例
public static HRManager getInstance() {
// 检查是否已经存在实例,如果不存在则创建一个新实例
if (instance == null) {
instance = new HRManager();
}
return instance;
}
// 入职方法,处理员工入职手续
public void processHiring(String employeeName) {
System.out.println("HR 经理正在处理 " + employeeName + " 的入职手续。");
// 这里可以添加具体的入职逻辑,比如填写表单、发放工作证等
}
// 档案管理方法,管理员工档案信息
public void manageEmployeeFiles(String employeeName) {
System.out.println("HR 经理正在管理 " + employeeName + " 的档案信息。");
// 这里可以添加具体的档案管理逻辑,比如归档文件、更新信息等
}
}
HRMain 类中的 main() 方法用于测试 HRManager 类的单例特性。通过调用 getInstance() 方法获取两个 HRManager 实例,并验证它们是否是同一个实例。然后,通过这两个实例分别调用 processHiring() 和 manageEmployeeFiles() 方法来处理员工的入职手续和管理员工的档案信息。
java
// Main.java - 主程序入口
public class HRMain {
public static void main(String[] args) {
// 获取 HRManager 实例
HRManager hrManager1 = HRManager.getInstance();
HRManager hrManager2 = HRManager.getInstance();
// 验证是否是同一个实例
System.out.println("hrManager1 == hrManager2:" + (hrManager1 == hrManager2));
// 使用 HRManager 处理员工入职手续和档案管理
hrManager1.processHiring("米勒斯");
hrManager2.manageEmployeeFiles("埃斯顿");
}
}
核心思想
单例模式的核心思想是确保一个类只有一个实例,并提供一个全局访问点以访问该实例。这意味着无论在何处创建实例,始终只能获得同一个实例。单例模式的核心思想可以归纳为以下几点:
-
单一实例: 单例模式确保一个类只有一个实例存在。无论多少次尝试创建该类的实例,都只会得到同一个实例。
-
全局访问点: 单例模式提供一个全局访问点来获取该实例。通常,这是通过一个静态方法实现的,例如 getInstance() 方法。这个方法允许客户端在需要时获取单例实例。
-
私有构造函数: 为了防止外部类直接实例化该类的对象,单例类通常会将其构造函数设置为私有的。这样可以确保只有单例类自己能够控制实例的创建过程。
-
延迟初始化(可选): 在某些情况下,单例实例可能不需要在应用程序启动时立即创建,而是在第一次访问时才进行初始化。这种延迟初始化可以节省资源,并且在需要时才创建实例。
-
线程安全(可选): 在多线程环境中,确保单例模式的线程安全性是很重要的。可以通过加锁、双重检查锁定等技术来确保在多线程环境中正确地创建唯一实例。
总的来说,单例模式的核心思想是通过限制类的实例化并提供全局访问点,确保在整个应用程序中只有一个实例存在,并且能够在需要时方便地访问该实例。
适用场景
单例模式适用于以下场景:
-
资源管理器: 当系统中有需要共享的资源,例如数据库连接池、线程池、缓存、配置信息等,可以使用单例模式确保全局只有一个资源管理器实例,避免资源被多次创建和浪费。
-
日志记录器: 在应用程序中通常只需要一个日志记录器来记录系统的运行状态和错误信息。单例模式可以确保只有一个日志记录器实例,并且在整个应用程序中都能够方便地访问。
-
对话框、窗口管理器: 在图形用户界面(GUI)应用程序中,通常只有一个对话框管理器或窗口管理器来管理系统中的对话框或窗口。使用单例模式可以确保在应用程序中只有一个对话框管理器实例,便于管理和控制窗口的显示和隐藏。
-
身份验证服务: 在多用户系统中,通常需要一个全局的身份验证服务来验证用户的身份。单例模式可以确保只有一个身份验证服务实例,提供统一的身份验证接口,便于在整个应用程序中进行身份验证。
-
配置信息管理: 当应用程序需要读取配置信息、环境变量或其他全局参数时,可以使用单例模式来管理这些配置信息,确保在整个应用程序中只有一个配置信息管理器实例,便于读取和修改配置信息。
单例模式适用于任何需要全局唯一实例的场景,特别是在需要共享资源、管理状态或提供全局服务的情况下。
优缺点
单例模式具有如下优点和缺点:
优点:
-
全局唯一实例: 单例模式确保一个类只有一个实例存在,这意味着在整个应用程序中只有一个全局唯一的实例,可以方便地被访问和使用。
-
资源共享: 单例模式可以确保共享资源在整个应用程序中被正确管理和共享,避免资源被多次创建和浪费。
-
懒加载: 在某些情况下,单例模式可以实现延迟加载(懒加载),即在第一次访问时才创建实例。这样可以节省系统资源,并且提高了应用程序的启动性能。
-
避免竞态条件: 单例模式在多线程环境中可以避免竞态条件(Race Condition),通过线程安全的方式确保只有一个实例被创建。
缺点:
-
对扩展性的影响: 单例模式会在全局状态中引入一个全局唯一的实例,这可能会增加代码的耦合性,并且对系统的扩展性产生一定影响。
-
隐藏依赖关系: 单例模式会隐藏类的实例化过程,使得其他类对该类的依赖关系不够明显,可能会增加代码的理解和维护难度。
-
对单元测试的影响: 单例模式的全局唯一实例可能会影响单元测试的编写和执行,特别是在涉及到单例实例的状态修改时。
-
可能引起内存泄漏: 如果单例模式中的实例长时间持有其他对象的引用,并且这些对象也长时间存活,可能会导致内存泄漏的问题。
总之,单例模式在设计时需要慎重考虑,确保它的使用能够符合实际需求,并且能够避免潜在的问题。在某些情况下,单例模式能够带来明显的优势,但在其他情况下可能会引入不必要的复杂性。
多例模式【Multition Pattern】
定义
当我们谈论多例模式时,我们在说的是一种在软件设计中常用的模式,它是单例模式的一种变体。单例模式确保一个类只有一个实例,并提供了一个全局访问点来获取该实例。然而,有时候我们可能需要类的多个实例,但是又不希望无限制地创建实例。这时就可以使用多例模式。
多例模式(Multiton Pattern)是一种设计模式,它允许类有多个实例,但实例数量是有限的,并且每个实例都有一个相关联的键来标识。与单例模式不同,多例模式中的每个实例都有自己的状态和行为。
在多例模式中,类维护一个私有的静态 Map 或者类似的数据结构,用于存储所有可能的实例。这个 Map 中的键通常是用来标识不同实例的唯一标识符,比如字符串、枚举类型等。当需要获取实例时,我们可以通过给定的键来查找对应的实例,如果实例不存在,则创建一个新的实例并存储在 Map 中,然后返回该实例。这样就确保了每个实例都是唯一的,并且可以通过给定的键来获取到相应的实例。
举个例子来说明,假设我们有一个 Logger 类,用于记录日志。在某些情况下,我们可能需要不同类型的日志记录器,比如控制台日志记录器、文件日志记录器和数据库日志记录器。这时,我们可以使用多例模式来创建这些日志记录器的有限数量的实例,并通过给定的键来获取所需类型的日志记录器实例。
总的来说,多例模式允许类拥有多个有限数量的实例,并通过唯一的键来标识和获取这些实例,从而提供了更多的灵活性和控制能力。这种模式适用于需要管理多个实例,但又不希望无限制地创建实例的情况。
举例说明
当我们谈论多例模式时,我们可以想象一个虚构的世界,充满了各种超级英雄,每个都有独特的能力和身份。这个世界面临着不同的威胁和挑战,需要这些超级英雄来保护。
在这个虚构的世界中,我们可以想象一个名为"HeroVerse"的宇宙,充满了各种不同的超级英雄,每个超级英雄都有着独特的身份、背景和超能力。他们的存在是为了保护这个宇宙免受各种威胁和挑战的影响。
在"HeroVerse"中,超级英雄是人们的守护者,他们时刻准备着为普通人和整个世界带来安全和和平。
然而,在某个特定的事件发生后,一个新的超级反派突然涌现出来,他的出现改变了整个宇宙的格局。这位新的反派拥有强大的力量和毁灭性的意图,成为了"HeroVerse"中的最大威胁。超级英雄们必须团结起来,共同对抗这个新的敌人,以维护宇宙的和平与安全。
我们的"Superhero"类就像是"HeroVerse"中超级英雄的抽象表示。每个超级英雄都是这个类的一个实例,而每个实例都有一个独特的名称作为其唯一的标识符。这个名称可能是"Spider-Man"、"Iron Man"等,用来区分不同的超级英雄。
在"HeroVerse"中,普通人们面临着各种各样的威胁和挑战,有时候他们可能会转向超级英雄来寻求帮助和保护。有些人可能会信任所有的超级英雄,而有些人可能更倾向于特定的超级英雄。无论是哪种情况,他们都可以通过获取对应的超级英雄实例来获得所需的支持和保护。
多例模式为"HeroVerse"中的超级英雄提供了一种灵活的管理方式。每个超级英雄实例都是唯一的,并且可以根据需要进行获取和使用。这种模式允许我们在需要管理多个有限实例的情况下,提供更多的灵活性和控制能力,确保"HeroVerse"中的每个超级英雄都能够发挥自己的作用,保护世界的和平与安全。
总之,多例模式允许类具有多个有限数量的实例,并通过唯一的键来标识和获取这些实例。这种模式提供了更多的灵活性和控制能力,适用于需要管理多个有限实例的情况,就像我们所描述的超级英雄的情景一样。
让我们将这个虚构的世界设想成一个名为"HeroVerse"的宇宙。在这个宇宙中,有许多超级英雄,每个都是保护世界的守护者。我们的任务是创建一个程序来管理这些超级英雄,并确保他们能够有效地应对各种威胁。
首先,我们需要一个类来表示超级英雄。我们称之为"Superhero"类。这个类具有属性和方法,用于描述每个超级英雄的特征和行为。例如,它可能有一个名称属性来表示超级英雄的名字,还可能有一个能力属性来描述超级英雄的特殊能力。
java
public class Superhero {
private String name;
private String ability;
public Superhero(String name, String ability) {
this.name = name;
this.ability = ability;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAbility() {
return ability;
}
public void setAbility(String ability) {
this.ability = ability;
}
public void displayAbility() {
System.out.println(name + "的特殊能力是:" + ability);
}
public void performSpecialAbility() {
System.out.println(name + "正在执行特殊技能:" + ability);
// 在这里可以添加具体的特殊技能实现逻辑
}
}
接下来,我们需要一个管理超级英雄的类。我们称之为"HeroManager"类。这个类负责创建、存储和获取超级英雄的实例。在这个类中,我们使用多例模式来确保每个超级英雄都是唯一的,并且可以根据需要获取对应的实例。
java
import java.util.HashMap;
import java.util.Map;
public class HeroManager {
private static final Map<String, Superhero> heroes = new HashMap<>();
// 静态代码块,用于初始化一些超级英雄实例
static {
// 初始化一些超级英雄实例
heroes.put("Spider-Man", new Superhero("Spider-Man", "超凡蜘蛛能力"));
heroes.put("Iron Man", new Superhero("Iron Man", "科技战甲"));
heroes.put("Superman", new Superhero("Superman", "超人力量"));
heroes.put("Batman", new Superhero("Batman", "黑暗骑士"));
// 添加更多的超级英雄...
}
// 私有构造函数,禁止外部实例化
private HeroManager() {
}
// 静态方法,根据给定的名称获取对应的超级英雄实例
public static Superhero getHero(String name) {
return heroes.get(name);
}
}
在"HeroManager"类中,我们使用一个静态的 Map 来存储所有可能的超级英雄实例。每个实例都有一个唯一的名称作为键,用来标识和区分不同的超级英雄。当需要获取超级英雄实例时,我们可以通过名称来获取对应的实例。
接下来,让我们看看如何在"HeroVerse"中使用这些类来管理超级英雄。
java
public class HeroVerse {
public static void main(String[] args) {
// 获取Spider-Man的实例
Superhero spiderMan = HeroManager.getHero("Spider-Man");
System.out.println("欢迎 " + spiderMan.getName() + ",他拥有:" + spiderMan.getAbility());
spiderMan.displayAbility();
spiderMan.performSpecialAbility();
System.out.println();
// 获取Iron Man的实例
Superhero ironMan = HeroManager.getHero("Iron Man");
System.out.println("欢迎 " + ironMan.getName() + ",他拥有:" + ironMan.getAbility());
ironMan.displayAbility();
ironMan.performSpecialAbility();
System.out.println();
// 获取更多超级英雄的实例
Superhero superman = HeroManager.getHero("Superman");
System.out.println("欢迎 " + superman.getName() + ",他拥有:" + superman.getAbility());
superman.displayAbility();
superman.performSpecialAbility();
System.out.println();
Superhero batman = HeroManager.getHero("Batman");
System.out.println("欢迎 " + batman.getName() + ",他拥有:" + batman.getAbility());
batman.displayAbility();
batman.performSpecialAbility();
}
}
通过上述代码,我们可以轻松地获取指定名称的超级英雄实例,并且确保每个超级英雄都是唯一的。这种多例模式的设计使得我们可以方便地管理和调用超级英雄,确保他们能够及时、有效地保护"HeroVerse"世界的和平与安全。
核心思想
多例模式(Multiton Pattern)的核心思想是允许一个类有多个实例,但实例数量是有限的,并且每个实例都有一个相关联的键来标识。与单例模式只允许一个类有唯一实例不同,多例模式允许创建有限数量的实例,并通过键来获取所需的实例。
核心思想可以总结为以下几点:
-
**有限实例:**多例模式允许类拥有多个实例,但是这些实例的数量是有限的。这个数量通常是预先确定的,根据应用程序的需求进行设置。
-
**关联键:**每个实例都有一个相关联的键来唯一标识它。这个键可以是字符串、枚举类型或其他类型的标识符。通过这个键,可以获取到对应的实例。
-
**实例管理:**多例模式通常会维护一个数据结构(如 Map)来存储所有可能的实例,以便在需要时快速获取。这个数据结构会将键与实例关联起来,使得可以通过给定的键来检索或创建实例。
-
**实例的唯一性:**尽管多例模式允许多个实例存在,但是每个实例仍然是唯一的。通过键来获取实例时,会根据键的唯一性来确保返回的实例是正确的。
总的来说,多例模式允许一个类拥有多个有限数量的实例,并通过唯一的键来标识和获取这些实例。这种模式提供了更多的灵活性和控制能力,适用于需要管理多个实例,但又不希望无限制地创建实例的情况。
适用场景
多例模式适用于以下场景:
-
**有限数量的实例:**当一个类需要有限数量的实例,而不是无限制地创建实例时,多例模式就很适用。例如,线程池、数据库连接池等资源池类,通常需要限制实例的数量,以便控制资源的分配和利用。
-
**根据标识符获取实例:**当需要根据特定的标识符来获取对应的实例时,多例模式非常有用。这样可以通过唯一的键来管理和获取实例,使得代码更加清晰和易于维护。
-
**共享资源的管理:**在需要共享和管理资源的情况下,多例模式可以确保每个资源实例都被正确地管理和使用。例如,日志记录器、缓存管理器等组件可能需要多个实例来处理不同的日志记录或缓存操作,但是需要确保每个实例都能够正确地管理和记录相关信息。
-
**对象池:**在某些情况下,需要维护一组预先创建的对象,以便在需要时快速获取和使用。多例模式可以用于创建和管理这样的对象池,以提高系统的性能和资源利用率。
多例模式适用于需要有限数量的实例,并且需要根据标识符来获取实例的情况,以及需要管理共享资源和对象池的场景。
优缺点
多例模式的优点:
-
**控制实例数量:**多例模式可以限制实例的数量,避免无限制地创建实例,从而有效地控制系统资源的消耗和管理。
-
**管理实例:**通过唯一的标识符来管理每个实例,使得实例的获取和管理更加清晰和方便。
-
**提高性能:**在某些场景下,预先创建并管理一组对象实例可以提高系统的性能和资源利用率,因为可以避免频繁地创建和销毁对象。
-
**灵活性:**多例模式可以根据需要创建不同类型的实例,并根据唯一标识符来获取对应的实例,使得系统具有更大的灵活性和扩展性。
多例模式的缺点:
-
**静态实例:**多例模式中的实例通常是静态的,一旦创建就会一直存在于内存中,可能会占用较多的内存空间,特别是当实例数量较大时。
-
**复杂性:**多例模式可能会增加系统的复杂性,因为需要维护一个数据结构来存储所有可能的实例,并确保实例的唯一性和正确性。
-
**难以管理:**在一些情况下,需要额外的管理机制来确保实例的正确释放和回收,以避免内存泄漏和资源浪费等问题。
-
**可能引入全局状态:**多例模式中的实例通常是全局可访问的,这可能导致实例之间共享状态,增加系统的耦合性和难以维护性。
综上所述,多例模式在控制实例数量和管理实例方面具有一定的优势,但也存在一些缺点,特别是在静态实例和复杂性方面需要注意。在使用多例模式时,需要根据具体的场景和需求来权衡利弊,并谨慎设计和实现。
工厂方法模式【Factory Method Pattern]
定义
工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它提供了一种将对象的实例化延迟到子类中进行的方式。在工厂方法模式中,我们定义一个用于创建对象的接口,但将实际的实例化过程交给子类来实现。 简而言之,工厂方法模式允许我们定义一个创建对象的接口,但具体的对象创建由子类来决定。这样可以将对象的创建与使用相分离,提高了代码的可扩展性和可维护性。
具体来说,工厂方法模式包含以下几个关键角色:
-
**抽象工厂(Creator):**定义了一个创建对象的接口,其中包含一个抽象方法,该方法用于创建产品对象。抽象工厂可以是一个接口或者抽象类。
-
**具体工厂(Concrete Creator):**实现了抽象工厂中定义的创建对象的抽象方法,负责实际创建产品对象。
-
**抽象产品(Product):**定义了产品对象的接口,描述了产品对象的特性和行为。
-
**具体产品(Concrete Product):**实现了抽象产品中定义的接口,具体描述了产品对象的具体实现。
在工厂方法模式中,客户端代码通过与抽象工厂交互来获取所需的产品对象,而具体的产品对象的创建过程则由具体工厂类来完成。这样一来,客户端代码与具体产品的实现细节相分离,使得系统更加灵活,并且更易于扩展和维护。
举例说明
假设我们有一家名为"ChocolateCakeFactory"的蛋糕店,他们使用工厂方法来烘焙不同浓度的巧克力蛋糕。这家蛋糕店意识到市场上有不同口味的顾客,有些人喜欢浓郁的巧克力味道,而另一些人则更喜欢淡淡的巧克力味道。于是,他们决定使用工厂方法来烘焙各种浓度的巧克力蛋糕,以满足不同顾客的口味需求。
在这个蛋糕店里,有一位名为"MasterChef"的主厨,他是这家蛋糕店的创始人,也是巧克力蛋糕的主要烘焙师。MasterChef 拥有一套独特的烘焙工艺和配方,可以根据顾客的口味需求来制作不同浓度的巧克力蛋糕。
首先,MasterChef 开始准备烘焙材料,包括巧克力、面粉、糖等。然后,根据顾客的口味偏好,他调整配方中巧克力的用量,以确定巧克力蛋糕的浓度。
第一次烘焙时,他可能选择增加一些巧克力,制作出一款浓郁的巧克力蛋糕。但不幸的是,配方还没有完全调试好,导致蛋糕的口感略微有些沉重。
第二次烘焙时,MasterChef 决定减少巧克力的用量,使蛋糕口感更轻盈。这次烘焙出的巧克力蛋糕口感确实更轻盈了,但由于减少过度,巧克力味道不够浓郁,有些顾客觉得不够满足。
第三次烘焙时,MasterChef 掌握了烘焙的技巧,成功烘焙出一款口感丰富、巧克力味道浓郁的巧克力蛋糕,完美地符合了顾客的口味需求。
尽管 MasterChef 可以手工调整每一款蛋糕的配方,但他觉得这样太过繁琐。于是,他想出了一个更简单的方法:他准备了不同浓度的巧克力粉,然后根据需要随机选择一种比例来烘焙蛋糕。这样一来,他就可以更轻松地烘焙出各种浓度的巧克力蛋糕,而不必费心费力地调试每一款蛋糕的配方。
在这个故事中,MasterChef 就是工厂方法模式中的工厂,巧克力蛋糕的烘焙过程则是工厂方法。通过工厂方法模式,MasterChef 可以根据需要随机烘焙各种浓度的巧克力蛋糕,而不必关心具体的烘焙过程。这样使得烘焙过程更加灵活、简单,提高了巧克力蛋糕的多样性和生产效率。
我们首先需要定义 ChocolateCake 接口,代表巧克力蛋糕,以及三种具体的巧克力蛋糕类 RichChocolateCake , LightChocolateCake 和 ModerateChocolateCake。
java
// 巧克力蛋糕接口
interface ChocolateCake {
void bake(); // 烘焙方法
}
java
// 浓郁巧克力蛋糕类
class RichChocolateCake implements ChocolateCake {
@Override
public void bake() {
System.out.println("烘焙出一款浓郁的巧克力蛋糕");
}
}
// 浅淡巧克力蛋糕类
class LightChocolateCake implements ChocolateCake {
@Override
public void bake() {
System.out.println("烘焙出一款浅淡的巧克力蛋糕");
}
}
// 适中巧克力蛋糕类
class ModerateChocolateCake implements ChocolateCake {
@Override
public void bake() {
System.out.println("烘焙出一款适中的巧克力蛋糕");
}
}
然后,我们将创建了一个 ChocolateCakeFactory 工厂类,其中有一个 createChocolateCake 方法用于根据不同类型创建对应的巧克力蛋糕实例。
java
// 巧克力蛋糕工厂
class ChocolateCakeFactory {
// 巧克力蛋糕烘焙方法
public static ChocolateCake createChocolateCake(String type) {
switch (type) {
case "Rich":
return new RichChocolateCake();
case "Light":
return new LightChocolateCake();
case "Moderate":
return new ModerateChocolateCake(); // 新增适中巧克力蛋糕
default:
return null;
}
}
}
最后,我们定义了一个 MasterChef 类,代表主厨,他负责烘焙巧克力蛋糕,并调用了工厂方法来获取不同类型的巧克力蛋糕实例。
java
// 主厨类
class MasterChef {
// 准备烘焙材料
public void prepareIngredients() {
System.out.println("准备巧克力、面粉、糖等烘焙材料");
}
// 调整巧克力蛋糕配方
public void adjustRecipe(String type) {
System.out.println("根据顾客的口味偏好,调整配方中巧克力的用量,以确定巧克力蛋糕的浓度:" + type);
}
// 烘焙巧克力蛋糕
public void bakeChocolateCake(String type) {
prepareIngredients();
adjustRecipe(type);
ChocolateCake cake = ChocolateCakeFactory.createChocolateCake(type);
if (cake != null) {
cake.bake();
} else {
System.out.println("抱歉,暂时无法烘焙此类巧克力蛋糕");
}
}
}
java
// 测试类
public class CakeMain {
public static void main(String[] args) {
MasterChef masterChef = new MasterChef();
System.out.println("第一次烘焙:");
masterChef.bakeChocolateCake("Rich");
System.out.println("\n第二次烘焙:");
masterChef.bakeChocolateCake("Light");
System.out.println("\n第三次烘焙:");
masterChef.bakeChocolateCake("Moderate"); // 使用新添加的适中巧克力蛋糕
// 尝试烘焙一种不存在的巧克力蛋糕类型
System.out.println("\n尝试烘焙不存在的巧克力蛋糕类型:");
masterChef.bakeChocolateCake("ExtraRich");
}
}
工厂方法模式还有一个非常重要的应用,就是延迟始化(Lazy initialization),什么是延迟始化呢?
延迟初始化(Lazy Initialization)是一种延迟对象创建或变量赋值的策略,直到需要使用时才进行初始化操作。这种方式可以节省系统资源,提高程序的性能和效率。
在工厂方法模式中,延迟初始化通常是指在获取对象实例时才进行对象的创建,而不是在程序启动或类加载时就立即创建对象。这种方式适用于对象初始化比较耗费资源或者不经常使用的情况,可以避免不必要的资源浪费。
为了实现延迟初始化,我们可以通过在获取对象实例时才进行对象的创建来实现延迟初始化。在这个例子中,我们可以使用一个 HashMap 来存储已创建的巧克力蛋糕对象,当需要获取特定类型的巧克力蛋糕时,先检查 HashMap 中是否已经存在该对象,如果存在则直接返回,否则再创建并存储在 HashMap 中。
java
import java.util.HashMap;
// 巧克力蛋糕工厂
class ChocolateCakeFactory {
// 存储已创建的巧克力蛋糕对象
private static HashMap<String, ChocolateCake> cakes = new HashMap<>();
// 巧克力蛋糕烘焙方法
public static ChocolateCake createChocolateCake(String type) {
if (cakes.containsKey(type)) {
return cakes.get(type); // 如果 MAP 中有,则直接从取出,不用初始化了
} else {
// 如果 MAP 中没有,则创建新的对象并放到 MAP 中
ChocolateCake cake = null;
switch (type) {
case "Rich":
cake = new RichChocolateCake();
break;
case "Light":
cake = new LightChocolateCake();
break;
case "Moderate":
cake = new ModerateChocolateCake();
break;
default:
System.out.println("抱歉,暂时无法烘焙此类巧克力蛋糕");
}
cakes.put(type, cake);
return cake;
}
}
}
// 主厨类
class MasterChef {
// 准备烘焙材料
public void prepareIngredients() {
System.out.println("准备巧克力、面粉、糖等烘焙材料");
}
// 调整巧克力蛋糕配方
public void adjustRecipe(String type) {
System.out.println("根据顾客的口味偏好,调整配方中巧克力的用量,以确定巧克力蛋糕的浓度:" + type);
}
// 烘焙巧克力蛋糕
public void bakeChocolateCake(String type) {
prepareIngredients();
adjustRecipe(type);
ChocolateCake cake = ChocolateCakeFactory.createChocolateCake(type);
if (cake != null) {
cake.bake();
}
}
}
但是这样创建太累了,主厨每次都要指定生成的蛋糕是哪种口味的,很麻烦,可以修改一下,让工厂可以自己随机制造生成蛋糕吗?
为了让工厂可以随机制造生成巧克力蛋糕,我们可以修改 ChocolateCakeFactory 类的实现,使其在创建蛋糕时随机选择一种口味。我们可以在工厂中维护一个包含所有口味的数组,然后随机选择一个口味来创建对应的巧克力蛋糕。
java
import java.util.HashMap;
import java.util.Random;
// 巧克力蛋糕接口
interface ChocolateCake {
void bake(); // 烘焙方法
}
// 浓郁巧克力蛋糕类
class RichChocolateCake implements ChocolateCake {
@Override
public void bake() {
System.out.println("烘焙出一款浓郁的巧克力蛋糕");
}
}
// 浅淡巧克力蛋糕类
class LightChocolateCake implements ChocolateCake {
@Override
public void bake() {
System.out.println("烘焙出一款浅淡的巧克力蛋糕");
}
}
// 适中巧克力蛋糕类
class ModerateChocolateCake implements ChocolateCake {
@Override
public void bake() {
System.out.println("烘焙出一款适中的巧克力蛋糕");
}
}
// 巧克力蛋糕工厂
class ChocolateCakeFactory {
// 存储已创建的巧克力蛋糕对象
private static HashMap<String, ChocolateCake> cakes = new HashMap<>();
// 可选的巧克力蛋糕口味数组
private static final String[] cakeTypes = {"Rich", "Light", "Moderate"};
// 随机数生成器
private static final Random random = new Random();
// 巧克力蛋糕烘焙方法
public static ChocolateCake createChocolateCake() {
// 随机选择一种口味
String randomType = cakeTypes[random.nextInt(cakeTypes.length)];
if (cakes.containsKey(randomType)) {
return cakes.get(randomType); // 如果 MAP 中有,则直接从取出,不用初始化了
} else {
// 如果 MAP 中没有,则创建新的对象并放到 MAP 中
ChocolateCake cake = null;
switch (randomType) {
case "Rich":
cake = new RichChocolateCake();
break;
case "Light":
cake = new LightChocolateCake();
break;
case "Moderate":
cake = new ModerateChocolateCake();
break;
default:
System.out.println("抱歉,暂时无法烘焙此类巧克力蛋糕");
}
cakes.put(randomType, cake);
return cake;
}
}
}
// 主厨类
class MasterChef {
// 准备烘焙材料
public void prepareIngredients() {
System.out.println("准备巧克力、面粉、糖等烘焙材料");
}
// 调整巧克力蛋糕配方
public void adjustRecipe() {
System.out.println("根据顾客的口味偏好,调整配方中巧克力的用量,以确定巧克力蛋糕的浓度");
}
// 烘焙巧克力蛋糕
public void bakeChocolateCake() {
prepareIngredients();
adjustRecipe();
ChocolateCake cake = ChocolateCakeFactory.createChocolateCake();
if (cake != null) {
cake.bake();
}
}
}
// 测试类
public class CakeMain {
public static void main(String[] args) {
// 尝试烘焙多次
System.out.println("尝试连续烘焙:");
for (int i = 0; i < 5; i++) {
System.out.println("第 " + (i + 1) + " 次烘焙:");
masterChef.bakeChocolateCake();
}
}
}
在修改后的代码中,ChocolateCakeFactory 类的 createChocolateCake 方法不再需要传入口味类型,而是直接从可选的巧克力蛋糕口味数组中随机选择一个口味来创建巧克力蛋糕。这样,主厨在烘焙巧克力蛋糕时不需要指定口味,而是由工厂自动随机选择口味。
核心思想
工厂方法模式的核心思想是将对象的创建延迟到子类中进行。它提供了一种创建对象的接口,但是具体的对象创建过程由子类来决定。通过这种方式,工厂方法模式实现了对象的创建与使用的分离,提高了代码的可扩展性和可维护性。
具体来说,工厂方法模式包含以下几个要点:
-
抽象工厂(Creator):抽象工厂定义了一个用于创建对象的接口,通常是一个抽象类或接口。这个接口中通常包含一个抽象方法,用于创建产品对象。抽象工厂负责声明创建对象的接口,而不涉及具体的产品对象的创建过程。
-
具体工厂(Concrete Creator):具体工厂是抽象工厂的子类,负责实际创建具体的产品对象。每个具体工厂实现了抽象工厂中定义的创建对象的抽象方法,并在其中实现了产品对象的创建过程。具体工厂根据客户端的需求来创建相应的产品对象,从而完成了对象的实例化过程。
-
抽象产品(Product):抽象产品定义了产品对象的接口,描述了产品对象的特性和行为。抽象产品通常是一个接口或抽象类,其中包含了产品对象的基本方法或属性。
-
具体产品(Concrete Product):具体产品是抽象产品的子类,负责实现抽象产品中定义的接口,描述了具体产品的具体实现。每个具体产品都代表了一种特定类型的产品,实现了抽象产品中定义的方法和属性。
通过工厂方法模式,客户端代码通过与抽象工厂进行交互来获取所需的产品对象,而具体的产品对象的创建过程由具体工厂类来完成。这样一来,客户端代码与具体产品的实现细节相分离,使得系统更加灵活,并且更易于扩展和维护。
适用场景
工厂方法模式适用于以下情况:
-
需要创建多个相关对象的情况:当一个类负责创建的对象种类不止一个,并且这些对象之间存在一定的关联或者相似性时,可以考虑使用工厂方法模式。工厂方法模式可以将对象的创建过程封装到各自的工厂类中,每个工厂类负责创建一种特定类型的对象。
-
需要灵活扩展对象创建的情况:工厂方法模式通过定义抽象工厂和具体工厂的方式,使得系统可以轻松地扩展新的产品类型,而无需修改现有的代码。只需要新增具体的工厂类和产品类即可,不会影响到已有的代码。
-
需要隐藏对象创建细节的情况:客户端代码不需要知道具体的产品对象是如何创建的,只需要通过抽象工厂接口来获取所需的产品对象。这样可以有效地隐藏对象创建的细节,提高了系统的安全性和稳定性。
-
需要遵循单一职责原则的情况:工厂方法模式可以将对象的创建和使用分离,每个具体工厂类负责创建一种特定类型的产品对象,符合单一职责原则。这样可以使得系统的设计更加清晰和易于理解。
工厂方法模式适用于需要创建多个相关对象、需要灵活扩展对象创建、需要隐藏对象创建细节以及需要遵循单一职责原则的情况。通过使用工厂方法模式,我们可以有效地提高代码的可维护性、可扩展性和灵活性。
优缺点
优点:
-
符合开闭原则:工厂方法模式通过定义抽象工厂和具体工厂的方式,使得系统可以轻松地扩展新的产品类型,而无需修改已有的代码,符合开闭原则。
-
隐藏对象创建细节:客户端代码不需要知道具体的产品对象是如何创建的,只需要通过抽象工厂接口来获取所需的产品对象。这样可以有效地隐藏对象创建的细节,提高了系统的安全性和稳定性。
-
提高代码的可维护性:工厂方法模式将对象的创建和使用分离,每个具体工厂类负责创建一种特定类型的产品对象,符合单一职责原则。这样使得系统的设计更加清晰、易于理解,提高了代码的可维护性。
-
灵活性和扩展性:工厂方法模式允许系统在不修改现有代码的情况下引入新的产品类型,只需要新增具体的工厂类和产品类即可。这样提高了系统的灵活性和扩展性,使得系统更容易适应需求的变化。
缺点:
-
增加了类的个数:引入工厂方法模式会增加系统中类的个数,每个具体产品类都需要对应一个具体工厂类,这样会导致类的数量增加,增加了系统的复杂度。
-
可能带来不必要的代码复杂性:如果产品种类非常多,可能会导致系统中存在大量的具体工厂类,这样会增加系统的维护成本和理解难度。
-
增加了代码的抽象程度:工厂方法模式引入了额外的抽象层次,客户端需要理解抽象工厂接口以及各个具体工厂类之间的关系,可能会增加代码的抽象程度,使得理解和调试变得困难。
总的来说,工厂方法模式具有灵活性、可扩展性和可维护性等优点,但也存在增加类的个数、增加代码复杂性和抽象程度等缺点。在使用时需要根据具体的需求和场景来权衡利弊,选择合适的设计模式。
抽象工厂模式【Abstract Factory Pattern】
定义
抽象工厂模式(Abstract Factory Pattern)是一种创建型设计模式,它提供了一种将一组相关或相互依赖的对象组合成为一个"工厂"的方式,而无需指定它们具体的类。这种模式通过提供一个接口来创建一系列相关或相互依赖的对象,而不需要指定具体的类。它属于工厂模式的一种扩展,常用于需要创建一组相关或相互依赖的对象时,以确保这些对象能够协同工作。
当谈论抽象工厂模式时,我们需要理解其中的两个核心概念:抽象工厂和具体工厂。
-
抽象工厂(Abstract Factory):抽象工厂是一个接口或抽象类,它定义了一组用于创建产品的方法,每个方法对应一个产品族。这里的"产品族"指的是一组相关或相互依赖的产品,通常是由多个具体产品组成的一个整体。抽象工厂的职责是为每个产品族提供一组创建方法,客户端可以通过这些方法创建需要的产品。但是抽象工厂并不知道或关心具体的产品是如何创建的,它只负责定义接口。
-
具体工厂(Concrete Factory):具体工厂是抽象工厂的实现,它实现了抽象工厂定义的接口,负责创建具体的产品对象。每个具体工厂对应一个产品族,它会创建该产品族中所有的产品对象。具体工厂根据需求创建不同的产品,确保这些产品之间彼此相互配合,组成一个完整的产品族。
举例来说,假设我们有一个抽象工厂叫做"披萨工厂(PizzaFactory)",它定义了两个方法:createDough() 和 createSauce(),分别用于创建面团和酱料。现在我们有两个具体工厂分别是"纽约披萨工厂(NYPizzaFactory)"和"芝加哥披萨工厂(ChicagoPizzaFactory)"。在纽约披萨工厂中,createDough() 方法会创建纽约风格的面团,而 createSauce() 方法会创建纽约风格的酱料。而在芝加哥披萨工厂中,这两个方法分别创建芝加哥风格的面团和酱料。这样,每个具体工厂都负责创建属于自己风格的面团和酱料,保证了产品之间的相关性。
我们需要理清抽象工厂模式的内部逻辑,里面涉及到四个关键概念:抽象工厂、具体工厂、抽象产品和具体产品。
-
抽象工厂(Abstract Factory):
- 抽象工厂是一个接口或抽象类,它声明了一组用于创建产品对象的方法,每个方法对应一个产品族。
- 抽象工厂负责定义产品对象的创建方法,但不涉及具体产品的实现。
-
具体工厂(Concrete Factory):
- 具体工厂是抽象工厂的实现类,它实现了抽象工厂中声明的方法。
- 每个具体工厂类负责创建特定产品族的产品对象。
-
抽象产品(Abstract Product):
- 抽象产品是一个接口或抽象类,它定义了产品对象的通用接口,包括产品族中所有产品的公共行为。
- 抽象产品描述了产品对象应该具有的方法,但不包含具体的实现。
-
具体产品(Concrete Product):
- 具体产品是抽象产品的实现类,它实现了抽象产品中定义的方法。
- 每个具体产品类表示一个特定的产品对象,属于某个产品族。
抽象工厂模式中的关键思想是将相关的产品组织成一个产品族,并提供一个统一的接口来创建这些产品族的对象。具体工厂负责实现这个接口,并创建属于特定产品族的具体产品对象。每个产品族都有对应的抽象产品接口,具体产品类实现这些接口来提供具体的产品功能。这样,客户端代码只需要与抽象工厂和抽象产品接口交互,而无需关心具体工厂和具体产品的实现细节。
具体工厂实现了抽象工厂接口,负责创建属于自己产品族的具体产品,因此它们只能创建自己产品族中定义的产品,而无法调用其他具体工厂的产品。这确保了产品之间的相关性和一致性,使得整个系统更加稳定和可靠。
抽象工厂模式允许客户端代码使用抽象的方式来创建一组相关的产品对象,而不需要知道具体的产品类。这样可以提高代码的灵活性和可维护性,同时使得系统更易于扩展和修改。
要注意的是,抽象工厂模式适用于需要创建一系列相关对象的情况,但是如果产品族的变化不频繁,而且产品等级结构固定不变,可能会造成类的数量过多,不易维护。因此,在使用抽象工厂模式时,需要权衡灵活性和复杂性之间的关系。
举例说明
在学习工厂方法模式的时候,我们举了一个关于制作巧克力蛋糕的例子。
要将这个例子修改成抽象工厂模式,需要满足以下条件:
- 定义一个抽象工厂接口,该接口声明了用于创建巧克力蛋糕对象的方法。
- 对每个具体的巧克力蛋糕口味,创建一个对应的具体工厂类,实现抽象工厂接口,并在其中实现用于创建巧克力蛋糕对象的方法。
由于在这个例子中只有一个产品族(巧克力蛋糕),因此抽象工厂模式的优势并不明显。通常情况下,抽象工厂模式适用于存在多个产品族的场景,每个产品族由多个相关的产品组成,且希望保持这些产品之间的一致性。在这种情况下,抽象工厂模式可以为每个产品族提供一个对应的抽象工厂,由具体的工厂类来创建这些产品,从而确保了产品族的一致性。
因此,虽然理论上可以将工厂方法模式改造成抽象工厂模式,但在这个具体的例子中,工厂方法模式已经很好地满足了需求,没有必要进行这样的修改。
那么我们换一个例子好了:
假设有一家名为"冰淇淋之家"的工厂,他们专门生产各种口味的冰淇淋。在这个工厂中,有一个抽象工厂接口称为"冰淇淋工厂",定义了两个方法:生产甜筒冰淇淋()和生产杯装冰淇淋(),分别用于生产甜筒冰淇淋和杯装冰淇淋。
接着,冰淇淋之家有两个具体工厂类,分别是"甜筒冰淇淋工厂"和"杯装冰淇淋工厂",它们都实现了"冰淇淋工厂接口"。
在甜筒冰淇淋工厂中,有多个具体产品类,例如"巧克力味甜筒冰淇淋"和"草莓味甜筒冰淇淋",它们都实现了一个抽象产品接口"冰淇淋"。每种甜筒冰淇淋都有各自的口味和配料,比如巧克力冰淇淋可能有巧克力颗粒和巧克力酱。
在杯装冰淇淋工厂中,同样也有多个具体产品类,例如"香草味杯装冰淇淋"和"芒果味杯装冰淇淋",它们同样实现了"冰淇淋"接口。杯装冰淇淋可能在某些口味上略有不同,但都是以杯装形式呈现。
整个过程如下:
-
抽象工厂接口:冰淇淋工厂接口定义了两个方法:生产甜筒冰淇淋()和生产杯装冰淇淋(),分别用于生产甜筒冰淇淋和杯装冰淇淋。
-
具体工厂类:
- 甜筒冰淇淋工厂实现了冰淇淋工厂接口,负责生产甜筒冰淇淋。
- 杯装冰淇淋工厂也实现了冰淇淋工厂接口,负责生产杯装冰淇淋。
-
抽象产品接口:冰淇淋接口定义了冰淇淋对象的通用方法。
-
具体产品类:
- 在甜筒冰淇淋工厂中,有多个具体产品类,例如巧克力味甜筒冰淇淋和草莓味甜筒冰淇淋,它们都实现了冰淇淋接口。
- 在杯装冰淇淋工厂中,同样有多个具体产品类,例如香草味杯装冰淇淋和芒果味杯装冰淇淋,同样实现了冰淇淋接口。
当客户端需要一份冰淇淋时,它可以通过与抽象工厂接口交互,而不需要了解具体的工厂和产品细节。客户端可以调用工厂提供的方法,比如生产甜筒冰淇淋()或生产杯装冰淇淋(),工厂会根据需要创建相应类型的冰淇淋对象,并返回给客户端使用。这种方式使得客户端代码与具体的产品和工厂解耦,提高了系统的灵活性和可维护性。
java
import java.util.Random;
// 抽象产品接口:冰淇淋
interface IceCream {
void taste(); // 品尝方法
}
// 具体产品类:巧克力味甜筒冰淇淋
class ChocolateConeIceCream implements IceCream {
@Override
public void taste() {
System.out.println("品尝巧克力味甜筒冰淇淋");
}
}
// 具体产品类:草莓味甜筒冰淇淋
class StrawberryConeIceCream implements IceCream {
@Override
public void taste() {
System.out.println("品尝草莓味甜筒冰淇淋");
}
}
// 具体产品类:香草味杯装冰淇淋
class VanillaCupIceCream implements IceCream {
@Override
public void taste() {
System.out.println("品尝香草味杯装冰淇淋");
}
}
// 具体产品类:芒果味杯装冰淇淋
class MangoCupIceCream implements IceCream {
@Override
public void taste() {
System.out.println("品尝芒果味杯装冰淇淋");
}
}
// 抽象工厂接口:冰淇淋工厂
interface IceCreamFactory {
IceCream createConeIceCream(); // 生产甜筒冰淇淋
IceCream createCupIceCream(); // 生产杯装冰淇淋
}
// 具体工厂类:甜筒冰淇淋工厂
class ConeIceCreamFactory implements IceCreamFactory {
@Override
public IceCream createConeIceCream() {
Random random = new Random();
if (random.nextBoolean()) {
return new ChocolateConeIceCream();
} else {
return new StrawberryConeIceCream();
}
}
@Override
public IceCream createCupIceCream() {
return null; // 甜筒冰淇淋工厂只生产甜筒冰淇淋,不生产杯装冰淇淋
}
}
// 具体工厂类:杯装冰淇淋工厂
class CupIceCreamFactory implements IceCreamFactory {
@Override
public IceCream createConeIceCream() {
return null; // 杯装冰淇淋工厂只生产杯装冰淇淋,不生产甜筒冰淇淋
}
@Override
public IceCream createCupIceCream() {
Random random = new Random();
if (random.nextBoolean()) {
return new VanillaCupIceCream();
} else {
return new MangoCupIceCream();
}
}
}
// 客户端类
public class Client {
public static void main(String[] args) {
// 创建甜筒冰淇淋工厂
IceCreamFactory coneFactory = new ConeIceCreamFactory();
// 生产甜筒冰淇淋
IceCream coneIceCream = coneFactory.createConeIceCream();
// 品尝甜筒冰淇淋
coneIceCream.taste();
// 创建杯装冰淇淋工厂
IceCreamFactory cupFactory = new CupIceCreamFactory();
// 生产杯装冰淇淋
IceCream cupIceCream = cupFactory.createCupIceCream();
// 品尝杯装冰淇淋
cupIceCream.taste();
}
}
这里我们可以尝试引入枚举类型。
当将枚举类型作为方法的参数传递时,在JUnit进行单元测试时,不需要额外判断输入参数是否为空或长度为0的边界异常条件,因为枚举类型在Java中是类型安全的,无法传递非枚举类型的参数进来。这意味着如果方法声明的参数是枚举类型,那么调用该方法时必须传入相应的枚举值,否则编译器会报错,这样就不需要在方法内部进行额外的参数校验。
引入枚举类型的好处主要体现在以下几个方面:
-
类型安全:枚举类型是Java语言的一种特殊类型,它限定了可以传递的参数类型,提高了代码的健壮性和安全性。在方法签名中使用枚举类型作为参数,可以确保调用方传入的参数是有效的枚举值,避免了传入非法参数的可能性。
-
可读性和可维护性:枚举类型通过名称来表示特定的常量值,使得代码更易读、更易理解。通过定义枚举类型,可以将相关的常量值组织在一起,便于管理和维护。
-
程序结构清晰:枚举类型可以将相关的常量值归类在一起,并提供统一的访问方式,使得程序的结构更清晰,降低了代码的复杂度。
java
import java.util.Random;
// 枚举类型:冰淇淋口味
enum IceCreamFlavor {
CHOCOLATE, STRAWBERRY, VANILLA, MANGO
}
// 抽象产品接口:冰淇淋
interface IceCream {
void taste(); // 品尝方法
}
// 具体产品类:甜筒冰淇淋
class ConeIceCream implements IceCream {
private IceCreamFlavor flavor;
public ConeIceCream(IceCreamFlavor flavor) {
this.flavor = flavor;
}
@Override
public void taste() {
System.out.println("品尝" + flavor + "味甜筒冰淇淋");
}
}
// 具体产品类:杯装冰淇淋
class CupIceCream implements IceCream {
private IceCreamFlavor flavor;
public CupIceCream(IceCreamFlavor flavor) {
this.flavor = flavor;
}
@Override
public void taste() {
System.out.println("品尝" + flavor + "味杯装冰淇淋");
}
}
// 抽象工厂接口:冰淇淋工厂
interface IceCreamFactory {
IceCream createIceCream(); // 生产冰淇淋
}
// 具体工厂类:甜筒冰淇淋工厂
class ConeIceCreamFactory implements IceCreamFactory {
@Override
public IceCream createIceCream() {
Random random = new Random();
IceCreamFlavor[] flavors = IceCreamFlavor.values();
IceCreamFlavor flavor = flavors[random.nextInt(flavors.length)];
return new ConeIceCream(flavor);
}
}
// 具体工厂类:杯装冰淇淋工厂
class CupIceCreamFactory implements IceCreamFactory {
@Override
public IceCream createIceCream() {
Random random = new Random();
IceCreamFlavor[] flavors = IceCreamFlavor.values();
IceCreamFlavor flavor = flavors[random.nextInt(flavors.length)];
return new CupIceCream(flavor);
}
}
// 客户端类
public class Client {
public static void main(String[] args) {
// 创建甜筒冰淇淋工厂
IceCreamFactory coneFactory = new ConeIceCreamFactory();
// 生产甜筒冰淇淋
IceCream coneIceCream = coneFactory.createIceCream();
// 品尝甜筒冰淇淋
coneIceCream.taste();
// 创建杯装冰淇淋工厂
IceCreamFactory cupFactory = new CupIceCreamFactory();
// 生产杯装冰淇淋
IceCream cupIceCream = cupFactory.createIceCream();
// 品尝杯装冰淇淋
cupIceCream.taste();
}
}
在这个重构后的代码中,我们使用枚举类型IceCreamFlavor来表示冰淇淋的口味,而不再使用字符串。每个具体的冰淇淋产品类(甜筒冰淇淋和杯装冰淇淋)在创建时都会指定口味,这样可以保证口味的类型安全性。同时,每个工厂类都只负责生产对应类型的冰淇淋,不再混合生产甜筒和杯装冰淇淋。
核心思想
抽象工厂模式的核心思想是提供一个接口或抽象类,用于创建一系列相关或相互依赖的对象,而不需要指定它们具体的类。这个接口定义了一组创建方法,每个方法对应一个产品族的创建过程。具体的工厂类实现了这个接口,并负责创建属于特定产品族的具体产品对象。
抽象工厂模式的核心思想可以通过以下几个要点来解释:
-
抽象工厂接口:抽象工厂模式定义了一个抽象工厂接口或抽象类,其中包含一组方法用于创建一系列相关或相互依赖的对象。这些方法通常对应于不同产品族的创建过程。
-
具体工厂类:具体工厂类实现了抽象工厂接口,负责创建属于特定产品族的具体产品对象。每个具体工厂类对应一个特定的产品族,它实现了抽象工厂接口中定义的方法,并根据具体需求创建相应的产品。
-
产品接口:抽象工厂模式还定义了一组产品接口或抽象类,用于描述不同产品族中的产品。这些产品可能存在不同的实现,但它们共享相同的接口,以确保客户端代码可以统一处理不同产品族的产品对象。
-
产品族:产品族是指一组相关的产品对象,它们共同由同一个具体工厂类创建。这些产品对象通常具有一定的关联性或依赖关系,例如同一品牌的汽车可能包括轿车、SUV等不同类型的产品。
-
解耦客户端和具体实现:抽象工厂模式将客户端代码与具体产品的创建过程解耦,客户端只需要通过抽象工厂接口来创建产品对象,而无需关心具体的产品类。这样可以使系统更加灵活,客户端可以轻松地切换不同的具体工厂类来获取不同风格或类型的产品。
通过这种方式,抽象工厂模式提供了一种组织和管理相关对象的方法,使得系统更具可扩展性和可维护性。
这种模式的关键在于将客户端与实际创建的对象解耦,客户端只需要知道抽象工厂和产品接口,而不需要了解具体的产品类。这样,客户端可以通过使用不同的具体工厂来创建不同风格或类型的产品,而无需修改现有的客户端代码。
抽象工厂模式强调了整体对象的创建过程,将对象的创建细节隐藏在工厂内部,使得系统更加灵活、可扩展。同时,它也有利于维护和管理相关的对象族,提高了代码的可复用性和可维护性。
适用场景
抽象工厂模式适用于以下场景:
-
需要创建一系列相关或相互依赖的产品对象:当一个系统需要一组相关的产品对象,且这些产品对象之间存在一定的关联或依赖关系时,可以考虑使用抽象工厂模式。例如,一个汽车工厂生产不同品牌的汽车,每个品牌的汽车由一组相关的零部件构成,这些零部件相互依赖,可以使用抽象工厂模式来管理这些产品对象的创建。
-
希望在不同产品族之间进行切换:如果系统需要支持在不同产品族之间进行切换,而且每个产品族包含一组相关的产品对象,可以使用抽象工厂模式。通过定义不同的具体工厂类来创建不同产品族的产品对象,可以实现系统在不同产品族之间的灵活切换。
-
客户端代码与具体产品的解耦:抽象工厂模式可以帮助客户端代码与具体产品的创建过程解耦,使得客户端代码不依赖于具体产品类。客户端只需要通过抽象工厂接口来创建产品对象,而无需关心具体的产品类,从而提高了系统的灵活性和可维护性。
-
需要保证一组相关产品对象的一致性:在某些情况下,系统需要保证一组产品对象之间的一致性,即这些产品对象应该相互配合使用,不能随意替换。抽象工厂模式可以确保一组产品对象是一起创建的,从而保证了它们之间的一致性。
抽象工厂模式适用于需要管理一组相关产品对象,且希望将产品对象的创建过程与客户端代码解耦的场景。
优缺点
抽象工厂模式具有以下优点和缺点:
优点:
-
封装性强:抽象工厂模式将产品的创建过程封装在具体工厂类中,客户端无需关心具体产品的创建细节,只需要通过抽象工厂接口来创建产品对象,从而降低了系统的耦合度。
-
产品族一致性:抽象工厂模式可以确保一组产品对象是一起创建的,保证了这些产品对象之间的一致性,从而避免了不同产品之间的不兼容性问题。
-
灵活性高:由于具体工厂类实现了抽象工厂接口,因此可以在运行时动态切换具体工厂类,从而实现在不同产品族之间的灵活切换,增强了系统的扩展性和灵活性。
-
符合开闭原则:抽象工厂模式通过增加新的具体工厂类和产品类来扩展系统功能,而无需修改已有代码,符合开闭原则,增加新的产品族对现有代码没有影响。
缺点:
-
扩展困难:当需要增加新的产品族时,需要同时修改抽象工厂接口和所有的具体工厂类,这可能会导致系统的扩展变得复杂,增加了系统的维护成本。
-
产品等级结构扩展困难:抽象工厂模式要求所有的具体产品类在同一等级结构中,这限制了产品等级结构的扩展性,使得新的产品等级结构较难引入系统。
总之,抽象工厂模式适用于需要创建一组相关产品对象,并且希望保持这些产品对象之间一致性的场景。虽然抽象工厂模式具有一定的扩展性和灵活性,但在产品等级结构发生变化或需要增加新的产品族时,可能会面临一些困难。