返回文章列表

掌握資料封裝的核心:深入解析 Getter 與 Setter

11 分鐘
OOP軟體設計

在物件導向程式設計 (OOP) 中,直接開放外部存取類別內部的私有屬性,往往會導致資料狀態不可控、缺乏校驗機制,進而破壞系統的穩定性。Getter (讀取器) 與 Setter (寫入器) 的出現,正是為了解決這個核心痛點。

這兩者本質上是定義在類別中的特殊存取方法,讓開發者能在物件屬性被「讀取」或「寫入」的當下攔截操作,進行資料校驗、動態運算或觸發副作用 (Side Effects),進而達到資料封裝 (Encapsulation) 的目的。

雖然許多前端工程師是在 JavaScript (ES6+) 中首次接觸到這個語法,但存取器 (Accessors) 其實是軟體工程中通用的設計概念,在現代主流語言中皆有對應的實作思維。


為什麼需要 Getter 與 Setter?

如果只看表面的存取行為,直接操作屬性與透過存取器操作似乎相同:

TypeScript
// 直接存取 (不推薦:缺乏控制力)
user.age = -5; // 髒數據成功寫入

// 透過存取器存取 (推薦:具備控制力)
user.age = -5; // 內部攔截,拋出錯誤或拒絕寫入

核心優勢

  1. 資料驗證 (Validation):在資料寫入前,確保其符合業務邏輯 (例如年齡不能為負數)。
  2. 動態計算屬性 (Computed Properties):屬性值不需要實際存在於記憶體中,而是在讀取時動態計算產生 (例如透過「生日」動態計算出「年齡」)。
  3. 唯讀或唯寫屬性:只實作 Getter 即可建立合法的唯讀屬性,防止外部惡意修改。
  4. 追蹤與紀錄 (Logging & Side Effects):在屬性變更時,自動觸發日誌紀錄、狀態同步或 UI 更新。

語法與實作範例

以下針對同一個業務情境 (擁有私有年齡屬性、具備負數校驗邏輯),展示不同語言的標準實作方式,幫助你融會貫通其核心邏輯。

1. JavaScript / TypeScript 實作

使用 getset 關鍵字來定義屬性存取器。配合 ECMAScript 的原生私有欄位 ()#),可以達到完美的封裝,讓存取方法在外部看起來就像操作一般屬性。

TypeScript
// TypeScript
class User {
  // 使用 # 宣告原生私有屬性,防止外部直接存取
  #age: number = 0;

  constructor(age: number) {
    this.age = age; // 呼叫 setter 進行初始校驗
  }

  // Getter: 當執行 user.age 時觸發
  get age(): number {
    return this.#age;
  }

  // Setter: 當執行 user.age = value 時觸發
  set age(value: number) {
    if (value < 0) {
      throw new Error('Age cannot be negative.');
    }
    this.#age = value;
  }
}

// 實務應用
const user = new User(25);
console.log(user.age); // 25 (觸發 get)
user.age = 30;         // 觸發 set,修改成功
// user.age = -5;      // 拋出 Error: Age cannot be negative.

2. Python 實作

Python 採用 @property 裝飾器 (Decorator) 來實作 Getter,並使用 @<property_name>.setter 來實作 Setter。這讓程式碼在保持簡潔屬性存取語法的同時,兼具方法封裝的能力。

Python
# Python
class User:
    def __init__(self, age: int):
        self.age = age # 呼叫 setter 進行初始校驗

    @property
    def age(self) -> int:
        """Getter 裝飾器"""
        return self._age # 慣例上使用單底線表示受保護的私有變數

    @age.setter
    def age(self, value: int) -> None:
        """Setter 裝飾器"""
        if value < 0:
            raise ValueError("Age cannot be negative.")
        self._age = value

# 實務應用
user = User(25)
print(user.age)  # 25 (隱式呼叫 getter)
user.age = 30    # 隱式呼叫 setter

3. Java 實作

Java 作為嚴謹的傳統物件導向語言,不具備將方法偽裝成屬性存取的語法糖。Java 遵循標準的 JavaBean 規範,透過明確的 getX()setX() 公開方法來存取 private 欄位。

Java
// Java
public class User {
    private int age; // 宣告私有屬性

    public User(int age) {
        setAge(age); // 呼叫 setter 進行初始校驗
    }

    // 標準 Getter 方法
    public int getAge() {
        return this.age;
    }

    // 標準 Setter 方法
    public void setAge(int age) {
        if (age < 0) {
            throw new IllegalArgumentException("Age cannot be negative.");
        }
        this.age = age;
    }
}

// 實務應用
public class Main {
    public static void main(String[] args) {
        User user = new User(25);
        System.out.println(user.getAge()); // 25 (明確呼叫方法)
        user.setAge(30);                   // 修改成功
    }
}

核心實務應用場景

場景一:動態計算屬性 (Computed Property)

物件內部不儲存最終值,而是由既有資料即時推導,避免資料同步出錯。

TypeScript
// TypeScript 範例
class Product {
  constructor(public price: number, public discount: number) {}

  // 唯讀計算屬性:外部只需讀取,不需手動維護實際總價
  get finalPrice(): number {
    return this.price * (1 - this.discount);
  }
}

const item = new Product(100, 0.2);
console.log(item.finalPrice); // 80

場景二:觸發副作用 (Side Effects / Reactive Patterns)

在屬性改變的當下,通知系統其他模組進行響應,常見於前端框架的響應式核心或後端狀態機。

TypeScript
// TypeScript 範例
class Task {
  #status: string = 'pending';

  get status(): string {
    return this.#status;
  }

  set status(newStatus: string) {
    const oldStatus = this.#status;
    this.#status = newStatus;
    // 攔截變更,觸發系統事件
    this.onStatusChange(oldStatus, newStatus);
  }

  private onStatusChange(oldState: string, newState: string) {
    console.log(`[LOG] Task state transitioned from ${oldState} to ${newState}`);
  }
}

技術誤區與陷阱

  1. 避免無限遞迴 (Stack Overflow): 在 JavaScript 與 Python 的 Setter 中,最常見的錯誤是方法名與內部賦值的屬性名完全相同。

    TypeScript
    // 錯誤示範
    set age(value) {
      this.age = value; // 錯誤:這會再次觸發 set age(),導致無限迴圈與記憶體溢位
    }

    解決方案:內部儲存變數必須使用不同的名稱 (例如原生私有屬性 #age 或慣例受保護屬性 _age)。

  2. 效能考量: Getter 內部不應該執行高耗時的複雜運算 (例如大量的 I/O 操作或深層資料遍歷)。因為外部呼叫者從語法上看它只是個「屬性存取」,會預期它在 O(1) 常數時間內返回。耗時操作應明確定義為一般「方法 (Method)」。


總結

Getter 與 Setter 是實踐軟體工程「開放-封閉原則 (Open-Closed Principle)」的重要基礎。它允許類別在不改變外部調用介面的前提下,自由修改內部的資料校驗邏輯與儲存結構。正確使用存取器,能讓你的程式碼在面對業務變更時具備更高的健壯性與可維護性。