适配器是常见的硬件概念,比如我们常用的USB转RS232适配器。我们之所以采用适配器,主要是由于以下原因:

一、原有硬件设备已经存在,新的连接设备不存在和旧设备一致的接口。
二、新的设备需要连接很多不同接口的设备,需要构建一个统一的接口。

      OOP适配器模式借用了硬件适配器的概念。软件的升级换代是非常常见的,对于已经成功运行的软件系统,我们在设计新的软件系统时,不可能完全放弃原有的软 件系统。但是经常遇到的情况是原有的软件系统和新的软件系统具有不同的理念,在考虑到扩展性的同时,必须兼容原有的软件系统,此时适配器模式是我们首选的设计模式。

我们看一下适配器模式的定义:
      适配器模式将一个类的接口转换成客户希望的另外一个类的接口。适配器模式使原本不兼容而不能一起工作的类可以一起工作。

Picture
      适配器模式并非特别的技术,其实质是在新的类中借用(翻译)原有类的动作和行为。在面向对象的编程中,当然是两个类之间的关系。对于LV来说,由于早期不存在LVOOP,因此适配器很少针对两个类,但是适配器模式的设计思想完全可以推广到面向过程和面向对象的混合编程中。

下面我们通过一个绘图类说明如何使用适配器模式。

      我们的目的是建立一套通用绘图程序类,包括点、线、圆等等,从图形的角度看,无论是点还是圆形,都存在很多共同点,比如绘图颜色、画笔等等,因此建立一个 形状的类作为基类是非常合适的,在基类的私有数据中存储公共特征,比如颜色和画笔,另外需要在基类中创建一个可重写的绘图函数,这样基类和继承的特殊形状 的子类都可以使用同一绘图函数,实现类的多态特性。

Picture
      我们创建一个形状类作为基类,形状类的私有数据包括画笔和颜色,并分别创建了私有数据的读写属性,形状类中,我们创建了可重写的绘制形状方法。属性的程序框图非常简单,就不贴图了。看一下形状类中可重写绘制形状的程序框图。

Picture
      在基类的绘图函数中,不执行任何实际绘图工作,具体绘图工作延迟到具体子类中实现。
      我们知道,绘制一个圆需要圆的半径和圆心,其中圆心是一个点,因此我们创建一个点类,并将点类作为圆类的私有数据成员之一。

Picture
      在绘制圆类中,我们创建了一个静态方法,用来设置圆心和半径,其实这完全可以通过属性实现。由于属性只能设置单一私有数据的值,而采用方法则不受此限制。

Picture
      我们今天讨论的是适配器模式,假设在原来版本中已经存在类似的绘制圆的类,但是其属性和方法与我们新创建的不同,比如名称,参数顺序等等。由于原有类已经 应用于程序中,并在多处使用。如果直接使用新的绘制圆类,必然导致原有程序的多处修改,这不符合开放封闭原则。这种情况下,适配器方式就非常实用了。

Picture
      为了说明适配器模式,我创建了一个Circle类,表示原来存在的绘制圆类,简单起见,直接封装了LV的绘制圆函数,未执行任何其他操作。
      回过头来,我们看看如何在新建的绘制圆类中,通过适配器方式,调用原有的CIRCLE类,来实现绘制圆的操作。

Picture
      上面的程序框图为绘制圆类中重写的绘制形状方法,在该方法中,建立一个原来的CIRCLE类的实例,并调用了CIRCLE中绘制圆的方法。

      适配器模式是比较简单的,借助于适配器,可以充分利用原有结构。原有结构处于封闭状态,但是通过适配器后,改变了原有类的接口,这样就实现了开发的功能扩展。
      适配器模式基本使用的是类的封装功能,对原有类进行进一步的封装,创建新的接口,使之融入到新的体系中。

Picture
 
      建造者模式是非常常用的设计模式,顾名思义,该模式主要一个对象。builder的意思是建造者或者建筑工人,想到建造者自然会想到楼房。楼房是千差万别 的,楼房的外形、层数、内部房间的数量、房间的装饰等等,但是对于建造者来说,抽象出来的建筑流程是确定的。建筑一座楼房,首先需要打桩、建立基础,然后 建立框架。建造者模式最为重要的是流程是不变的,每个流程实现的具体细节则是经常变化的。
      建造者模式保证了流程不会变化,流程即不会增加、也不会遗漏或者产生流程次序错误,这是非常重要的。我们熟知的楼歪歪事件,官方的解释就是由于先建立楼房后,再建设停车场造成的,这是典型的建造次序错乱。
      其实在编程实践中,建造者模式比比皆是。
      比如文件操作,包括打开、读写文件和关闭三个步骤,无论是二进制文件、文本文件、TDMS 文件都必须遵循这个流程。其实、打开、读写、关闭并不仅限于文件,串口操作、GPIB等等,都遵循着同样的流程,不过是每个流程的实现细节不同。
      任何人的成长过程也是一个典型的建造者模式,都包括婴儿阶段、少年儿童阶段、青年阶段、中年阶段和老年阶段。每个人在各自阶段成长的结果是各自不同的,但是整个成长过程肯定是确定不变的,不可能出现先老年、后儿童的情形。
      我们看看建造模式的定义。

