Site hosted by Angelfire.com: Build your free website today!
Google
About US
Computer Programming
Favorites

Home >> Columns >> Effective C# >>

4.使用类厂(Class Factory)模式实现基于接口的客户激活远程对象

陈铭                   Microsoft .NET MVP
  简便的分布式应用程序开发无疑是.NET平台最引人注目的部分。通过使用.NET Remoting技术,
我们可以轻松的跨越运行环境(Context)、线程抑或进程的边界,甚至透过Internet访问远在他乡的另一台
计算机上的某个对象。而且,这种远程访问是近乎透明的——在完成远程对象的一些初始化工作之后,对其方
法的调用与普通本地对象几乎完全相同。

  根据生命周期控制方法的不同,.NET Remoting将远程对象分为服务器激活对象
(SAO, Server Activated Object)客户激活对象(CAO, Client Activated Object)两种。顾名思义,
服务器掌握着SAO对象生杀予夺的大权——更具体地说,将由服务器控制实际生成的远程对象的数量以及每
个客户请求究竟由哪个远程对象处理;对于CAO对象,服务器会根据客户的请求建独立的远程对象,每一
个方法调用都会被指派到与这个客户相关联的远程对象上。CAO对象的生存周期则是由客户端通过定期更
新它与服务器签订的远程对象的“租用协议”来控制的。

  让我们先来看一下SAO的情形。以下是一个简单的.NET Remoting SAO对象应用的完整程序*:
//share.cs, Remote Object
namespace Effective.CSharp.Chapter4 {
	//Must inherit from MarshalByRefObject
    	public class RemoteObject : System.MarshalByRefObject {
		//a very simple method implementation
        	public string SayHello(string name) {
            		return "Hello, " + name;
        	}
    	}
}

//server.cs, Server side code
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
    	public class Server {
        	public static int Main() {
			//Register the channel
            		TcpChannel chan = new TcpChannel(8085);
            		ChannelServices.RegisterChannel(chan);

		   	//Register the remote object
            		RemotingConfiguration.RegisterWellKnownServiceType(
			System.Type.GetType(
				"Effective.CSharp.Chapter4.RemoteObject, share"), 
				"Hello.rem", WellKnownObjectMode.SingleCall);
		   
		   	//Hold the server, wait for client
            		System.Console.WriteLine("Hit  to exit...");
            		System.Console.ReadLine();
            		return 0;
        	}
    	}
}

//client.cs, Client Side code
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
  	public class Client
  	{
    		public static int Main()
    		{
      			TcpChannel chan = new TcpChannel();
      			ChannelServices.RegisterChannel(chan);

			//Create the remote object
			RemoteObject obj = (RemoteObject)Activator.GetObject(
					typeof(RemoteObject), 
					"tcp://localhost:8085/Hello.rem");
     	
			Console.WriteLine(obj.SayHello("World"));
      			return 0;
    		}
  	}
}

  整个应用实际上被分解成三个独立编译的部分:share.cs包含了远程对象RemoteObject的定义,
server.cs是创建和维护RemoteObject的服务器端代码,而client.cs则是实际应用RemoteObject的客户。
一个特别需要注意的问题是:client.cs代码多次引用了RemoteObject类型(恰恰相反,server.cs没有直
接引用RemoteObject)。在条款1中我们曾经提到如果使用了在某个类集中定义的类型,就必须在编译期间
使用/r选项引用相关的类集。换言之,在编译client.cs的时候必须显式引用share类集,而编译之后的
client应用程序在缺少了share类集的情况下根本无法正确运行:

	csc server.cs
	csc /t:library share.cs
	csc /r:share.dll client.cs

  但是,在真实的分布式应用当中,绝不可能把远程对象的实现代码和客户端程序一起发布——尤其是当这些
对象用于实现一些至关重要的算法(例如加密、用户验证等)的时候!

  事实上客户端的编译和运行并不真的需要远程对象的实现——客户端只是负责将对象方法的调用转换成消息的
形式,然后发送给服务器做进一步处理。之所以需要引用包含远程对象实现的类集,只是因为编译器需要验证
这些方法调用代码的参数和返回值类型正确无误,而运行时需要利用这些信息生成将方法调用转换成消息的代
理。也就是说,编译客户端程序真正需要的只是远程对象的类型元数据(MetaData),而不是实现!


  由此想到的一个显而易见的解决方法是使用接口和抽象基类——它们恰好是用于将具体的类实现与类方法定义
分离开来。下面给出了利用接口继承实现上述远程对象应用实例的程序代码:

//share.cs, Remote Object Interface definition
namespace Effective.CSharp.Chapter4 {
    	public interface IRemoteObject {
        	string SayHello(string name);
    	}
}

