账号:
密码:
最新动态
产业快讯
CTIMES / 文章 /
物件导向之大话西游
 

【作者: 吳明皓】2004年02月25日 星期三

浏览人次:【7077】

今年一月份的时候,笔者受命去为一家营造公司上课。课程的主题除了讲解一般所谓的资讯系统为何物之外,还包括了讲授何谓物件导向的程式设计,为时两个小时。


本来以为这是一项轻松简单的任务,但是没想到老板交代下来的客户要求是上课内容不但要丰富精彩,而且重点必须是放在「物件导向的程式设计」上面。这下子笔者的脸上不但开始变得非常尴尬,且这一下子马上将原本简单的任务变成了「不可能的任务(Missing Impossible)」。因为在短短的两个小时中,除了扣掉自我介绍、咽口水、咳嗽、打喷嚏,以及段落之间的休止符之外,大概剩下不了多少时间。


所以要在这么短的时间内介绍这样的主题,笔者只好尽量捡一些重点来说明,也因此上课的结果是:笔者在台上比手划脚,讲的口沫横飞,口水肆溢。台下则是问号掉满了一地,大有丈二金刚摸不着脑袋之态。不过在准备教材的过程中却让笔者想起了两段往事。


是定理还是方法

第一件事情是在1995年初,某天我的第一个老板突然兴冲冲跑来跟我说:「公司即日起开放员工申请订购工作上所需的杂志或书籍。这是申请单,你填一下吧。」我记得当时只填了一份高焕堂先生的「物件导向杂志」。过了没多久,老板就一脸臭臭的回覆我说:「这本杂志用的是VB作范例,而VB又不是物件导向语言,没有参考的价值。」所以我的申请,就这么被驳回了。


第二件事情是在 1995 年中时,笔者奉命参加中台湾地区的Delphi研讨会。记得当时讲师说了一段话让笔者印象十分深刻。他说:「我们使用Delphi来开发应用系统,其实就已经在用物件导向的方式来写程式了。因为在Delphi之中,我们几乎都是在物件的事件中,使用物件的属性以及方法来解决事情。」


这两件事情跟本章的主题有何关系呢?其实关系大了,因为如果笔者询问什么是物件导向的程式设计?相信很多读者都可以肯定的答覆笔者说:「物件导向的程式设计,有别于传统水瀑式(Water Fall)的程式设计以模组作为思考基础,而它则是以物件作为思考对象,进而进行应用系统的分割与应用。」


再热心一点的会再接着告诉笔者,现在有哪些程式语言是支援物件导向,哪些则不支援。是的!这些答案都是蛮标准,但也蛮制式。不过基本上还是没有说清楚什么是物件导向的程式设计?只是假如答案是上述案例中讲师所描述的那样,那么事实上恐怕也不太尽然。


物件导向的特性

根据Brad Cox于1980年所发表文献中指出,所谓的物件导向应该具备下列的特性:


  • ●物件(Object)以及讯息(Message)


  • ●继承(Inheritance)


  • ●封装(Encapsulation)


  • ●动态连结(Dynamic Binding)



很惊讶吧!早在二十多年前,就已经有大师级的人物提出已经相当成熟的物件导向软体设计的概念了。如果我们回溯那个年代,其实我们还可以发现很多令人惊讶的事情。比如1969年第一个纯物件导向的程式语言Small Talk 被发表出来,那时候才正刚刚结束电晶体进入IC的时代而已。至于我们常用的C语言则是到了1971年才发表,而现在所用的PC则是一直到1981年才由IBM公司正式推出。


其中继承、封装、动态连结是物件导向中,最重要也具盛名的特性,所以很多人都据此来评断,哪种语言或工具支援OO。我们常在一些论坛中见到诸如此类的叙述:「VB不支援类别(Class)的继承概念,同时也不支援资讯封装的方法,所以VB不是一个OO程式语言。Java由于不支援指标型态,因此更能彻底封装类别的资讯,跟C++比较起来,更能贴近OO的精神。」


