資料對應 Data Mapper

資料對應,這是一種常用於處理物件導向與資料庫資料的模式,與 Repository 不同,Data Mapper 主要處理的事單個物件本身,而 Repository 處理的是物件的集合。這次實作舉個例子,你在買大頭菜之前,需要有一個草圖去評估你要前往哪些島上買大頭菜,決定好之後再開始行動。

UML

UML

實作

首先我們要先把大頭菜給實作出來,這次因為大頭菜可以來自不同的島嶼,因此多了島嶼名稱,以及透過 new self(...) 的方式來回傳新的大頭菜物件。

Turnips.php

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
46
47
48
/**
* Class Turnips.
*/
class Turnips
{
/**
* @var string
*/
protected $island;

/**
* @var int
*/
protected $price;

/**
* @var int
*/
protected $count;

/**
* Turnips constructor.
*
* @param string $island
* @param int $price
* @param int $count
*/
public function __construct(string $island, int $price, int $count)
{
$this->island = $island;
$this->price = $price;
$this->count = $count;
}

/**
* @param array $island
*
* @return Turnips
*/
public static function fromIsland(array $island): Turnips
{
return new self(
$island['island'],
$island['price'],
$island['count']
);
}
}

Turnips 你可以理解為 ORM 物件,所以接下來要製作所謂的草稿(StorageAdapter)以及對應的中間層(TurnipsMapper),把草稿寫好之後,丟進去中間層(TurnipsMapper)就可以對應到每個大頭菜物件。

【補充說明】
物件關聯對映(英語:Object Relational Mapping,簡稱ORM,或O/RM,或O/R mapping),是一種程式設計技術,用於實現物件導向程式語言裡不同類型系統的資料之間的轉換。從效果上說,它其實是建立了一個可在程式語言裡使用的「虛擬物件資料庫」。如今已有很多免費和付費的ORM產品,而有些程式設計師更傾向於建立自己的ORM工具。
資料來源:維基百科

TurnipsMapper.php

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* Class TurnipsMapper.
*/
class TurnipsMapper
{
/**
* @var StorageAdapter
*/
protected $adapter;

/**
* TurnipsMapper constructor.
*
* @param StorageAdapter $storage
*/
public function __construct(StorageAdapter $storage)
{
$this->adapter = $storage;
}

/**
* @param int $id
*
* @return Turnips
*/
public function findById(int $id): Turnips
{
$result = $this->adapter->findById($id);

if ($result === null) {
throw new \InvalidArgumentException("找不到 ID 為「 $id 」的島嶼。");
}

return $this->mapRowToTurnips($result);
}

/**
* @param int $id
*
* @return Turnips
*/
public function findByIsland(string $island): Turnips
{
$result = $this->adapter->findByIsland($island);

if ($result === null) {
throw new \InvalidArgumentException("找不到名稱為「 $island 」的島嶼。");
}

return $this->mapRowToTurnips($result);
}

/**
* @param array $row
*
* @return Turnips
*/
protected function mapRowToTurnips(array $row): Turnips
{
return Turnips::fromIsland($row);
}
}

StorageAdapter.php

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
46
47
48
49
/**
* Class StorageAdapter.
*/
class StorageAdapter
{
/**
* @var array
*/
protected $data = [];

/**
* StorageAdapter constructor.
*
* @param array $data
*/
public function __construct(array $data)
{
$this->data = $data;
}

/**
* @param int $id
*
* @return array|null
*/
public function findById(int $id)
{
if (isset($this->data[$id])) {
return $this->data[$id];
}

return null;
}

/**
* @param string $island
*
* @return array|null
*/
public function findByIsland(string $island)
{
$key = array_search($island, array_column($this->data, 'island'));
if (false !== $key) {
return $this->data[$key];
}

return null;
}
}

測試

撰寫完資料對應器之後,接下來要寫些測試來驗證資料對應器是否能夠正確的運作,預期是繪製好草圖,把草圖塞入對應器之後,獲得相對應的大頭菜物件,所以會有以下幾個重點項目需要測試:

  1. 測試塞入大頭菜列,並且取得 id 為 1 的大頭菜。
  2. 測試塞入空的大頭菜列,並且成功獲得例外錯誤。
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
/**
* Class DataMapperTest.
*/
class DataMapperTest extends TestCase
{
/**
* 測試塞入大頭菜列,並且取得 id 為 1 的大頭菜。
*
* @test
*/
public function test_can_map_turnips_from_storage()
{
$storage = new StorageAdapter(
array(
1 => array(
'island' => 'kantai',
'price' => 100,
'count' => 40,
),
),
);

$mapper = new TurnipsMapper($storage);

$turnips = $mapper->findById(1);

$this->assertInstanceOf(Turnips::class, $turnips);
}

/**
* 測試塞入空的大頭菜列,並且成功獲得例外錯誤。
*
* @test
*/
public function test_will_not_map_invalid_data()
{
$this->expectException(\InvalidArgumentException::class);

$storage = new StorageAdapter([]);
$mapper = new TurnipsMapper($storage);

$mapper->findById(1);
}
}

最後測試的執行結果會獲得如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PHPUnit Pretty Result Printer 0.28.0 by Codedungeon and contributors.
==> Configuration: ~/php-design-pattern/vendor/codedungeon/phpunit-result-printer/src/phpunit-printer.yml

PHPUnit 9.2.6 by Sebastian Bergmann and contributors.


==> AbstractFactoryTest ✔ ✔ ✔ ✔
==> BuilderPatternTest ✔ ✔ ✔ ✔
==> FactoryMethodTest ✔ ✔ ✔ ✔
==> PoolPatternTest ✔ ✔
==> PrototypePatternTest ✔ ✔
==> SimpleFactoryTest ✔ ✔ ✔ ✔
==> SingletonPatternTest ✔
==> StaticFactoryTest ✔ ✔ ✔ ✔ ✔
==> AdapterPatternTest ✔ ✔
==> BridgePatternTest ✔ ✔ ✔
==> CompositePatternTest ✔ ✔ ✔
==> DataMapperTest ✔ ✔

Time: 00:00.050, Memory: 6.00 MB

OK (36 tests, 83 assertions)

完整程式碼

設計模式不難,找回快樂而已,以大頭菜為例。

參考文獻