//server.cs, Server side code
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
	//Must inherit from MarshalByRefObject
 	public class RemoteObject : System.MarshalByRefObject,
	IRemoteObject {
		//a very simple method implementation
        	public string SayHello(string name) {
            		return "Hello, " + name;
        	}
    	}

    	public class Server {
        	public static int Main() {
			//Register the channel
            		TcpChannel chan = new TcpChannel(8085);
            		ChannelServices.RegisterChannel(chan);

		   	//Register the remote object
            		RemotingConfiguration.RegisterWellKnownServiceType(
				typeof(RemoteObject), 
				"Hello.rem", WellKnownObjectMode.SingleCall);
		   
		   	//Hold the server, wait for client
            		System.Console.WriteLine("Hit  to exit...");
            		System.Console.ReadLine();
            		return 0;
        	}
    	}
}

//client.cs, Client Side code
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
	public class Client
  	{
    		public static int Main()
    		{
      			TcpChannel chan = new TcpChannel();
      			ChannelServices.RegisterChannel(chan);
	
			//Create the remote object
			IRemoteObject obj = (IRemoteObject)Activator.GetObject(
				typeof(IRemoteObject), 
				"tcp://localhost:8085/Hello.rem");
      	
			Console.WriteLine(obj.SayHello("World"));
      			return 0;
    		}
  	}
}

  整个应用仍然被分成三个部分,但这一次share.cs不再包含远程对象的实现,而仅仅是用于描述远程对
象方法的一个接口。远程对象实际的实现被转移到了server.cs当中。这样,在client.cs和server.cs的编
译过程中只需要引用share.cs中定义的接口:
	csc /t:library share.cs
	csc /r:share.dll client.cs
	csc /r:share.dll server.cs
  实际发布的客户端程序只需要包含client.exe和share.dll,不再包括远程对象方法实现的代码了。

  除了使用接口以外,使用.NET框架提供的SoapSuds.exe工具同样可以完成上述工作。SoapSuds可以从一个
已有的类集中分离出类型定义的元数据信息,分离出的部分可以用于客户端代码的编译和发布:

	//Extract remote object metadata from share.dll
	SOAPSUDS -types: Effective.Csharp.Chapter4.RemoteObject,share 
		-oa:RemoteObject.dll
   	//compile the client using extracted dll
   	csc /r:RemoteObject.dll client.cs
   	
  与使用接口的解决方法相比,使用SoapSuds工具要简单的多,但是使用接口继承可以使你的远程对象具备多态
特性——同样的客户端不需要任何改动就可以用于任何一个实现了特定接口的远程对象。而且,使用接口的解决方
案要更清晰一些——两个同名的RemoteObject类多少会令程序员感到有些混淆。

  至此,关于SAO对象编译和发布的问题已经解决。再让我们回过头来看一下关于CAO对象的类似问题,是否可以同
样通过使用接口来解决问题呢?
  照例,我们从一个完整的应用实例开始:
  
//share.cs, Remote Object
namespace Effective.CSharp.Chapter4 {
	//Exactly same as the original one
	public class RemoteObject : System.MarshalByRefObject {
		//a very simple method implementation
        	public string SayHello(string name) {
            		return "Hello, " + name;
        	}
    	}
}

//server.cs, Server side code
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
	public class Server {
        	public static int Main() {
			//Register the channel
            		TcpChannel chan = new TcpChannel(8085);
            		ChannelServices.RegisterChannel(chan);

		   	//Register the client activated remote class
		   	RemotingConfiguration.ApplicationName = “MyServer”;
			RemotingConfiguration.RegisterActivatedServiceType(
				typeof(RemoteObject));
		   
		   	//Hold the server, wait for client
            		System.Console.WriteLine("Hit  to exit...");
            		System.Console.ReadLine();
            		return 0;
        	}
    	}
}

//client.cs, Client Side code
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
	public class Client
	{
    		public static int Main()
    		{
      			TcpChannel chan = new TcpChannel();
      			ChannelServices.RegisterChannel(chan);

			//Register at client side
	    		RemotingConfiguration.RegisterActivatedClientType(
				typeof(RemoteObject), 
				"tcp://localhost:8085/MyServer");
				
			//Create the remote object
			RemoteObject obj = new RemoteObject();
     			Console.WriteLine(obj.SayHello("World"));
      			return 0;
    		}
  	}
}

  显然,在CAO对象的编译和发布方面,我们会碰到与SAO对象完全相同的问题。Client.cs的编译和
运行离不开share.dll。CAO对象的应用还有几处明显的不同:CAO对象不仅要求在服务器端进行注册,
而且在客户端也需要完成一定的注册工作;更重要的是,CAO对象是通过new创建的!后者彻底阻断了
简单的通过接口解决问题的幻想——不可能通过new创建出接口的实例,而必须显式的指明要创建的实
现特定接口的类型名称!

  

  很高兴看到你愿意进一步深入了解.NET Remoting的具体运作。J