然而对于这些叙述,笔者姑且不讨论他们之间的对与错,只是觉得发表这些评论的人,把上述OO的特性当作是个「定理」来遵从罢了,所以才会造成论坛中每隔一段时间就会引起类似这样话题的争议。平心而论,在各家OO的先驱中对于OO的定义与规范并不尽相同,例如Br​​ad Cox、Grady Booch、James Rumbaugh,及Ivar Jacobson等几位大师所提出的概念就不完全一样。如果读者有兴趣,不妨到图书馆查一查他们彼此之间也曾有过精彩的辩论。所以就各家程式语言中,对于OO所支援的程度有所不一也是理所当然,特别是喜欢自称OO的先驱C++都是如此。


物件导向定义的验证

假如我们硬是以某一种语言作为标准来评断其他的语言,在作法上并不公平。因此为了证明这一点,笔者根据Brad Cox所公布的OO特性,在此大胆的进行一项有趣的验证:


首先我们假设「凡是支援继承、封装、动态连结的程式语言或工具,我们可以说它们是支援 OO 的,反之则亦然」。然后为了证明这个假设能够成立,我们先对刚刚所谓的OO特性进行以下的定义:


  • ●继承 → Source的Reuse。


  • ●封装 → 保护程式中的资讯,并透过验证过的程序来做存取。


  • ●动态连结 → 于执行时期决定执行物件的种类与分法。



现在笔者以继承和封装为例,并以素有OO之称的Delphi与不支援OO的VB两种工具,来逐一比对它们之间的差异。


SayHello的函数范例


unit Main001;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TA = class(TObject)
  public
     procedure SayHello;
  end;

  TB = class(TA)
  public
     procedure SayHello;
  end;

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    ﹛Private declarations﹜
  public
    ﹛Public declarations﹜
  end;

var
  Form1: TForm1;

implementation

﹛$R *.单方面﹜

{ TA }

procedure TA.SayHello;
begin
   ShowMessage('Hello by Class A ...');
end;

{ TB }

procedure TB.SayHello;
begin
   Inherited;
   ShowMessage('Hello by Class B ...');
end;

procedure TForm1.Button1Click(Sender: TObject);
var Obj: TA;
begin
   Obj := TA.Create;
   Obj.SayHello;
   Obj.Free;
end;

procedure TForm1.Button2Click(Sender: TObject);
var Obj: TB;
begin
   Obj := TB.Create;
   Obj.SayHello;
   Obj.Free;
end;

end.

在上面的范例中,我们可以看出其中简单直接的继承关系,类别TB继承自类别TA。其中类别TB的成员函数SayHello只不过是从一个函数再去呼叫另一个函数而已,所以当呼叫类别TB的成员函数SayHello时,它会分别显示「Hello by Class A ...」以及「Hello by Class B ...」两个讯息。因此如果单单从这个角度来看的话,VB 也是可以做的到。至于继承中的负载(Override),我们只要在 VB 中加上一些遮罩用的旗标,一样可以达到相同的效果。


资讯封装范例


 TC = class(TObject)
  private
    FCaption: String;
    function GetCaption: String;
    procedure SetCaption(const Value: String);
  public
    property Caption: String read GetCaption write SetCaption;
  end;

上面是一个资讯封装的例子,其中属性Caption必须透过GetCaption与SetCaption这两个方法才能存取得到,目的在于保护FCaption不会被外界任意的存取,并且保证FCaption所储存的资料会经过一定程序的验证。现在我们再看看下面VB的程式码。


VB的cls档的范例


Option Explicit
 
Private Member() As String
Private pCount As Integer
 
Private Sub Class_Initialize()
   pCount = 0
End Sub
 
Public Property Get Count() As Variant
   Count = pCount
End Property
 
