(原創) 什麼是物件導向(Object Oriented)? (OO) (C/C++) (.NET) (C#) (Database) (Visual FoxPro)

Abstract
什麼是物件導向(Object Oriented)?一個好基本的問題,卻困擾了我10年之久...

Introduction
或許是我的駑鈍,物件導向困擾了我10年之久,1995年使用Visual FoxPro 3,VFP3已經是物件導向語言,繼承,封裝沒問題,至於多型,我不太確定是否支援,當時也搞不懂多型,所以也沒用VFP3寫過多型,從VFP3,VFP5,一直用到VFP6,接下來用VB6,VB6沒有繼承,但有interface,若要說也算是有interface繼承,也算有多型,不過當時還是沒搞懂,所以VB6不算是完整的物件導向語言,但卻是物件基礎的語言(Object Based),到了C#時代,C#無疑是完整的物件導向語言,繼承、封裝、多型通通有,不過由於C#過度的語言簡化(Syntax Sugar),多型用起來沒有什麼感覺,一直要到我這學期學了C++後,我才完全體會了什麼是多型。

首先一個基本的問題,為什麼稱為物件導向?明明C++強調的是class,稱為『類別導向』不是更適當嗎?

其實OOP的精隨是在多型(Polymorphism),侯捷這樣說過,C++ Primer也這樣說過,所有OOP所規定的一堆機制,目的也只是為了實現多型,多型利用繼承(Inheritance)和動態繫結(Dynamic Binding)實現,其目的是讓程式能在run-time才決定物件型別,所以同一個程式,只要其所能處理的物件屬於同一個繼承體系(Inheritance hierarchy),則不須修改程式就能正常執行。

為什麼要提供這樣的機制呢?開發軟體最常遇到的問題就是『需求改變』,假如是完全新的需求還沒話說,就重新寫新的程式即可,最怕的就是只是小改變,如有90%的功能和原本一樣,只有10%不一樣,所以之前90%的程式必須留下來,剩下的10%就改寫,若你覺得這樣很簡單,那你肯定是外行人了,沒真正寫過大程式,另外90%的程式可能是別人寫的,要想改原來的程式,可能必須看懂之後,才能下手該怎麼改,難怪很多人說,要看懂別人的程式,還不如重寫來的快,但實務上,Boss不可能讓你重寫,一來等於完全放棄之前的投資,二來原來的程式已經穩定,重寫等於得重新測試。物件導向就是希望當你有新的需求時,可以不必去更改原來的程式,只要繼續新增你的需求即可,既然不用更改原來的程式,就不會有維護的問題,卻又可達成新的需求。物件導向怎麼做到的?就是靠『多型』,所以物件的『多型』才是物件導向的精隨,類別(class)只是達成多型中的手段而已,所以該稱『物件導向』而非『類別導向』。

物件導向在內地稱為『面向對象』,其實翻的很傳神,傳統如C語言的寫法屬於Functional Decomposition,也就是依功能來分析而如C++、C#、Java屬於Object Decomposition,是依『對象』,依『角色』來分析,這兩種寫法有什麼差別?若依功能來分析,將來有新的需求,就是再多寫一個function,然後在main()或其他function中用if做判斷什麼樣的對象,角色該執行什麼function,這種寫法勢必要更改原來的程式。

物件導向是依對象、角色來分析,若有新的對象要加入,只要加上描述該對象的程式即可,原來的程式不需再更動。

這樣講或許很抽象,我在這透過一個簡單的範例分別用Functional Decomposition和Object Decomposition的寫法來寫,各位就可明顯的看出物件導向中多型的威力,我先用C++來寫這兩個程式,最後我會用C#改寫。

程式架構很簡單,想列出一個研究室中每個成員作了哪些事情,原來研究室中只有大學生和碩士生,但後來教授也收了博士生,所以得修改程式,所以看看用這兩種寫法的差異。

使用Functional Decomposition的寫法(Function based寫法)

 1/* 
 2(C) OOMusou 2006 http://oomusou.cnblogs.com
 3
 4Filename    : Polymorphism.cpp
 5Compiler    : Visual C++ 8.0 / ISO C++
 6Description : Demo how to use Function Decomposition.
 7Release     : 01/12/2007 1.0
 8*/

 9#include <iostream>
10#include <vector>
11#include <string>
12
13using namespace std;
14
15string bachelorJob(); // Bachelor's job
16string masterJob();   // Master's job
17// string doctorJob();
18
19// List all member's job
20void listAllJob(vector<pair<string,string>>);
21
22int main() {
23  vector<pair<stringstring>> CSlab;
24  CSlab.push_back(make_pair("John","bachelor"));
25  CSlab.push_back(make_pair("Mary","master"));
26  //lab.push_back(make_pair("Jack","doctor"));
27  listAllJob(CSlab);
28}

29
30// Bachelor's job
31string bachelorJob() {
32  return "study";
33}

34
35// Master's job
36string masterJob() {
37  return "study, research";
38}

39
40/*
41string doctorJob() {
42  return "study, research, teach";
43}
44*/

45
46// List all member's job
47void listAllJob(vector<pair<string,string>> lab) {
48  // Function Decomposition has to use if to determine
49  // object's type and modify source code.
50  for(vector<pair<stringstring>>::iterator iter = lab.begin(); iter != lab.end(); ++iter) {
51    if (iter->second == "bachelor"{
52      cout << iter->first << "'s job:" << bachelorJob() << endl;
53    }

54    else if (iter->second == "master"{
55      cout << iter->first << "'s job:" << masterJob() << endl;
56    }
 /*
57    else if (iter->second == "doctor") {
58      cout << iter->first << "'s job:" << doctorJob() << endl;
59    }
60    */

61  }

62}


使用Object Decomposition的寫法

  1/* 
  2(C) OOMusou 2006 http://oomusou.cnblogs.com
  3
  4Filename    : ObjectDecomposition.cpp
  5Compiler    : Visual C++ 8.0 / ISO C++
  6Description : Demo how to use Object Decomposition and Polymorphism.
  7Release     : 01/12/2007 1.0
  8*/

  9#include <iostream>
 10#include <vector>
 11#include <string>
 12
 13using namespace std;
 14
 15class Student {
 16protected:
 17  // constructor of abstract base class, since student
 18  // can't be initiated, constructor just can be called
 19  // by constructor of derived class, so put it in protected
 20  // level.
 21  Student(const char* _name) : name(string(_name)) {};
 22
 23public:
 24  string getName() const return this->name; }
 25  // pure virtual fuction
 26  virtual string job() const = NULL;
 27
 28private:
 29  string name;
 30}
;
 31
 32// public inheritance
 33class Bachelor : public Student {
 34public:
 35  // call constructor of abc myself.
 36  Bachelor(const char* name) : Student(name) {};
 37
 38public:
 39  string job() const return "study"; };
 40}
;
 41
 42class Master : public Student {
 43public:
 44  Master(const char* name) : Student(name) {};
 45
 46public:
 47  string job() const return "study, research"; };
 48}
