抽象工厂模式
案例
我们要开发一套界面皮肤库,对界面进行美化。用户通过选择不同的皮肤,实现界面中元素颜色的相应变化。其中这个界面可能有:按钮、窗口、图标等等组件,而客户通过切换皮肤可以显示不同颜色的组件样式。接下来让我们试着用工厂方法模式的思路来开发两款皮肤(蓝色经典/绿色护眼)来:
1.首先分别定义两种抽象产品及其实现类:
抽象窗口类:
/**
* 抽象的产品类:窗口
*/
public interface Window {
void show();
}
蓝色窗口类:
/**
* 具体的产品类:蓝色窗口
*/
public class BlueWindow implements Window {
public void show() {
System.out.println("显示蓝色窗口~~");
}
}
绿色窗口类:
/**
* 具体的产品类:绿色窗口
*/
public class GreenWindow implements Window {
public void show() {
System.out.println("显示绿色窗口~~");
}
}
抽象按钮类:
/**
* 抽象的产品类:按钮
*/
public interface Button {
void click();
}
蓝色按钮类:
/**
* 具体的产品类:蓝色按钮
*/
public class BlueButton implements Button {
public void click() {
System.out.println("点击蓝色按钮~~");
}
}
绿色按钮类:
/**
* 具体的产品类:绿色按钮
*/
public class GreenButton implements Button {
public void click() {
System.out.println("点击绿色按钮~~");
}
}
2.接下来分别定义抽象工厂及其具体实现类:
抽象窗口工厂:
/**
* 抽象的工厂类:窗口工厂
*/
public interface WindowFactory {
Window getWindow();
}
蓝色窗口工厂类:
/**
* 具体的工厂类:蓝色窗口工厂类
*/
public class BlueWindowFactory implements WindowFactory {
public Window getWindow() {
return new BlueWindow();
}
}
绿色窗口工厂类:
/**
* 具体的工厂类:绿色窗口工厂类
*/
public class GreenWindowFactory implements WindowFactory {
public Window getWindow() {
return new GreenWindow();
}
}
抽象按钮工厂类:
/**
* 抽象的工厂类:按钮工厂类
*/
public interface ButtonFactory {
Button getButton();
}
蓝色按钮工厂类:
/**
* 具体的工厂类:蓝色按钮工厂类
*/
public class BlueButtonFactory implements ButtonFactory {
public Button getButton() {
return new BlueButton();
}
}
绿色按钮工厂类:
/**
* 具体的工厂类:绿色按钮工厂类
*/
public class GreenButtonFactory implements ButtonFactory {
public Button getButton() {
return new GreenButton();
}
}
3.客户端使用类:
/**
* 客户端使用不同款式的皮肤界面
*/
public class Main {
public static void main(String[] args) {
// 模拟选择一款皮肤
System.out.println("请输入皮肤颜色:\nblue:蓝色经典款\tgreen:绿色护眼款");
Scanner sc = new Scanner(System.in);
String type = sc.nextLine();
Window window;
Button button;
if ("blue".equals(type)) {
window = new BlueWindowFactory().getWindow();
button = new BlueButtonFactory().getButton();
} else if ("green".equals(type)) {
window = new GreenWindowFactory().getWindow();
button = new GreenButtonFactory().getButton();
} else {
throw new RuntimeException("选择的皮肤不存在");
}
window.show();
button.click();
}
}
虽然我们实现了上面的需求,但是使用工厂方法模式会存在如下的一些问题:
- 当需要增加新的皮肤时,虽然不要修改现有代码,但是需要增加大量类,针对每一个新增具体组件都需要增加一个具体工厂,类的个数成对增加,这无疑会导致系统越来越庞大,增加系统的维护成本和运行开销。
- 由于同一种风格的具体界面组件通常要一起显示,因此需要为每个组件都选择一个具体工厂,用户在使用时必须逐个进行设置,如果某个具体工厂选择失误将会导致界面显示混乱。
针对上面的问题,我们可以考虑使用抽象工厂模式来改进一下。
模式介绍
抽象工厂模式(Abstract Factory Pattern)隶属于设计模式中的创建型模式,用于产品族的构建。抽象工厂是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂是指当有多个抽象角色时使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体情况下,创建多个产品族中的产品对象。
上面是关于抽象工厂模式的简介,我们从中可以看出它是一种创建型模式,用于产品族的创建。这里面有个产品族的概念,那么什么是产品族呢?
- 产品族:类比我们上面的案例,我们的需求是开发界面皮肤库,从而实现皮肤的切换。在这个界面中可能会有窗口、按钮、图标(大家可以想象我们的电脑桌面)等等具体事物,这些具体事物可以组成我们的具体界面,这些可以组成具体界面的具体组件就是我们抽象工厂模式中的产品族的概念。
现在如果我们开发出蓝色经典和绿色护眼两款皮肤出来,那么这里的蓝色经典款和绿色护眼款就引出了我们另一个概念,叫做产品等级结构
- 产品等级结构:在组合界面组件时,我们会创建出抽象的窗口类出来,那么在蓝色经典款皮肤中,我们就会创建出蓝色经典样式的窗口,在绿色护眼款皮肤中,我们就会创建出绿色护眼样式的窗口。这里抽象的窗口类与具体的蓝色经典款窗口和绿色护眼款窗口就构成了一个产品等级结构。其他在创建按钮和图标时类似,简而言之就是抽象的父类与具体的子类就构成了产品等级结构。
角色构成:
- AbstractFactory(抽象工厂):它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。
- ConcreteFactory(具体工厂):它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。
- AbstractProduct(抽象产品):它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。
- ConcreteProduct(具体产品):它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。
UML类图:
特点:
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族。
代码改造
接下来,我们就按照上面的抽象工厂模式中介绍的角色构成进行代码改造:
1.首先是两个AbstractProduct(抽象产品)角色:
抽象窗口:
/**
* 窗口产品接口:担当抽象产品角色
*/
public interface Window {
void show();
}
抽象按钮:
/**
* 按钮产品接口:担当抽象产品角色
*/
public interface Button {
void click();
}
2.接下来是ConcreteProduct(具体产品)角色:
蓝色经典款窗口:
/**
* 蓝色窗口产品:担当具体产品角色
*/
public class BlueWindow implements Window {
public void show() {
System.out.println("显示蓝色窗口~~");
}
}
绿色护眼款窗口:
/**
* 绿色窗口产品:担当具体产品角色
*/
public class GreenWindow implements Window {
public void show() {
System.out.println("显示绿色窗口~~");
}
}
蓝色经典款按钮:
/**
* 蓝色按钮产品:担当具体产品角色
*/
public class BlueButton implements Button {
public void click() {
System.out.println("点击蓝色按钮~~");
}
}
绿色护眼款按钮:
/**
* 绿色窗口产品:担当具体产品角色
*/
public class GreenButton implements Button {
public void click() {
System.out.println("点击绿色按钮~~");
}
}
3.然后是AbstractFactory(抽象工厂)角色:
/**
* 皮肤抽象工厂:担当抽象工厂角色,定义生成一族产品的方法
*/
public interface SkinFactory {
Window getWindow();
Button getButton();
}
4.最后是ConcreteProduct(具体产品)角色:
蓝色经典款皮肤工厂:
/**
* 蓝色皮肤抽象工厂:担当具体工厂角色,实现生成一族产品的方法
*/
public class BlueFactory implements SkinFactory {
public Window getWindow() {
return new BlueWindow();
}
public Button getButton() {
return new BlueButton();
}
}
绿色护眼款皮肤工厂:
/**
* 绿色皮肤抽象工厂:担当具体工厂角色,实现生成一族产品的方法
*/
public class GreenFactory implements SkinFactory {
public Window getWindow() {
return new GreenWindow();
}
public Button getButton() {
return new GreenButton();
}
}
5.客户端使用:
/**
* 客户端通过选择不同的皮肤,显示不同的皮肤样式
*/
public class Main {
public static void main(String[] args) {
// 模拟选择一款皮肤
System.out.println("请输入皮肤颜色:\nblue:蓝色经典款\tgreen:绿色护眼款");
Scanner sc = new Scanner(System.in);
String type = sc.nextLine();
SkinFactory skinFactory;
// 选择生成一族产品的工厂
if ("blue".equals(type)) {
skinFactory = new BlueFactory();
} else if ("green".equals(type)) {
skinFactory = new GreenFactory();
} else {
throw new RuntimeException("选择的皮肤不存在");
}
Window window = skinFactory.getWindow();
Button button = skinFactory.getButton();
window.show();
button.click();
}
}
通过上面的改造,最大的改变就是:
- 首先创建了一个可以生成多种产品的抽象工厂类。
- 然后分别创建了生成蓝色经典款皮肤和绿色护眼款皮肤的具体工厂,在工厂类里面分别实现了生成多种产品的具体方法。
这样做的好处,就是我们减少了类的创建。同时我们的界面还有其他的组件,比如说图标组件。我们只用分别创建图标抽象类及其具体子类,然后再在抽象工厂中定义抽象方法,最后修改实现类就能使得图标也能随皮肤的选择二变化了。
模式应用
我们在开发中,可以说大部分的数据都会持久化下来保存到数据库中,常见的数据库有:MySQL、Oracle、SQL server等等。而在我们 Java 中要保存数据,首先就要获取数据库连接,下面演示连接 MySQL 数据,获取连接的代码:
1.首先是 pom 依赖:
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.2</version>
</dependency>
</dependencies>
2.创建一个获取数据库连接的工具类:
/**
* 获取数据连接的工具类
*/
public class DbUtils {
// 数据库驱动全类名
private static final String driverClass = "com.mysql.jdbc.Driver";
// 数据库地址
private static final String url = "jdbc:mysql://localhost:3306/test";
// 用户名
private static final String user = "user";
// 密码
private static final String password = "password";
private static Connection connection;
static {
try {
// 加载驱动
Class.forName(driverClass);
// 获取连接
connection = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取数据库连接方法
* @return 数据库连接
*/
public static Connection getConnection() {
return connection;
}
}
3.客户端获取连接:
/**
* 客户端通过 DbUtils 工具类获取链接
*/
public class Main {
public static void main(String[] args) throws SQLException {
// 获取数据库连接
Connection connection = DbUtils.getConnection();
// 创建 Statement 语句对象
Statement statement = connection.createStatement();
// 执行 sql 语句,得到结果集
ResultSet rs = statement.executeQuery("select name,age from student");
while (rs.next()) {
System.out.println(rs.getString("name"));
System.out.println(rs.getInt("age"));
}
}
}
这里使用了最原始连接 MySQL 数据库连接的方法,其它连接 Oracle、SQL server 等其它数据库也是类似的。只要引入相应的数据库驱动,同时修改上面的数据库驱动类、连接地址、用户名和密码就可以获取相应的数据库连接了。
可以看到上面只需要几个步骤,我们就可以实现连接到数据库。这源于 JDK 设计了一套与数据库交互的高度抽象的方式,就是我们常说的 JDBC 。其在内部定义了Connection
、Statement
和ResultSet
对象,这些对象中定义了包括获取连接,执行 sql 语句,获取结果集等等的操作,然后由各个数据库厂商实现各自的具体操作,使得我们只用针对三个对象编程就可以了,大大减轻了我们的开发量。下面是关于MySQL和Oracle实现 JDBC 后类与类之间的 UML 类图:
通过上面的图示,我们可以分析出Connection
和Statement
等对象间的关系与我们上面介绍的抽象工厂模式的关系非常相似。
- 我们可以将上图中
Statement
、PreparedStatement
以及CallableStatement
三个接口看成是三种抽象的产品,同时这三个产品就可以看作抽象工厂模式中的产品族。 - 在 MySQL 中的实现是
Statement
、PreparedStatement
以及CallableStatement
三个实现类。 - 在 Oracle 中的实现是
OracleStatement
、OraclePreparedStatement
以及OracleCallableStatement
三个实现类。 - 接口与实现类之间就构成了抽象工厂模式中所讲的产品等级结构概念。
然后我们看下面Connection
中的部分源码:
package java.sql;
import java.util.Properties;
import java.util.concurrent.Executor;
public interface Connection extends Wrapper, AutoCloseable {
// 返回普通的 sql 执行器
Statement createStatement() throws SQLException;
// 返回具有参数化预编译功能的 sql 执行器
PreparedStatement prepareStatement(String sql)
throws SQLException;
// 返回可以执行存储过程的 sql 执行器
CallableStatement prepareCall(String sql) throws SQLException;
// 省略其他代码
}
Connection
对象我们就可以看作是一个抽象的工厂角色,它的作用就是返回三种不同的 sql 执行器对象,这样我们就可以有不同的方式来执行 sql 语句。同时在 MySQL 和 Oracle 中则分别实现了如何获取执行器对象的方法,他们分别是上面途中的Connection
和OracleConnection
实现类,这两个实现类我们则可以看成是分别实现了一族产品的具体工厂类。
总结
1.主要优点:
- 抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
- 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象。
- 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”。
2.主要缺点:
- 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。
3.使用场景:
- 一个系统不应当依赖于产品类实例如何被创建、组合和表达的细节,这对于所有类型的工厂模式都是很重要的,用户无须关心对象的创建过程,将对象的创建和使用解耦。
- 系统中有多于一个的产品族,而每次只使用其中某一产品族。可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族。
- 属于同一个产品族的产品将在一起使用,这一约束必须在系统的设计中体现出来。同一个产品族中的产品可以是没有任何关系的对象,但是它们都具有一些共同的约束,如同一操作系统下的按钮和文本框,按钮与文本框之间没有直接关系,但它们都是属于某一操作系统的,此时具有一个共同的约束条件:操作系统的类型。
- 产品等级结构稳定,设计完成之后,不会向系统中增加新的产品等级结构或者删除已有的产品等级结构。
参考资料
- 大话设计模式
- 设计模式Java版本-刘伟
- 抽象工厂模式及在源码中的实践
本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/abstract-factory
转载请说明出处,本篇博客地址:https://blog.csdn.net/Phoegel/article/details/109298013