Public Sub Add(a Value As String)
   pCount = pCount + 1
   ReDim Preserve pMember(怕Count - 1)
   怕Member(怕Count - 1) = vNewValue
End Sub
 
Public Sub Clear()
   pCount = 0
   ReDim pMember(怕Count)
End Sub
 
Public Sub MoveItem(Idx As Integer)
   Dim i As Integer
   
   For i = vIdx To UBound(怕Member) - 1
      怕Member(i) = 怕Member(i + 1)
   Next
   pCount = pCount - 1
   ReDim Preserve pMember(怕Count)
End Sub
 
Public Function Index OF(vSearch As String) As Integer
   Dim i As Integer
   
   IndexOF = -1
   For i = LBound(怕Member) To UBound(怕Member)
      If vSearch = 怕Member(i) Then
         IndexOF = i
         Exit Function
      End If
   Next
End Function
 
Public Function Item(index As Integer) As String
   Item = 怕Member(vIdx)
End Function
 
Public Sub SetItem(Idx As Integer, ByVal vNewValue As Variant)
   怕Member(vIdx) = vNewValue
End Sub

以上是笔者用VB写的一个cls档(即是 VB 的物件类别模组),用以实做一个TsingleList。虽然它不是所谓的Class形式,但是透过private与public的宣告,一样具有所谓资讯封装的效果。换言之,别的VB单元是无法存取到private的内容。


Delphi的动态连结的范例


procedure TForm1.Button1Click(Sender: TObject);
begin
   Widget Control(Sender).Enabled := not Widget Control(Sender).Enabled;
end;

上面是Delphi一个动态连结的范例,我们可以看到参数Sender在编辑时时,并未确定它的真正型态,反而是到了执行时期,我们才藉由TWidgetControl类别取得Enabled的属性值。这样的效果在 VB 中,用不定型态、物件变数或利用COM的特性一样可以达成。我们可以运用物件变数,如Object或Control来达到这样的结果:


物件变数的范例


Public Sub EnableControl( Sender as Object)
   Sender.Enabled = not Sender.Enabled
End Sub

此处若只是要检查型态,只需运用Typeof或TypeName来确认型态即可,因此我们可以对上面的程式做些修改:


检查型态的范例


Public Sub EnableControl( Sender as Object)
   If TypeName(Sender) = "TWidgetControl" 他很
       Sender.Enabled = not Sender.Enabled
   End If
End Sub

从上面的推演过程中,相信笔者的假设获得成立的有VB可以支援继承、封装,及动态连结。换句话说VB是支援OO的程式语言。这样的说法相当吊诡,而且在理论基础上也有些牵强附会。不过本文撰写的目的,不是要卖弄诡辩之术,而是要借此机会向各位读者说明其实物件导向程式设计,是一种约定成俗的设计方法,和一种推理归纳的思考模式,而不是一种条文式定理。不过这样说实在太模糊了,我们还是用范例来说明吧!


不同做法的物件导向架构

(图一)的范例是一个含有三组以三个Button作为Group的程式,当使用者按下第一个按钮时,会Disable本身并会Enable第二个按钮,同理其他的按钮也是如此。这样的作法,在于让使用者依次序来按下Button,而不让有次序不同的操作出现,这种次序性安排方式在很多防呆的应用中是经常看得见。在这个范例之中,笔者提供了三种不同的撰写方式,现在我们来看看它们的程式码是如何撰写:



《图一 三个Button的案例》
《图一 三个Button的案例》

第一种范例,分别在各自Button的OnClick事件撰写处理的程式码。


procedure TForm1.Button1Click(Sender: TObject);
begin
   Button1.Enabled := False;
   Button2.Enabled := True;
   Button3.Enabled := False;
end;
 
procedure TForm1.Button2Click(Sender: TObject);
begin
   Button1.Enabled := False;
   Button2.Enabled := False;
   Button3.Enabled := True;
end;
 
procedure TForm1.Button3_Click(Sender: TObject);
begin
   Button1.Enabled := True;
   Button2.Enabled := False;
   Button3.Enabled := False;
