C# Null Object Pattern

使物件可以承擔處理null的責任

(KJH) Kuan-Jung, Huang
7 min readMay 15, 2019

系列文:

C# 處理空值(Null)物件

C# Null Object Pattern

甚麼是 Null Object Pattern

是一種軟體設計模式。目的是使物件可以承擔處理null的責任。

透過物件導向的概念來刪除或是降低處理空值參考的數量,用以達到降低重複性檢查是否為空值的程式邏輯來達到減少 codebase 的負擔。

Why Null Object Pattern

所以根據前面對於 Null Object Pattern 的介紹,我們可以知道導入這樣的設計模式,可以讓程式處理空值,降低執行期發生例外的狀況,使軟體變得更堅固,在開發的方面,因為我們的物件可以處理 null ,我們可以減少很多個空值檢查,這意味著我們也同時降低空值檢查而造成分支(if null then do someting…)的狀況,降低分支數量除了使程式碼執行變快之外,做測試也比較容易,不用特定針對檢查是否為 Null 再撰寫好幾個測試案例。

Pattern Overivew

我們將上面的概念化成下面的關係圖:

大家可以注意到,我們的共用抽象層讓實作邏輯跟空值物件繼承,透過這樣的設計,我們讓客戶端程式碼處理空值物件可以跟一般物件一樣的處理方式。這樣講或許還是有些抽象,我再畫一張圖,並且搭配下面的程式碼修改來讓大家看到 Null Object Pattern 的導入過程。

導入 Null Object Pattern 到實際應用中

我們建立一個專案叫作 GameConsole

其中我們定義 ISpecialDefence

interface ISpecialDefence
{
int CalculateDamageReduction(int totalDamage);
}

以及各種特殊防守的類別,其中一般防守就是不會做任何防守

class IronBonesDefence : ISpecialDefence
{
public int CalculateDamageReduction(int totalDamage)
{
return 5;
}
}

class DiamondSkinDefence : ISpecialDefence
{
public int CalculateDamageReduction(int totalDamage)
{
return 1;
}
}

然後在 PlayerCharacter.cs 中針對攻擊做一些邏輯設計

class PlayerCharacter
{
private readonly ISpecialDefence _specialDefence;

public PlayerCharacter(ISpecialDefence specialDefence)
{
_specialDefence = specialDefence;
}

public string Name { get; set; }
public int Health { get; set; }

public void Hit(int damage)
{
int damageReduction = 0;

if (_specialDefence != null)
{
damageReduction = _specialDefence.CalculateDamageReduction(damage);
}

int totalDamageTaken = damage - damageReduction;

Health -= totalDamageTaken;

Console.WriteLine("{0}'s health has been reduced by {1} to {2}.", Name, totalDamageTaken, Health);
}
}

最後在 Program.cs 中做呼叫

class Program
{
static void Main(string[] args)
{
PlayerCharacter sarah = new PlayerCharacter(new DiamondSkinDefence())
{
Name = "Sarah"
};

PlayerCharacter amrit = new PlayerCharacter(new IronBonesDefence())
{
Name = "Amrit"
};

// Gentry 沒有任何特殊防守
PlayerCharacter gentry = new PlayerCharacter(null)
{
Name = "Gentry"
};

sarah.Hit(10);
amrit.Hit(10);
gentry.Hit(10);


Console.ReadLine();
}
}

程式運行得很好,可是有個地方我們可以改進,就是以下這段,我們為了處理普通防守,結果我們在 Hit 的 Method 中要特別定義如果 _specialDefence 存在的話,才設立 damageReduction。所以這邊可以使用 Null Object Pattern 來讓 Hit 變得更好維護。

if (_specialDefence != null)
{
damageReduction = _specialDefence.CalculateDamageReduction(damage);
}

為了達成目的,我們再建立一個 class 叫作 NullDefense,他一樣繼承 ISpecialDefence

class NullDefense : ISpecialDefence
{
public int CalculateDamageReduction(int totalDamage)
{
return 0; // no operation / "do nothing" behavior
}
}

接著我們就可以在 PlayerCharacter 中做調整,將_specialDefence.CalculateDamageReduction(damage); 直接使用

public void Hit(int damage)
{
int totalDamageTaken = damage - _specialDefence.CalculateDamageReduction(damage);
Health -= totalDamageTaken;
Console.WriteLine("{0}'s health has been reduced by {1} to {2}.", Name, totalDamageTaken, Health);
}

並且在 Program.cs 中,修改 Gentry 的 Construction

PlayerCharacter gentry = new PlayerCharacter(new NullDefense())
{
Name = "Gentry"
};

這樣子,我們的程式就可以完美的處理 Null 的問題,我們仔細看一下流程,當 gentry 被創造後,在 PlayerCharacter 的 _specialDefence 屬性就會被定義為 NullDefense, 當我呼叫 _specialDefence.CalculateDamageReduction 這個 Method 時,就會去執行 NullDefense 的 CalculateDamageReduction,程式 return 0 。

結語

如果在處理值的時候,確定取得的每個值都必定不會是空值,使用 error handling 來處理遇到空值的問題,如果回傳值可以是空值或者可不存在,就直接回傳 Null Object,舉例來說,如果數字加上某數字必定會回傳一個數字 ,在遇到回傳空值時就使用 error handling,這樣也比較好除錯。

所以在開發的時候要確定哪些情況是回傳 Null,或者是做 error handling。

--

--

(KJH) Kuan-Jung, Huang
(KJH) Kuan-Jung, Huang

Written by (KJH) Kuan-Jung, Huang

CTO at Metablox.co, Founder of AI Users Community in Taiwan

No responses yet