工厂模式。何时使用工厂方法?

336

在对象内部使用工厂方法而不是使用工厂类是什么时候一个好主意?


3
为什么我应该使用工厂类而不是直接创建对象? - tkit
有关不同类型工厂的概述,请参见:使用工厂设计模式如何防止类预测其必须创建的对象类? - jaco0646
有关何时特别使用工厂方法,请参见:Factory Method模式适用性 - jaco0646
18个回答

444

我喜欢将设计模式看作是我的类“人员”,而这些模式则是人们相互交流的方式。

因此,对我来说,工厂模式就像一个招聘机构。你有一个需要不同数量工人的人。这个人可能知道他们需要雇佣的人的一些信息,但也只是如此。

所以,当他们需要新员工时,他们会给招聘机构打电话告诉他们需要什么。现在,实际上要“雇用”某人,你需要了解很多东西——福利、资格验证等等。但是招聘者不需要知道这些——招聘机构会处理所有事情。

同样地,使用工厂模式允许消费者创建新对象,而无需知道它们的创建细节或依赖关系——他们只需要提供他们真正想要的信息即可。

public interface IThingFactory
{
    Thing GetThing(string theString);
}

public class ThingFactory : IThingFactory
{
    public Thing GetThing(string theString)
    {
        return new Thing(theString, firstDependency, secondDependency);
    }
}

现在,ThingFactory的使用者可以获取一个Thing,而无需了解Thing的依赖项,除了来自使用者的字符串数据之外。


22
GetThing()的具体实现从哪里获取firstDependency和secondDependency的值? - Mikeyg36
112
有人能告诉我这如何回答问题的原始帖子?这仅仅是描述了“工厂模式”的定义,然后添加了一个“工厂方法”的示例,而这只是三种“工厂模式”中的一种。换句话说,我没有看到任何比较。 - Forethinker
4
抱歉,但这个解释很差,因为构造函数也可以以隐藏依赖项的方式编写。你想要区分依赖项创建和依赖项管理信息的关键背景信息缺失了。此外,问题是关于同一类内部的,答案与此无关。 Translated: 抱歉,但是这个解释很差,因为构造函数也可以被写成隐藏依赖关系的形式。您想要将依赖项创建的信息与依赖项管理信息区分开来的关键背景信息已经丢失。此外,问题是关于在同一个类中的,答案与此毫不相关。 - Christian Hujer
12
OP问了“什么时候”,Kyoryu却回答了“如何”。虽然回答的方式值得称赞,但在这个问题的背景下,它只是噪音。 - 8bitjunkie
5
当我写下这句话时,我误解了问题。这并没有回答问题,但它确实描述了为什么人们通常会使用工厂。我有删除它的冲动,但我想我会把它留下来,因为人们似乎认为它有价值。 - kyoryu
显示剩余6条评论

123

当构造函数表达力不够时,应将工厂方法视为构造函数的替代品。

class Foo{
  public Foo(bool withBar);
}

不如表达力强:

class Foo{
  public static Foo withBar();
  public static Foo withoutBar();
}

当您需要一个复杂的对象构建过程时,工厂类是非常有用的;当构建过程需要某个实际类不需要的依赖项时;当您需要构建不同的对象时等。


4
这里的工厂类在哪里? - Koray Tugay
26
没有工厂类,只有工厂方法。这个问题是关于何时使用工厂方法而不是工厂类的。但是,工厂方法更像是构造函数的替代品,而不是工厂类的替代品。(我不知道为什么最高评分的答案只谈论了工厂类)。 - Rasmus Faber
8
请注意,静态工厂方法与四人帮(Gang of Four)中的工厂方法设计模式完全不同。 - jaco0646

94

个人认为使用单独的工厂类有意义的情况是,当你创建的最终对象依赖于其他多个对象。例如在PHP中:假设你有一个House对象,它又有一个Kitchen和一个LivingRoom对象,而LivingRoom对象里面还有一个TV对象。

