重構到觀察者模式 Refactor to Observer Pattern

iDotNetSpace發表於2010-03-18
對模式有了初步瞭解的朋友都知道,模式的使用與演算法的使用有一個很大的區別,那就是它不是一個上來就能套用的東西。而是應該在軟體的開發過程中通過捕捉軟體的變化點並加以封裝,並儘可能使用面向介面的程式設計和物件組合的方式一步一步地對程式碼進行重構,從而得到所適用的模式模式的目的是為了將變化比較頻繁的部分與其餘的比較固定部分之間的分離出來,降低他們之間的耦合性,從而提高程式碼的服用率及穩定。所以說種種介紹模式的程式碼結構的書往往讓讀者(比如我)看完後好像挺明白的,可還是不知道怎麼用。就是因為這種介紹完全忽略了從最初的程式碼一步一步重構演化到這個模式的動態的歷史。而這個動態演化的過程對我們認識模式和加深對OO的瞭解是非常重要的。下面我就一個真實的軟體開發過程來介紹一下OO開發需要注意的問題,以及這個例子是如何一步一步重構到Observer模式的。

這個軟體功能很簡單:從一個指定的埠接收資訊(8888埠)。程式碼如下:

   class Receiver
    
{
        
//用來接收資訊
        public void Receive()
        
{
            Byte[] receiveBytes;
            
string receiveString;
            UdpClient udpClientB 
= new UdpClient();

            
try
            
{
                IPEndPoint RemoteIpEndPoint 
= new IPEndPoint(IPAddress.Any, 8888);
                udpClientB.Client.Bind(RemoteIpEndPoint);

                
//一直等待著,直到從指定埠收到資訊。
                while (true)
                
{
                    receiveBytes 
= udpClientB.Receive(ref RemoteIpEndPoint);                   
                    receiveString 
= Bytes2String(receiveBytes);

                    
//把接收到的字串在視窗上列印出來。
                    Console.WriteLine(receiveString);
                }

            }

            
catch (Exception ex) { Console.WriteLine(ex.Message); }
            
finally { udpClientB.Close(); }
        }


        
//用來把接收到的bytes轉換為string
        string Bytes2String(Byte[] bytes)
        
{
            MemoryStream ms 
= new MemoryStream(bytes, 0, bytes.Length);
            BinaryFormatter bf 
= new BinaryFormatter();
            
return (string)bf.Deserialize(ms);
        }

}


        
static void Main(string[] args)
        
{
            Receiver recieve 
= new Receiver();
            recieve.Receive();
     }




第一次需求改變

原來的需求:把接收到的資料在螢幕上列印出來。
現在的需求:要求我們把接收到的資料寫到log檔案裡。

為了方便下一次還有類似的改動,我們把寫的log的操作從方法Receive()裡抽出來放到獨立的方法ProcessRecievedData裡。如下

        void ProcessRecievedData(string rString)
        
{
            
using (StreamWriter sw = File.AppendText("Receive.log"))
            
{
                sw.WriteLine(
string.Format("Receive <> at {1}", rString, DateTime.Now));
            }
            
        }



 

原來的 Console.WriteLine(receiveString) 的地方也就變成了對ProcessRecievedData的呼叫。這是我們遇到的第一個變化點,我們用Extract Method的思路,將變化封裝到一個方法裡面,這是最基本最簡單的重構思路。

第二次需求改變:因為接收資訊的功能很常用,為了方便複用,我們決定把它放到一個獨立的Assembly裡,供其別人呼叫。

為了實現這一目標,我們仔細分析了一下,需要做到以下兩點:

1.要為接收到的各種型別的資料提供支援。

2.要能夠在接收到資料後,通知客戶程式,從而讓客戶程式對收到的資料進行相應的處理。

對於第1點,由於.net 2.0支援泛型,我們可以很容易通過定義一個泛型的Receiver,從而把型別作為引數從而支援接收到的各種型別的資料。

對於第2點,也就是observer模式的目的,其實現過程涉及到訊息的訂閱和退訂,還有什麼釋出(publish)、訂閱(subscribe)、以及(Subject)和觀察者(Observer)等詞彙。這些對於初學者來講,起碼對當初的我來說有些繞。

我們先來看看比較直觀的做法。仔細分析一下上面的第2點,這個地方的變化點有兩個:

1. 在收到資料後,客戶程式碼的處理方式會是不同的。

