包含標籤 Object-Oriented 的文章

菜雞與物件導向 (15): 最少知識原則

上一篇我們紀錄了依賴反轉原則,到此五大原則介紹完畢…是這樣嗎?太天真了!就像四天王總是五個人一樣,五大原則當然也有第六個!

今天的主角就是五大原則中L位的第一候補:最少知識原則,也被稱作迪米特法則

最少知識原則 (Least Knowledge Principle)

只和直接的朋友溝通,不和陌生人說話

那麼所謂的朋友是什麼呢?就是指這個物件或方法有直接相關的物件啦。例如當我們使用一個方法時,這個方法應該只認識:

  • 該方法所屬的類別
  • 該方法所接收的參數
  • 該方法中建立的類別
  • 該方法所屬的類別所依賴的對象

除此之外對這個方法而言都是陌生人。什麼情況會遇到陌生人呢?有一個蠻常遇到的狀況就符合定義:當我們使用依賴對象的方法,該方法給了我們另一個類別時,我們就正在接觸毫無關係的陌生人。

……

閱讀全文


菜雞與物件導向 (14): 依賴反轉原則

在聊依賴反轉之前,先讓我們聊聊什麼是依賴,所謂的依賴就是一種「受到某個東西影響、牽制」的狀態。

例如說如果有個像我一樣的肥宅每天一定要來一片雞排才能療癒身心,那我就是依賴雞排;
同樣的,如果有個大叔不抽菸就會全身不舒服,就是對香菸有所依賴。

當有「必須要藉由某個人事物來達到目的」的狀況時,就是依賴。

而在程式設計裡面的概念也差不多,如果A模組直接受到B模組的影響,我們就稱A依賴了B,最明顯的狀況就是A模組需要藉由B模組的實例來完成某個功能的時候。

例如「匯出報表」功能建立了一個「Excel 控制類別」的實例以建立檔案;
或是「會員查詢」功能建立了一個「DB 連線」的實例來進入資料庫取得會員資料

遇見這種「必須要藉由某個模組的實例來完成想要的動作」的狀況時,就是依賴。

依賴與耦合

我們在之前的 耦合篇 提過,如果模組和另一個模組之間有關連,那這兩者之間就耦合。以此來看,依賴就是一種耦合的關係,那麼,依賴是健康還是不健康的耦合呢?

現在讓我們用 多型篇 用過的「老闆徵工程師」的例子來舉例一下:現在有間小小公司,老闆請來了小明當工程師,並請他開工撰寫產品程式碼。

當「撰寫產品程式」對「工程師」直接依賴的時候,狀況可能是這樣的:

public Product Work()
{
    Ming programmer = new Ming();
    var product = programmer.Programming();
    return product;
}

過一陣子,老闆發現小明寫出來的東西似乎不太行,於是把小明趕走,另外請了小華。這時候因為「工程師」這個實作類別不一樣了,我們就必須要改一次程式碼:

public Product Work()
{
    Hua programmer = new Hua();
    var product = programmer.Programming();
    return product;
}
……

閱讀全文


菜雞與物件導向 (13): 介面隔離原則

今天要記錄的是介面隔離原則,顧名思義是和介面高度相關的原則。因此在閱讀本篇之前,可能需要先對 介面 有一點了解呦。

事情就從上一篇 里氏替換原則 的鳥類物流公司開始說起。老闆痛定思痛,決定先用介面先規定好物流士們的應徵條件,例如裝貨、卸貨、飛行、必須有帥氣的喙等等。

這道命令下來後,倉庫們的企鵝都慌了,來檢查的編譯器瘋狂跳出 Error:「您未實作 IBird 的 Fly() 方法!」這下怎麼辦呢,為了要保住飯碗,企鵝們就必須實作出飛行才行,可是企鵝真的就不會飛呀!

這下子企鵝們只剩下兩個選擇:不實作飛行,但是就不能被當成物流士,最後就會被開除;或是……空實作。

public class Penguin : IBird
{
    public void Fly()
    {
      // Do nothing;
    }
}

企鵝們終於騙過了編譯器檢查員,然而當送貨的命令下來之後,企鵝們再一次卡在倉庫門口發呆,最終物流公司仍然踏上了虧損的老路,再度面臨倒閉危機…

……

閱讀全文


