初學者接觸OOP,幾乎都會有以下疑惑:
我到底為什麼要學OOP?OOP解決了什麼問題?書上這些範例就算不用OOP也寫得出來吧?
然後覺得「繼承」、「多型」、「介面」、「抽象類別」等等的名詞很難,覺得OOP很難。
其實這些名詞雖然重要,但對新手來說,本來就很難在一開始就搞懂。
建議先搞懂「資料跟行為在一起」是什麼,以及它的好處在哪,就可以了,其它的慢慢來。
假設我們在開發一個「中英文互助學習網」,鼓勵中文人士與英語人士登入討論。
這個系統的貼文、留言功能會顯示「發文日期」。
發文日期要根據使用者的註冊身份(台灣人、英語人士)顯示不同格式(台灣格式、西方格式)。
下面就以這個日期格式的功能舉例說明「資料跟行為在一起」是什麼意思。
初學者通常會用最簡單、也最直覺的作法,直接硬寫出來,像這樣:
<?php
$postDate = '2016-06-02'; # 假設資料庫取出來的發文日期長這樣
if (/* 判斷是否顯示台灣格式 */) {
# 轉換成這樣 2016.6.2
$arr = explode('-', $postDate);
$year = $arr[0];
$month = $arr[1];
$day = $arr[2];
echo "$year.$month.$day";
} else { // 西方格式
# 轉換成這樣 6/2/2016
$arr = explode('-', $postDate);
$year = $arr[0];
$month = $arr[1];
$day = $arr[2];
echo "$month/$day/$year";
}
這種寫法的資料(日期)跟行為(轉換成各種格式)混在一起。
它的優點是寫起來很簡單,缺點則有兩個:
div
或是 span
的裡面,導致它跟HTML混在一起,很亂為了解決作法一遇到的問題,聰明的初學者很快就想到可以用「自訂函數」!就像這樣:
<?php
function localFormat($date)
{
$arr = explode('-', $date);
$year = $arr[0];
$month = $arr[1];
$day = $arr[2];
return "$year.$month.$day";
}
function englishFormat($date)
{
$arr = explode('-', $date);
$year = $arr[0];
$month = $arr[1];
$day = $arr[2];
return "$month/$day/$year";
}
$postDate = '2016-06-02'; # 假設資料庫取出來的發文日期長這樣
if (/* 判斷是否為台灣人身份 */) {
echo localFormat($postDate);
} else { // 英語人士身份
echo englishFormat($postDate);
}
這種寫法將行為(轉換成各種格式)用自訂函數給獨立出來,也大幅改善了作法一遇到的問題。
對小型的網頁程式來說,這招非常好用,不但開發快速、簡單,還漂亮地將資料跟行為拆開。
但是程式規模變大之後,為了將各種行為拆出來,會寫出很多自訂函數,類似這樣:
?php
function localFormat($param)
{
// blah blah ...
}
function englishFormat($param)
{
// blah blah ...
}
function someTask($param)
{
// blah blah ...
}
function anotherTask($param)
{
// blah blah ...
}
function otherTask($param)
{
// blah blah ...
}
//...
於是又衍生出三個問題:
像localFormat、englishFormat這樣的函數名稱意義模糊,看不出是處理日期、人名,還是什麼東西的格式
這些自訂函數各有不同的行為,全部放在一起顯得很亂,應該要想辦法分類、整理這些函數
像localFormat、englishFormat這樣的函數,只吃特定格式的參數,最好能跟某種資料的形式綁在一起,以後要改程式時,能讓相關的資料跟行為一起被看到
問題1很好解決,只要替函數名稱加前綴字變成dateLocalFormat、dateEnglishFormat就行了。
問題2也很好解決,只要多開幾個檔案,把相關的函數放進同一個檔案就行了。
問題3就很棘手,資料跟行為拆開之後,如何在概念上又找方法整理在一起?
正是這些處理資料、整理行為的問題,導致了OOP的誕生:
<?php
class Date
{
public $year;
public $month;
public $day;
public function __construct($date)
{
$arr = explode('-', $date);
$this->year = $arr[0];
$this->month = $arr[1];
$this->day = $arr[2];
}
public function localFormat()
{
return $this->year . '.' .$this->month . '.' . $this->day;
}
public function englishFormat()
{
return $this->month . '/' .$this->day . '/' . $this->year;
}
}
$postDate = '2016-06-02'; # 假設資料庫取出來的發文日期長這樣
$date = new Date($postDate);
if (/* 判斷是否為台灣人身份 */) {
echo $date->localFormat();
} else { // 英語人士身份
echo $date->englishFormat();
}
OOP的寫法,一次解決了前述三個問題:
問題1 => 現在從類別名稱就可以知道底下方法的意義了
問題2 => 現在相關的函數都整理進同一個類別底下成為方法了
問題3 => 現在資料的形式都統一在constructor處理一次,之後不管新增多少方法都不用處理資料了
這就是所謂的「資料跟行為在一起」,也正是OOP的核心概念。
利用這種方式整理程式碼、寫出一個又一個的類別,可以大幅提昇程式碼的品質。
上述的作法一跟作法二並沒那麼糟糕,但確實會帶來一些問題。
對於小型的網頁程式來說,可能還算夠用。
但是隨著程式規模變大,如果將概念上相關的資料跟行為整理在一起,會很有幫助。
實務上也可以先從作法二開始寫起,直到發現某些資料跟行為關係密切,再拉出來整理成類別即可。
至於很多OOP教學會提到的「繼承」、「多型」、「介面」、「抽象類別」等等名詞,一時搞不懂沒有關係,你可能實務上也暫時用不到。之後找時間慢慢搞懂它們的用途就好。
光是知道「將資料跟行為放在一起」的技巧,就能夠開始寫OOP程式碼了。
(註:本篇文章的程式碼純屬教學用途。實務上PHP已經有DateTime類別可以使用,或是用更漂亮的Carbon類別。)
Q1:我常常設計一些類別,只有資料沒有行為,聽起來OK嗎?
不OK,這很不OOP,而且沒意義。
乾脆直接用關聯式陣列去表示那些資料就好。
Q2:我常常設計一些類別,只有行為沒有資料,聽起來OK嗎?
這個要看情況,不一定。
但唯一可以確定的是,這種作法很不OOP。
因為OOP的核心是「資料跟行為在一起」。
這也是為什麼你會看到有人明明寫了類別、用了物件,別人卻說「這不夠OOP」。
然後你又會看到像JavaScript這樣連「類別」關鍵字都沒有(ES5以前),卻能夠寫出很OOP程式碼的關係。
判定的標準都是一樣的,而且也就只有這麼一個標準:資料跟行為有沒有在一起。
Q3:一個類別包含的概念是越大越好,還是越小越好?
不一定。不過我們從作法一到作法三的過程,有一個明確目的:希望讓程式碼更好懂。
如果聲稱一個類別包含的概念很大(例:設計LanguageHelpWebsite類別,用來代表「中英文互助學習網」需要的所有功能),那會把幾乎整個網站的所有行為跟資料都放進去,成為所謂的God object。它可沒有讓程式碼更好懂。
相反地,如果聲稱一個類別包含的概念很小(例:分別設計LocalDate、EnglishDate類別),雖然意義可能更精準了,但用一整個Date類別的概念去思考,程式碼會更容易理解,也就是所謂的內聚性(Cohesion)更高。
所以要替OOP就是「資料跟行為在一起」加個但書:
要以方便理解程式為前提,將資料和行為放在一起。
反覆看了兩遍,覺得真的是非常簡單易懂,印象中書本裡面的OOP常常都是寫成厚厚一疊,看完還完全搞不懂
其實核心概念就是要把重複的程式碼整合在一起,讓程式碼更容易去閱讀&維護
(題外話:目前我還停留在階段二,只用過function,沒有實作過class,看完這篇文章,感覺下次可以在自己的side project練習實作看看!)