实现这个的最简单方法是让每个对象在它们的构造方法中创建它们的子对象,但如果属性比较嵌套,当你的House创建失败时,你可能会花费一些时间来确定具体是哪里出了问题。

另一种选择是采用以下方法(如果您喜欢花哨的术语,可以称之为依赖注入):

$TVObj = new TV($param1, $param2, $param3);
$LivingroomObj = new LivingRoom($TVObj, $param1, $param2);
$KitchenroomObj = new Kitchen($param1, $param2);
$HouseObj = new House($LivingroomObj, $KitchenroomObj);

如果创建 House 的过程失败,那么只有一个地方需要查看,但是每次想要新建一个 House 时都必须使用这个代码块,这显然很不方便。于是引入了工厂模式:

class HouseFactory {
    public function create() {
        $TVObj = new TV($param1, $param2, $param3);
        $LivingroomObj = new LivingRoom($TVObj, $param1, $param2);
        $KitchenroomObj = new Kitchen($param1, $param2);
        $HouseObj = new House($LivingroomObj, $KitchenroomObj);

        return $HouseObj;
    }
}

$houseFactory = new HouseFactory();
$HouseObj = $houseFactory->create();

感谢这里的工厂,创建一个House的过程被抽象化了(这意味着当您想要创建一个House时,您不需要创建和设置每个单独的依赖项),同时也是集中化的,这使得维护更容易。使用单独的工厂有其他好处(例如可测试性),但我发现这个特定的用例最能说明工厂类的有用之处。


2
但是,有人该如何对此进行单元测试呢?我认为在类中使用“new”关键字被认为是不良实践,因为它无法进行单元测试。或者工厂是这个规则的一个例外吗? - AgmLauncher
2
@AgmLauncher 当我开始进行单元测试时,我也有同样的问题,请查看:https://dev59.com/e2kw5IYBdhLWcg3wHW3n - Mahn
2
不太明白。创建不同对象的参数是如何传递给“HouseFactory”类的? - atiyar
2
@Mahn,最终你不会有很多参数吗? - Pacerier
1
@Pacerier 这是你需要根据自己的需求来决定如何建模,但你不必总是将每个参数都传递给 create 方法。例如,如果你的 House 总是拥有相同类型的 LivingRoom,那么在工厂类中将其参数硬编码可能比作为参数传递更有意义。或者,如果你有几种不同类型的 LivingRoom 并且在内部使用 switch 语句,则可以为 HouseFactory::create 方法提供一个 type 参数,并为每种类型硬编码参数。 - Mahn
显示剩余4条评论

23

当以下情况发生时,在对象内部使用工厂方法是一个好主意:

  1. 对象的类不知道需要创建哪些具体的子类
  2. 对象的类被设计成由子类来指定它所创建的对象
  3. 对象的类将其职责委托给辅助子类,并不知道哪个具体的类将接管这些职责

当以下情况发生时,使用抽象工厂类是一个好主意:

  1. 您的对象不应依赖于其内部对象如何被创建和设计
  2. 一组相关联的对象应该一起使用,并且您需要满足这个限制
  3. 对象应该由几个可能的相关对象家族之一配置,这些家族将成为父对象的一部分
  4. 需要共享子对象,只显示接口而不是实现

19

明确区分使用工厂或工厂方法的概念非常重要,它们都旨在解决互不相同的对象创建问题。

让我们具体说明“工厂方法”:

首先,当您正在开发库或API时,这些库或API将被用于进一步的应用程序开发,那么工厂方法是用于创建模式的最佳选择之一。原因是:我们知道何时创建所需功能的对象,但对象类型仍未确定或将通过传递的动态参数确定

现在问题是,使用工厂模式可以实现类似的效果,但如果将工厂模式用于上述突出问题,则会引入一个巨大的缺点,即创建不同对象(子类对象)的逻辑将特定于某些业务条件。因此,将来如果需要为其他平台扩展库的功能(更技术上地说,需要添加基本接口或抽象类的更多子类,以便工厂根据某些动态参数返回这些对象),那么每次都需要更改(扩展)工厂类的逻辑,这将是一项昂贵的操作,并且从设计角度来看也不好。

