###### tags: `Clen Code` `Refactor` `CSharp` # Use Delgate to Clean Code Record ## Old Code脈絡 近期在寫RS232設備控制,在下完Command後需要開Task Check Response後,再開一條Wait Task偵測TimeOut這件事情。因此程式碼就會非常的長,如下 ``` csharp = public async Task<bool> CheckSetMaxSwitchChannel(int ch) { var tokenSouce = new CancellationTokenSource(); CancellationToken ct = tokenSouce.Token; // Send Command SetSwitchChannel(ch); var readBuffer = new List<byte>(); byte[] readBufftemp = new byte[9]; SpinWait.SpinUntil(() => false, 50); try { // Read Task var readTask = Task.Run(() => { ct.ThrowIfCancellationRequested(); while (readBuffer.Count <= 0 || readBufftemp[8] == 0) { if (Com.BytesToRead > 8) { Com.Read(readBufftemp, 0, Com.BytesToRead); readBuffer.AddRange(readBufftemp); } if (ct.IsCancellationRequested) { ct.ThrowIfCancellationRequested(); } } }, tokenSouce.Token); // Timeout Task Check if (await Task.WhenAny(readTask, Task.Delay(5000)) == readTask) { // Parase var isSuccess = readBuffer[5] == 0x00 ? true : false; return isSuccess; } else { tokenSouce.Cancel(); throw new Exception("Read Time Out!"); } } catch { throw; } } ``` 而另一個設備我也是用此邏輯去撰寫,當Read Task完成任務後,會進入Parase區段,針對讀到資料去做解析。若在Task.Delay(5000)時間到,Read Task還為滿足條件,則會拋Read Time Out訊息。另一個設備讀取Code如下 ``` csharp = public async Task<double> GetCrossTLK() { var tokenSouce = new CancellationTokenSource(); CancellationToken ct = tokenSouce.Token; // Send Command Com.DiscardInBuffer(); Com.Write("GTLK\r"); var readBuffer = new List<byte>(); byte[] readBufftemp = new byte[8]; SpinWait.SpinUntil(() => false, 50); try { // Read Task var readTask = Task.Run(() => { ct.ThrowIfCancellationRequested(); var endTagIndex = 0; while (endTagIndex <= 0) { if (Com.BytesToRead > 3) { Com.Read(readBufftemp, 0, Com.BytesToRead); readBuffer.AddRange(readBufftemp); Array.Clear(readBufftemp, 0, readBufftemp.Length); endTagIndex = Array.IndexOf(readBuffer.ToArray(), RS232EndTag); } if (ct.IsCancellationRequested) { ct.ThrowIfCancellationRequested(); } } }, tokenSouce.Token); // Timeout if (await Task.WhenAny(readTask, Task.Delay(5000)) == readTask) { // Parase var value = readBuffer.ToArray(); var trkValue = Encoding.UTF8.GetString(value); var tlkValue = Convert.ToDouble(trkValue) * -0.01; tokenSouce.Cancel(); return tlkValue; } else { tokenSouce.Cancel(); throw new Exception("Read Time Out!"); } } catch { throw; } } ``` 稍微比較兩段Code,大至分成幾個區段 - 1. 下指令(Send Command) - 2. 下Delay Time - 3. 開Task讀取資料 - 4. Timeout Task - 5. Parse資料 雖然邏輯很相似,但靠單純的Extract Method是無法完全Clean很完美的,對於Clean完後,理想上的構圖是新Command只需寫下什麼Command,然後如何解析,其餘就不去管它。 ![](https://i.imgur.com/VvKEZPw.png) 先稍微Show一下Clean完後的結果,如下 ``` csharp= public Task<double> GetCrossTLK() { // Send Command Com.DiscardInBuffer(); Com.Write("GTLK\r"); var taskToken = new TaskToken(); // Read Response var readTask = StartRS232Read(3, taskToken, RS232EndTag); var readValue = ParseReceiveData(() => { // Parase var value = readTask.Result.ToArray(); var trkValue = Encoding.UTF8.GetString(value); var tlkValue = Convert.ToDouble(trkValue) * -0.01; taskToken.TokenSource.Cancel(); return tlkValue; },readTask, taskToken); return readValue; } ``` 相對於原本的程式碼簡化非常多,而且在下指令上大部分的設備也能用同一個寫法去下指令與等待回應資料 ``` csharp public Task<bool> CheckSetMaxSwitchChannel(int ch) { // Send Command SetSwitchChannel(ch); var taskToken = new TaskToken(); // Read Response var readTask = StartRS232Read(8, taskToken); var readValue = ParseReceiveData(() => { // Parase var value = readTask.Result.ToArray(); var isSuccess = value[5] == 0x00 ? true : false; return isSuccess; }, readTask, taskToken); return readValue; } ``` ## Clean Code脈絡 如上述描述,下指令後等待回應的步驟為 - 1. 下指令(Send Command) - 2. 下Delay Time - 3. 開Task讀取資料 - 4. Timeout Task - 5. Parse資料 ### Extract StartRS232Read Method 在這五步驟內,最大的不同在於步驟1與步驟5,其餘的步驟內容大致上會是一樣的。因此我們可以針對2,3步驟做Extract Method新增一個StartRS232Read方法,且需要傳入三個參數 - 1.回應資料的長度預計是多少 - 2.Read Task Token Source(Expection時需要取消Read Task) - 3.EndTag定義 ```csharp= protected Task<List<byte>> StartRS232Read(int rcvLength, TaskToken tokenSource, byte endTag= 0x0D) { var readBuffer = new List<byte>(); var readTask = ReadAction(() => { if (endTag != RS232EndTag) { readBuffer = ReadWithEndTag(readBuffer, tokenSource, rcvLength, endTag); } readBuffer = ReadWithoutEndTag(readBuffer, tokenSource, rcvLength); return readBuffer; }, tokenSource); return readTask; } ``` 這個Extract Method會發現到一件事情,基本上若沒有endTag的判定,StartRS232Read會是很單純把readTask Run程式碼包起來,然後回傳Task的Funcation。 一般外部設備溝通上基本上都會定義一些格式,然後會有結尾符定義。但有些設備沒有。所以在讀取資料上會分成兩種狀況。所以在裡面基本需要針對此兩種去分別撰寫判定機制。此時Task.Run會有部分重複程式碼需要再被撰寫,因此針對此處額外提取一個Func Delgate Method叫ReadAction如下 ``` csharp= private Task<List<byte>> ReadAction(Func<List<byte>> action, TaskToken tokenSource) { SpinWait.SpinUntil(() => false, 50); return Task.Run(() => { tokenSource.Token.ThrowIfCancellationRequested(); // 委派 return action.Invoke(); }, tokenSource.Token); } ``` 此時我們可以把Send完命令後等待的Delay Time安置在此Method。 接著我們針對兩種狀況再去提取Method #### 狀況一、讀取判定沒有結尾符 如果是此狀況,我只需單純判斷接收資料是否有大於我預期的回應資料長度即可,方法如下 ```csharp= private List<byte> ReadWithoutEndTag(List<byte> readBuffer, TaskToken tokenSource, int rcvLength) { while (readBuffer <= rcvLength) { // 判斷處 if (Com.BytesToRead > rcvLength) { var readBufftemp = new byte[Com.BytesToRead]; Com.Read(readBufftemp, 0, Com.BytesToRead); readBuffer.AddRange(readBufftemp); } if (tokenSource.Token.IsCancellationRequested) tokenSource.Token.ThrowIfCancellationRequested(); } return readBuffer; } ``` #### 狀況二、讀取判定有結尾符 有結尾符狀況下,須根據是否有偵測到EndTag去做,其程式碼如下 ```csharp= private List<byte> ReadWithEndTag(List<byte> readBuffer, TaskToken tokenSource, int rcvLength, byte endTag) { var endTagIndex = 0; while (endTagIndex <= 0) { if (Com.BytesToRead > rcvLength) { var readBufftemp = new byte[Com.BytesToRead]; Com.Read(readBufftemp, 0, Com.BytesToRead); readBuffer.AddRange(readBufftemp); Array.Clear(readBufftemp, 0, readBufftemp.Length); endTagIndex = Array.IndexOf(readBuffer.ToArray(), endTag); } if (tokenSource.Token.IsCancellationRequested) tokenSource.Token.ThrowIfCancellationRequested(); } return readBuffer; } ``` 上述講完兩種情境,大致應該可以理解為何要再多一個ReadAction的方法,因為委派可透動態去插入我們要的讀取情境。 ### Extract ParseReceiveData Method 在StartRS232Read方法後,接著我們要針對步驟4去做ExtractMethod。我們在看原始程式碼小區段如下 ```csharp= if (await Task.WhenAny(readTask, Task.Delay(5000)) == readTask) { // Parase var value = readBuffer.ToArray(); var trkValue = Encoding.UTF8.GetString(value); var tlkValue = Convert.ToDouble(trkValue) * -0.01; tokenSouce.Cancel(); return tlkValue; } else { tokenSouce.Cancel(); throw new Exception("Read Time Out!"); } ``` 基本上只有在Parase會不一樣,所以一樣須使用委派去動態安插我們要的Parase方式,且須根據不同情境須回傳不同的資料Type,因此需要提取一個泛型的Func Delgate Method如下,需傳入的參數有 - 1. ReadTask (傳入與Wait做比較) - 2. Read Task Cancel Token (逾時須取消Read Task) ```csharp= protected async Task<T> ParseReceiveData<T>(Func<T> func, Task readTask, TaskToken tokenSource) { if (await Task.WhenAny(readTask, Task.Delay(3000)) == readTask) { return func.Invoke(); } else { tokenSource.TokenSource.Cancel(); throw new Exception("讀取資料逾時!"); } } ``` ### Extract結果 提取完後,使用上就如同一開始Show的結果。我們拿兩種Command流程來比較 ![](https://i.imgur.com/oRnqFXU.png) 想對於一開始很冗長的程式碼整潔多了,而以上的Extract Method我們也可以寫在RS232設備的基底Class Template統一使用。但有個地方還需要做修正,Parase部分是有可能拋Expection的.這邊會需要再做補強。