使用變數建立物件取代 if else

傳統我們會使用 if else 判斷,建立不同的物件,但由於 JavaScript 與 PHP 動態語言的特性,我們可以將要建立的物件的 class 名稱變數表示,直接以該變數建立物件。

Version


ECMAScript 5
PHP 7.0
Laravel 5.2.22

JavaScript


if else

傳統若要根據不同的變數值,建立不同的物件,我們會使用 if else 方式 :1 1GitHub Commit : JavaScript : 傳統使用 if else 建立物件

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Customer() {
this.show = function() {
print('I am a customer');
};
};

function Admin() {
this.show = function() {
print('I am a admin');
};
};

var type = 'admin';

if (type == 'admin') {
var user = new Admin();
} else {
var user = new Customer();
}
user.show()

15行

1
2
3
4
5
if (type == 'admin') {
var user = new Admin();
} else {
var user = new Customer();
}

使用 if else 去判斷變數值,建立不同物件。

Variable

若將 class 名稱使用變數表示,則不需要 if else 判斷 : 2 2GitHub Commit : JavaScript:動態使用變數建立物件

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Customer() {
this.show = function() {
print('I am a customer');
};
};

function Admin() {
this.show = function() {
print('I am a admin');
};
};

var type = 'admin';
var className = (type == 'admin') ? 'Admin' : 'Customer';
var user = new this[className];
user.show()

14行

1
2
var className = (type == 'admin') ? 'Admin' : 'Customer';
var user = new this[className];

判斷 type,並將要建立物件的 class 名稱存入 className 變數。

搭配 this,將 class 名稱以變數方式傳入 []

PHP


PHP 也可以達到類似 JavaScript 的功能。

if else

傳統若要根據不同的變數值,建立不同的物件,我們會使用 if else 方式 :3 3GitHub Commit : PHP : 傳統使用 if else 建立物件

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
<?php
class Customer
{

public function show()
{

echo('I am a customer');
}
}

class Admin
{

public function show()
{

echo('I am a admin');
}
}

$type = 'admin';

if ($type == 'admin') {
$user = new Admin();
} else {
$user = new Customer();
}
$user->show();

20行

1
2
3
4
5
if ($type == 'admin') {
$user = new Admin();
} else {
$user = new Customer();
}

使用 if else 去判斷變數值,建立不同物件。

Variable

若將 class 名稱使用變數表示,則不需要 if else 判斷 : 4 4GitHub Commit : PHP : 動態使用變數建立物件

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Customer
{

public function show()
{

echo('I am a customer');
}
}

class Admin
{

public function show()
{

echo('I am a admin');
}
}

$type = 'admin';
$className = ($type == 'admin') ? 'Admin' : 'Customer';
$user = new $className;
$user->show();

18行

1
2
$className = ($type == 'admin') ? 'Admin' : 'Customer';
$user = new $className;

判斷 $type,並將要建立物件的 class 名稱存入 $className 變數。

在 PHP,允許我們 new 之後直接加上變數建立物件。

Type Hint

談到這裡,雖然我們已經可以在 JavaScript 與 PHP 使用變數來建立物件,由於 class 名稱是字串,我們甚至可以將 class 名稱存在設定檔內,如config/app.php,將來若因為需求改變,需改變建立物件的 class 名稱時,只需修改設定檔即可,並透過 config::get() 讀取 class 名稱,原來程式碼完全不用修改,也算是達到開放封閉原則的要求。

不過這種方式有個致命傷,就是那個 show() 函式並沒有硬性規定(如 interface 或 abstract class),也就是動態語言所謂的 duck type,也因為並沒有任何機制規定 show(),只有程式設計師自己知道必須要有 show(),也因此 PhpStorm 將無法幫你對 show()語法提示語法檢查

但隨著 PHP 的 Java 化之後 (type hint + service container),PHP成為唯一一個同時擁有 duck type 與 strong type 的程式語言,尤其 PHP 7 之後的type hint 更加完整(scalar type hint + return type),若搭配 service container,將可解決之前 show() 的問題。

routes.php5 5GitHub Commit : 修改 routes.php

app/Http/routes.php
1
Route::get('/show', 'UserController@show');

在 routes.php 加上 URI 與其對應的 controller action。

UserController.php6 6GitHub Commit : 建立 UserController.php

