這個做法講解了 Partial ClassesPartial Methods 建議的使用場景與背後原理。

Partial Classes 提供了一個機制能讓程式設計師把一個 Class 分割成多份,例如下面這段程式碼把 Employee 類別分割成兩份, 並且裡面包含著不同的方法,最後經過編譯器處理後會把它們重新集合起來,這種機制能讓我們把把一個大的類別分割成多個小類別,讓我們能更容易的讀懂內容。

public partial class Employee
{
	public void DoWork() {}
}

public partial class Employee
{
	public void GoToLunch() {}
}

public class Program
{
	public static void Main()
	{
		Employee emp = new Employee();
		emp.DoWork();
		emp.GoToLunch();
	}
}

另一種使用場景是在程式碼產生器,由於產生出來的程式碼不建議修改,因為可能把你修改的內容覆蓋掉,所以就可以透過 Partial Classes 把產生出來的程式碼分割成一份,然後把需要客製化的內容分割成另外一份就可以避免覆蓋的問題。

可以想像成同時有兩個開發者在開發同一個功能,並且由於是分割的檔案所以以後也沒有合併衝突的問題,這個對版本管理有幫助。

要想達到與程式碼產生器一起工作則需要添加一些額外的設計,這時候就可以用到 Partial Methods,首先程式碼產生器需要在建立出來的程式碼中 穿插 hooks,這樣在未來才可以讓工程師把客製化的程式碼掛上流程,不過使用 Partial Methods 有一些限制例如:

  1. 只能是 void 方法。
  2. 不能是 abstract 或 virtual 方法。
  3. 不能包含 out 引數。

有三種成員型別建議可以加上 Partial Methods 這樣在未來能讓開發者增加監控程式碼或者調整類別的行為。

  1. Mutator Methods
  2. Event Handlers
  3. Constructors

Mutator Methods

Mutator methods 泛指那些會修改狀態的任何方法,我們可以在修改過程中穿插一些程式碼,這樣就能做到事前驗證或者 狀態修改後進行記錄,這裡建議提供兩個擴展點,分別是變更前與變更後。

例如我可以產生 ReportValueChanging 方法用來在狀態改變之前呼叫,用於驗證或中斷變更,ReportValueChanged 方法用來在狀態改變之後呼叫,用於處理後續邏輯。

下面這段程式碼新增了兩個 Partial Methods 特別是不需要提供實做細節也能編譯成功,另外注意到 UpdateValue 方法 在狀態修改前呼叫 ReportValueChanging 還有在狀態修改後呼叫 ReportValueChanged

public partial class GeneratedStuff
{
	private struct ReportChange
	{
		public readonly int OldValue;
		public readonly int NewValue;
		public ReportChange(int oldValue, int newValue)
		{
			OldValue = oldValue;
			NewValue = newValue;
		}
	}
	private class RequestChange
	{
		public ReportChange Values { get; set; }
		public bool Cancel { get; set; }
	}
	
	partial void ReportValueChanging(RequestChange args);
	partial void ReportValueChanged(ReportChange values);
	private int storage = 0;
	
	public void UpdateValue(int newValue)
	{
		// Precheck the change
		RequestChange updateArgs = new RequestChange
		{
			Values = new ReportChange(storage, newValue)
		};
		ReportValueChanging(updateArgs);
		if (!updateArgs.Cancel)
		{
			storage = newValue;
			ReportValueChanged(new ReportChange(storage, newValue));
		}
	}
}

這種做法的好處是就算 Partial Methods 不提供實做細節也不會影響編譯流程,因為編譯器會自動把這些沒有實做的方法移除掉, 這樣當使用者想加入額外邏輯就實做 Partial Methods,不想也沒有關係,因為也不會有影響。

如果沒有提供實做細節,編譯器會把自動移除方法,類似下方這樣。

public void UpdateValue(int newValue)
{
   RequestChange updateArgs = new RequestChange
   {
       Values = new ReportChange(this.storage, newValue)
   };
   if (!updateArgs.Cancel)
   {
       this.storage = newValue;
   }
}

如果想要也可以提供細節,像下方這樣可以做到額外的日誌記錄功能,或者事前驗證功能。

public partial class GeneratedStuff
{
   partial void ReportValueChanging(RequestChange args)
   {
       if (args.Values.NewValue < 0)
       {
           Console.WriteLine($@"Invalid value: {args.Values.NewValue}, canceling");
           args.Cancel = true;
       }
       else
           Console.WriteLine($@"Changing {args.Values.OldValue} to {args.Values.NewValue}");
   }
   partial void ReportValueChanged(ReportChange values)
   {
       Console.WriteLine($@"Changed{values.OldValue} to {values.NewValue}");
   }
}

Event Handlers

其實事件想要做到的事情也差不多,就是在事件發生前與事件發生後提供兩個 hooks 讓我們掛上自己的程式碼,例如下面這樣。

public partial class GeneratedStuff
{
    public event EventHandler<EventArgs> SomeEvent;

    partial void BeforeEventTriggered();
    partial void AfterEventTriggered();

    public void TriggerEvent()
    {
        // 事件觸發前
        BeforeEventTriggered();
        // 觸發事件
        SomeEvent?.Invoke(this, EventArgs.Empty);
        // 事件觸發後
        AfterEventTriggered();
    }
}

Constructors

最後是建構函式,能夠在初始化過程中新增部分方法 Initialize(),之後開發者就能透過實作此方法插入自己的初始化邏輯。

public partial class GeneratedStuff
{
    private int storage;
    partial void Initialize(); // 提供給開發者的初始化擴展點
    public GeneratedStuff() : this(0) { } // 預設建構函式
    public GeneratedStuff(int initialValue) // 帶參數建構函式
    {
        storage = initialValue;
        Initialize(); // 呼叫部分方法以執行用戶邏輯
    }
}

Summary

Partial ClassesPartial Methods 能夠讓程式碼產生器和開發者的合作更加靈活,並且讓開發者不會去修改自動生成的程式碼 避免被覆蓋的風險,也能在事件或建構函式中穿插 Partial Methods 提供擴展點做到日誌追蹤等功能,關鍵就是提供擴展點避免人們手動去修改 更關鍵的程式碼,這種模式很適合用來開發框架。