简单工厂模式
案例
试想这样一个场景:要求通过控制台或文件来记录日志。遇到这样的问题,通过面相对象的思考方式,很容易就可以写出下面的代码来:
1.首先定义一个接口:
/**
* 日志记录器
*/
public interface Logger {
void log(String info);
}
2.控制台记录器实现类:
/**
* 控制台记录器
*/
public class ConsoleLogger implements Logger {
@Override
public void log(String info) {
System.out.println("控制台记录:" + info);
}
}
3.文件记录器实现类:
/**
* 文件记录器
*/
public class FileLogger implements Logger {
@Override
public void log(String info) {
System.out.println("文件记录:" + info);
}
}
4.客户端使用时通过输入不同的类型后,来使用:
/**
* 客户端使用日志记录器
*/
public class Main {
public static void main(String[] args) {
// 客户端输入一个类型
System.out.println("请输入记录类型:\nconsole:控制台记录\tfile:文件记录");
Scanner sc = new Scanner(System.in);
String type = sc.nextLine();
Logger logger;
if ("console".equalsIgnoreCase(type)) {
System.out.println("初始化控制台记录器");
logger = new ConsoleLogger();
} else if ("file".equalsIgnoreCase(type)) {
System.out.println("初始化文件记录器");
logger = new FileLogger();
} else {
throw new RuntimeException("选择的记录器不存在");
}
logger.log("今天天气真好");
}
}
从实现的方面来说,上面的代码完全没有问题,但是代码无错就是优?
仔细思考上面的代码,得出下面的结论:
Main
客户端使用类中充满了if...else...
的判断,使得客户端使用时非常繁琐。客户端只能通过
new
关键字来直接创建Logger
对象,Logger
类与客户端类耦合度较高,对象的创建和使用无法分离。实际上客户端本不用关心对象的创建。-
如果要增加一种数据库记录的方式,那么我们就必须修改客户端类。
接下来我们通过使用简单工厂模式的方式来对上述问题进行改造。
模式介绍
简单工厂模式是属于创建型模式,又叫做静态工厂方法(Static Factory Method)模式,但不属于23种GOF设计模式之一。简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。
角色构成:
- Factory(工厂角色):工厂角色即工厂类,它是简单工厂模式的核心,负责实现创建所有产品实例的内部逻辑;工厂类可以被外界直接调用,创建所需的产品对象;在工厂类中提供了静态的工厂方法factoryMethod(),它的返回类型为抽象产品类型Product。
- Product(抽象产品角色):它是工厂类所创建的所有对象的父类,封装了各种产品对象的公有方法,它的引入将提高系统的灵活性,使得在工厂类中只需定义一个通用的工厂方法,因为所有创建的具体产品对象都是其子类对象。
- ConcreteProduct(具体产品角色):它是简单工厂模式的创建目标,所有被创建的对象都充当这个角色的某个具体类的实例。每一个具体产品角色都继承了抽象产品角色,需要实现在抽象产品中声明的抽象方法。
UML 类图:
特点:
简单工厂模式的实质是由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类(这些产品类继承自一个父类或接口)的实例。
代码改造
按照上面的介绍的简单工厂模式类图,来改造日志记录器的例子,改造后的代码如下:
1.Product(抽象产品角色):
/**
* 记录器接口:担当 Product(产品)角色
*/
public interface Logger {
void log(String info);
}
2.两个具体产品类
ConcreteProductA(具体产品角色):
/**
* 控制台记录器:担当 ProductA (产品A)角色
*/
public class ConsoleLogger implements Logger {
@Override
public void log(String info) {
System.out.println("控制台记录:" + info);
}
}
ConcreteProductB(具体产品角色):
/**
* 文件记录器:担当 ProductB (产品B)角色
*/
public class FileLogger implements Logger {
@Override
public void log(String info) {
System.out.println("文件记录:" + info);
}
}
3.工厂类Factory(工厂角色):
/**
* 记录器工厂:担当 Factory(工厂类)角色
*/
public class LoggerFactory {
public static Logger getLogger(String type) {
if ("console".equalsIgnoreCase(type)) {
return new ConsoleLogger();
} else if ("file".equalsIgnoreCase(type)) {
return new FileLogger();
} else {
throw new RuntimeException("选择的记录器不存在");
}
}
}
4.客户端通过工厂类得到具体的记录器类:
/**
* 客户端使用日志记录器
*/
public class Main {
public static void main(String[] args) {
// 客户端输入一个类型
System.out.println("请输入记录类型:\nconsole:控制台记录\tfile:文件记录");
Scanner sc = new Scanner(System.in);
String type = sc.nextLine();
Logger logger = LoggerFactory.getLogger(type);
logger.log("今天天气真好");
}
}
通过上面的改造,我们将原来在Main
客户端中初始化不同类型记录器类的代码放到了LoggerFacory
类中,它的作用就是创建记录器类,这样使得客户端类使用时更加简洁。同时,如果我们要添加一个数据库记录器的话,只用新建一个DataBaseLogger
的类实现Logger
接口,然后在LoggerFactory
中的getLogger()
方法添加DataBaseLogger
对象的生成即可。
可以看到我们仅仅是将客户端中初始化各个记录器类的代码放到了新创建的LoggerFactory
类中,对于使用简单工厂类来说就是这么的简单。
模式应用
在我们使用JDK自带的Calendar
类时,我们通过它的getInstance()
来获得Calendar
的实例:
public class Main {
public static void main(String[] args) {
Calendar instance = Calendar.getInstance();
System.out.println(instance.getTime());
}
}
让我们深入看一下它是怎么创建实例的,下面是其中创建实例的最主要方法:
// 1.首先调用了这个方法
public static Calendar getInstance()
{
return createCalendar(TimeZone.getDefault(), Locale.getDefault(Locale.Category.FORMAT));
}
// 2.实际上是调用的这个重载的私有方法,这个方法的作用就是通过传入的TimeZone和Locale来创建Calendar类实例,在这里传入的是我们系统环境中默认TimeZone和Locale,也可以通过其他重载方法指定TimeZone和Locale
private static Calendar createCalendar(TimeZone zone,
Locale aLocale)
{
CalendarProvider provider =
LocaleProviderAdapter.getAdapter(CalendarProvider.class, aLocale)
.getCalendarProvider();
if (provider != null) {
try {
return provider.getInstance(zone, aLocale);
} catch (IllegalArgumentException iae) {
// fall back to the default instantiation
}
}
Calendar cal = null;
// 2.1如果上面没有提供一个CalendarProvider,即provider == null,那么这里就会根据情况的判断来创建不同的Calendar类实例,这里就体现了简单工厂模式的思想。
if (aLocale.hasExtensions()) {
String caltype = aLocale.getUnicodeLocaleType("ca");
if (caltype != null) {
switch (caltype) {
case "buddhist":
cal = new BuddhistCalendar(zone, aLocale);
break;
case "japanese":
cal = new JapaneseImperialCalendar(zone, aLocale);
break;
case "gregory":
cal = new GregorianCalendar(zone, aLocale);
break;
}
}
}
if (cal == null) {
// If no known calendar type is explicitly specified,
// perform the traditional way to create a Calendar:
// create a BuddhistCalendar for th_TH locale,
// a JapaneseImperialCalendar for ja_JP_JP locale, or
// a GregorianCalendar for any other locales.
// NOTE: The language, country and variant strings are interned.
if (aLocale.getLanguage() == "th" && aLocale.getCountry() == "TH") {
cal = new BuddhistCalendar(zone, aLocale);
} else if (aLocale.getVariant() == "JP" && aLocale.getLanguage() == "ja"
&& aLocale.getCountry() == "JP") {
cal = new JapaneseImperialCalendar(zone, aLocale);
} else {
cal = new GregorianCalendar(zone, aLocale);
}
}
return cal;
}
通过阅读Calendar
类中创建实例的方法,我们发现Calendar
既充当了Product(抽象产品角色)又充当了Factory(工厂角色),但其本质上是使用了简单工厂模式的思想。
总结
1.主要优点:
- 工厂类包含必要的判断逻辑,可以决定在什么时候创建哪一个产品类的实例,客户端可以免除直接创建产品对象的职责,而仅仅“消费”产品,简单工厂模式实现了对象创建和使用的分离。
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以在一定程度减少使用者的记忆量。
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性。
2.主要缺点
- 由于工厂类集中了所有产品的创建逻辑,职责过重,一旦不能正常工作,整个系统都要受到影响。
- 使用简单工厂模式势必会增加系统中类的个数(引入了新的工厂类),增加了系统的复杂度和理解难度。
- 系统扩展困难,一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护。
- 简单工厂模式由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构。
3.适用场景
- 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的业务逻辑太过复杂。
- 客户端只知道传入工厂类的参数,对于如何创建对象并不关心。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 简单工厂模式 在JDK中的使用(Calendar)
- 简单工厂模式详解
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/simple-factory
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/b87f2e16ca8b