;
 49
 50// new class for further 
 51/*
 52class Doctor : public Student {
 53public:
 54  Doctor(const char* name) : Student(name) {};
 55
 56public:
 57  string job() const { return "study, research, teach"; };
 58};
 59*/

 60
 61class Lab {
 62public:
 63  // pass reference of student 
 64  void add(Student&);
 65  void listAllJob() const;
 66
 67private:
 68  // put pointer of student in member vector, can't 
 69  // put reference in vector.
 70  vector<Student *> member;
 71}
;
 72
 73void Lab::add(Student& student) {
 74  // _student is reference of student object
 75  // &_student is pointer of _student reference
 76  this->member.push_back(&student);
 77}

 78
 79void Lab::listAllJob() const {
 80  // POWER of Polymorphism !!
 81  // (*iter) automatically refer to derived object, 
 82  // this is called "dynamic binding".
 83  // if you add new object in the future, you don't
 84  // need to maintain this code.
 85  for(vector<Student *>::const_iterator iter = this->member.begin(); iter != this->member.end(); ++iter) {
 86    cout << (*iter)->getName() << "'s job:" << (*iter)->job() << endl;
 87  }

 88}

 89
 90int main() {
 91  Bachelor John("John");
 92  Master   Mary("Mary");
 93  // Doctor   Jack("Jack");
 94
 95  Lab CSLab;
 96  CSLab.add(John);
 97  CSLab.add(Mary);
 98  // CSLab.add(Jack);
 99
100  CSLab.listAllJob();
101}


執行結果

John's job:study
Mary's job:study
, research


首先看Functional Decomposition的寫法,當要加入博士生時,除了在41行和44行要加上博士生要做的事情外,最大的問題是在47行和59行之間必須加上對博士生的判斷,如此必須更改到原來的程式碼。

再來看Object Decomposition的寫法,52行和58行加入博士生的描述後,這樣就好了,79行和88行完全不需修改程式碼,而且程式中完全不需要用if做判斷。

為什麼這麼神奇呢?這就是物件導向的『多型』,透過多型,程式會在run-time自動判別是大學生、碩士生、還是博士生,然後自動執行大學生、碩士生或博士生相對應的程式碼,所以我們不用另外去維護既有的程式碼。

C++是怎麼達到多型的?用的就是『繼承』(Inheritance)和『動態繫結』(Dynamic Binding)。

注意15行到30行特別設計了一個student的abstract base class,你可視為C#或Java的interface,然後33行到40行的大學生class,42行到48行的碩士生class都繼承了student,這樣子的繼承有什麼用呢?繼承是一種is a的關係,大學生是一種學生,碩士生也是一種學生,所以大學生和碩士生都是學生,既然都是學生我在65行中的add(Student&)就可以這樣寫,這表示我必須輸入Student型別物件的Reference,但既然大學生也是學生,碩士生也是學生,所以理應也可將大學生和碩士生傳給add(Student&),開了這個後門後,等於開了『多型』的第一道巧門,我們可以將同一個繼承體系(Inheritance hierarchy)的物件傳進function了,而不再限制只能傳一種物件。

