下面我们使用一个更贴近实际生活的例子来演示一下面向对象编程的整个流程。
需求
假设有一家家具店,店里只卖两种家具,桌子和椅子。我们编写一个程序来模拟一下家具店里这些家具的属性和方法。假设我们需要模拟如下的属性和方法:
- 编号(属性):每个家具都有一个唯一的编号。
- 成本价(属性):商店进货的价格。
- 返回售价(方法):每个家具都有一个固定的售价,它等于
家具的成本价格 *(预计利润率 + 1)*(税率 + 1)
。假设成本价格,利润率和税率都是已知的。 - 组装(方法):桌子和椅子组装过程。
- 桌子的组装:把桌腿固定在桌面上,然后反转过来。具体过程在演示程序中用一段文字代替即可。
- 椅子的组装:把靠垫和椅背固定在一起,然后再安装椅子腿。具体过程在演示程序中用一段文字代替即可。
- 以上几个属性和方法都是桌子椅子共有的,此外,我们再要求每个类提供一个自己独特的方法:
- 铺桌布(桌子类的独特方法):在演示程序中用一段文字表示桌布铺好即可。
- 放置靠枕(椅子类的独特方法):在演示程序中用一段文字表示靠枕放好了即可。
此外,我们将编写一个模拟程序,调用家具店中每一个家具,打印出它们的售价和承重。
设计
根据上面的需求,我们可以做如下的设计
- 需要三个类:家具类,桌子类和椅子类。桌子类和椅子类都继承自家具类。
- 家具类包含有两个数据:编号,成本价;两个方法:返回售价,组装。这些是桌子类和椅子类都共同拥有的属性和方法。
- 返回售价这个方法在所有类中的逻辑都完全相同,所以只需要在父类,家具类中实现,其它两个子类不需要在实现一遍,直接继承父类方法即可。
- 组装这两个方法虽然是桌子类和椅子类都有的方法,但它的实现在两个类里却不相同,需要分别在两个子类中实现。
- 我们还将在桌子类和椅子类中各实现一个 “初始化” 方法,虽然初始化方法在两个子类中同名,但是因为它们的输入参数不同,它们其实是完全不同的方法,所以并不能在父类中定义这个方法。
- 桌子类包含四个方法:初始化(设置产品编号,成本价,和桌布型号),返回售价,组装,铺桌布。
- 椅子类包含四个方法:初始化(设置产品编号,成本价,和靠枕型号),返回售价,组装,放置靠枕。
- 此外还需要定义两个常量:利润率和税率
创建类
按照上面已经介绍过的方法,创建一个新的项目,然后再创建三个类:分别为 Furniture(家具类),Table(桌子类)和 Chair(椅子类)。Table 和 Chair 都继承自 Furniture。
属性(数据)
家具类 Furniture 包含两个数据:id(编号)和 cost(成本价)。为了在桌子椅子初始化的时候可以设置这两个数据,我们给这两个数据添加了数据访问 VI。
桌子类和椅子类分别需要保存桌布型号(tablecloth_type)和靠枕型号(cushion_model)数据。我们最好为这两个数据定义两个自定义数据类型。自定义的数据类型也可以放在类中保存。比如下图中,桌子类的桌布型号控件就保存在了类中。
方法(VI)
首先实现父类家具类中的方法。返回售价(get_price.vi)方法可被子类直接调用,不需要被子类重写,所以可以使用基于静态分配模板的 VI。它的功能就是把家具的成本价乘以利润和交税参数后返回:
父类家具类中组装(assemble)方法是需要被子类重写的,所以它必须是基于动态分配模板的 VI。父类中实现的默认方法仅仅是返回家具的编号:
组装方法会被子类重写,比如下图是椅子类中重写的组装方法,它首先调用了父类的同名方法,得到家具的编号,然后插入一段带有 “椅子(Chair)” 的文字,返回。这样我们将来就能够清楚的知道这个 VI 被调用过了。桌子类中组装方法的实现类似,就不再贴图了。
椅子类中还有一个构造方法(construct.vi),用于初始化椅子的数据。它首先调用家居类中的数据访问 VI,设置产品编号和成本价,然后再把靠垫型号写入到椅子类的数据当中去。桌子类也有一个构造方法,与之类似。
椅子类独有的方法是放置靠垫(put_cushion.vi)。我们就让程序读出靠垫的型号,然后返回一段文字表示坐垫放好了。桌子类的铺桌布(put_tablecloth.vi)方法与之类似。
这样,我们就把用于家具店的几个类都实现好了。
测试
首先写一个简单的 VI 演示为一组椅子放置靠垫,这个 VI(put_chair_cushions.vi)功能很简单,输入一组椅子类的实例,分别调用每个实例的放置靠垫方法:
接下来再写一个 VI 用于组装所有的家具(setup_funitures.vi)。因为这个 VI 要处理所有类型的家具,它的输入输出控件就不能再是桌子或椅子类了,而必须是家具类型。这个 VI 稍微复杂一点,首先针对每一个家具调用 “组装方法”,再调用 “返回售价” 方法,再把两个方法返回的字符串合并起来:
最后,可以编写用于测试的程序了:
这个测试程序大致可以分成三个部分:
- 最左面那一部分是初始化的部分,它调用桌子和椅子类的构造方法,创建了两个椅子对象,和一个桌子对象。
- 中间一部分,把两个椅子的对象放置在了一个数组中,然后传递给 put_chair_cushions.vi 为每个椅子放置靠垫。
- 最右边一部分,把两把椅子和一张桌子防止在了一个数组中,这个数组的数据类型会自动变为家居类的数组,否则无法既存放椅子也存放桌子。
运行这个测试 VI,输出结果如下:
按照传统的编程方式,如果需要对不同的输入对象调用不同方法,需要写一个条件结构,用于判断输入对象的类型,然后按照不同类型去调用不同的子 VI。但是,借助类的多态特性,应用程序(测试程序)不再需要程序员编写代码去判断实例数据所属的子类,以及调用不同子 VI。在程序中,我们完全可以把所有实例用它们共同的父类的类型来传递,代码中也只使用父类的方法。而程序执行到父类的方法时,会自动执行已经重写了它的相应的子类的方法。
在我们的示例中桌子类和椅子类同时从家具类那里继承了 "组装" 这个方法。但是,它们都重写了这个方法,这样就实现了多态。尽管 setup_funitures.vi 的输入控件类型是家具类,但是程序在执行到组装(assemble.vi)这个方法时,会自动判断输入对象的具体类型,然后调用相应的方法,所以我们可以在测试的返回结果中看到桌子类的对象都返回了 “Table” 字符串,而椅子类的对象都返回了 “Chair” 字符串。
在程序调用了 assemble.vi 的地方双击这个子 VI,LabVIEW 不会像对待普通 VI 那样立刻打开子 VI,而是会列出所有类中的同名 VI,询问用户需要看哪一个。