app/Http/Controllers/UserController.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
namespace App\Http\Controllers;

use App\Http\Requests;
use App\Services\UserService;

class UserController extends Controller
{

/** @var UserService */
private $userService;

/**
* UserController constructor.
* @param UserService $userService
*/

public function __construct(UserService $userService)
{

$this->userService = $userService;
}

public function show()
{

$this->userService->show('admin');
}
}

第8行

1
2
3
4
5
6
7
8
9
10
11
/** @var UserService */
private $userService;

/**
* UserController constructor.
* @param UserService $userService
*/

public function __construct(UserService $userService)
{

$this->userService = $userService;
}

注入 UserService

20行

1
2
3
4
public function show()
{

$this->userService->show('admin');
}

show() 呼叫 $this->userServiceshow(),並將 admin 變數傳入。

UserService.php7 7GitHub Commit : 建立 UserService.php

app/Services/UserService.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
namespace App\Services;

use App\Services\User\UserFactory;

class UserService
{

/** @var UserFactory */
private $userFactory;

/**
* UserService constructor.
* @param UserFactory $userFactory
*/

public function __construct(UserFactory $userFactory)
{

$this->userFactory = $userFactory;
}

public function show(string $type)
{

$user = $this->userFactory->create($type);
$user->show();
}
}

第7行

1
2
3
4
5
6
7
8
9
10
11
 /** @var UserFactory */
private $userFactory;

/**
* UserService constructor.
* @param UserFactory $userFactory
*/

public function __construct(UserFactory $userFactory)
{

$this->userFactory = $userFactory;
}

注入 UserFactory,專門負責建立 AdminCustomer 物件。

19行

1
2
3
4
5
public function show(string $type)
{

$user = $this->userFactory->create($type);
$user->show();
}

使用 $this->userFactorycreate() 建立物件,將 $type 變數傳入,由 UserFactory 決定要建立哪個物件。

無論建立什麼物件,都使用相同的 show(),這就是物件導向的多型

AbstractUser.php8 8GitHub Commit : 建立 AbstractUser.php

app/Services/User/AbstractUser.php
1
2
3
4
5
6
namespace App\Services\User;

abstract class AbstractUser
{

abstract public function show();
}

abstract class 定義 show(),如此 PhpStorm 就能幫我們做語法提示與語法檢查了。

Admin.php9 9GitHub Commit : 建立 Admin.php

app/Services/User/Admin.php
1
2
3
4
5
6
7
8
9
namespace App\Services\User;

class Admin extends AbstractUser
{

public function show()
{

echo('I am a admin');
}
}

Admin 繼承 AbstractUser,因為之前定義了 show() abstract method,所以必須在此實作 show()

Customer.php10 10GitHub Commit : 建立 Customer.php

app/Services/User/Customer.php
1
2
3
4
5
6
7
8
9
namespace App\Services\User;

class Customer extends AbstractUser
{

public function show()
{

echo('I am a customer');
}
}

Customer 繼承 AbstractUser,因為之前定義了 show() abstract method,所以必須在此實作 show()

UserFactory11 11GitHub Commit : 建立 UserFactory.php

app/Services/User/UserFactory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Services\User;

use Illuminate\Support\Facades\App;

class UserFactory
{

/**
* @param string $type
* @return AbstractUser
*/

public function create(string $type) : AbstractUser
{

App::bind(AbstractUser::class, 'App\Services\User\\' . ucfirst($type));
return App::make(AbstractUser::class);
}
}

重點在此,使用了 App::bind(),將 abstract class 與 concrete class 綁定,其中 concrete class 的 class 名稱為變數。

然後再使用App::make()建立綁定後的物件。

Conclusion


  • 實務上 PHP 建議先考慮使用 type hint 的 strong type,這種方式 PhpStorm 支援較好,但是 duck type 也不是不能用,可以當成泛型使用,也就是當你不想考慮其型別時,就不要加上 type hint,故意模糊其型別。
  • 在 PHP 與 Laravel,透過 App::bind()App::make() 也可以使用變數建立物件,還額外得到 type hint 與 PhpStorm 的支援。

Sample Code


完整的範例可以在我的GitHub上找到。

  1. JavaScript 與 PHP 使用變數建立物件
  2. PHP 與 Laravel 使用 Service Container 建立物件
2016-02-29