设计模式: 工厂模式
设计模式: 工厂模式
工厂是每一个开发人员应该知道的关键创造模式之一。它们是许多高级模式的主要组成部分。很长一段时间,我对不同类型的工厂模式感到困扰。此外,在同一篇文章中很难找到关于这些类型的信息。本文介绍了四种工厂模式:
工厂方法模式
抽象工厂模式
静态工厂方法
简易工厂
“四人帮”在《设计模式: 可复用面向对象软件的基础》一书中对工厂方法模式进行了描述。当我第一次读到这个模式时,我用静态模式误解了它,这是由 Java api 的主要设计师之一 Joshua Bloch 在他的书“ Effective Java”中描述的。这个简单的工厂(有时称为工厂)是非正式的,但是在网上出现了很多次。最后一种是抽象的工厂模式,在《四人帮》一书中也有描述,它是工厂方法模式的更广泛概念。
在这篇文章中,我将解释工厂有什么用,然后我将用著名的 Java 框架或 Java API 的示例介绍每种类型。我将使用 Java 代码来实现工厂,但是如果您不了解 Java,您仍然可以理解这个想法。此外,我将使用 UML 正式描述模式。
反面教材
尽管本文是关于工厂模式的,但仅仅为了使用模式而使用模式比从来不使用模式更糟糕。这种行为是一种反模式。实际上,大多数模式使代码更难理解。大多数时候,我不用工厂。例如:
不会改变很多的小项目
涉及多个开发人员使用相同代码的大中型项目,我发现它们很有用
我认为工厂模式是在它们的优势[^将在下一部分中看到它们]以及代码的可读性和理解性之间的权衡。
工厂的主要目标是实现对象,但是为什么不直接用构造函数调用创建对象呢?
对于简单的用例,不需要使用工厂。
在这段代码中,SimpleClass 是一个非常简单的类,具有状态、没有依赖性、没有多态性、没有业务逻辑。您可以使用工厂来创建这个对象,但是它将使代码量翻倍。因此,这会使代码更难理解。如果你能避免使用工厂来做这件事,你最终会得到一个更简单的代码!
但是,在编写需要许多开发人员和许多代码更改的大型应用程序时,您经常会遇到更复杂的情况。对于这些复杂的情况,工厂的优势取代
了它们的劣势。
对工厂的需求
既然我已经警告过你工厂的用途,让我们来看看为什么它们如此强大,因此在大多数项目中得到了应用。
静态工厂
企业级应用程序的一个常见用例是限制一个类的数量。如何设法只拥有一个(或 2 个或 10 个)类实例,因为它消耗了诸如套接字、数据库连接或文件系统描述符等资源?
>
使用构造函数的方法,不同的函数(来自不同的类)很难知道一个类的实例是否已经存在。而且,即使有一个实例,一个函数怎么能得到这个实例呢?你可以通过使用每个函数都会检查的共享变量来做到这一点
它将链接所有的函数的行为,因为它们使用和修改相同的共享变量,所以需要在同一个类中实现。
代码的多个部分将具有相同的逻辑来检查类是否已经被即时化,这将导致代码重复(非常糟糕!)
使用静态工厂方法,你可以很容易做到这一点:
在这段代码中,我们使用的工厂将 Singleton 类的实例数限制为一个。通过限制对象的数量,我们创建了一个实例池,这个实例池基于一个工厂。
注意: 我们可以修改一个实例的创建方式,而不是限制数量,例如使用一个原型模式,而不是每次都从头创建一个新对象。
松散耦合
工厂的另一个优点是松耦合。
假设您编写了一个计算东西并需要写日志的程序。由于这是一个大项目,所以在编写业务类时,您的同事之后会对将日志写入文件系统(FileSystemLogger 类)的类进行编码。在没有工厂的情况下,在使用 FileSystemLogger 之前,你需要在一个构造函数中注明 FileSystemLogger:
但是,如果突然发生变化,您现在需要使用实现的 DatabaseLogger 在数据库中写入日志,那会发生什么呢?如果没有工厂,则必须使用 FileSystemLogger 类修改所有函数。因为这个记录器无处不在,你需要修改数百个函数/类,而使用工厂,你只需修改工厂就可以轻松地从一个实现切换到另一个实现:
如果查看这段代码,您可以轻松地将日志记录器实现从 SystemLogger 更改为 DatabaseLogger。您只需要修改 createLogger ()函数(这是一个工厂)。对于客户机(业务)代码来说,这种更改是不可见的,因为客户机代码使用了 Logger 的接口,而记录器实现的选择是由工厂决定的。通过这样做,您将在日志记录器的实现和使用日志记录器的代码部分之间创建松散耦合。
封装
有时,使用工厂可以提高代码的可读性,并通过封装降低其复杂性。
假设您需要使用一个比较两辆汽车特性的类 CarComparator。这个类需要一个 DatabaseConnection 来获取数百万辆汽车的特性,并需要一个 FileSystemConnection 来获取参数化比较算法的配置文件(例如: 增加的燃料消耗量大于最大速度)。如果没有工厂,你可以编写这样的代码:
这段代码可以正常工作,但是您可以看到,为了使用比较方法,您需要稍加留意
数据库连接
一个文件系统连接
然后是 CarComparator
如果您需要在多个函数中使用比较,那么您必须复制您的代码,这意味着如果 CarComparator 的结构发生变化,您必须修改所有重复的部分。使用 factory 可以对代码进行因数分解,并隐藏 CarComparator 类构造的复杂性。
如果你比较这两个代码,你可以看到使用一个工厂:
减少代码行数
避免代码重复
组织代码: 使用工厂构建一个 CarComparator,只是使用它的公共方法。
最后一点很重要 (事实上,它们都很重要 ) ,因为这是关于关注点分离的。业务类不应该知道如何构建它需要使用的复杂对象, 业务类只需要关注业务。此外,它还增加了同一个项目的开发人员之间的分工:
一个在 CarComparator 和它的创建方式上工作
其他人则处理使用 CarComparator 的业务对象
消除歧义
假设您有一个具有多个构造函数(具有非常不同的行为)的类。如何确保不会错误地使用错误的构造函数?让我们来看看下面的代码:
虽然第一个和第二个构造函数的参数数量不一样,但是你可能很快就无法选择正确的参数,特别是在你忙碌了一天之后,使用你最喜欢的 IDE 的自动补全方法。更难看出构造函数 1 和构造函数 3 之间的区别。这个示例看起来像一个假的,但是我在项目的遗留代码中看到了它(真实故事!).问题是,如何使用相同类型的参数实现不同的构造函数(同时避免使用类似于构造函数 1 和 3 的肮脏方式) ?
下面是一个使用工厂的解决方案:
在这个例子中,使用工厂添加了关于创建内容的描述,使用工厂方法名称: 您可以从笛卡尔坐标系或极坐标系创建一个复数。在这两种情况下,你确切地知道创造是关于什么的。
工厂模式
现在我们看到了工厂的利弊,让我们关注不同类型的工厂模式。
我将介绍从最简单到最抽象的每一个工厂。如果你想使用工厂,记住越简单越好。
静态工厂方法
注意: 如果您阅读了本文并且对 Java 了解不多,那么静态方法就是类方法。
>
Joshua Bloch 在《Effective Java 》一书中描述了静态工厂方法:
>
一个类可以提供一个公共静态工厂方法,这是一个简单的静态方法,它返回一个类的实例。
>
换句话说,类可以提供返回实例的静态方法,而不是使用构造函数来创建实例。如果此类具有子类型,则静态工厂方法可以返回该类或其子类型的类型。虽然我讨厌 UML,但是我在文章的开头说过,我将使用 UML 给出一个正式的描述。下面就是:
在这个关系图中,ObjectWithStaticFactory 类有一个静态工厂方法(称为 getObject ())。这个方法可以识别任何类型的 ObjectWithStaticFactory,这意味着类型 ObjectWithStaticFactory 或类型 SubType1 或类型 SubType2。当然,这个类可以有其他方法、属性和静态工厂方法。
让我们来看看这段代码:
这段代码展示了创建 MyClass 实例的两种方法:
MyClass 中的静态工厂方法 getInstance ()
MyClass 的构造函数
但是这个概念可以更深入。如果一个具有静态工厂方法的类可以实时存储另一个类,那该怎么办?约书亚 · 布洛赫描述了这种可能性:
接口不能有静态方法,因此根据约定,名为 Type 的接口的静态工厂方法放在名为 Types 的不可实例化类
在这种情况下,factory 方法 getObject 位于抽象类名 Types 中。Factory 方法可以创建类 Type 的实例或类 Type 的任何子类型(图中的 SubType1 或 SubType2)。getObject ()方法可以有参数,以便为给定的参数返回 SubType1,否则返回 SubType2。
>
让我们回到 Java,假设我们有两个类: Ferrari 和 Mustang,它们实现了一个接口 Car。静态工厂方法可以放在一个名为“ CarFactory”的抽象类中(遵循 Joshua Boch 的约定,该类的名称应该是“ Cars”,但我不喜欢它) :
与其他工厂模式相比,这种模式的强大之处在于您不需要
实例化工厂以便使用它(几分钟后您就会明白我的意思)
工厂实现一个接口。
它易于使用,但仅适用于提供类方法 ( 即 static java 关键字修饰)。
注意:关于工厂,网络上的许多帖子都是错误的 ,例如stackoverflow上的[该帖子](https://stackoverflow.com/questions/1673841/examples-of-gof-design-patterns-in-javas-core-libraries)被提高了 1.5k 次。给定的工厂方法模式示例的问题在于它们是静态工厂方法。如果我引用 约书亚·布洛赫(Joshua Bloch)的话:
静态工厂方法不同于设计模式中的工厂方法模式。本文中描述的静态工厂方法在设计模式中没有直接等价物
如果您查看 stackoverflow 文章,那么只有最后一个示例URLStreamHandlerFactory
是 GoF 的工厂方法模式(我们将在几分钟后看到这个模式)
真实的例子
下面是 Java 框架和 Java API 中静态工厂方法的一些示例。在 Java API 中找到示例非常容易,因为 Joshua Bloch 是许多 Java API 的主要架构师。
日志框架(Logging frameworks)
Java 日志框架 slf4j、 logback 和 log4j 使用一个抽象类 LoggerFactory
。如果开发人员想写日志,他需要从 LoggerFactory
的静态方法 getLogger ()
中获得 Logger 的实例。getLogger ()
返回的 Logger 实现将取决于 getLogger ()
的实现以及 getLogger ()
使用的开发人员所写的配置文件。
注意: 无论使用 slf4j、 log4j 还是 slf4j,工厂类的名称及其静态工厂方法都不完全相同。
Java String class
Java 中的 String 类表示一个字符串。有时,您需要从布尔值或整数获取字符串。但是 String 不提供像 String (Integer i)或 String (Boolean b)这样的构造函数。相反,它提供了多个静态工厂方法 String.valueOf (...)
简易工厂
这种模式不是“真实的”,但是我在网上看到过很多次。它没有一个正式的描述,但这里是我的: 一个简单的工厂(或工厂)是一个工具
他们的工作是创建/实时化对象
既不是工厂方法模式
我们将在后面看到这个模式
也不是一个抽象的工厂模式
我们将在后面看到这个模式
您可以看到它具有静态工厂模式的一般化,但是这一次可以暂存(或不暂存)工厂,因为“工厂方法”不是一个类方法(但是它可以)。对于 Java 开发人员来说,使用非静态形式的简单工厂是很少见的。因此,这种模式在大多数情况下等价于静态模式。下面是非静态形式的 UML:
在这种情况下,Factory 方法 getObject ()位于名为 Factory 的类中。Factory 方法不是 class 方法,因此在使用它之前需要先了解 Factory。工厂方法可以创建类 Type 或其任何子类型的实例。
下面是来自静态工厂方法的前一个示例,但是这一次我在使用它之前先对工厂进行了检查
如你所见,这次我需要留意工厂以便使用它。我没有在 Java 中找到真正的例子,因为使用静态工厂方法比使用简单的工厂更好。不过,如果您的工厂方法需要一些实例才能工作,那么您可以在其非静态形式中使用此模式。例如,如果您需要一个数据库连接,那么您可以首先实现您的工厂(这将实现数据库连接) ,然后使用需要这个连接的工厂方法。就个人而言,在这种情况下,我仍然会使用带有惰性初始模式的静态工厂(以及一个数据库连接池)。
告诉我您是否知道一个 Java 框架,它以非静态的形式使用简单的工厂。
工厂方法模式
工厂方法模式是一个更抽象的工厂。以下是“四人帮”给出的定义:
定义用于创建对象的接口,但让子类决定实例化哪个类。工厂方法允许类将实例化推迟到子类
下面是工厂方法模式的简化 UML 图:
这个图看起来像简单的工厂图以非静态形式
。唯一的也是最大的
不同之处在于界面工厂:
工厂代表“用于创建对象的接口”。它描述了一种工厂方法:getObjects()
ConcreteFactory 代表“决定实例化哪个类的子类之一”。每个 ConcreteFactory 都有自己的工厂方法 getObjects ()的实现
在关系图中,getObjects ()必须返回一个 Type (或其子类型)。这意味着一个 contrete factory 可以返回 Subtype1,而另一个可以返回 SubType2。
为什么使用工厂方法模式而不是简单的工厂?
仅当您的代码需要多个工厂实现时。这将迫使每个工厂实现具有相同的逻辑,以便使用一个实现的开发人员可以轻松地切换到另一个实现,而不必担心如何使用它(因为他只需调用具有相同签名的工厂方法)。
>
由于这是抽象的,让我们回到汽车的例子。这不是一个很好的例子,但是我使用它是为了让你看到简单工厂的不同之处(我们将看到真实的示例以了解此模式的强大功能) :
如果将此代码与简单工厂进行比较,这次我添加了一个接口(CarFactory)。真正的工厂(ConcreteCarFactory)实现了这个接口。
>
正如我说的那样,这不是一个很好的示例,因为在此示例中,您不应该使用工厂方法模式,因为 *只有一个具体的工厂。* 这将是唯一的,如果我有多个实现像有用*SportCarFactory,VintageCarFactory,LuxeCarFactory,CheapCarFactory* ...。在这种情况下,由于工厂方法始终是 getCar(),因此开发人员可以轻松地从一种实现切换到另一种实现。
真实的例子
在 Java 中,一个常见的例子是集合 API 中的 Iterable()函数。每个集合都实现了接口 Iterable < e > 。这个接口描述了一个函数迭代器() ,它返回一个迭代器 < e > 。数组列表 < e > 是一个集合。因此,它实现了接口 Iterable < e > 和它的工厂方法 Iterator () ,后者返回 Iterator < e > 的子类
下面是 ArrayList 的一个标准用法
我展示了一个 ArrayList,但是我可以展示 HashSet,LinkedList 或 HashMap,因为它们都是集合 API 的一部分。这种模式的优势在于您不需要知道您使用的是哪种类型的集合,每个集合都将通过工厂方法 iterator()提供一个 Iterator。
另一个很好的例子是 Java 8 CollectionAPI 中的stream()
方法。
SPring
Spring 框架基于工厂方法模式。ApplicationContext 实现 BeanFactory 接口。此接口描述返回 Object 的函数 Object getBean (param)。这个例子很有趣,因为 java 中的每个 Class 都是从 Object 派生的。因此,这个工厂可以返回任何类的实例取决于参数
。
The abstract factory 抽象工厂
四人帮这样描述这家工厂:提供一个接口,用于创建相关或依赖对象的系列,而不指定它们的具体类
如果你不理解这个句子,不要担心,这是正常的。它不是无缘无故被称为抽象工厂!
如果它可以帮助您,我将抽象工厂模式看作是工厂方法模式的泛化,这次工厂接口有多个相关的工厂方法。当我说相关的时候,我的意思是概念上的联系,这样它们就形成了一个工厂方法的“家庭”。让我们看看 UML 图,看看它与工厂方法模式的区别:
Factory 是定义多个工厂方法的接口,在我们的示例中为:getObject1()和 getObject2()。每个方法都会创建一个类型(或其子类型)。
ConcreteFactory 实现 Factory 接口,因此具有自己的 getObject1()和 getObject2()实现。现在假设有两个具体工厂:一个可以返回
SubType1.1
和SubType2.1
的实例,另一个可以返回SubType1.2
和SubType2.2
的实例。
由于这是非常抽象的,让我们回到 CarFactory 示例。
使用工厂方法模式,工厂接口只有一种方法getCar()
。抽象工厂可以是具有 3 种工厂方法的接口:getEngine()
,getBody()
和getWheel()
。您可能有多个具体工厂:
SportCarFactory 可以返回 PowerfulEngine,RaceCarBody 和 RaceCarWheel 的实例
CheapCarFactory 可以返回 WeakEngine,HorribleBody 和 RottenWheel 的实例
如果要制造跑车,则需要实例化 SportCarFactory 然后使用它。而且,如果您想制造一辆廉价汽车,则需要实例化 CheapCarFactory 然后使用它。
这个抽象工厂的 3 种工厂方法是相关的。它们都属于汽车生产概念。
当然,工厂方法可以具有参数,以便它们返回不同的类型。例如,SportCarFactory 的 getEngine(String model)工厂可以根据参数返回 Ferrari458Engine 或 FerrariF50Engine 或 Ferrari450Engine 或···。
下面是 Java 中的示例只有 SportCarFactory 和2个 factory 方法
。
这个工厂不容易,你什么时候用呢?
决不!!!!哼,很难回答。我将这种工厂模式视为组织代码的一种方式。如果您在代码中得到许多工厂方法模式,并且看到了它们之间的共同主题,则可以使用抽象工厂来收集该组。我不赞成“让我们使用抽象工厂,因为将来我们可能需要一个抽象工厂”,因为这种模式非常抽象。我更喜欢构建简单的东西,并在需要后对其进行重构。
但是,常见的用例是需要创建外观和感觉不同的用户界面。这个例子被“四人帮”用来呈现这种模式。该 UI 需要一些产品,例如窗口,滚动条,按钮…您可以为每种外观创建带有具体工厂的 Factory。当然,此示例是在 Internet 时代之前编写的,现在您甚至可以为桌面应用程序使用 CSS(或某些脚本语言)来拥有一个组件并修改其外观。这意味着大多数情况下,静态工厂方法就足够了。
但是,如果您仍然想使用此模式,则可以使用 GoFGang of Four的简称 -> 设计模式:可复用面向对象软件的基础 的作者
中的一些用例:
真实的例子
大多数 DAO(数据访问对象)框架使用抽象工厂来指定具体工厂应该执行的基本操作。尽管工厂方法的名称取决于框架,但通常不适合:
createObject(...)或 persistObject(...)
updateObject(...)或 saveObject(...)
deleteObject(...)或 removeObject(...)
readObject(…)或 findObject(…)
对于您处理的每种类型的对象,您都需要一个具体的工厂。例如,如果您需要使用数据库来管理人员,房屋和合同。我将有一个 PersonFactory,一个 HouseFactory 和一个 ContractFactory。
Spring 的CrudRepository是抽象工厂的一个很好的例子。
如果您需要 Java 代码,则可以查找 JPA,Hibernate 或 SpringData 教程。
结论
我希望您现在对不同类型的工厂模式以及何时使用它们有所了解。尽管我在本文中多次说过,但请记住,大多数情况下,工厂会使代码更复杂/更抽象。即使您了解工厂(如果您不知道,请再次阅读本文!),您的同事又如何呢?但是,在中型/大型应用程序上工作时,值得使用工厂。
我在工厂苦苦挣扎了很长时间,以了解是哪一个,像这样的一篇文章对我有帮助。我希望本文是可以理解的,并且我没有写太多错误。随时告诉我您阅读的内容是否困扰您,以便我改善本文。
版权声明: 本文为 InfoQ 作者【爱笑的小雨】的原创文章。
原文链接:【http://xie.infoq.cn/article/a0d24b3c70b3f025ce2fca5f7】。文章转载请联系作者。
评论