今天我們要討論的是物件導向的第四個特性,封裝 (Encapsulation)。封裝的概念是把類別中的細節,例如變數跟函式的實作方式等,都保留在該類別裡面,不讓類別外的程式影響到這些細節。這樣做的目的是為了讓類別在提供功能的同時可以保持安全性跟獨立性,簡單的說,就是對於類別外的程式部分來說,我們只需要知道這個類別可以用來做什麼,但我們不用管它是怎麼做的,也不應該插手去改變它的任何事。這就像是我們在平常生活中身邊的物品,如電腦、手機等,我們知道它們可以用來做什麼,但我們不用知道它們運作的原理。
我們先來看一下一個沒有進行封裝時可能會遇到的問題:[C#]
using System;
public class Enemy {
public int hp;
public int atk;
public int def;
...
public void hurt(int power) {
hp -= Math.Max(power - def, 1);
if (hp <= 0) {
Console.WriteLine("Enemy Died!");
}
else {
... // 敵人反擊玩家
}
}
}
public class UnencapsulationExample
{
public static void Main(string[] args)
{
int playerAtk = 10;
Enemy enemy = new Enemy();
// 我們在類別外設定敵人的能力
enemy.hp = 50;
enemy.hp = 5; // 這裡應該是在設定敵人的攻擊力, 但我們不小心打錯字,修改了 enemy 的血量!
enemy.def = 5;
enemy.hurt(playerAtk); // 原本這裡敵人只受了一次傷,應該要反撃玩家,可是因為我們不小心修改了 enemy 的血量,結果顯示:Enemy Died!
}
}
當我們沒有把類別封裝起來時,我們便有可能會在程式的任何位置修改類別內的一些內容,而這樣做便有可能因為誤操作而導致出現了一些我們不希望的結果,而且當程式變得龐大跟複雜時,這會使除錯變得非常困難。可是,如果我們把類別封裝起來,難道不會發生同樣的事情嗎?當然會,可是由於我們把所有的內容跟實作的細節都放在類別中,因此我們便可以把可能出錯的範圍大幅減少,幫助我們更容易找出錯誤。
那我們要怎麼讓把類別的內容封裝起來呢?那就是使用**保護層級 (Protection Level)**來限制變數跟函式的存取範圍。保護層級是我們在宣告變數及定義函式時放在它們前面的修飾詞,我們可以很常在類別中看到:
保護層級修飾詞
| 變數名稱
V V
public int num;
^
資料型態
保護層級修飾詞
| 函式名稱
V V
public int getSum(...) { ... }
^ ^
| 函式參數
回傳資料型態
以下是大多數支援物件導向的程式語言會提供的保護層級修飾詞:
1. public 公開
使用 public 修飾詞時,這代表了該變數或函式可以在程式中的任意地方自由存取。也就是說,變數可以在類別內及外被隨意修改,而函式則可以在類別內及外被呼叫或是被子類別覆寫。
2. protected 保護
使用 protected 修飾詞時,這代表了該變數或函式可以在自身或子類別中存取,而自身或子類別外則是被隱藏起來。也就是說,變數可以在自身或子類別中被修改,而函式則可以在自身或子類別中被呼叫或是被子類別覆寫。
3. private 私人
使用 private 修飾詞時,這代表了該變數或函式只可以在自身類別中存取,而自身類別外則是被隱藏起來。也就是說,變數只可以在自身類別中被修改,而函式則只可以在自身類別中被呼叫,而且不可以被子類別覆寫。
讓我們來看一個例子:[C#]
using System;
public class Parent {
public int publicInt;
protected int protectedInt;
private int privateInt;
public Parent() {
publicInt = 0;
protectedInt = 0;
privateInt = 0;
publicFunction();
protectedFunction();
privateFunction();
}
public virtual void publicFunction() {
Console.WriteLine("Public Function From Parent Class");
}
protected virtual void protectedFunction() {
Console.WriteLine("Protected Function From Parent Class");
}
private void privateFunction() {
Console.WriteLine("Private Function From Parent Class");
}
}
public class Child : Parent { // 繼承 Parent 類別
public Child() {
publicInt = 10;
protectedInt = 10;
privateInt = 10; // 錯誤! Child 類別沒辦法存取 Parent 類別的 privateInt 變數
publicFunction();
protectedFunction();
privateFunction(); // 錯誤! Child 類別沒辦法呼叫 Parent 類別的 privateFunction 函式
}
public override void publicFunction() {
Console.WriteLine("Public Function From Child Class");
}
protected override void protectedFunction() {
Console.WriteLine("Protected Function From Child Class");
}
private override void privateFunction() { // 錯誤! Child 類別沒辦法覆寫 Parent 類別的 privateFunction 函式
Console.WriteLine("Private Function From Child Class");
}
}
public class PublicProtectedPrivateExample
{
public static void Main(string[] args)
{
Parent parent = new Parent();
Child child = new Child();
parent.publicInt = 20;
parent.protectedInt = 20; // 錯誤! 主函式中沒辦法存取 Parent 類別的 protectedInt 變數
parent.privateInt = 20; // 錯誤! 主函式中沒辦法存取 Parent 類別的 privateInt 變數
parent.publicFunction();
parent.protectedFunction(); // 錯誤! 主函式中沒辦法存取 Parent 類別的 protectedFunction 函式
parent.privateFunction(); // 錯誤! 主函式中沒辦法存取 Parent 類別的 privateFunction 函式
}
}
從上面的例子中我們可以看到在使用不同保護層級的修飾詞後:private 的變數及函式只可以在自身類別中存取及呼叫;protected 的變數及函式可以在子類別中存取、呼叫及複寫;只有 public 的變數及函式可以在無關類別及主函式中存取及呼叫。因此,透過適當使用保護層級修飾詞,我們便能控制變數及函式的可存取範圍。
封裝的核心概念是隱藏類別中的資訊,那如果我們需要在類別外查詢或使用這些資訊時,該怎麼辦呢?封裝的原則便建議我們去使用 修改函式 (Mutator/Setter) 跟 存取函式 (Accessor/Getter) 來作為類別外的存取資訊方式,它的意思是當我們需要在類別外查詢或修改類別中的變數時,應該在類別中準備 public 的函式讓類別外的程式呼叫來進行查詢或修改的行為來代替把變數設定為 public 讓我們可以在類別外直接存取。以下是把第一個例子修改為遵守封裝的原則後的版本:[C#]
using System;
public class Enemy {
private int hp; // 變數的保護層級變成 private
private int atk; // 變數的保護層級變成 private
private int def; // 變數的保護層級變成 private
public Enemy() { // 在建構子中為變數準備預設的初始值
hp = 100;
atk = 10;
def = 10;
}
public Enemy(int _hp, int _atk, int _def) { // 使用多載讓我們在類別外可以使用建構子來為變數初始化
hp = _hp;
atk = _atk;
def = _def;
}
// 我們不希望 hp 被隨意修改,因此只提供存取函式
public int getHP() { // hp 的存取函式,讓我們在類別外可以查詢 hp 的值
return hp;
}
public void setAtk(int _atk) { // atk 的修改函式,讓我們在類別外可以修改 atk 的值
atk = _atk;
}
public int getAtk() { // atk 的存取函式,讓我們在類別外可以查詢 atk 的值
return atk;
}
public void setDef(int _def) { // def 的修改函式,讓我們在類別外可以修改 def 的值
def = _def;
}
public int getDef() { // def 的存取函式,讓我們在類別外可以查詢 def 的值
return def;
}
public void hurt(int power) {
hp -= Math.Max(power - def, 1);
if (hp <= 0) {
Console.WriteLine("Enemy Died!");
}
else {
... // 敵人反擊玩家
}
}
}
public class EncapsulationExample
{
public static void Main(string[] args)
{
int playerAtk = 10;
Enemy enemy = new Enemy(50, 10, 10); // 透過建構子進行初始化
...
// enemy.def = 5; <-- 錯誤! 由於 Enemy 類別中的 def 變數的保護層級是 private,因此我們不能在主函式中直接修改它的值
enemy.setDef(5); // 使用修改函式修改 Enemy 類別中的 def 變數的值
...
enemy.hurt(playerAtk);
}
}
雖然使用修改及存取函式看起來跟我們直接存取變數的差別不大,而且還多了一個步驟,好像是多此一舉的想法。可是,透過修改及存取函式,我們可以讓對變數的修改行為都維持在類別中進行來保持類別自身對變數的控制權,而且,我們也可以在修改及存取函式中增加其他的檢查行為來減少修改變數後可能導致的錯誤。另一方面,我們可以透過設立修改及存取函式來決定及限制變數在類別外能否被查詢及修改。例如在上面的例子中,由於沒有 hp 的修改函式,因此我們便等於限制了 hp 變數在 Enemy 類別外只能被查詢而不能被修改。