Builder模式的缘起:
      假设创建游戏中的一个房屋House设施,该房屋的构建由几部分组成,且各个部分富于变化。如果使用最直观的设计方法,每一个房屋部分的变化,都将导致房屋构建的重新修正.....

动机(Motivation):
      在软件系统中,有时候面临一个"复杂对象"的创建工作,其通常由各个部分的子对象用一定算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合到一起的算法却相对稳定
      如何应对种变化呢?如何提供一种"封装机制"来隔离出"复杂对象的各个部分"的变化,从而保持系统中的"稳定构建算法"不随需求的改变而改变?

意图(Intent):
      将一个复杂对象的构建与其表示相分离,使得同样的构建过程可以创建不同的表示
                                                              -------《设计模式》GOF
      我们前面提及的流程就是子对象的一定算法,这个子对象的具体算法是经常变化的,但是复杂对象包含这个子对象是确定的,也就是说建造复杂对象的过程中,必须 包含子对象这个过程。但是这个部分“经常面临剧烈的变化”。显然,对于打开文件和打开设备,内部具体实现肯定是剧烈变化的。
      子对象内部是剧烈变化的,但是复杂对象的构建流程是确定的,也就是所谓的“组合到一起算法相对稳定”
      所谓“复杂对象的构建与其表示分离”,构建就是抽象出确定的流程,表示指的每个流程的具体实现。

Picture
下面通过具体实例说明如何使用建造者模式。
问题:通过建造者模式解决零件生产问题。

一、分析流程,创建抽象类
      我们综合零件的生产过程,可以抽象出如下过程。

1、零件设计 2、零件工艺 3、材料准备 4、加工 5、检查 6、入库

      工厂生产的零件有多种,各不相同,但是总的流程是确定的,因此需要创建一个Builder抽象类,这个类描述了所有零件的生产过程,但是对于任何实际的零件,从设计到工艺,从加工到检查,具体细节各不相同。

Picture
      零件抽象类作为所有具体零件的基类,因为LV无法实现真正的抽象类,所以把零件类放在库中,并设置零件类的库属性为私有,这样在库外部无法直接访问,间接实现了抽象的效果。
      零件抽象类的私有数据成员数据类型为字符串,用来描述零件的整个生产过程。
      零件的整个生产过程用5个方法来描述,这些方法都是可重写的,具体零件真正的生产过程延迟到子类中实现。

Picture
      上图为零件设计方法的程序框图,其它方法类似,目的是把当前过程写入到私有数据成员中,表示正在进行的过程。
      接下来创建零件类的属性,用来读取存储在字符串中的运行过程。

Picture
二、创建具体零件类

      零件抽象类设计完成后,具体零件的实现类就很容易了,只需要继承零件抽象类,并重写其中几个方法就可以了。

Picture
      我们创建了两个具体的零件,分别是齿轮和连杆,其基本流程是相同的,但是每个零件各个具体流程的实现方法是不同的,我们只是不同的字符串表示其不同的加工过程。

三、创建指挥者

      我们在零件抽象类中,创建了零件加工过程的各个过程,但是并没有真正体现出整个零件的加工次序,比如是先设计还是先加工等等。所以需要创建一个指挥者类, 该类私有数据成员中包括一个零件抽象类的实例,并负责具体建造的过程,指挥者保证了无论是齿轮还是连杆,都必须遵循同样的流程。

Picture
      上图中,最上面部分为指挥者类的私有数据成员,存储的是零件抽象类的实例。下面部分是指挥类的属性,负责写入具体类的实例,替换存储的私有数据。
      指挥者类中创建了一个方法,用来指挥具体零件的生产过程,对于任何具体零件,其生产过程都是一致的,这是抽象类中定义的可重写方法,指挥者不过是规定生产流程的次序。

Picture
      上面的方法确定了零件的生产过程为零件设计、零件工艺制作、零件加工、零件检查、零件入库,最后返回一个字符串,描述整个生产过程。

