(原創) 什麼是物件導向(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寫法)
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<string, string>> 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<string, string>>::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的寫法
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}
執行結果
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#的寫法
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)