另一方面,如果使用“工厂方法”模式执行相同的操作,那么您只需要创建附加功能(子类)并通过注入动态地将其注册,而无需更改基本代码。

interface Deliverable 
{
    /*********/
}

abstract class DefaultProducer 
{

    public void taskToBeDone() 
    {   
        Deliverable deliverable = factoryMethodPattern();
    }
    protected abstract Deliverable factoryMethodPattern();
}

class SpecificDeliverable implements Deliverable 
{
 /***SPECIFIC TASK CAN BE WRITTEN HERE***/
}

class SpecificProducer extends DefaultProducer 
{
    protected Deliverable factoryMethodPattern() 
    {
        return new SpecificDeliverable();
    }
}

public class MasterApplicationProgram 
{
    public static void main(String arg[]) 
    {
        DefaultProducer defaultProducer = new SpecificProducer();
        defaultProducer.taskToBeDone();
    }
}

19

当你需要相同参数类型但有不同行为的多个“构造函数”时,它们也非常有用。


17
问题陈述:使用工厂方法创建游戏工厂,定义游戏接口。

代码片段:

import java.util.HashMap;


/* Product interface as per UML diagram */
interface Game{
    /* createGame is a complex method, which executes a sequence of game steps */
    public void createGame();
}

/* ConcreteProduct implementation as per UML diagram */
class Chess implements Game{
    public Chess(){
        
    }
    public void createGame(){
        System.out.println("---------------------------------------");
        System.out.println("Create Chess game");
        System.out.println("Opponents:2");
        System.out.println("Define 64 blocks");
        System.out.println("Place 16 pieces for White opponent");
        System.out.println("Place 16 pieces for Black opponent");
        System.out.println("Start Chess game");
        System.out.println("---------------------------------------");
    }
}
class Checkers implements Game{
    public Checkers(){
       
    }
    public void createGame(){
        System.out.println("---------------------------------------");
        System.out.println("Create Checkers game");
        System.out.println("Opponents:2 or 3 or 4 or 6");
        System.out.println("For each opponent, place 10 coins");
        System.out.println("Start Checkers game");
        System.out.println("---------------------------------------");
    }
}
class Ludo implements Game{
    public Ludo(){
        
    }
    public void createGame(){
        System.out.println("---------------------------------------");
        System.out.println("Create Ludo game");
        System.out.println("Opponents:2 or 3 or 4");
        System.out.println("For each opponent, place 4 coins");
        System.out.println("Create two dices with numbers from 1-6");
        System.out.println("Start Ludo game");
        System.out.println("---------------------------------------");
    }
}

/* Creator interface as per UML diagram */
interface IGameFactory {
    public Game getGame(String gameName);
}

/* ConcreteCreator implementation as per UML diagram */
class GameFactory implements IGameFactory {
        
     HashMap<String,Game> games = new HashMap<String,Game>();
    /*  
        Since Game Creation is complex process, we don't want to create game using new operator every time.
        Instead we create Game only once and store it in Factory. When client request a specific game, 
        Game object is returned from Factory instead of creating new Game on the fly, which is time consuming
    */
    
    public GameFactory(){
        
        games.put(Chess.class.getName(),new Chess());
        games.put(Checkers.class.getName(),new Checkers());
        games.put(Ludo.class.getName(),new Ludo());        
    }
    public Game getGame(String gameName){
        return games.get(gameName);
    }
}

public class NonStaticFactoryDemo{
    public static void main(String args[]){
        if ( args.length < 1){
            System.out.println("Usage: java FactoryDemo gameName");
            return;
        }
     
        GameFactory factory = new GameFactory();
        Game game = factory.getGame(args[0]);
        if ( game != null ){                    
            game.createGame();
            System.out.println("Game="+game.getClass().getName());
        }else{
            System.out.println(args[0]+  " Game does not exists in factory");
        }           
    }
}

输出:

java NonStaticFactoryDemo Chess
---------------------------------------
Create Chess game
Opponents:2
Define 64 blocks
Place 16 pieces for White opponent
Place 16 pieces for Black opponent
Start Chess game
---------------------------------------
Game=Chess

这个例子展示了通过实现FactoryMethod来实现Factory类。

  1. Game是所有类型游戏的接口。它定义了复杂的方法:createGame()

  2. Chess, Ludo, Checkers是不同类型的游戏变体,它们提供了createGame()的实现。

  3. public Game getGame(String gameName)IGameFactory类中的FactoryMethod

  4. GameFactory在构造函数中预先创建了不同类型的游戏。它实现了IGameFactory工厂方法。

  5. 游戏名称作为命令行参数传递给NotStaticFactoryDemo

  6. GameFactory中的getGame接受一个游戏名称并返回相应的Game对象。

定义来自Wikipedia/Gang of Four ( Design Patterns: Elements of Reusable Object-Oriented Software book)

工厂模式:创建对象而不向客户端公开实例化逻辑。

工厂方法定义一个用于创建对象的接口,但让子类决定实例化哪个类。工厂方法允许类推迟它所使用的实例化到子类中。(四人帮)

使用情况:

何时使用:客户端不知道在运行时需要创建哪些具体类。它只想得到一个能够完成工作的类。


感谢您的主题演讲部分,对我来说非常简洁。但是,“getArea()是Shape接口中的工厂方法”这一点难以想象,因为getArea方法从不实例化任何类,它只进行计算。请重新审视“为创建对象定义一个接口,但让子类决定实例化哪个类”的概念。谢谢。 - reco
getArea()根本不是工厂方法。 - Cranio
1
我有一个不同的观点-专家请验证并做笔记。 1.客户端(或调用者)需要感兴趣的对象...因此不应该调用*new GameFactory(),而是工厂类应该具有静态getInstance()方法。 2.如果是这样,那么games.put(Chess.class.getName(),new Chess())*将始终返回相同的棋子引用[如果实现为静态] - 如何以最有效的方式处理这种情况? - Arnab Dutta
我已经给出了非静态工厂的示例。如果您愿意,可以使用静态块和方法来实现它。关于您的查询:1.客户端将调用工厂以获取游戏。2.我只放置一次对象,所有的Get都将返回相同的实例-每个get都返回相同的引用,即对于每个get,都会返回Chess的相同引用。 - Ravindra babu

6

这实际上是个口味问题。工厂类可以根据需要抽象/接口化,而工厂方法更加轻量级(也更容易进行测试,因为它们没有定义类型,但需要一个众所周知的注册点,类似于服务定位器但用于定位工厂方法)。


4

当返回的对象类型具有私有构造函数,不同的工厂类在返回对象上设置不同的属性,或者特定的工厂类型与其返回的具体类型耦合时,工厂类非常有用。

WCF 使用 ServiceHostFactory 类在不同情况下检索 ServiceHost 对象。标准 ServiceHostFactory 由 IIS 用于检索 .svc 文件的 ServiceHost 实例,但是 WebScriptServiceHostFactory 用于向 JavaScript 客户端返回序列化的服务。ADO.NET 数据服务具有自己的特殊 DataServiceHostFactory,并且 ASP.NET 具有 ApplicationServicesHostFactory,因为其服务具有私有构造函数。

如果只有一个类正在使用工厂,则可以在该类内部使用工厂方法。


3
考虑这样一种情况,您需要设计一个订单和客户类。为了简便起见并满足初始需求,您不需要为订单类使用工厂,而是在应用程序中填充许多“new Order()”语句。事情进展顺利。
现在出现了一个新的要求,即不能在没有客户关联(新依赖项)的情况下实例化订单对象。现在您有以下考虑:
1- 您可以创建构造函数重载,但它只适用于新实现。(不可接受) 2- 您可以更改Order()签名并更改每个调用。(不是一个好的做法且非常痛苦)
相反,如果您已经为Order类创建了一个工厂,那么您只需要更改一行代码就可以轻松解决问题。我建议几乎每个聚合关联都使用工厂类。希望这能帮到您。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接