2. 在收到資料後,客戶程式碼要進行處理的方式的個數是變化的、不固定的。

既然我們的目的無非是讓客戶程式對接收到的資料進行處理,那麼我們在收到資料後直接呼叫客戶的處理方法不就行了。結合要封裝第1個變化點的考慮,我們決定在類Receiver裡放一個成員物件,它是客戶程式定義的某個型別,讓客戶程式(也就是使用這個Assembly的上層程式碼)為這個變數賦值,然後我們再呼叫這個物件的某個方法。具體說來就是定義一個介面,讓客戶實現這個介面,然後我們的在收到資料的時候,呼叫這個介面裡的方法。如下:

    interface IProcess 
    

       
void Process(string sString);
    }


  
class Receiver
    
{
        
public IProcess ProcesseData;     
……………
……………

        
void ProcessRecievedData(string rString)
        
{
            ProcesseData.Process(receiveString);     
        }


    }


 

只要客戶程式碼實現了IProcess Process方法,他就能在收到資料是被我們的程式碼呼叫,也就是在時間發生時受到了我們的通知。客戶方使用的程式碼如下:

   class WriteToFile : IProcess
{
    
public void Process(string s)
        
{
            
using (StreamWriter sw = File.AppendText("Receive.log"))
            
{
                sw.WriteLine(
string.Format("Receive <> at {1}", s, DateTime.Now));
            }

        }

}

        
static void Main(string[] args)
        
{
            Receiver recieve 
= new Receiver();

            recieve.ProcesseData 
= new WriteToFile();

            recieve.Receive();
        }


再看第2個變化點:在收到資料後,客戶程式碼要進行處理的操作的個數是變化的、不固定的。這個很簡單,我們只需要把Receiver 宣告的ProcesseData改成連結串列就行了:

class Receiver
    
{
        
public List<IProcess> mProcesses=new List<IProcess> ();     
……………


 

這樣一來,我們的ProcessRecievedData也要做相應的更改:


  void ProcessRecievedData(string rString)
        
{
            
if (mProcesses.Count > 0)
            
{
                
foreach (IProcess mprocess in mProcesses)
                
{
                    mprocess.Process(rString);
                }

            }

        }


   

呼叫過程也跟著變:

    class CosoleDisplay : IProcess
    
{
        
public void Process(string s)
        
{
            Console.WriteLine(s);

        }

}


static void Main(string[] args)
        
{
            Receiver recieve 
= new Receiver();
            recieve.mProcesses.Add(
new CosoleDisplay());
            recieve.mProcesses.Add(
new WriteToFile());          
            recieve.Receive();
        }



 

好了,讓我們先來回頭看看Observer Pattern要解決的是什麼問題,怎麼解決的。

Observer(觀察者)模式的定義:定義物件間的一種一對多的關係,當一個物件的狀態發生改變時,所有依賴它的物件都得到通知,並被自動更新.

我們進一步來看這裡的變化點是什麼?需要注意到雖然“物件的狀態”發生了變化,但是“物件的狀態”本身卻並不是我們開發過程中需要捕捉的變化點。在這兒,狀態發生變化這一事實是穩定的,所以我們的涉及到狀態變化的程式碼一直沒有重構過,在我們的例子中是Receive方法中的大部分程式碼。

發生變化的有兩個:

1依賴狀態變化這一事件的物件們。

2這些物件對與狀態變化後採取的動作。

我們用語言的多型(介面)和類庫的連結串列方便的將這兩個變化點封裝到一個成員mProcesses裡。從而,所有這一過程中的變化都可以通過對mProcesses這一個成員的操作來加以實現。這也就大大降低了Observer類和呼叫程式之間的耦合性。

所以說我們用了模式的方法達成了Observer的目的,也就是實現了Observer Pattern

因為Oberser模式要解決的問題具有非常的普遍性,.net提供了語言上的支援delegate以及eventDelegate使用起來更方便,因為介紹delegateevent的文章已經很多了,這就不再介紹了。最後給出delegate版的支援泛型的完整的程式碼。

完整程式碼

  

當然最後要給出傳送方的程式碼來測試接受是否能夠成功。


傳送方程式碼

    現在回過頭來再看Observer Pattern UML圖,是不是感到非常清晰了。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/12639172/viewspace-629782/,如需轉載,請註明出處,否則將追究法律責任。

相關文章