菜雞與物件導向 (12): 里氏替換原則

里氏替換原則 (Liskov Substitution Principle)

子類別必須能夠替換父類別

里氏原則還包含了一個概念:子類別替換父類別後,不需要改變,也不會發生任何錯誤或異常

從定義就可以看出來,這項原則是來替我們處理繼承問題的。因此,在開始本篇之前,可能需要先對 繼承 以及 多型 有基本的認識。如果可以的話,也請先看過 介面

那麼,就讓我們從很久很久以前開始說起…

我的子類別進入叛逆期了,怎麼辦?

很久很久以前,有一間公司受到 鴿子封包 所啟發,打算發展鳥類運輸技術,強勢打入無人機市場,用生物智慧掀起對人工智慧的革命。既然鳥類都會飛行,理所當然可以藉由飛行來進行空運,甚至還可以偷偷擊墜那些無人機對手,野心勃勃的老闆立馬徵了一批鳥類物流士,打出「凡是鳥類都可應徵」的旗號,各式各樣的猛禽響應而來,一時之間掀起整個物流業的風暴!

但是好景不常,公司營運之後發貨狀況不佳,頻繁發生丟包問題,甚至有些貨根本就出不了倉庫,虧損越來越大,心急如焚的老闆下令徹查,這才發現—

……

閱讀全文


菜雞與物件導向 (11): 開放封閉原則

開放封閉原則 (Open-Close Principle)

軟體實體(類別、模組、函式等等)應該對擴展開放,而對修改封閉

在我們了解什麼是「對擴展開放」和「對修改封閉」之前,先讓我們談談:什麼是擴展,什麼又是修改呢?

用白話一點的方式來形容,修改就是把東西拆開來改,像是手術;而擴展就是對東西額外加裝模組,像是添購設備。我們用飛行來舉例,像是鳥類直接用翅膀飛行,如果有需要修改飛行方法的話就得對鳥直接進行手術;但如果今天是一個裝備了噴射背包的人,我們只需要把噴射背包換成噴射鞋子、甚至噴射翅膀就可以了,不需要去修改人這個本體。

這邊可以發現開放封閉原則是針對「改變的時候」去做一個行動的建議,例如需求追加和變更等等。凡是變化都有成本,例如變動的難易度、變動造成的影響範圍等等都會影響到成本,若是程式碼冗長、內部邏輯複雜,類別之間互相耦合、影響範圍很廣,導致綁手綁腳或壞東壞西等等狀況,使得修改很困難,成本就會變高,進而使得開發效率變低。

然而,軟體並不是製造完畢就完工的東西,而是隨需求而生、隨需求而變的動態作品,因此程式碼的修改或重構相當頻繁。就像我們在 內聚耦合篇 提過的:軟體面對改變的能力,就像基因適應環境並生存下去的能力。因此,程式必須具有彈性,也就是需要盡可能降低修改的成本。

那麼讓我們回到前面:動手術跟換道具,哪個的成本比較高呢?

面對需求,對程式碼的改動是透過增加新程式碼進行的,而不是更改現有的程式碼  
(《大話設計模式》)

……

閱讀全文


菜雞與物件導向 (10): 單一職責原則

我們在前面的 內聚和耦合 有提到過,內聚並不是無腦把相關的程式碼都封在一起就好了,也有分成健康的和不健康的。但我們要怎麼知道這個類別是否足夠健康呢?單一職責原則就是很好的檢驗方式,這篇就讓我們來紀錄一下。

單一職責原則 (Single Responsibility Principle)

「單一職責」原則顧名思義,就是一個類別應該只負責一個職責

但是這樣太過籠統了,「職責」相當容易產生誤會,容易變成各說各話。畢竟咱們工程師最愛戰定義了嘛。

「你這類別不優,它有兩個職責!登入跟登出!」

『沒有啦,我這個類別就是負責帳戶管理的啊』

OSSO。乾脆你全部放一起,然後說是負責網站管理算了,呵」

『……你存心來找碴的是不是?』

為了避免像這樣產生職場糾紛,我們需要先定義一下什麼是「職責」。經過前輩們的努力(解釋)之後,單一職責的定義就成了:

就一個類別而言,應該只有一個引起它變化的原因