先来看一下.NET内部是如何根据客户请求创建CAO对象的。.NET Remoting服务会注册一个通用的
Singleton SAO对象,用于处理所有创建CAO对象的请求。这个SAO对象实现了IActivator接口,其缺省
URI为/RemoteActivationService.rem,其中application name可以通过
RemotingConfiguration. ApplicationName进行设置。

  当客户程序执行new语句创建远程对象时,实际上客户端的.NET Remoting服务会通过远程对象的URL
地址连接到服务器端相应的RemoteActivationService.rem入口(上面的例子中是
"tcp://localhost:8085/MyServer/RemoteActivationService.rem"),调用SAO对象IActivator接口的
Activate方法;Activate方法会根据参数查找所有在服务器端注册过的CAO对象类型,如果找到相对应
的注册信息(包括类型名称、所在的类集以及版本等),Activate就会根据这些信息创建一个该对象的实例,
并且为新创建的对象生成一个唯一的URI入口,然后将该对象返回给客户端。这样,此后的客户方法调用
就会与这个特定的CAO对象实例对应起来。(其中为新生成对象指定URI的部分会由底层的.NET Remoting
服务自动完成,Activate需要做的仅仅是生成一个新的对象,并且返回该对象的引用)。

  可见,远程对象的客户激活机制仅仅是架设在SAO基础上的一层包装,在简化程序员工作的同时,
这种客户激活机制也剥夺了对CAO对象使用接口和抽象基类的权力。既然.NET对CAO对象的实现没有用到
任何特别的方法,我们就完全可以模仿系统实现自己的CAO机制,只需少许更改,就可以为子定义的CAO
提供基于接口的创建方法。

  不难看出在整个CAO实现体系中,IActivator实际上是作为一个通用类厂(Class Factory)接口出现的,
所以我们也需要针对IRemoteObject接口定义自己的类厂接口IRemoteFactory:

//share.cs, Remote Object and Factory interface
namespace Effective.CSharp.Chapter4 {
	public interface IRemoteObject {
        	string SayHello(string name);
    	}

	public interface IRemoteFactory {
		IRemoteObject CreateInstance();
 	}
}

  在服务器端的代码中,我们需要实际实现RemoteObject和一个用于创建RemoteObject的类厂,
并且将后者注册为SAO对象:

//server.cs, Server side code
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
	public class RemoteObject: MarshalByRefObject,
				IRemoteObject {
			public string SayHello(string name) {
				return “Hello, “ + name;
			}
 	}

 	public class RemoteFactory: MarshalByRefObject,
				IRemoteFactory {
		public IRemoteObject CreateInstance() {
			return new RemoteObject();
		}
 	}
					
	public class Server {
		public static int Main() {
			//Register the channel
            		TcpChannel chan = new TcpChannel(8085);
            		ChannelServices.RegisterChannel(chan);

		   	//Register the client activated remote class
            		RemotingConfiguration.RegisterWellKnownServiceType(
				typeof(RemoteFactory), 
				"MyFactory.rem", WellKnownObjectMode.Singleton);

		   
		   	//Hold the server, wait for client
            		System.Console.WriteLine("Hit  to exit...");
            		System.Console.ReadLine();
            		return 0;
        	}
    	}
}

  最后,客户端的实现代码也略有不同:必须先通过Activator.GetObject得到IRemoteFactory接口,
然后在需要创建RemoteObject的时候调用IRemoteFactory.CreateInstance:

//client.cs, Client Side code
using System;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Channels;
using System.Runtime.Remoting.Channels.Tcp;

namespace Effective.CSharp.Chapter4 {
	public class Client
	{
		public static int Main()
		{
      			TcpChannel chan = new TcpChannel();
      			ChannelServices.RegisterChannel(chan);
			IRemoteFactory factory = (IRemoteFactory)Activator.GetObject(
				typeof(IRemoteFactory), 
				"tcp://localhost:8085/MyFactory"); 	     

			//Create the remote object
			RemoteObject obj = factory.CreateInstance();
     			Console.WriteLine(obj.SayHello("World"));
      			return 0;
    		}
  	}
}

  在以上所有应用接口的实例当中,我们同样可以使用抽象基类代替接口(关于抽象基类和接口的比较,
详见条款X)。

  对于应用.NET Remoting技术的分布式应用程序开发,使用接口以及基于接口的类厂是比较好的设计方案,
因为这种方案不仅提供了更加清晰的应用模块结构,而且具有更好的可扩展性。
2002 Copyright by Ming Chen & Qiu Zhang
and contributors who are credited in the articles.