但這樣還不夠,要怎麼讓程式在run-time自動判斷該跑大學生的job()還是碩士生的job(),若還是得用if()判斷,那將來還是得修改程式,多型的意義就不大了,別忘了剛剛我們是傳reference進來,reference其實就是pointer,只是他是一個不用dereference的smart pointer,所以我們傳進了大學生物件和碩士生物件的記憶體位址,有了記憶體位址,程式就自然可以找到相對應的function了,這就是『動態繫結』(Dynamic Binding),說穿了其實也沒那麼神奇了,:~D,真的如一句閩南語的俗話所言,『江湖一點訣,打破無價值』。

所以才說『多型』(Polymorphism)是靠『繼承』(Inheritance)和『動態繫結』(Dynamic Binding),而動態繫結又是靠reference和pointer完成的。

除此之外,我們還看到了Abstract Base Class / Interface,看到了virtual/override,所以物件導向中種種的機制,其實只是為了實踐多型而已。

我們再來看看C#的寫法

 1/* 
 2(C) OOMusou 2006 http://oomusou.cnblogs.com
 3
 4Filename    : ObjectDecomposition.cs
 5Compiler    : Visual Studio 2005 / C# 2.0
 6Description : Demo how to use Object Decomposition and Polymorphism.
 7Release     : 01/12/2007 1.0
 8 *            01/13/2007 2.0 Advised by Allen Kuo
 9*/

10using System;
11using System.Collections.Generic;
12
13abstract class Student {
14  private string name;
15
16  protected Student(string name) {
17    this.name = name;
18  }

19
20  public string getName() {
21    return this.name;
22  }

23  // Derived class must override this method
24  public abstract string job();
25}

26
27// Bachelor implement Student interface
28class Bachelor : Student {
29  // Call constructor of based class
30  public Bachelor(string name) : base(name) {}
31
32  public override string job() {
33    return "study";
34  }

35}

36
37class Master : Student {
38  public Master(string name) : base(name) {}
39
40  public override string job() {
41    return "study, research";
42  }

43}

44
45/*
46class Doctor : Student {
47  public Doctor(string name) : base(name) {}
48
49  public override string job() {
50    return "study, research, teach";
51  }
52}
53*/

54
55class Lab {
56  // .NET 2.0's List, like STL's vector
57  private List<Student> member = new List<Student>();
58
59  public void add(Student student) {
60    this.member.Add(student);
61  }

62
63  public void listAllJob() {
64    // C# is cleaner than C++
65    foreach(Student student in this.member) {
66      Console.WriteLine("{0}'s job:{1}",student.getName(), student.job());
67    }

68  }

69}

70
71class MainClass {
72  public static void Main() {
73    Bachelor John = new Bachelor("John");
74    Master Mary = new Master("Mary");
75    //Doctor Jack = new Doctor("Jack");
76
77    Lab CSLab = new Lab();
78    CSLab.add(John);
79    CSLab.add(Mary);
80    //CSLab.add(Jack);
81
82    CSLab.listAllJob();
83  }

84}


首先感謝網友Allen Kuo 對以上C#程式碼的建議,改用abstract class而不是interface,這樣C#的程式碼將完全和C++程式碼意義一樣。

13行用了abstract class寫法,30行由derived class呼叫base class的constructor,C#使用了base這個keyword。57行的List為C# 2.0新的寫法,很類似template寫法,提供了類似C++ STL vector的功能。59行的add(Student student)是C#提供多型的地方,很明顯的不須reference,只要傳進物件即可,剩下的compiler幫你處理了,程式是乾淨很多,但卻無法如C++那樣感受用reference/pointer達成dynamic binding,所以我覺得,對初學者來說,還是得學C和C++,pointer雖然難用,但卻是很多機制的原理,直接學C#或Java這類語言,雖然語法乾淨,卻無法體會出背後的原理,這也是我10年來一直無法體會多型,一直要看到C++的Dynamic Binding之後,我才很放心的完全了解多型是怎麼做出來的。

65行和66行也明顯比C++乾淨很多。

另外一個問題,物件導向和資料結構/演算法相衝突嗎?

一點也不衝突,資料結構/演算法類似學習武功,而物件導向類似學兵法,是學萬人敵的工夫。一個資料結構/演算法高強的,是獨來獨往的大俠,而物件導向高強的,是擅長行軍佈陣的大帥,只有在大程式、大專案中,才可以顯現物件導向的威力,正如『韓信點兵,多多益善』一樣,所以資料結構/演算法和物件導向都很重要。

Conclusion
最後一個問題,那物件導向該怎麼學好?

簡單的說,就是去學Design Pattern。學好C++,只學到了物件導向的語法而已,至於該怎麼用,就是去學Design Pattern,裡面有一條條的兵法讓你去用。下學期陳俊杉教授要開的就是Design Pattern,很期待下學期趕快開學,讓我能一解10年來物件導向的渴望。

See Also
(原創) 程序導向和物件導向的思維主要區別在哪裡? (OO)

posted on 2007-01-13 00:38  真 OO无双  阅读(27354)  评论(0编辑  收藏  举报

导航