基于组件的.NET软件开发

From:  http://blog.csdn.net/bitfan/archive/2004/12/20/223457.aspx

(注:原文图片不能正常显示)

前言

    随着软件技术的飞速进步,现代的大型软件都广泛采用了基于软件组件的开发方式。以成熟的CBD(Component Based Design:基于组件的系统设计)理论为指导,在对系统的分析与设计完成之后,系统开发体现为复用已有组件、开发新组件以及将所有组件装配起来的过程。J2EE大规模地使用各种组件构照复杂的企业信息系统,获得了巨大的成功。

    作为后来者,.NET framework汲取了J2EE的成功经验,在组件化开发方面有着自己独到的设计,在本文中,我们将介绍.NET组件化开发技术的最重要的两个范畴:混合语言开发与组件反射。笔者有充分的理由确信读者在了解了.NET的强大组件开发功能之后,一定会对.NET framework的设计和开发者敬佩不已,并会激起应用.NET来开发软件系统的强烈兴趣。

混合语言开发

    Java语言跨平台的设计,是J2EE在企业级系统中占据优势的重要原因,为了与J2EE竞争,.NET framework在设计体系结构上采用了分层的设计模式,从而在理论上使跨平台成为了可能(事实上,已有真实可用的系统,MONO就是运行在非Windows操作系统之上的.NET framework),.NET设计师们还发现了J2EE的一个死穴——J2EE组件必须用Java语言开发!这种使用语言上的“独裁”无疑让喜爱其它语言的程序员们很不高兴。为此,.NET在软件开发史上首次在设计软件运行平台时就考虑到了混合语言开发,在笔者看来,这真是一次影响深远的技术变革。

    本文不打算全面介绍.NET framework中混合语言开发的内部机理,而只是从应用角度,通过几个短小却典型的实例,来看看我们如何在一个工程中集成多种语言开发的.NET组件。

 

组件的组合

    当我们需要复用已有的组件的功能来开发新系统时,我们经常让新开发的组件简单地包容另一个已有的组件,以达到代码重用的功能,这种开发方式在面向对象设计理论中被称为“组合”。我们看看一个小例子:

    我们要设计一个C#组件,其接口如图1所示:


1 C#组件的UML图示

    打开VS.NET,创建一个C#类库工程:CSharpComponent,删除原有的Class1,往工程中添加一个新类CSharpClass,在其中增加一个函数SaySomething,代码如下:

 

public void SaySomething(string str)

        {

            MessageBox.Show(" 这是 C#实现的功能,传入的字串是:"+str);

        }

    然后,从菜单中选“生成”à“生成解决方案”,编译完成后会生成一个动态链接库:CSharpClass.dll。

    现在,我们已有了一个可复用的软件组件,虽然它的功能简直不值一提,但它的的确确是一个软件组件,在本质上与那些卖上几千美元的商业组件并无区别。

    在这里我们需要明确:CSharpClass.dll实际上可以称之为组件库,而类CSharpClass则可以看成是一个可以复用的组件。显然,一个组件库(DLL文件)可以容纳多个组件(即完成某种功能的类)。

    接着我们再创建一个Windows应用程序项目,但这时我们用的不再是C#,而是VB.NET了。我们给工程起名为:VBTestComponent,从工具箱中把一个按钮拖到窗体上,我们打算在用户单击此按钮时,让它直接调用C#组件   CSharpClass.dll中的SaySomething()方法,并从VB中传送一个字串给此方法。

    为了能使用开发好的C#组件,我们必须给VB工程添加对CSharpClass.dll的引用。在解决方案资源管理器窗口中的项目节点:VBTestComponent上右击,从弹出菜单中选:“添加引用…”,将出现以下窗体:

2 添加对C#组件CSharpClass.dll的引用

    单击“浏览…”按钮,找到CSharpClass.dll,确定后如图2所示。单击“确定”按钮关闭窗口,现在,我们就给VB工程增加了一个对C#组件的引用,可以在解决方案资源管理器窗口中很清楚地看到这点:


3 添加了对C#组件引用后的“解决方案管理器”

 

    后面的事情就很简单了,在按钮的单击事件中书写以下代码:

 


Private
Sub Button1_Click_1(……) Handles Button1.Click

        Dim obj As CSharpClassNameSpace.CSharpClass

        obj = New CSharpClassNameSpace.CSharpClass()

           obj.SaySomething("我是从VB中调用C#组件中的方法而出现的。 ")

End Sub

 

    OK,我们现在就实现了在VB中调用C#组件的功能,这简直太简单了吧!

    聪明的你这时脑瓜一定在转了:既然从VB中可以这么容易地调C#组件,那么,反过来也一定行。或者,我可以做一个VB组件,然后,它又调用一个C#组件,而C#组件又可以调用某个C++开发的组件,……,调来调去,浑然一体。当代愚公曰:“子子孙孙无穷尽也,何愁项目完不成?!”

    现在再不用强迫所有程序员在一个项目中都用一种语言了,尽可以给每人分配一个独立的组件,让他们用自己喜欢的语言去开发,只要遵守事先定好的接口就行了。

    笔者回想过去使用VB来调用Win32 API的惨状,不禁欢呼那种痛苦的日子一去不复返了!

组件的继承

       前面实现的将C#组件组合进VB工程中只是牛刀小试,现在我们来实现混合语言的面向对象编程(OOP),首先,我们来尝试一下混合语言组件的继承。

      新建一个VB类库工程VBComponent,在工程中按上面的方法添加对CSharpClass.dll组件的引用,然后组件中增加一个新类:ExtendsFromCSharp,它继承自C#类CSharpClass,ExtendsFromCSharp类提供一个Add(x,y)方法,将两数相加,用UML表示如图4:


4 C#类继承而来的VB

具体代码如下:

 

Imports CSharpClassNameSpace.CSharpClass

'继承自C#组件中的类CSharpClass

Public Class ExtendsFromCSharp

    Inherits CSharpClassNameSpace.CSharpClass

    Public Function Add(ByVal x As Integer, ByVal y As Integer) As Long

        Return x + y

    End Function

End Class

 

    编译生成VBComponent.DLL组件(至于如何使用和测试这个类,就不用我多说了吧?)。

    现在,VBComponent组件中的ExtendsFromCSharp类将拥有两个方法:SaySomething来自C#基类CSharpClass,而Add()函数则由VB实现。你创建的ExtendsFromCSharp类对象居然组合了两种不同语言开发的功能,你甚至不知道这些方法和函数是由哪种语言开发的,是不是很神奇?

    让我们稍微休息一会,进行一个小结:

*      现在我们已掌握了从现成的组件中派生新类的方法,这就是说,我们可以建立自己的组件库,并在合适的时候动态地扩展它,这种扩展是跨语言的。

*      在VS.NET中使用混合语言组件时,当所使用的组件修改过后,注意需要动态地更新工程引用。方法是重新生成解决方案,或者是删除原有引用后再重新加入工程。

*      由于组件间相互继承,这就造成了组件库之间的依赖性(比如VBComponent.DLL组件就依赖于CSharpClass.DLL),因此,在实际项目开发中,任何一个项目小组成员所开发的组件库,都要求在文档中声明其组件库依赖性。另外,在系统开发完成部署到用户计算机上时,相互依赖的组件库必须绑在一起同时复制到用户计算机上,不能只提供一个。

 

    下面,让我们走得更远一些,在组件间实现多态!

组件间的多态调用

    我们来实现一个“经典”的多态例子,它说明以下事实:圆和矩形都是几何图形,都可以求面积。

    继续上面的例子,在C# 工程中选定原先的CSharpClass.cs文件,在类CSharpClass之外,输入以下代码:

 

     public interface IMyShape

     {

         double Area();

     }

     public class CSharpRect :IMyShape

     {

         public float width;

         public float height;

       

         public double Area()

         {

              return width*height;

         }

     }

   

    很明显,在这里我们定义了一个接口IMyShape,类CSharpRect实现了这个接口,它对接口所定义的求面积的函数double Area()提供了自己的实现:

                矩形面积=长*宽

    编译生成解决方案,CSharpClass.DLL得到更新。

    打开VBComponent工程,注意到刷新此工程对CSharpClass.DLl的引用,检查方法是在选菜单:视图à对象浏览器,查看CSharpClass是否体现了新的更改,如5图所示:


5 检查组件是否更新

     新建一个类文件VBCircle.vb,输入以下代码:

 

Public Class VBCircle

    Implements CSharpClassNameSpace.IMyShape

 

    Private _radius As Single

 

    Property Radius() As Single

        Get

            Radius = _radius

        End Get

        Set(ByVal Value As Single)

            _radius = Value

        End Set

    End Property

 

    Public Sub New()

        _radius = 0

    End Sub

 

    Public Function Area() As Double Implements CSharpClassNameSpace.IMyShape.Area

        Return System.Math.PI * _radius * _radius

    End Function

End Class

 

    编译,生成VBComponent.DLL。

    现在,我们就实现了基于组件的以下面向对象系统设计:

 

 

    新建一个使用VB的Windows应用程序,在工程中加入对以上两个组件库的引用,往窗体上加入一个按钮,在其Click事件中书写以下代码:

 

Private Sub Button_Click() Handles Button6.Click

        Dim VBObj As New VBComponent.VBCircle()

        VBObj.Radius =10

        Dim CSharpObj As New CSharpClassNameSpace.CSharpRect()

        Dim area As Double

        CSharpObj.width = 10

        CSharpObj.height = 20

        CalArea(VBObj)

        CalArea(CSharpObj)

    End Sub

   

    其中CalArea()是一个Sub过程,它接收一个IMyShape类型的参数,可以传给它任何一个实现了IMyShape接口的对象,这种以接口而不是具体类来编定代码的方法,就是典型的多态编码方式。

 

Private Sub CalArea(ByVal obj As CSharpClassNameSpace.IMyShape)

        MessageBox.Show(Convert.ToString(obj.Area()))

End Sub

 

    经过测试,跨语言、跨组件的多态的确可以正常地动作。

 

    好了,现在让我们好好想想都实现了什么东西,可以怎样应用到开发实践中:

    我们可以将某个接口封装到一个组件中,然后,具体实现这个接口的类我们可以放到不同的组件中,交给不同的程序员以他们所喜爱的语言来开发。使用这些组件的程序员只要在他的代码中始终使用接口变量而不是具体类的变量,我们就可以动态地替换掉某个具体的实现接口的组件,而不会影响使用这些组件的程序。

    哇,这不就意味着我们可以把一个完整的软件系统给大卸八块,然后根据具体情况选一个组件插入系统?太棒了!下面,就让我们来开始进行组件化编程最精彩之旅---软件组件的动态装配与插拔。

动态组件插拔

    在实现动态组件插拔之前,我们必须了解一个概念:反射(Reflection)。

    所谓反射,就是仅依据名字来创建一个对象

    举个例子,在上面的例子中,我可以直接给定一个字串:“ CSharpClassNameSpace.CSharpRect”,然后我就可以生成这个对象,并调用它的Area()方法求面积!其前提条件是程序能访问存放这些类的DLL文件。

    好了,现在可以说说我们的构想了:

    我们把某个程序要用到的DLL和其中的类名放在一个配置文件中,程序启动时读取这个文件,就可以自动地加载外部指定的DLL,实现程序的组件动态装配。当我需要更换程序中的某个组件时,我只需要将新DLL复制到程序文件夹之下,系统就马上可以使用新的组件,而不用重新编译或修改组件调用者原有的任何代码!

    现在看看我们的示例是怎么样的!

    我们创建两个VB类库,每个类库都封装了一个窗体。VBDynamicComponent组件中封装了以下窗体:


6 组件一

VBDynamicComponent2组件中封装了以下窗体:


7 组件二

    主程序是一个Windows应用程序,它可以动态创建两个组件中的任何一个窗体对象,并显示它们:


8 示例程序主窗体

