搭建.NET灵活体系结构
本文是一篇介绍如何搭建.NET灵活体系结构的文章。前一篇文章介绍了一种可满足应用程序绝大部分配置需求的方法。在那篇文章中,假定某一配置文件的具体内容如下所示:
<GeneralConfiguration>
<section1>
<key1>value1</key1>
</section1>
<section2>
<key name="x">value_for_x</key>
<key name="y" a1="10" a2 ="15">
<a3>16</a3>
<a4>17</a4>
</key>
</section2>
</GeneralConfiguration>
可以通过使用下列描述性调用序列从上述配置文件中访问相关取值:
// get a class responsible for implementing IConfig
IConfig cfg;
// reading a simple key
cfg.getValue("/section1/key1") -> value1
// reading a key with reserved name attribute
cfg.getValue("/section2/key/x") -> value_for_x
// reading a key with reserved name attribute and its
// attribute/child nodes
cfg.getValue("/section2/key/y/a1") -> 10
cfg.getValue("/section2/key/y/a3") -> 16
使用IConfig诸多优点包括:
· 提供了使用简单的API来访问信息层次结构的能力;
· 不必取代 XPath,只需使用一些可行的约定来对其进行补充和完善;
· 允许使用默认值;
· 可区分必选值和可选值;
· 可以在处理关键字取值时不区分大小写;
· 基于特定的假设,可以使用相似的方式来处理属性和子元素。
工厂服务: 开始体系结构领域之旅
构建大型结构的关键就是要从简单的想法入手。这就是说,复杂性并不能构建在复杂性基础之上。接下来,我们将详细描述如何在这种简单的配置服务基础之上对其进行扩展并形成应用级的工厂服务。
究竟什么是工厂服务?
语言总会产生这样一种倾向:对流行的编程习惯或实践加以形式化,然后将其引入到语言的构建中。例如,Java 引入了关键字interface,而在Java之前的C++中这仅仅是一种没有正式化的概念。
C#同样具有关键字interface。同时,C#也通过关键字delegate对事件处理进行了形式化处理。为了能够更好地理解工厂服务的含义,需要首先来准确理解接口。之所以这样做,就是因为接口是体系结构的核心概念。
理解接口
接口就像一个具有一套方法的类。因此,如果您得到了一个对象并且明确它实现的接口,您就能以一种可靠的方式来调用这些方法。看看下面的代码,您就会发现它事实上还是很简单的:
//Let us define an interface
interface LoggingInterface
{
public void logMessage(String message);
}
//Let's see how we can use this interface
public void myLoggingFunction( Object o)
{
if (o is LoggingInterface)
{
LoggingInterface log = (LoggingInterface)o;
log.message("test message");
}
else
{
// sorry not sure of the type of this object
throw new Exception("Unexpected type");
}
}
现在问题是当您调用logMessage函数时,谁将记录这一消息呢?上面的接口并没有相应代码来负责记录此消息,这应该怎么处理呢?这种情况下,就需要实现下列内容:
public class FileLogger : LoggingInterface
{
private FileOutputStream fs = null;
public FileLogger(string filename)
{
fs = new FileOutputstream(filename);
}
// implement the following method to complete the
// contract required by LoggingInterface
public void logMessage(String message)
{
fs.println(message);
}
public close()
{
fs.close();
}
}
好了,您现在可以加入下列代码:
FileLogger fileLogger = new FileLogger("myfile");
myLoggingFunction(fileLogger);
现在,提出这样一个要求:需要将所有的日志记录都写入到操作系统所支持的事件日志中。因此,创建一个新类来实现此需求,如下所示:
public class EventLogger : ILog
{
... other code
public void logMessage(string message)
{
.. do the needful
}
}
将
FileLogger fileLogger = new FileLogger("myfile");
myLoggingFunction(fileLogger);
更改为
EventLogger eventLogger = new EventLogger(..);
myLoggingFunction(eventLogger);
接下来您就是希望能够得到一个实用函数,该函数可为您提供一个正确的日志接口:
LoggingInterface log = MyUtility.getLogger();
myLoggingFunction(log);
现在可以开始编写 MyUtility.getLogger() 函数,如下所示:
public LoggingInterface getLogger()
{
return new FileLogger("myfile");
// return new EventLogger();
}
如果用挑剔的目光来看待目前的进展,您可能就会产生这样的感觉:很长一段时间以来一直忽略的配置服务可能将成为接下来需要认真考虑的内容。您设想有这样一个配置文件,如下所示:
<request name="logger'>
<classname>EventLogger
</request>
很多语言中,只要知道了类名和它的继承或派生关系,就能很容易地对此类进行实例化。具备了这些信息,您就可以将 getLogger() 实用函数修改为:
public LoggingInterface getLogger()
{
IConfig cfg = ...
string loggerClassname = cfg.getValue
("/request/logger/classname","EventLogger");
Object o = instantiateAnObjectForItsClassname(
loggerClassName);
// code is not provided for this function
LoggingInterface log = (LoggingInterface)o;
return log;
}
到现在为止,您的目标几乎完全实现了。您能够动态使用配置文件将一个日志类进行实例化。这时,您的需求也随之增长,您又希望此项功能可以为各种各样的类实例化动态对象,而不仅仅局限于当前的日志类。所以,您又再次对实用函数进行了修改:
public static Object getObject(string interfacename)
{
IConfig cfg = ...
string interfaceClassname = cfg.getValue("/request/" +
interfacename + "/classname");
Object o = instantiateAnObjectForItsClassname(
loggerClassName);
// code is not provided for this function
return o;
}
现在,您所拥有的函数可以实现通用目标,该函数能够为您提供一个与指定符号名相对应的对象。此实用函数被称作工厂服务。不过这仅仅是工厂服务的简单框架。我们将以此为基础作进一步的扩展,以便能够覆盖更多的情况,争取实现最大限度的利用
形式化的工厂服务
我们将需求形式化为一个工厂服务接口。
public interface IFactory
{
public Object getObject(string symbolicname):
}
所以,工厂服务将返回指定符号名所对应的对象。
使用缓存增强工厂服务
假设这样一种情况:现在有两个客户端正在尝试使用工厂服务登录到应用程序。具体情形如下所述:
//client1
IFactory fact;
LoggingInterface log = (LoggingInterface)fact.getObject(
"logger");
log.logMessage("Message from client1");
//client2
IFactory fact;
LoggingInterface log = (LoggingInterface)fact.getObject(
"logger");
log.logMessage("Message from client2");
同时假定使用FileLogger 作为我们的实现工具。所以现在的问题就是,您预期在应用程序中会有多少 FileLogger对象呢?如果两个客户端能够共享同一对象,这是非常理想的结果。因此,工厂服务就应该将这一要求考虑在内,对所请求的对象进行缓冲存储。但是,工厂服务如何才能知道要对该对象进行缓冲存储?这就需要为工厂服务提供必要的线索,以便使其明确该类在整个应用程序中 只有一个实例。
要做到这一点,可以采用以下两种方式:
一种方法就是更改配置,如下所示:
·<request name="logger'>
· <classname>EventLogger</classname>
· <instance-count>single|multiple</instance-count>
·</request>
现在工厂服务就能够确定是否需要将此 instance-count 读取到缓存。
另一种实现方法如下所示:
·public class FileLogger : LoggingInterface,
· SingeInstanceInterface
·{
· ...
·}
SingleInstanceInterface是一个接口标记,FileLogger使用该接口标记来告知工厂服务该对象设计为单实例,因此工厂服务可以放心地对此对象进行缓冲存储。
以上这两种方法有一个很重要的不同之处。 例如,如果假定支持多实例,您很可能会将 FileLogger 设计成不对线程进行检测。同时,负责管理配置文件的人员却将“多实例”错误地更改为“单实例”。这样的话,就可能会引发线程问题。
一个类确定为单实例还是多实例成为设计时一个很重要的限制。我个人认为,将其表述为一个接口标记更为合适。接口标记仅仅只是一个名称而已,它主要用来说明所提到的接口中没有任何方法。
接下来,我们来定义缓存描述。
public interface SingleInstanceInterface {}
public interface MultiInstanceInterface {}
如果我们所提及的类并没有实现上述的任何一个接口,则默认为MultipleInstanceInterface接口。
使用初始化改善工厂
到目前为止,我始终未提及 FileLogger的一个很重要的细节。下面,我们再来看一下配置文件吧:
<request name="logger'>
<classname>FileLogger
</request>
您已经开始使用如下所示的代码:
IFactory fact;
LoggingInterface log = (LoggingInterface)fact.getObject(
"logger");
log.logMessage("Message from client2");
现在有这样一个问题:FileLogger。如何才能知道要写入到哪一个文件呢?因为工厂并不负责将某一参数传递给FileLogger类。接下来我们来研究一种很明显的情形。我们为什么不选择通过修改工厂服务来将参数传递给getObject 方法,以便此方法能够将参数传递给FileLogger?为了实现上述目标,上面的函数应该为:
IFactory fact;
LoggingInterface log = (LoggingInterface)fact.getObject(
"logger", "filename");
log.logMessage("Message from client2");
如果想将日志更改为 EventLogger又该怎么办呢? 实际上,这些附加参数可能会违反工厂服务的约定。所以,我们将不得不使用另一可选方法。我们首先来看看第一个示例:
public class FileLogger
{
FileLogger(string filename);
FileLogger() // default constructor, one that the
// factory is interested in
{
// read the filename from config
IConfig cfg..
string filename = cfg.getValue("/Logging/filename",null);
this(filename);
}
}
要实现上述所描述的内容,您还需要具备以下配置:
<request name="logger'>
<classname>FileLogger</classname>
</request>
<Logging>
<filename>abc</filename>
</Logging>
现在我们要回过头来解决前面遗留的问题:配置信息作为FileLogger的一部分在其他地方也将会提到。那么。我们又打算如何来解决该问题呢?首先,我们将配置设为:
<request name="logger'>
<classname>FileLogger</classname>
<filename>abc</filename>
</request>
下面使用下列接口来处理FileLogger 类:
public interface InitializableInterface
{
public void initialize(string requestname, IConfig cfg);
}
public class FileLogger :
LoggingInterface
// main job
,SingleInstaceInterface
// only one copy exists
,InitializableInterface
// we will see now what this is
{
private string filename;
FileLogger();
public void initialize(string requestname, IConfig cfg)
{
filename = cfg.getValue("/request/" + requestname +
"/filename",null);
.. other initialization stuff
}
}
与工厂相关的接口
现在,我们来对工厂服务所涉及的接口进行简单的总结。请注意:为了方便起见,以下将使用"I" 来代表接口。
public interface IFactory
{
// Instantiates an object and returns it
public object getObject(string symbolicName);
.. More methods to be covered in subsequent articles
}
public interface ISingleInstance {}
// Indicates to the factory that the intantiated object
// needs to be cached
public interface IMultiInstance {}
// Object can not be cached
public interface IInitializable
{
// used to read further unified configuration information
public void initialize(string symbolicname);
}
使用上述工厂服务实现组件
我们可以将组件看作是一类方法的集合。
Public interface IComponent1
{
ReturnType1 method1(arg1, arg2, etc.);
ReturnType2 method2(arg1,arg2, etc..)
}
实现此组件的方法如下所示:
Public class MyComponentImplementation : IComponent1,
ISingleInstance
{
// any local variables you may have
ReturnType1 method1(arg1, arg2, etc.)
{
.. Method1 implementation
}
ReturnType2 method2(arg1,arg2, etc..)
{
.. Method2 implementation
}
}
可以使用下列配置文件来指定运行时的实现。
<request name="IComponent1>
<type>MyComponentImplementation,MyAssembly</type>
</request>
这里的“type”是.NET用以指定类名的一种机制。一旦上述的配置正确,就可以照此来使用组件:
Ifactory fact;
IComponent1 compInterface = (Icomponent1)fact.getObject
("IComponent1");
compInterface.method1(..);
compInterface.method2(..);
这些组件的具体优势
首先,这些组件可以保证类型安全。这就是说参数能够使用类型安全的方法来调用组件之上的方法。一旦获得了组件,利用符号名,该组件之上的方法就可以保证类型安全。
其次,这些组件是独立存在的。您可以根据需要在应用程序中的任一位置申请和使用某一组件。不过,这里有这样一个假定:将组件作为无状态方法集来实现。如果想使用有状态方法,将不能实现 ISingleInstance 标记。