工厂方法模式 vs 组合

4

来自GoF的工厂方法模式章节:

定义一个创建对象的接口,但是让子类决定实例化哪个类。工厂方法允许类将实例化推迟到子类中。

这个想法是让子类决定实例化哪个类,而GoF的实现方式是在超类中提供一个抽象方法,子类需要重写该方法,从而提供自己的实例化逻辑。

我不明白的是,为什么要使用抽象方法来实现它,而不是使用一个就是工厂的类成员。我来解释一下。

这是GoF的方法:

public abstract class Creator {
    public void doStuff() {
        Product product = createProduct();
        /* etc. */
    }

    public abstract Product createProduct();
}

使用方法:

    MyCreator myCreator = new Creator() {
        @Override
        public Product createProduct() {
            return new MyProduct();
        }
    }

    myCreator.doStuff();

但是为什么要费心去做子类化等操作呢?我建议采用以下方法:

public class Creator {
    private Factory factory;

    /* constructor */
    public Creator(Factory factory) {
        this.factory = factory;
    }

    public void doStuff() {
        Product product = factory.createProduct();
        /* etc. */
    }
}

这是如何使用它的方法:

    MyFactory myFactory = new MyFactory();
    Creator creator = new Creator(myFactory);
    creator.doStuff();

那么GoF的方法有什么好处呢?为什么不将工厂组合到创建者类中,而是使用抽象方法来表达它呢?


这是因为您希望在抽象类中共享所有具体工厂的公共代码。现在您所做的是正确的,但是如何在工厂之间共享/继承公共代码呢?因此,在更灵活的情况下使用抽象类。 - Number945
4个回答

5
为什么不将工厂组合到创建者类中,而是使用抽象方法来表达它?
因为在这种情况下,客户端“知道”要实例化哪个工厂类,这违背了模式的整个目的。整个想法是客户端不知道构建哪个工厂,它只调用一个抽象类。想象一下,您向框架插入某些第三方工厂提供程序,并且不知道他们实例化了什么(也不关心),只要达到目的就可以。

那是错误的答案。客户端始终知道具体工厂,只是不知道正在创建的具体产品。 - Number945

4
工厂方法模式用于让子类控制它们生成的对象类型。在您的情况下,不是子类控制对象类型,而是类的用户。
下面的示例是从 工厂方法模式的维基百科条目复制的。我们有一个迷宫游戏,存在两个变体:普通迷宫游戏和具有适应规则的魔法迷宫游戏。普通迷宫游戏由普通房间组成,魔法迷宫游戏由魔法房间组成。
public class MazeGame {
    public MazeGame() {
        Room room1 = makeRoom();
        Room room2 = makeRoom();
        room1.connect(room2);
        this.addRoom(room1);
        this.addRoom(room2);
    }

    protected Room makeRoom() {
        return new OrdinaryRoom();
    }
}

public class MagicMazeGame extends MazeGame {
    @Override
    protected Room makeRoom() {
        return new MagicRoom();
    }
}

使用工厂方法模式,我们可以确保MagicMazeGame由MagicRooms组成,但仍然可以重用超类的部分。
在您的建议中,MazeGame类将被更改为:
public class MazeGame {
    public MazeGame(RoomFactory factory) {
        Room room1 = factory.makeRoom();
        Room room2 = factory.makeRoom();
        room1.connect(room2);
        this.addRoom(room1);
        this.addRoom(room2);
    }
}

现在可以这样做:

MazeGame game1 = new MazeGame(new MagicRoomFactory());
MazeGame game2 = new MagicMazeGame(new OrdinaryRoomFactory());

这是您希望避免的情况,因为这样就可能会创建错误类型的房间的MazeGames。


为什么我们需要防止这种情况发生?我的意思是代码的最后一部分。 - Tony Lin
@Tony Lin:因为这样迷宫游戏里的房间类型就会出错。(我编辑了答案以使这一点更加清晰明了。) - Hoopje

2

在这种情况下,使用实际的英语类型作为示例通常比使用更抽象的术语(如“产品”)更有用。

因此,让我们以宠物为例,并说每种宠物(子类)都有一个家(例如,狗有一个狗窝)。

对于您的示例,我们将有:

Public void doStuff() {
    Home home = factory.createHome();
}

但在这种情况下,谁来决定狗住狗窝,鸟住鸟笼的逻辑呢?这个逻辑必须放在工厂对象中,这又意味着宠物的继承层次结构必须在工厂逻辑中显式重复。这又意味着每次添加新类型的宠物时,您都必须记得将该类型添加到工厂中。

工厂方法模式意味着您需要像这样:

public class Pet {
    private String name;
    private Home home;
    public Pet(String name) {
        this.name = name;
        this.home = this.createHome();
    }
    public abstract Home createHome();

这样做能够让你在创建新种类的宠物时,可以指定该宠物类型的家庭,而无需更改任何其他类。在此过程中,你还可以知道每种宠物类型都有一个家。例如,你可以拥有以下类:
public class Dog extends Pet {
    public Home createHome() {
        return new Kennel();
    }
}

编辑以添加狗的示例


1
我认为对于这个问题的答案并不完全令人满意,因此我想提供我的看法...
引用@Hoopje的回答,我们可能会得到以下内容:
public class MazeGame {
    public MazeGame(RoomFactory factory) {
        Room room1 = factory.makeRoom();
        Room room2 = factory.makeRoom();
        room1.connect(room2);
        this.addRoom(room1);
        this.addRoom(room2);
    }
}

然后

MazeGame game1 = new MazeGame(new MagicRoomFactory());
MazeGame game2 = new MagicMazeGame(new OrdinaryRoomFactory());

正如上面所讨论的那样,这是毫无意义的。然而,我认为最后两行也没有意义,因为在OP的观点中,你根本不需要创建一个MagicMazeGame。相反,你只需要实例化一个MazeGame并传入new MagicRoomFactory()即可。
MazeGame magicOne = new MazeGame(new MagicRoomFactory());
MazeGame normalOne = new MazeGame(new OrdinaryRoomFactory());

然而,让我们想象一下我们的MazeGame有多个工厂。比如说除了需要一个房间工厂之外,还需要一个积分工厂和一个怪物工厂。在这种情况下,我们可能会把所有这些内容都强制放入MazeGame构造函数中,像这样:
public MazeGame(RoomFactory rmFactory, PointsFactory pointsFactory, MonsterFactory monstersFactory) 

这样做相当复杂,最好创建具体的子类来控制各自类型的游戏。对我来说,那个很长的构造函数看起来像是代码异味,但更有经验的人可能会补充一些。

此外,这意味着实例化游戏的任何对象都必须知道所有这些内部工作类,这破坏了封装性。总之,通过这种方式,我们给客户端过多的控制权,而客户端并不想了解所有这些实现细节。

这就是我看到的情况...


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