前言

在阅读本文章之前请先确保自己能熟练使用 PHP 的面向对象技术,当我们提到 构造函数抽象类接口多态等名词的时候希望你不要惊讶。

如果这些你感觉自己还不能清楚的理解,那么请先找一些介绍面向对象的教程和文章学习一下会更好。

何为创建型模型?

创建型模型抽象了实例化的过程。它们帮助一个系统独立于如何创建、组合和表示它的那些对象。

问题抛出

我们将使用《设计模式:可复用面向对象软件的基础》一书中的迷宫一例来作为我们的示例。相关实现代码全部由 C++ 换成 PHP

需求

我们需要为一个电脑游戏创建一个迷宫,我们将忽略迷宫游戏中的很多细节,例如一个用户还是多个用户,而是仅关注迷宫是如何创建的。

我们将迷宫定义为一系列房间,一个房间知道他的邻居,可能的邻居要么是另一个房间,要么是一堵墙,或者是到另一个房间的一扇门。

起步

DoorRoomWall 定义了我们所有例子中使用的构件,我们仅定义这些类中对创建一个迷宫起到重要作用的一些部分,其余一些很重要但与创建迷宫没有关系的的功能将被忽略。

Direction 类继承自 MyCLabs\Enum ,包含 NorthSouth East West 四个常量,用来代替 C++ 中的枚举。表示房间的四个方向。

MapSite 是所有迷宫组件的公共抽象类。为了简化,MapSite 仅定义了一个 enter 方法,他的含义决定与你在进入什么,房间、墙还是一道门。进入不同的构件得到的结果是不一样的,例如:

  1. 进入房间,你的位置会发生改变。
  2. 进入一扇门。
    • 如果门是开的,进入另一个房间。
    • 如果门是关着的,则碰壁。
  3. 进入墙壁,直接呵呵。

类关系图如下:

MapSite

每个类的初始代码

Direction

1
2
3
4
5
6
7
8
9
<?php

class Direction extends MyCLabs\Enum\Enum
{
    const North = 'north';
    const South = 'south';
    const East = 'east';
    const West = 'west';
}

MapSite

1
2
3
4
5
6
<?php

abstract class MapSite
{
    abstract public function enter();
}

Room

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?php

class Room extends MapSite
{
    /* @var array */
    private $sides = [
        Direction::North => null,
        Direction::South => null,
        Direction::East => null,
        Direction::West => null
    ];

    /* @var int */
    private $room_number;

    public function __construct(int $no)
    {
        $this->room_number = $no;
    }
    
    public function getNo()
    {
        return $this->no;
    }

    public function getSide($side)
    {
        return $this->sides[$side] ?? null;
    }
    
    public function setSide($side, MapSite $site): bool
    {
        if (isset($this->sides[$side])) {
            $this->sides[$side] = $site;
            return true;
        }
        
        return false;
    }

    public function enter()
    {
        //
    }
}

Wall

1
2
3
4
5
6
7
8
9
<?php

class Wall extends MapSite
{
    public function enter()
    {
        //
    }
}

Door

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php

class Door extends MapSite
{
    /* @var Room|null */
    private $room1 = null;
    
    /* @var Room|null */
    private $room2 = null;
    
    /* @var boolean */
    private $is_open = true;
    
    public function __construct(Room $room1, Room $room2, bool $open = true)
    {
        $this->room1 = $room1;
        $this->room2 = $room2;
        $this->is_open = $open;
    }

    public function otherSideFrom(Room $room): Room
    {
        if ($room == $this->room1) {
            return $this->room2;
        }
        
        return $this->room1;
    }

    public function enter()
    {
        //
    }
}

我们不仅仅要知道迷宫的各个部分,还要定义一个用来表示房间集合的 Maze 类。用 getRoomByNo 操作和给定的房间号,Maze 就可以找到一个特定的房间:

Maze

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

class Maze
{
    /* @var array */
    protected $rooms = [];
    
    public function addRoom(Room $room)
    {
        $this->rooms[$room->no] = $room;
    }
    
    public function getRoomByNo(int $no)
    {
        return $this->rooms[$no] ?? null;
    }
}

我们定义的最后一个类是 MazeGame,由他来创建迷宫。一个简单直接创建迷宫的方法是使用一系列操作将构建增加到迷宫中,然后连接他们。例如,在 MazeGamecreateMaze 函数中将创建一个迷宫,这个迷宫由两个房间和他们之间的一扇门组成。

MazeGame

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

class MazeGame
{
    public static function createMaze(): Maze
    {
        $maze = new Maze();
        $room1 = new Room(1);
        $room2 = new Room(2);
        $theDoor = new Door();
        
        $maze->addRoom($room1);
        $maze->addRoom($room2);
        
        $room1->setSide(Direction::North, new Wall());
        $room1->setSide(Direction::East, $theDoor);
        $room1->setSide(Direction::South, new Wall());
        $room1->setSide(Direction::West, new Wall());
        
        $room2->setSide(Direction::North, new Wall());
        $room2->setSide(Direction::East, new Wall());
        $room2->setSide(Direction::South, new Wall());
        $room2->setSide(Direction::West, $theDoor);
        
        return $maze;
    }
}

问题核心

考虑到这个函数只是创建一个有两个房间的迷宫,这就相当复杂了。显然有办法使它变的简单。你可以使用 Room 的构造函数提前初始化每一面墙壁,但这仅仅是将代码移动到了其他地方。这个成员函数的真正问题不在于大小,而在于它不灵活。他对迷宫布局进行硬编码,更改布局意味着改变这个成员函数,或者冲定义他,这将很容易出错,且不利于重用。

使用创建者模式将会告诉你如何使得设计变得更加灵活,但未必会更小,他们将便于修改定义一个迷宫组件。