end;

第二种范例,在某一个Button的OnClick事件中撰写处理程序,然后其他Button的OnClick事件再指向这个处理程序。


   Button4.Enabled := False;
   Button5.Enabled := False;
   Button6.Enabled := False;
   if Sender = Button4 then Button5.Enabled := True;
   if Sender = Button5 then Button6.Enabled := True;
   if Sender = Button6 then Button4.Enabled := True;

第三种范例,我们撰写了一个TButtonGroup新的类别,专门来处理按钮顺序的问题。


type
  TButtonGroup = class(TGroupBox)
  private
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure OnClick(Sender: TObject);
  public
    constructor Create(Aown​​er: TComponent); override;
    destructor Destroy; override;
  end;
 
constructor TButtonGroup.Create(Aown​​er: TComponent);
begin
   inherited;
   Button1 := TButton.Create(Self);
   Button2 := TButton.Create(Self);
   Button3 := TButton.Create(Self);
   Button1.Parent := Self;
   Button2.Parent := Self;
   Button3.Parent := Self;
   Button1.Name := 'Button7';
   Button2.Name := 'Button8';
   Button3.Name := 'Button9';
   Button1.Top := 25;
   Button2.Top := Button1.Top + Button1.Height + 10;
   Button3.Top := Button2.Top + Button2.Height + 10;;
   Button1.Left := 28;
   Button2.Left := 28;
   Button3.Left := 28;
   Button2.Enabled := False;
   Button3.Enabled := False;
   Button1.OnClick := OnClick;
   Button2.OnClick := OnClick;
   Button3.OnClick := OnClick;
end;
 
destructor TButtonGroup.Destroy;
begin
   Button1.Free;
   Button2.Free;
   Button3.Free;
   inherited;
end;
 
procedure TButtonGroup.OnClick(Sender: TObject);
begin
   Button1.Enabled := False;
   Button2.Enabled := False;
   Button3.Enabled := False;
   if Sender = Button1 then Button2.Enabled := True;
   if Sender = Button2 then Button3.Enabled := True;
   if Sender = Button3 then Button1.Enabled := True;
end;
 
procedure TForm1.FormShow(Sender: TObject);
begin
   BG := TButtonGroup.Create(Self);
   BG.Parent := Self;
end;
 
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
   BG.Free;
end;

从上面的范例中,我们可以清楚地看到虽然程式码都是由Delphi来撰写,所表现的行为也相同,但很明显的是第一种方法非常不高明,因为它把程式码分散到每一个物件的OnClick事件中来处理。假如这时所需要的物件不多,那么还感觉不出来有任何的问题,但是一旦需要控制的物件很多,那么整支程式岂不是到处都以这种使用者控制方式的程式码,这岂非是不智之举,所以我们归类这种写法是属于新手级的写法。


第二种方法虽然还是需要在程式之中撰写控制程式码,但明显地就比第一种方法简洁许多,基本上我们归类这种写法为老手级的写法。因为这样写法已经具备模组化的观念,所以程式可以写的十分地有条不紊。


第三种方法我们称之为物件导向式的写法,因为这种写法已经将这种类似物件操作的控制程式码,转移到新增的TButtonGroup类别中,所以主程式里面就可以完全不用理会那些Button之间要如何运作,让程式看起来更为简洁有力。


从上面的范例中,相信聪明的读者已经感受到同样的程式,及不同写法之间的差异了。虽然这些差异虽然对于使用者而言,完全感受不到它们之间的不同所在,就像不管白猫或是黑猫,也是只要会抓老鼠的猫,就是好猫,但是对于程式员在实作上,这些差异影响就很大了。因为一支有条不紊的程式,毕竟总是比较容易维护。当然对此有些人可能会不服气,并且强调这是个人程式撰写的风格问题。但是笔者要强调的是撰写程式原本就是希望要迅速有效的为客户解决问题,而不是在玩「记忆金库」这样的游戏。更重要的是程式要让别人看的懂,这样才有办法进行团队的合作,提升软体的价值。而使用OO也正是使用它所提供方法来达成这样的目的。