另外,我也看過「一個類別應該只對一個角色負責」的說法,這兩者的核心概念是一樣的。

……

閱讀全文


菜雞與物件導向 (9): SOLID

終於進入了原則篇,接下來的幾篇我們會介紹幾個物件導向的原則(基本上就是指 SOLID 原則)。因此這篇就讓我 水一下 當成後半段的目錄,方便之後可以把相關的部分整理進來。

為什麼我們需要這些原則?

我們在前面的章節已經說明了一些物件導向的特性,例如繼承和多型等等。然而我們並沒有討論到怎麼運用、或是怎樣設計才能算是更好的、更優雅的、更符合物件導向精神的;我們並沒有提到一個評估的標準,或是指引一個更好的方向。

然而,混亂的使用物件導向對整個專案的毀滅性甚至比乾脆不使用物件導向還高。

這些特性使用起來很簡單,大多數語言只需要一個符號或標示就能完成繼承,把一堆東西全部塞在一起就可以說我在封裝。但怎麼使用得好,又該什麼時候使用呢?這就是難的地方吧。

例如說濫用繼承,或是封裝時完全不隱藏複雜度一路 Puuuuublic 到底,又或者是類別之間過於相互依賴,全部耦合成一團等等。如果隨便地使用物件導向的各項特性,就會讓整個架構變得僵化、脆弱、危險、充滿臭味。

更可怕的是,這個發臭的過程是每一次設計、每一次修改都會有所影響,所謂「持續發生,腐敗成真」,隨著物件導向的亂用、誤用、無腦用,軟體就會逐漸腐化。一組腐化的軟體可能會有以下特徵:大量的依賴使得修改變得困難、修改後看似不相干的各個地方發生問題、或是修改時沒辦法依循原本的設計、到處出現不必要的複雜性和不必要的重複,模組也變得難以理解等等。

阻止程式碼的腐化、追求更好的架構和設計、寫出更好的代碼,當然是我輩所追求的目標。儘管面對的可能是不同的問題和不同的環境,那些優質、穩固、具有反脆弱特質的程式碼也必然會有些共通之處。例如說:需要具有面對改變的能力、具有方便管理的能力、具有隱藏複雜性的能力。

因此,大前輩們整理並提出了一些可以致力的方向,也就是所謂的「原則」。如同心法、教義一般,只要實作的同時將其牢記在心,就能讓我們作為一些行動的準則和依據。

……

閱讀全文


菜雞與物件導向 (8): 內聚、耦合

做為前後段落的分水嶺,這篇文章我將紀錄一下 「內聚」(Cohesion)「耦合」(Coupling),這兩者是評估一個類別或元件的重要概念。

在實務上,為了提升擴展性,降低維護成本等因素,我們對於單個類別或元件,會有著 「低耦合」「高內聚」 的期待。例如我們在 菜雞與物件導向 (3): 封裝 中,我們就有提到封裝的好壞相當重要,其中也包含了「提高類別內的內聚性,降低對外的耦合性」。那麼,到底什麼是內聚,什麼又是耦合呢?

內聚

「把需要的程式和資料都包裝在同一個模組內,使得該模組能夠做為一個單獨的個體執行」

白話一點說,就是就是把用到的東西都打包到一處,該有的自己都有了,所以即使單獨一個人也能完成工作的能力、可以自己 Carry 整場不用看豬隊友臉色的能力。越能自己單幹,越不需要依賴其他類別的時候,內聚力也就越高。

也就是說:如果你的類別什麼都要依賴其他類別,像小嬰兒一樣需要呵護照顧,那內聚力就很低。反之,如果像野外求生大師,啥都靠自己,那內聚力就超高。

內聚代表的是該模組的獨立性,當這個模組可以獨力完成工作,就代表我們能夠重複使用它,且不需要擔心影響到其他模組。

並且也基於這點,我們不用擔心變動這個模組時需要先處理其他的模組,因為這個工作所需的都包含在模組內了,這樣就可以單獨修改該模組,減少維護成本。

例如你的筆已經包含了所有寫字工具的條件,具有墨水跟筆芯等等,可以只使用筆就完成寫字這個工作。那麼我們就可以隨身帶著,在任何需要的時候重複使用它,而不用擔心我們會不會漏了什麼必要零件沒有帶出門。同時,如果我們需要換筆芯或墨水,我們也知道要更換的部份就在筆裡面,不需要去找鉛筆盒中別的地方。

