從 PHP 再探物件導向

也是花了點時間來筆記。

簡述

物件導向有幾個原則,基本上可以分成這四點:

  1. Class 類別
  2. Instance 實體
  3. Encapsulation 封裝
  4. Inheritance 繼承

由於 PHP 的物件導向概念比 JavaScript 更完整一些,所以等一下的範例都會以 PHP 為主。

Class / Instance 類別和實體

物件導向最基本的概念就是由「Class」和「Insatnce」來組成。

簡單來說,Class 可以想成是「模板(設計圖),而 Instance 則是「實際做出來的東西(成品)」。

在物件導向裡面你一定會很常聽到這句話:

new 一個 Instance 出來

這句話比較完整的意思是「從 Class 來 new 出一個 Instance」。

來看個例子:

1
2
3
4
5
6
7
8
9
10
11
class Dog {
function __construct ($name) {
$this->name = $name;
}
function sayHello () {
echo "Hello~~~";
}
}

$dog1 = new Dog('PeaNu');
$dog1->sayHello();

這邊的 Class 就是設計圖,我希望透過 Class 生出來的 instance 具備 $namesayHello 這兩東西。

接著就用 new 來生出一個 dog1,所以現在就有了新的 instance,它有名字,也會說哈囉。

Encapsulation 封裝

比較複雜的邏輯留在 Class 裡面,自己決定要對外開放哪些 methods

簡單來說,有些時候我們可能會在 Class 裡寫一些單純用來協助程式運作的函式或變數,跟實際使用上沒有太大關聯。這時候就會利用「封裝」的概念來做一點限制,讓 instance 沒辦法直接存取這些函式或變數。

封裝的實作是透過 publicprivate 來把要對內 / 對外的東西來做區隔。

1
2
3
4
5
6
7
8
9
10
11
12
class Person {
private function get_greeting_sentence () {
return 'Hello';
}
public function greeting () {
echo $this->get_greeting_sentence();
}
}

$person1 = new Person();
$person1->greeting();
$person1->get_greeting_sentence(); // fatal error

外面的 instance 不必知道 greeting() 是怎麼實作的,只要知道有這個方法可以用就好了。就算真的它知道裡面是用 get_greeting_sentence 來實作的,也沒有辦法直接去存取它,因為它被歸類成 private 的內容。

總而言之,封裝就是像這樣透過 publicprivate 來做區隔,優點是結構比較明確,或者是能防止別人亂改 Class 裡的資訊。

還是想存取的話怎麼辦?利用 getter 和 setter

剛剛有提到 Class 中可能會用變數來儲存一些資訊,但如果 instance 完全沒辦法存取到的話會有點麻煩,來看個例子:

1
2
3
4
5
6
7
8
9
10
class Dog {
private $name = 'default';
public function say_hello () {
echo "Hello my name is $this->name";
}
}

$dog1 = new Dog();
$dog1->name = 'peanu'; //Fatal error: Uncaught Error: Cannot access private property
$dog1->say_hello();

因為 $nameprivate 資訊,所以存取的時候就會噴 Error。可是我們就是希望可以自己設定 $name 的話怎麼辦?

這時候會用到兩個東西:getter 和 setter

  • getter 是用來讀取 Class 中的資訊
  • setter 是用來設定 Class 中的資訊

簡單來說就是你可以對外 public 兩個函式,一個專門讀取資訊,一個專門設定資訊,這樣不就好了嗎?

來把剛剛的例子做點修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dog {
private $name = 'default';
public function set_name ($name) {
// 不雅名稱
if ($name === 'fuck') return;
// 設值
$this->name = $name;
}
public function get_name () {
echo $this->name;
}
}

$dog1->set_name('fuck');
$dog1->get_name(); // default

$dog1->set_name('peanu');
$dog1->get_name(); // peanu

有了 settergetter 以後就能達到我們的目的。除此之外,還可以在裡面做一些限制,例如說不可以取髒話啦,或者是只能讀取到哪些內容,這些都是可以透過 getter 和 setter 來設計的。

Inheritance 繼承

就如同字面意思,繼承某個 Class

舉例來說,假設你有一個狗的 Class,它有名字,還會打招呼。現在你希望在多一個 Class 是黑狗,它跟狗有 87 分像,只差在多了跑步這個功能,那你有可能會這樣寫:

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
// 狗
class Dog {
function __construct ($name) {
$this->name = $name;
}
public function say_hello () {
echo "Hello my name is " . $this->name;
}
}
// 黑狗
class BlackDog {
function __construct ($name) {
$this->name = $name;
}
public function say_hello () {
echo "Hello my name is " . $this->name;
}
public function run () {
echo "Yo! I am running";
}
}
$dog = new Dog('dog');
$black_dog = new BlackDog('black dog');
$dog->say_hello(); // Hello my name is dog
$black_dog->say_hello(); // Hello my name is black dog
$black_dog->run(); // Yo! I am running