四、具体调用

创建了上述几个类后,具体调用就非常简单了。

Picture
      上面的程序框图分别创建了齿轮和连杆两个具体实例,在字符串中输出了他们整个生产过程。

Picture
Picture
 
      单例模式:保证类只有一个实例,并提供一个全局访问点。

      前面讨论过类的原型模式,这说明使用类的一大优势是类是可以创建多个实例,而且彼此之间是相互隔离的,互相之间并不影响。今天讨论的是相反的情况,即类的单一实例问题。

      EXE文件、VI都存在单一实例的问题,比如 我们在操作一个硬件时,一般不允许多个执行文件同时操作,这就要求该EXE 文件不能启动两次,VI 也是如此。默认的VI 设置是不可重入的,不可重入VI本身就保证了该VI 必须运行完毕后,下一次调用才能执行,所以常规VI是单一实例的。可重入VI恰恰相反,可以同时运行VI的多个实例,类也是如此,个别情况下,只允许单个实例,这就是所说的单例模式。

      单例模式有两个重要的特点:

一、只能创建唯一的一个实例。
二、必须提供一个基于全局的访问方法,在任何场合都可以随意调用这个实例。


      我们自然会想到能否用LV内置的全局变量解决这个问题,如果把类的实例存储在全局变量中,这解决了全局访问点的问题,任何场合都可以调用这个实例。但是, 这种方法有两个重大的缺陷,一是全局变量的竞争问题,更为重要的他无法满足只能创建唯一一个实例的要求。通过全局变量,并不能防止其他地方创建这个类的实 例。

      常规的面向对象编程语言是这样解决的。

Picture
Picture

      在类的定义中, 定义一个本身类的引用,该引用为静态变量,VB.NET 称作共享型变量,在所有类的实例中共享。LVOOP 中,我们在类中也可以定义这样的共享变量。在LV的类中,如果定义一个全局变量作为类的数据成员,这实际上限定了这个全局变量的作用范围仅限于类中。

      构造函数为私有函数,这意味着无法在外部直接创建类的实例。提供了一个静态(共享)方法,通过它返回唯一的实例。如果尚未创建实例,则创建之,如果已经创建,则返回这个类的实例。

      类的引用(指针)作为类的数据成员,这是一般面向对象编程语言都支持的,事实上即使面向过程的C语言中,结构类型虽然不允许把结构本身所为成员,但是支持结构指针作为结构的成员,由此导致了链表等重要的数据结构。

      在类本身检查是否是唯一实例无疑是最为合适的,因为这非常有利于数据封装,不需要外部参与,但是LVOOP是值传递的,是无法在类中定义类本身的引用作为数据成员的,因此无法直接在类中实现单例模式。
     
       LV 的例程中,提供了一个单例模式的例子,下面我们分析一下它实现的过程。


Picture
      如上所述,我们无法在类本身实现单例模式,所以只能通过再次封装这个类实现。在Singleton.lvlib库中建立一个一个类,它的库属性为私有。注意上图中,如果通过快捷菜单设置为私有访问范围,则显示一个暗红色的钥匙,表示这是库中的私有数据。这样就不保证了外部无法直接访问这个类,自然也就无法 直接建立类的实例了, 这实际上对外隐藏了类的存在。

      由于单例模式要求任意时刻只能有唯一一个类的实例存在,并且全局都可以访问,我们自然应该想到用LV2 函数变量存储这个类的实例,这样就可以保证访问是全局的,而且实例是唯一的,因为无法在外部访问类,所以自然不会有其它地方能够实例化这个类。

      Get Queue.VI就是一个典型的LV2 型全局变量,程序框图如下图所示。

Picture

      在首次调用Get Queue.vi或者队列为空的时候,建立一个类的实例。使用之包含一个元素的队列,是LV 常用的传引用的方法,而不是直接传值,这个方法我在之前的文章中提及过。如果不是首次调用,则直接返回队列引用。

      我们注意到该LV2全局变量也设置为库私有的,所以库的外部是不允许直接使用的。上面我们解决了单例模式中的唯一实例化问题,下面要解决的是如何全局访问的问题,当然如果该VI设置为公共,是可以直接进行全局访问的。

      该例子中建立两个公有VI,分别是Check In Vi 和Check Out VI,作为外部访问接口。
Picture

      在Check Out VI 中取出类的单一实例,由于存储类的应用的对列为单一元素,因此多个Check Out 同时操作时,同一时刻只能有一个能够获取类的实例,这样就保证了可以多处访问,访问是全局的,但是唯一实例是共享的。

      取出类实例后,进行相应操作后,完成时必须重新把类实例再次写入队列,这就是Check In.vi实现的功能,写入队列后,由于队列中已经存在一个元素,因此其他Check Out 可以获取类的实例。程序框图如下所示:

Picture

      由于是LV 本身提供的例子,这里就不过多地描述了,上面主要是解释一下单例模式实现的思想。

Picture
 
原型模式的定义:
      用原型实例指定要创建对象的种类,并且通过拷贝这些原型创建新的对象。
Picture
      LVOOP 是值传递方式,这意味着LV自动实现原型模式,不需要任何编程。
      一个对象的实例意味着在堆中已经建立了对象的变量,这个对象的变量即包含了对象的类型,也包括了对象的私有数据。原型模式即根据已有的对象实例,建立一个或者多个新的实例,这个实例具有和它的原型图同样的变量类型和私有数据。
      在文本方式面向对象的编程语言中,复制一个对象的实例并非一件简单的事情,在VS.net编程中,分为深层复制和浅层复制两种方式。
      浅层复制复制本身是值复制,复制后的实例具有和原型完全相同的私有变量值。如果一个类中包含另一个类作为它的数据成员,此时复制的指向包含类的引用。也就是说,浅层复制中,如果类中包含了另一个类,那么浅层复制后,原型和新的实例中,包含类的引用指向同一地址,并非完全创建了一个不相关的实例。
      浅层复制在VS.NET 中比较简单,因为自动提供了Clone接口,但是深层复制则编程比较复杂。
      在LVOOP 中,原型复制是非常自然的。LV 的数据流方式与LVOOP的值传递方式本身就是深层复制的,可以轻易实现原型复制模式。
     
      下面我们创建一个例子,演示一下LV 自然的原型模式。 例子中创建了个人信息类和公司信息类,在雇员信息类的私有数据中,将个人信息类和公司信息类作为雇员信息类的数据成员。
      这样做的目的是,对于很多雇员信息,他们的个人信息是不同的,但是多人的公司信息可能是相同的,这样通过原型方式,可以创建多个具有相同公司信息,不同个人信息的具体实例。这就是原型模式的重要作用。


Picture
      个人信息类的私有数据成员和获取个人信息方法以及设置个人信息方法如下图所示:
Picture
      公司信息类的私有数据成员和获取信息以及设置信息方法如下图所示:
Picture
Picture
      下面我们创建一个测试VI,根据一个雇员信息为原型,创建多个具体实例,所有实例具有相同的公司信息。
Picture
      上面的例子中首先创建一个公司信息类的具体实例,由于LVOOP 值传递的特点,替代了雇员信息类的中公司信息类成员,结果输出了一个雇员信息类的具体实例。用FOR 循环自动索引,自动创建了三个雇员信息类的具体实例并写入数组。
      可以看到这三个具体信息类的实例,具有相同的公司信息,这样就实现了原型复制。
      LVOOP 是天生的原型复制,这得益于他的值传递方式。但是由于并非传递引用,所以很多OOP的面向对象的功能在LV 中很难实现。
      原型模式的主要目的是如何创建实例的问题,我们知道,LVOOP不支持构造函数,所以它创建实例的过程本身采用的就是原型复制模式。
      我们重新考虑一下,如何在LV 中创建一个类的实例,通常有三种方法。

一、通过复制类常量,构造一个具体实例。
Picture
二、通过类的输入控件,建立一个新的类实例
Picture
三、使用函数导出类的实例,这是最为重要的创建实例的方法。
      通过类的方法,可以输出类的实例。对于一般的方法,类的输入端子选择必须连接,这样在使用方法时,必须传入类的实例。
      一个重要的技巧是,类的方法中,如果类的输入端子采用推荐或者可选方式,在不连接输入的情况下,则输出默认值,实际上就是根据默认值自动构建一个类的实例。

      通过这种方式,可以实现类似于构造函数的功能。