主程序在启动时会读取一个系统配置清单,是XML格式的,如下所示:

 <?xml version="1.0" encoding="GB2312" ?>

  <Config>

     <Component ComponentName=" DynamicComponent.VBForm1"   ComponentFileName="DynamicComponent.dll" />

     <Component ComponentName="VBDynamicComponent2.VBForm2" ComponentFileName="VBDynamicComponent2.dll" />

  </Config>

 
 

    主程序根据清单装入指定的组件DLL文件,并依据ComponentName属性值创建对象,排列在第一位的组件窗体被自动显示。

    大家看一看,这样一个系统组件装配清单,象不象一个厨师准备大餐时的原料清单?现在就开始制作我们的“满汉全席”吧!

设计示例用到的组件

    创建两个VB.NET类库工程:DynamicComponent和VBDynamicComponent2,分别创建两个窗体VBForm1和VBForm2(如图6图7所示),前者放在DynamicComponent工程中,后者放在VBDynamicComponent2工程中。

    分别编译生成两个DLL文件:DynamicComponent.dll和VBDynamicComponent2.dll。

    接着,我们创建一个Windows应用程序VBTestDynamicComponent用于测试我们的组件装配技术。

读取XML配置文件

    在测试程序启动时,它从XML配置文件中读取信息,我们看到,相关信息可以分为两类:一类是要装入的DLL文件清单,另一类是需要装入的类名。这两者是一一对应的,所以,我们可以创建两个ArrayList,一个用于存放文件名,一个用于存放类名,然后,用一个类MyComponentList把这两个ArrayList给封装起来,外部使用者只需给出索引,就可以马上同时得到文件名与类名。

    类的接口设计如下:

 


9 用于实现动态装入组件的类

 

    参见图9,只要给MyComponentList类的对象指定一个XML配置文件名,再调用beginRead(),调用者就可以通过索引(0,1,2……)来获取文件名和类名。

    读取XML格式数据可以使用.NET framework所提供的XmlTextReader类。完整代码如下:

 

'从XML配置文件中读取组件的类名与文件名

Imports System.Collections.Specialized

Imports System.Windows.Forms

Public Class MyComponentList

    Private xmlreader As Xml.XmlTextReader

    Private _FileName As String 'XML配置文件名

    Private _ComponentFileName As String '组件库文件名

    Private _ComponentName As String '组件库中的类名

    Private componentNames As ArrayList   '存放配置文件中列出的所有组件类名

    Private componentFiles As ArrayList   '存放配置文件中列出的所有组件库名

 

     '在构造函数中创建相关对象

    Public Sub New(ByVal FileName As String)

        _FileName = FileName

        _ComponentFileName = ""

        _ComponentName = ""

        componentNames = New ArrayList()

        componentFiles = New ArrayList()

        xmlreader = New Xml.XmlTextReader(FileName)

    End Sub

 

     'XML配置文件名

    Public Property FileName() As String

        Get

            Return _FileName

        End Get

        Set(ByVal Value As String)

            '文件名空则应抛出异常.

            _FileName = Value

        End Set

    End Property

     '读取组件库

    Public Sub beginRead()

        Dim b1, b2 As Boolean

        b1 = False

        b2 = False

     '以下循环读入文件,使用标记b1和b2来实现“组件库文件ß à组件名”的配对,

     '并分别存入对应的两个ArrayList中(componentFiles ß àcomponentNames

        While xmlreader.Read

            If xmlreader.Name = "Component" Then

                xmlreader.MoveToFirstAttribute()

                If xmlreader.Name = "ComponentName" Then

                    _ComponentName = xmlreader.Value

                    b1 = True

                End If

                If xmlreader.Name = "ComponentFileName" Then

                    _ComponentFileName = xmlreader.Value

                    b2 = True

                End If

                While xmlreader.MoveToNextAttribute()

                    If xmlreader.Name = "ComponentName" Then

                        _ComponentName = xmlreader.Value()

                        b1 = True

                    End If

                    If xmlreader.Name = "ComponentFileName" Then

                        _ComponentFileName = xmlreader.Value()

                        b2 = True

                    End If

                    If b1 And b2 Then

                        componentNames.Add(_ComponentName)

                        componentFiles.Add(_ComponentFileName)

                        b1 = False

                        b2 = False

                    End If

                End While

            End If

        End While

 

    End Sub

     '取出指定索引的文件名(即组件库文件名)

    Public Function getfilename(ByVal index As Integer) As String

       Return componentFiles.Item(index)

    End Function

     '取出指定索引的类名(即组件名)

    Public Function getClassName(ByVal index As Integer) As String

        Return componentNames.Item(index)

    End Function

