September 28, 2023

Viiisit [Development] - SOLID!

#web development

繼上篇的物件導向程式設計概念,近期在面試上有遇到一個很重要的相關觀念,
當時不知道為什麼,現在整理出來分享~

:有聽過 SOLID 嗎?
:SOLID 是什麼?

SOLID 是什麼?

SOLID 是一組五個物件導向程式設計的基本原則,旨在幫助開發者創建可維護、可擴展且易於理解的程式碼。這些原則有助於確保軟體設計具有高內聚性和低耦合性,這對於長期維護和協作開發非常重要。

  1. 單一職責原則 (Single Responsibility Principle - SRP)

    • 一個模組應該只對唯一的一個角色負責。
    • 通過將不同的職責分開,我們可以輕鬆地修改和擴展每個類別,而不會影響其他程式碼。

    假設我們有一個名為 Employee 的類別,
    表示公司的員工,我們將根據 SRP 創建兩個具有不同職責的類別。

    1. EmployeeInfo 類別(負責員工的基本資訊):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class EmployeeInfo
    attr_reader :name, :employee_id

    def initialize(name, employee_id)
    @name = name
    @employee_id = employee_id
    end

    def display_info
    "Name: #{@name}, Employee ID: #{@employee_id}"
    end
    end
    1. EmployeeSalary 類別(負責員工的薪水計算):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class EmployeeSalary
    attr_reader :employee, :salary

    def initialize(employee, salary)
    @employee = employee
    @salary = salary
    end

    def calculate_bonus
    @salary * 0.1 # 假設獎金是薪水的10%
    end
    end

    以上兩個不同的類別,每個類別都具有不同的職責。

    • EmployeeInfo 負責管理員工的基本資訊(如姓名和員工ID)
    • EmployeeSalary 負責計算員工的薪水和獎金。
      這兩個類別各自專注於其單一職責,符合 SRP 的原則。

    attr_reader 是 Ruby 中一個用於自動生成讀取實例變數(instance variables)的方法的簡便方式。主要用途是使類別的實例變數能夠被外部程式碼讀取,而不需要額外編寫自訂的 getter 方法。attr_reader 通常用於創建只讀取(read-only)的實例變數,這意味著外部程式碼可以訪問變數的值,但不能修改它。
    以下是 attr_reader 的基本用法:

    1
    2
    3
    4
    5
    6
    7
    class MyClass
    attr_reader :my_variable

    def initialize(value)
    @my_variable = value
    end
    end

    在這個示例中,attr_reader :my_variable 被用來定義一個名為 my_variable 的實例變數,並自動生成了一個名為 my_variable 的讀取方法(getter 方法)。這意味著我們可以在外部程式碼中訪問 my_variable 的值,如下所示:

    1
    2
    obj = MyClass.new(42)
    puts obj.my_variable # 輸出: 42

    需要注意的是,attr_reader 只創建了讀取方法,不允許外部程式碼修改實例變數的值。如果你需要允許變數被修改,則可以使用 attr_writerattr_accessor,分別用於生成寫入方法(setter 方法)或同時生成讀取和寫入方法。


  1. 開放-封閉原則 (Open-Closed Principle - OCP)
    • 這個原則強調軟體實體(如類別、模組、函數等)應該是開放擴展的,但封閉修改的。
    • 可以擴展現有的功能,而不必修改現有的程式碼。通常,這可以通過使用抽象化和多型來實現。
      可以往前篇回顧抽象化和多態性範例:Day 13 - 理解 Ruby - 物件導向程式設計語言

  1. 里氏替換原則 (Liskov Substitution Principle - LSP)

    • 這個原則強調子類別應該能夠替換其基類別,而不會破壞程式的正確性。換句話說,如果某個程式使用基類別,則應該可以安全地替換為其子類別,而不會導致不正確的行為。

    假設我們有一個基類別 Bird,代表鳥類,並且有一個 fly 方法用來表示鳥類飛行的行為:

    1
    2
    3
    4
    5
    class Bird
    def fly
    "This bird can fly."
    end
    end

    然後,我們建立了一個子類別 Penguin,代表企鵝,但企鵝不能飛行,所以我們需要重寫 fly 方法:

    1
    2
    3
    4
    5
    class Penguin < Bird
    def fly
    "This penguin can't fly."
    end
    end

    現在,我們可以使用里氏替換原則,確保子類別 Penguin 可以替換其基類別 Bird,並且在不影響程式正確性的情況下重寫 fly 方法。

    1
    2
    3
    4
    5
    bird = Bird.new
    penguin = Penguin.new

    puts bird.fly # 輸出: "This bird can fly."
    puts penguin.fly # 輸出: "This penguin can't fly."

    雖然 Penguin 重寫了 fly 方法,但它仍然可以替換 Bird 並遵守里氏替換原則。這使得我們能夠根據需要擴展程式碼,同時保持對基類別的一致性使用,而不會破壞程式的正確性。


  1. 介面隔離原則 (Interface Segregation Principle - ISP)

    • 這個原則建議將一個大型的、具有多個方法的介面分解成多個小型的、專注於特定用途的介面。這樣可以避免實現不需要的方法,從而減少類別之間的耦合性,並提高代碼的可讀性。

    假設我們正在建立一個文件系統應用程式,需要處理文件(File)和資料夾(Folder)。我們想確保這些元素都可以執行基本的操作,如開啟(open)、關閉(close)和刪除(delete)。

    我們可以創建兩個模組,分別是 OpenableDeletable

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    module Openable
    def open
    puts "Opening..."
    end
    end

    module Deletable
    def delete
    puts "Deleting..."
    end
    end

    接下來,我們創建文件(File)和資料夾(Folder)類別,並混入相應的模組:

    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
    class File
    include Openable
    include Deletable

    def initialize(name)
    @name = name
    end

    def name
    @name
    end
    end

    class Folder
    include Deletable

    def initialize(name)
    @name = name
    @contents = []
    end

    def add_item(item)
    @contents << item
    end
    end

    在以上例子中,FileFolder 類別分別混入了 OpenableDeletable 模組。這樣,File 可以執行開啟、關閉和刪除操作,而 Folder 只能執行刪除操作。 確保每個類別僅實現了他們需要的方法,而不包含不必要的方法,從而減少不必要的耦合。 如果未來需要添加新的操作,可以輕鬆擴展相應的模組,而不會影響其他部分的程式碼。

  2. 依賴反轉原則 (Dependency Inversion Principle - DIP)

    • 這個原則強調高層次模組不應該依賴於低層次模組,兩者都應該依賴於抽象。此外,抽象不應該依賴於具體實現,而具體實現應該依賴於抽象。這可以通過使用介面或抽象類別來實現。

    假設我們有一個介面 Device 代表設備,並且我們希望能夠將不同類型的設備連接到應用程式。
    設備可以執行 connect 操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    # 設備介面
    class Device
    def connect
    end
    end

    # 具體設備類別
    class Keyboard < Device
    def connect
    puts "Keyboard connected."
    end
    end

    class Mouse < Device
    def connect
    puts "Mouse connected."
    end
    end

    我們定義了一個 Device 類別,並建立兩個具體的設備類別 KeyboardMouse,分別覆蓋了 connect 方法以提供實際的連接功能。

    這樣,當我們需要添加新的設備類別時,只需創建一個新的設備類別,覆蓋 connect 方法,這種方式可以實現 DIP,因為高層模組(使用這些設備的程式碼)依賴於 Device 抽象介面,而不需要關心具體的設備類別。


SOLID 原則有助於創建有彈性、可擴展和易於維護的程式碼。這些原則通常與設計模式和良好的軟體設計實踐一起使用,以實現高品質的軟體。當設計和編寫程式碼時,考慮這些原則可以幫助避免常見的設計問題並提高代碼的質量。
但是,即便這樣簡短的講完這些內容,還是必須反覆去複習才會理解!

今天先分享至此,我們下篇待續~!

參考資料:
物件導向程式設計基本原則 - SOLID
J.J.’s Blogs