……

閱讀全文


菜雞與物件導向 (7): 介面

如果說繼承是用來表明物件「屬於什麼」;那麼介面就是用來表明物件「能做什麼」。

如果說封裝是將物件視作一個整體,是隱藏複雜度;那麼介面就是封裝精神的體現。

如果說多型是指藉著繼承後能實作不同的行為的可能性達到擴展的彈性;那麼介面就是在實作多型。

介面就是這麼厲害,這麼瀟灑。介面就是我大哥,今天誰不服介面,對不起!我們不認識。

介面就像是針對類別的實作、物件的行為去做規定的一個契約書,會先定義好要實作這個介面的類別所必須要有的方法,而當我們建立符合這個介面的類別時,就必須實作出所有介面中定義好方法才可以。……這樣說起來實在太繞口,總而言之介面的核心概念只有一條:

我不在乎你是誰,我只在乎你能做什麼。

還是公司誠徵工程師的例子

由於介面基本上就是封裝繼承多型抽象之大雜燴,所以我們把前面多型的小明小華例子稍微修改來用吧。也就是以公司徵人的方式去理解介面。

介面就像是老闆開出來的要求列表,例如說:要會寫 C#、要會寫 SQL、要會 VB…等等,於是老闆就貼出了徵人啟示,要求新來的員工必須要有 IProgrammer 寫的能力:

public interface IProgrammer
{
    void WriteCSharp();
    void WriteSQL();
    void WriteVB();
}

特別注意和前面多型的例子的不同處,介面只需要先定義好該做的事,裡面怎麼做不需要管;所以只需要宣告要求的方法,不需要撰寫方法本體

於是今天小華就又(?)來面試了,但是他其實並不會寫 C#:

public class Hua : IProgrammer
{
    // Error: Hua 未實作 IProgrammer.WriteCSharp()

    public void WriteSQL() { /* Work */ }
    public void WriteVB() { /* Work */ }
}

這時候編譯器就會跳出錯誤了:很抱歉,你不符合我們 IProgrammer 的規定,因為我們只喜歡訓練精英(略),請你實作完之後再來。否則你就不能掛上我們 : IProgrammer 的頭銜。

……

閱讀全文


菜雞與物件導向 (6): 抽象、覆寫

備註:這邊的抽象是指程式語言中的抽象類別,而非抽象化

抽象的概念很直接,請回想一下前面的例子就可以了:

當我們在用卡牌的例子時,雖然怪獸卡跟魔法卡都繼承了 Card 這個類別,但是我們仍然能
new Card() 來建立一張新卡牌,那…怪怪的吧,這張卡牌到底是什麼呀,空白的卡片嗎?

又或是動物的例子,我們的狗跟貓都繼承了哺乳類,那我們能實例化一個哺乳類嗎?我們的狗跟鳥都是動物,那我們能實例化一個動物嗎?

小明跟小華都繼承了工程師,那我們能 new 一個工程師嗎…?

有些類別就是這樣,它們負責定義共通的那些特性,然而它們本身不應該被實體化成一個物件,這種類別我們就應該把它們標記為抽象類別

抽象類別在 C# 裡用 abstract 這個修飾詞來表示,可以加在類別或方法上。例如 abstract class Animal 就代表動物這個類別是個抽象類別,它不能被實例化。

而當加在方法上時,例如 public abstract void Eat() 就是代表這個進食的方法無法被叫用,只能由繼承者去重新定義這個方法。

abstract class Animal
{
    public string color { get; set; }
    public abstract void Eat();
}

那麼繼承者們,也就是衍生類別如何去重新定義父類別的方法呢?

所謂「欲戴王冠,必 Override」,這時候就必須使用覆寫(override

覆寫是指對於像是前述的抽象方法時,在同名的方法前加上 override 關鍵字就可以讓程式知道你要覆寫這個方法(你不覆寫的話,編譯器還會生氣)。

例如前述的 Eat,狗就可以用 public override void Eat() 的方式去覆寫吃東西這個方法:

public class Dog : Animal
{
    public string color { get; set; } = "Black";
    public override void Eat()
    {
        /* 嚼嚼嚼 */
    }
}
……

閱讀全文