End Class

 

    这些代码非常清晰,就不多废话了。

动态创建对象

    知道了需要装入的组件,下一步就是如何创建对象了,这需要用到System.Reflection中的类。

    同样地,我们也设计一个类LoadComponent用于封装创建对象的功能:

 


10 完成创建组件对象的类

 

    这个类的使用非常简单,给定一个DLL文件名,LoadComponentLibrary()函数用于将此DLL装入内存,之后,便可使用LoadClass创建指定类的对象。

    现在我们来逐步分析一下这个类。

    (1)装入组件库:

    首先必须了解,在.NET中把可以复用的组件库称为“Assembly”,一般译为“程序集”(在上文中所提到的组件库,其实就是指程序集),大多数情况下,每个.NET DLL都是一个程序集。而可以复用的类就放在程序集中。为此,要动态根据类的名字来创建对象,就必须首先把程序集装入内存。在.NET中,可以通过反射技术来实现这一点。示例代码如下:

 

Private myDll As System.Reflection.Assembly

Public Function LoadComponentLibrary(ByVal ComponentFileName As String) As Boolean

        '装入指定的组件代码库

        '成功返回true

        Try

            myDll = System.Reflection.Assembly.LoadFrom(ComponentFileName)

            If myDll Is Nothing Then

                MessageBox.Show("Can't Load library")

                Return False

            End If

        Catch Errobj As SystemException

            MessageBox.Show(Errobj.Message)

            Return False

        End Try

        Return True

    End Function

 

(2)创建对象:

    一旦程序集被装入内存,我们就可以根据类名字串来创建对象了,如下所示:

 

 Private obj As Object

     Public Function LoadClass(ByVal classname As String) As Object

        If myDll Is Nothing Then

            Return Nothing

        End If

        Try

            obj = myDll.CreateInstance(classname)

        Catch Errobj As SystemException

            MessageBox.Show(Errobj.Message)

            Return Nothing

        End Try

        Return obj

 

    End Function

 

    有了LoadComponent类,我们以后要做的事就简单多了。

实战:

    创建一个VB.NET Windows应用程序:VBTestDynamicComponent。将前面介绍的两个类ComponentList和LoadComponent加入到工程中。

    我们在程序启动时从配置文件中读入相关的程序集信息和类名信息,为此在主窗体的Form_Load过程中添加以下代码:

 

01    Private objLoadComponent As LoadComponent

02    Private VBObj As Object

03    Private ComponentReader As MyComponentList

04

05    Private Sub VBTestDynamicForm_Load(……) Handles MyBase.Load

06        objLoadComponent = New LoadComponent()

07        ComponentReader = New MyComponentList("Componentlist.xml")

08        Dim filename As String

09        Dim classname As String

10        ComponentReader.beginRead()

11        filename = ComponentReader.getFileName(0)

12       classname = ComponentReader.getClassName(0)

13        Dim ret As Boolean

14        ret = objLoadComponent.LoadComponentLibrary(filename)

15        VBObj = objLoadComponent.LoadClass(classname)

16        VBObj.show()

17    End Sub

    我们看见,第07--10句从配置文件中取出DLL文件名和类名,第11、12句获取第一个DLL文件名和类名,然后在第14句装入组件库,第15句装入对象,因为返回的对象其实是一个窗体,所以,在第16句可以直接调用Show()方法显示此窗体。

    从代码中可以看见,在.NET中动态创建对象是多么地方便和简单!请注意:在工程中我们甚至不需要给工程添加对特定组件的引用!本例中动态创建的对象是窗体,而Form对象是缺省就可使用的。在真实工程中,可以把所有的接口放入一个特定的DLL中,我称之为“Facade Assembly”,其它工程只需引用这个DLL,则实现了这些接口的所有组件都可自由地通过名字来创建!