讨论到这里,我们可以回应本文之初所描述的那两件事情,那就是虽然VB普遍不被人认为是属于OO的程式语言,但事实上使用VB,我们还是可以很OO。同理,既使我们用的是OO的程式语言,例如C++、Java等程式语言,一样可以写出很不OO的系统出来。其中的关键,就是程式员是否真的具有OO的概念。


(图二)是笔者以前用VB所撰写的专案。请留意画面右方专案清单里的档案型态,其中除了一般的vbs,以及frm类型的档案之外,还多了cls类型的档案,且笔者还故意的使用类似Delphi对于类别命名的惯例,在档案名称之前加上『T』这个字母,其作法就是仿照OO中,对于类别的定义。至于VB所编译出来的程式,以前在工程师间一直被诟病有不稳定的传闻,但是这支程式在客户使用的这些年以来,一直都很稳定。这证明了不管使用何种语言,只要采用一个好的方法,的确可以产生一个稳定的程式,因此有谁能说VB不好呢!



《图二 VB所撰写的项目》
《图二 VB所撰写的项目》

@大標:物件导向的思考模式


记得笔者刚刚所说的物件导向程式设计是一种设计的方法,也是一种思考的模式吗?刚刚笔者的演绎,只是示范了物件导向的设计方法,而物件导向的思考的模式,则又是怎样的一回事呢?我们以一个范例来说明:


现在我们预备撰写一部西游记的游戏,其中的角色包括唐三藏、孙悟空、猪八戒,以及妖怪等。然后在游戏之中会遇到过河,或遇到妖怪等事件,在传统的思考模式之中,系统通常会用切割成下面的样子。


  • 1. 西游记主程式。


  • 2. 过河。


  • 3. 遇到妖怪。


  • 4. 妖怪遇到三藏师徒。



其中过河部分会包括三个步骤:


  • a. 如果唐三藏过河则划船。


  • b. 如果孙悟空过河则用飞的。


  • c. 如果猪八戒过河则用游的。



而三藏师徒遇到妖怪也会分成三个步骤:


  • a. 如果唐三藏遇到妖怪则念经。


  • b. 如果孙悟空遇到妖怪则斩妖。


  • c. 如果猪八戒遇到妖怪则大喊救命。



而妖怪遇到三藏师徒也会分成三个步骤:


  • a. 如果是唐三藏则吃掉。


  • b. 如果是孙悟空则逃跑。


  • c. 如果是猪八戒则戏弄他。



然而在物件导向的思考模式下,系统通常会用切割成下面的样子。


  • 1. 系统主程式。


  • 2. 唐三藏。


  • a. 过河则划船。


  • b. 遇到妖怪则念经。


  • 3. 孙悟空。


  • a. 过河则飞。


  • b. 遇到妖怪则斩妖。


  • 4.猪八戒。


  • a. 过河则游。


  • b. 遇到妖怪则呼救。


  • 5. 妖怪。


  • a. 遇到唐三藏则吃。


  • b. 遇到孙悟空则跑。


  • c. 遇到猪八戒则戏弄。



聪明的读者看出上面范例中的不同点了吗?很明显在传统的方式里虽然模组化可以将程式规划的很清楚,但是免不了每一个模组都必须跟全域变数或物件或模组纠缠在一起。这样的结果,在团队的开发中是非常不利的,因为除了在系统开发之前,系统负责人需要将所有沟通的介面规格制订的清清楚楚之外,任何人、任何模组在事后的所做的任何修正都会影响到系统的正确性,这正可谓之『牵一发而动全身』而物件导向的模式则可以避开这样的问题。