雖然沒什麼問題,但在工程師的世界不是有句名言嗎:

DRY(Don’t Repeat Yourself)

既然它們都是「狗」,而黑狗只是多了跑步這個功能,那幹嘛不直接讓它「繼承」狗就好?就跟現實中的小孩會繼承父母 DNA 是一樣的感覺。

所以呢,在大部分物件導向的語言裡會提供一個東西叫做 extends,讓你可以繼承某個 class,這樣就不必再重複寫一遍。

一樣來改一下剛剛的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Dog {
function __construct ($name) {
$this->name = $name;
}
public function say_hello () {
echo "Hello my name is " . $this->name;
}
}
// 繼承狗的 Class
class BlackDog extends Dog {
public function run () {
echo "Yo! I am running";
}
}
$dog = new Dog('dog');
$black_dog = new BlackDog('black dog');
$dog->say_hello(); // Hello my name is dog
$black_dog->say_hello(); // Hello my name is black dog
$black_dog->run(); // Yo! I am running

以上就是繼承最基本的用法,接著來介紹幾個使用繼承時可能會碰到的問題。

想要覆寫原本 Class 的 method

其實還蠻直覺的,如果想要覆寫的話,就直接在「繼承的 Class」裡寫一個一模一樣的來蓋掉就好,跟你寫 CSS 的概念有點像:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Dog {
function __construct ($name) {
$this->name = $name;
}
public function say_hello () {
echo "Hello my name is" . $this->name;
}
}
class BlackDog extends Dog {
// 直接寫一樣的來覆蓋
public function say_hello() {
echo "yoyoyo black dog is better";
}
public function run () {
echo "Yo! I am running";
}
}
$dog = new Dog('dog');
$black_dog = new BlackDog('black dog');
$dog->say_hello(); // Hello my name is dog
$black_dog->say_hello(); // yoyoyo black dog is better

想要存取原本 Class 的 private 變數

如果你在「繼承的 Class」中存取原本的 private 變數,就會發現存取不了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Dog {
private $test = 'yo';
public function log_private() {
echo $this->test;
}
}
class BlackDog extends Dog {
public function log_private() {
echo $this->test;
}
}
$dog = new Dog();
$black_dog = new BlackDog();
$dog->log_private(); // yo
$black_dog->log_private(); // Warning: Undefined property: BlackDog::$test

所以要改用 protected 來設定才行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Dog {
// 從 private 改成 protected
protected $test = 'yo';
public function log_private() {
echo $this->test;
}
}
class BlackDog extends Dog {
public function log_private() {
echo $this->test;
}
}
$dog = new Dog();
$black_dog = new BlackDog();
$dog->log_private(); // yo
$black_dog->log_private(); // yo

protected 一樣沒辦法讓外面的 instance 存取,但是繼承的 Class 可以存取。

想要存取原本 Class 的 method

有兩種方法:

  1. 透過 $this->method
  2. 透過 parent::method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Dog {
public function say_hi() {
echo 'hi';
}
}
class BlackDog extends Dog {
public function say_yo_and_hi_1 () {
echo 'yo';
$this->say_hi();
}
public function say_yo_and_hi_2 () {
echo 'yo';
parent::say_hi();
}
}
$black_dog = new BlackDog();
$black_dog->say_yo_and_hi_1(); // yohi
$black_dog->say_yo_and_hi_2(); // yohi

實際差別我不是很清楚,但比較常見的做法會是第二種,詳細可以參考這篇:multiple ways of calling parent method in php

順便附個 JS 的寫法,其實就是把 parent:: 改成 super

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent {
sayHi () {
console.log('hi');
}
}

class Child extends Parent {
sayHiAndYo () {
console.log('yo');
super.sayHi();
}
}

const c2 = new Child();
c2.sayHiAndYo(); // yo hi

關於 static

前面已經介紹過三種在 Class 設定的變數和函式:

  • public
  • private
  • protected

最後要來介紹一個東西,叫做 static

簡單來說,透過前面那三種設定的資訊會綁在 instance 身上,不是 Class。

用講的有點難懂,來舉個例子:

1
2
3
4
5
6
7
8
9
class V1 {
public $name = 'PPB';
}

$a = new V1();
$b = new V1();

echo $a->name; // PPB
echo $b->name; // PPB

僅管這兩個 instance 的 $name 看起來好像一樣,但其實這兩個$name不同的變數。也就是說你有幾個 instance ,就會有幾個 $name 這個變數。

但如果你用 static 來設定就不一樣了:

1
2
3
4
class V1 {
static $name = 'PPB';
}
echo V1::$name;

static 的意思是說:

我要把這個變數 or 函式綁在這個 Class 身上。

所以要存取的話也得透過 Class,而不是 instance,可以想成是一種全域變數的感覺,因為每一個 instance 都會共用同個變數。

就如同 static 的字面意思,它是「靜態」的,所以一般是用在「你不會去改變的變數或函式」。

mentor-program-day96 mentor-program-day95
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×