小结

    我们在短短的一篇短文中介绍了.NET的混合语言开发特性和动态装配组件技术,展示了在.NET下开发基于组件的应用软件之简便性与灵活性,这就为我们开发“拥抱变化”的软件系统提供了新的可能性,可以总结如下:

    (1)我们可以通过开发前的OOAD(面向对象分析与设计)将系统功能分块,然后分配给不同的开发人员开发成多个DLL,最后再方便地将这些组件组合起来构造一个新的系统,一旦把某个功能做成了组件,它就可以在以后的项目中不断复用,BUG会越来越少,功能会越来越强,运行会越来越稳定,这样的组件,将是无比宝贵的技术资源。

    (2)由于.NET强大的组件对象模型,原来适用于源代码级别的一些面向对象特性:比如继承和多态,现在都可以推广应用到组件级别。特别地,在组件间现在我们也可以应用设计模式了。

    同志们,赶紧去翻翻GoF的经典书籍,把上面的24种模式应用在组件间,你一定会深深地感到软件技术进步所给程序员带来的广阔发挥余地。

    (3)通过在外部保存配置信息,使我们可以在不重新编译程序的情况下,动态地让整个软件显现出一种完全不同的风格与功能。想想如果在我们的示例中创建的两个窗体是另两个复杂的程序的主窗体,这不就意味着我们的程序可以非常方便地集成它们吗?实现起来太简单了,不就是在配置文件中增加一两行代码吗?

    (4)配置文件采用XML标准,可以很容易地为元素增加新的属性,从而扩充程序的功能。另外,通过提供DTD和schema,可以在动态装入配置文件之前就验证文件是否是有效的,还有XSLT可以对配置文件进行动态地置换……,XML的灵活性与可扩展性就不用我多说了吧?这方面的灵活应用就看你的啦!

    (4)有了以上的技术支撑,我们开发软件的过程中重点就放在OOAD之上了,这正好体现出在面向对象时代系统分析与设计重于编码的特点。

    比如某软件公司需要开发某个行业的产品,它可以对这个行业业务功能进行详细的分析,抽象出不同级别的业务,其中最基本的业务被做成可以动态装载的组件。当需要为此行业的一家新客户定制软件时,可以通过装配现有的基本业务组件,再开发少量的专用组件来完成,从而极大地提高开发效率。这就是CBD(Component-Based Develop:基于组件开发)。

    目前很热门的MDA(Model Driven Architecture),其实就是基于组件的,没有大量的稳定的组件做支撑,MDA是“Drive”不起来的。

    笔者发现,凡是成功的软件公司,尤其是那些一流的跨国公司,比如CA、SAP、爱立信等,其内部都有着一个大规模的组件库,库中的组件都经过了严格的实践考验,基于已有的成熟的组件开发新系统,其代码复用率有时可达80%到90%,要达到这种代码复用程度,需要软件企业深入某一个行业内进行几年十几年甚至几十年的深厚积累,这也是年轻的国内软件企业与国外一流企业的差距所在。

    (5)……

    好了,不用再多说了,相信聪明的读者可以体会到组件开发的巨大威力。

    再回到.NET,微软在给VS.NET做广告时说:它大大提高了开发效率。这并不是吹牛,我个人感觉,使用.NET开发项目,较之传统的Win32平台上的技术,开发效率可以提高1/3!真正深入地学习与应用.NET,笔者不禁对开发与设计.NET技术的科学家和工程师们敬佩不已,向他们的创造性劳动致意。同时,笔者也更期望中国的软件技术能奋起直追,我们的确落后得太多!

   

 

 

 

 

 

posted @ 2008-10-29 22:22  emanlee  阅读(1766)  评论(0编辑  收藏  举报