Picture
Picture
 
      LabVIEW 是独特的面向过程的图形式编程语言,LabVIEW编程的核心是数据流。数据流的实质是值的传递过程,也就是说对于一般的数据,LV 采用的是值传递的方式,而不是引用(指针)传递方式。
      目前,流行的编程语言多采用面向对象编程技术,面向过程的编程方法由于其运行效率比较高,在嵌入式和硬件密切相关的编程中,仍然广泛采用,所以在相当长的阶段内,面向过程的编程方法仍然是LV编程的主要方法。
      当然,对于常规的桌面应用和大型综合性的软件开发,面向对象具有无可比拟的优势。自从LV8后续版本后,增加了对面向对象的支持。LV中面向对象虽然具有继承、封装和多态三大特性,但仍然与常规的面向对象的方法有很大不同,比如LV 中的类对象(实例)仍然采用值传递方式,没有明显的构造函数和析构函数,不支持类的的事件等等。

      面向对象编程最为重要的是选择合适的设计模式,有很多专业的书籍介绍了众多的设计模式。所谓设计模式就是编程前辈们总结的各类成型的设计套路,LV 中一般称作框架。随着面向对象技术的引入,LV常规的编程方法不可避免地会做相应的改变。比如NI 的数据库工具包和报表生成工具包都采用的新的面向对象的编程方法。
      在今后的LVOOP系列文章中,我将尝试结合常用的设计模式,看看LV中能否实现和如何实现各类成型的设计模式。


      下面要讨论的是简单工厂设计模式。

      面向对象编程存在两个重要的原则,单一职责原则和开放封闭原则。面向对象编程的最大优点是软件系统已于维护和开放性,客户的要求总是不断在变化,对于面向过程的编程方式,客户不经意间增加的简单功能都可能会导致软件的多处修改。而对于面向对象的编程,只要遵循单一职责原则和开放封闭原则,会使软件的修改达到最小。

      下面我们讨论一个具体的实际问题,以说明如何单一职责原则和开放封闭原则,以及简单工厂模式的运用方法。

      假如我们考虑两个数之间的运算,可能会涉及加、减、乘、除等,甚至还有两个数之间的关系运算,比如大于、不等等等。对于这个具体应用,我们不可能预先设计 所有情况,因为这取决于用户的要求,比如这是计算器的一部分,这个计算器可能是简单的计算器,后来要求改变为高级计算器,这不可避免地涉及到改动和增加功 能的问题。

      首先我们考虑如何把这个问题抽象化,固定不变的是存在两个数,变化的是运算方式问题。所以构造 一个运算类,运算作为虚拟函数(LVOOP没有提供抽象类和虚拟函数的方法,但是我们可以模拟这种方式)。运算类作为所有具体运算的基类,具体的运算方法延迟到子类中实现。

Picture

      运算基类作为所有具体运算类的基类,其私有成员(LVOOP只允许私有数据成员,不允许有保护、公共数据成员)包括A、B 两个操作数。

      运算基类建立四个类属性成员(LVOOP 称作数据成员访问的VI),分别用于读写A、B 两个成员。


Picture
      操作数B的读写属性类似。我们特别要注意下面的运算基类的方法,在真正面向对象的语言中,该方法应该是纯粹的虚方法,但是LVOOP 中没有虚方法的概念,我们我们采用一般的重写VI。
Picture
      该重写VI 不执行任何实际操作,只是返回一个结果,运算基类的运算结果重写VI 返回0.
      在运算基类的基础,具体的运算继承运算基类,继承类中私有数据成员为空,我们需要做的是重写运算结果VI.

Picture
      对于两个数的加法运算,我们构造一个加法类,在加法类中重写了运算结果VI。在运算结果VI 中,通过基类的两个属性,分别读取A和B的值,并执行加法运算,返回具体运算结果。

      减法类与加法类类似,不过是返回A-B的运算结果。

      同理,如果需要计算乘法,则直接建立一个乘法类即可。由于乘法类不需要对加法类和加法类进行任何改变,这就符合了单一职责原则和开放封闭原则。也就是说具体 运算类是开放的,这体现在增加不同的具体运算类,则增加了新的算法。在增加新算法的同时,不需要对原来的算法进行任何改变,这就遵循了封闭原则。

      所谓简单工厂,就是根据不同的要求,返回具体类的实例。对于我们的运算类来说,就是根据具体要求,返回加法、减法等不同的具体实例。

Picture
      需要注意的是,工厂类不包含任何私有数据,仅包括一个操作方法,该方法返回一个具体的实例。返回的类实例必须为基本运算类,只有这样,才能实现多态运算。

      下面通过一个VI测试一下简单运算工厂。
Picture
      使用面向对象的方法,最重要的是封装,封装不仅仅是指封装了具体数据,更为重要的封装了具体的方法。在上面的测试VI中,通过简单工厂类,我们仅仅是通过枚举简单选择一下,就可以返回不同的实例,实现不同的算法。

假如我们需要增加一个新的算法,只需要做如下改动:
一、建立一个新的运算类,继承与运算基类。
二、修改运算工厂,增加一个新的实例选择。

      对于原有的具体算法,不需要任何形式的改动,这就是简单工厂模式的重要作用。

Picture