給OOP初學者的建議:先搞懂「資料跟行為在一起」就好,其它的慢慢來

初學者接觸OOP,幾乎都會有以下疑惑:

我到底為什麼要學OOP?OOP解決了什麼問題?書上這些範例就算不用OOP也寫得出來吧?

然後覺得「繼承」、「多型」、「介面」、「抽象類別」等等的名詞很難,覺得OOP很難。

其實這些名詞雖然重要,但對新手來說,本來就很難在一開始就搞懂。

建議先搞懂「資料跟行為在一起」是什麼,以及它的好處在哪,就可以了,其它的慢慢來。

什麼叫做「資料跟行為在一起」?

假設我們在開發一個「中英文互助學習網」,鼓勵中文人士與英語人士登入討論。

這個系統的貼文、留言功能會顯示「發文日期」。

發文日期要根據使用者的註冊身份(台灣人、英語人士)顯示不同格式(台灣格式、西方格式)。

下面就以這個日期格式的功能舉例說明「資料跟行為在一起」是什麼意思。

作法一:直接硬寫(不OOP、資料跟行為混在一起)

初學者通常會用最簡單、也最直覺的作法,直接硬寫出來,像這樣:


這種寫法的資料(日期)跟行為(轉換成各種格式)混在一起。

它的優點是寫起來很簡單,缺點則有兩個:

* 日期格式的邏輯會重複出現在很多地方,整段code會到處重複出現
* 整段code大概會塞在<div>或是<span>的裡面,導致它跟HTML混在一起,很亂

作法二:自訂函數(不OOP、資料跟行為沒混在一起)

為了解決作法一遇到的問題,聰明的初學者很快就想到可以用「自訂函數」!就像這樣:


這種寫法將行為(轉換成各種格式)用自訂函數給獨立出來,也大幅改善了作法一遇到的問題。

對小型的網頁程式來說,這招非常好用,不但開發快速、簡單,還漂亮地將資料跟行為拆開。

但是程式規模變大之後,為了將各種行為拆出來,會寫出很多自訂函數,類似這樣:


於是又衍生出三個問題:

1. 像localFormat、englishFormat這樣的函數名稱意義模糊,看不出是處理日期、人名,還是什麼東西的格式

2. 這些自訂函數各有不同的行為,全部放在一起顯得很亂,應該要想辦法分類、整理這些函數

3. 像localFormat、englishFormat這樣的函數,只吃特定格式的參數,最好能跟某種資料的形式綁在一起,以後要改程式時,能讓相關的資料跟行為一起被看到

問題1很好解決,只要替函數名稱加前綴字變成dateLocalFormat、dateEnglishFormat就行了。

問題2也很好解決,只要多開幾個檔案,把相關的函數放進同一個檔案就行了。

問題3就很棘手,資料跟行為拆開之後,如何在概念上又找方法整理在一起?

作法三:使用class(OOP、資料跟行為在一起)

正是這些處理資料、整理行為的問題,導致了OOP的誕生:

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類別。)

Q&A

Q1:我常常設計一些類別,只有資料沒有行為,聽起來OK嗎?

不OK,這很不OOP,而且沒意義。

乾脆直接用關聯式陣列去表示那些資料就好。

Q2:我常常設計一些類別,只有行為沒有資料,聽起來OK嗎?

這個要看情況,不一定。

但唯一可以確定的是,這種作法很不OOP。

因為OOP的核心是「資料跟行為在一起」。

這也是為什麼你會看到有人明明寫了類別、用了物件,別人卻說「這不夠OOP」。

然後你又會看到像JavaScript這樣連「類別」關鍵字都沒有(ES5以前),卻能夠寫出很OOP程式碼的關係。

判定的標準都是一樣的,而且也就只有這麼一個標準:資料跟行為有沒有在一起。

Q3:一個類別包含的概念是越大越好,還是越小越好?

不一定。不過我們從作法一到作法三的過程,有一個明確目的:希望讓程式碼更好懂。

如果聲稱一個類別包含的概念很大(例:設計LanguageHelpWebsite類別,用來代表「中英文互助學習網」需要的所有功能),那會把幾乎整個網站的所有行為跟資料都放進去,成為所謂的God object。它可沒有讓程式碼更好懂。

相反地,如果聲稱一個類別包含的概念很小(例:分別設計LocalDate、EnglishDate類別),雖然意義可能更精準了,但用一整個Date類別的概念去思考,程式碼會更容易理解,也就是所謂的內聚性(Cohesion)更高。

所以要替OOP就是「資料跟行為在一起」加個但書:

要以方便理解程式為前提,將資料和行為放在一起。

(完)

(Photo via brando.n, CC licensed.)