对模式有了初步了解的朋友都知道,模式的使用与算法的使用有一个很大的区别,那就是它不是一个上来就能套用的东西。而是应该在软件的开发过程中通过捕捉软件的变化点并加以封装,并尽可能使用面向接口的编程和对象组合的方式一步一步地对代码进行重构,从而得到所适用的模式模式的目的是为了将变化比较频繁的部分与其余的比较固定部分之间的分离出来,降低他们之间的耦合性,从而提高代码的服用率及稳定。所以说种种介绍模式的代码结构的书往往让读者(比如我)看完后好像挺明白的,可还是不知道怎么用。就是因为这种介绍完全忽略了从最初的代码一步一步重构演化到这个模式的动态的历史。而这个动态演化的过程对我们认识模式和加深对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 <<{0}>> 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 <<{0}>> 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图,是不是感到非常清晰了。