另外还有一个重点是在OO的思考模式里,我们不但可以顺利切割整个应用程式的程式码部分,就连程式中的逻辑部分,也一并切割的清清楚楚。这是一个在开发应用系统中,不容易出错的最主要关键。


结论

讨论到这里,很多人都会问为什么我们要采用物件导向程式设计(OOP)的方式来设计系统呢?其实深究起来原因很简单,答案就是要尽量减少系统开发的时间以及出错的机率。然而很遗憾的,在笔者接触许多国内的案例中,虽然他们都是使用Java、Delphi或C++,但却鲜少有人真正使用物件导向的思维来设计程式。更有什者连基本模组化的工作都做不好,大有人在。


这是很令人痛心的现象,因为这样的结果,将导致国内的资讯市场中,充斥一些不健全的系统,而在劣币驱逐良币的情况下,国内将根本无法摆脱软体代工的命运。而所谓的资讯人,或许说是有机会看到本篇文章的人而言,也会因为环境的因素,逐渐放弃应有专业素养,进而演变成一头受过高等教育但是只会拉磨(编写程式码)的驴子而已。犹记得位大师曾经说过一句话:「程式码的价值,不在于作者是否使用任何奇幻的技巧,而是在于它的臭虫是最少。」笔者在只是想藉由本文传达一个讯息,那就是其实我们还有更好的选择,可以让我们的未来更加美好而已。


<作者联络方式:mhwul@pchome.com.tw>


延 伸 阅 读
物件导向的概念不只用在一般的电脑或资料库里,也可用在Modeling的制程里,而这个阶 段的工作便称为「物件导向分析」(Object Oriental Analysis),又名「视觉塑型分析」(Visualizing Modeling Analysis)。 「物件导向分析」代表从分析阶段就开始建立正确的物件导 向概念,而「视觉塑型分析」则彰显分析在此是一种视觉化与模型化的过程。相关介绍请见「物件导向分析概略」一文。
历经千辛万苦所设计出来的程式,为何总是无法被重复使用呢?通常有两个原因。第一、 这些产品知识只存放在某些成员的脑袋里。第二个原因则是这项产品本身就不具备「可回收利用」 特质,要不成为垃圾也难。要解决第一个问题,必须仰赖一整套的软体开发制度与标准作业规范。 至于第二个问题,则可以用物件导向的分析与技术来做为解决的方案。你可在「程式设计也要有「环保」概念」一文中得到进一步的介绍。
一个新世代IC设计资料库大部分的内容,涉及IC设计中用以完成目的(intent)与履行(
implementation)的综合资料模组。而具备的高效能、物件导向API等特质的设计资料库,更可让应
用程式开发人员在无须其内部履行细节的情况下轻松使用。
在「新世代IC设计资料库的开放与互通」一文为你做了相关的评析。
相关组织网站
OO发展组织官方网站
升阳公司以Java设计出OO的官方网站
Cetus Link的OO相关组织连结网站
相关文章
制作动态物件导向网站的软体 – XOOPS
面向对象设计的统独问题
comments powered by Disqus
相关讨论
  相关新闻
» 达梭系统携手云达虚拟双生 推动永续资料中心解决方案
» 宜鼎全面扩充边缘AI智慧应用与智慧储存
» 趋势科技指漏洞修补为资安预防针 企业须知4大生命周期样态
» TeamT5资安开运馆进驻资安大会 知己知彼防范於未然
» TXOne Networks揭示工控资安3大挑战 展出最新SageOne整合平台


刊登廣告 新聞信箱 读者信箱 著作權聲明 隱私權聲明 本站介紹

Copyright ©1999-2024 远播信息股份有限公司版权所有 Powered by O3  v3.20.1.HK86D5WWL1OSTACUK0
地址:台北数位产业园区(digiBlock Taipei) 103台北市大同区承德路三段287-2号A栋204室
电话 (02)2585-5526 #0 转接至总机 /  E-Mail: webmaster@ctimes.com.tw