當咱們說面向XX編程時,咱們實際在說什麼?

面試官:「談談面向對象的特性」面試

碼農:「封裝」、「繼承」和「多態」編程

面試官:能具體說一下嗎?設計模式

碼農:「封裝」隱藏了某一方法的具體運行步驟,取而代之的是經過消息傳遞機制發送消息。「繼承」即子類繼承父類,子類比本來的類(稱爲父類)要更加具體化。這意味着咱們只須要將相同的代碼寫一次。而「多態」可使同一類型的對象對同一消息會作出不一樣的響應。markdown

上面是一個普通的面試場景。這麼回答是否正確呢?數據結構

你有沒有想過何謂「特性」?架構

「特性」指某事物所特有的性質函數式編程

那麼問題來了,「封裝」、「繼承」和「多態」是面向對象所特有的嗎函數

  • Java是面嚮對象語言
  • C是面向過程語言
  • Go是面向類型的語言
  • Clojure是函數式語言

這四種範式的語言都支持「封裝」、「繼承」和「多態」嗎?工具

咱們經過例子來驗證四種不一樣範式的語言是否能實現「封裝」、「繼承」和「多態」!oop

封裝

先來看Java:

  • Java是經過類來進行封裝,將相關的方法和屬性封裝到了同一個類中

  • 經過訪問權限控制符來控制訪問權限。

    public class Person { private String name;

    public String say(String someThing) { ... } }

而C則是:

  • 經過方法來進行封裝,將具體的過程封裝到一個個方法中

  • 經過頭文件來隱藏具體的細節

    struct Person;

    void say(struct Person *p);

相對於Java來講,C的封裝性實際更好!由於Person裏的結構都被隱藏了!

對Go語言來講,乍看之下像是以函數進行封裝的,可是實際上在Go語言中函數也是一種類型。因此能夠說Go語言是以類型來進行封裝的。

func say(){
 fmt.Println("Hello")
}

func main() {
 a := say
 a()
}
複製代碼

而Clojure則主要以函數的形式進行封裝。

(defn say []
 (println "Hello"))
複製代碼

能夠看出來,四種語言都支持封裝,只是封裝的方式不一樣而已

繼承

再來看繼承,繼承實際就是代碼複用

繼承自己是與類或命名空間無關的獨立功能。只不過面嚮對象語言將繼承綁定到了類層面上,而面嚮對象語言是比較廣泛的語言,一直強調繼承,因此當咱們說繼承的時候,默認就是在說基於類的繼承。

Java是基於類的繼承。也就是說子類能夠複用父類定義的非私有屬性和方法。

class Man extends Person {
 ...
}
複製代碼

C語言能夠經過指針來複用。和下面Go語言比較相似,Go語言相對更簡單。而C則更像是奇技淫巧!

struct Person {
 char* name;
}

struct Man {
 char* name;
 int age;
}

struct Man* m = malloc(sizeof(struct Man));
m->name = "Man";
m->age = 20;
struct Person* p = (struct Person*) m; // Man能夠轉換爲Person
複製代碼

而Go語言則是經過匿名字段來實現繼承。即一個類型能夠經過匿名字段複用另外一個類型的字段或函數。

type Person struct {
 name string
}

type Man struct {
 ...
 Person // 引入Person內的字段
}
複製代碼
  • Man經過直接在定義中引入Person,就能夠複用Person中的字段
  • 在Man中,既能夠經過this.Person.name來訪問name字段,也能夠直接經過this.name來訪問
  • 若是Man中也有name這個字段,則經過this.name訪問的則是Man的name,Person裏的name被覆蓋了
  • 此方案對函數也適用

對於Clojure來講,則是經過高階函數來實現代碼的複用。只須要將須要複用的函數做爲參數傳遞給另外一個函數便可。

; 複用的打印函數
(defn say [v]
 (println "This is " v))
 
; 打印This is Man
(defn man [s]
 (s "Man"))

; 打印This is Women
(defn women [s]
 (s "Women")) 
複製代碼

同時Clojure能夠基於Ad-hoc來實現繼承,這是基於symbol或keyword的繼承,適用範圍比基於類的繼承普遍。

(derive ::man ::person)
(isa? ::man ::person) ;; true
複製代碼

能夠看出,四種語言也都能實現繼承

多態

多態實際是對行爲的動態選擇

Java經過父類引用指向子類對象來實現多態。

Person p = new Man();
p.say();
p = new Woman();
p.say();
複製代碼

C語言的多態則是由函數指針來實現的。

struct Person{
 void (* say)( void ); //指向參數爲空、返回值爲空的函數的指針
}

void man_say( void ){
 printf("Hello Man\n");
}
 
void woman_say( void ){
 printf("Hello Woman\n");
}
...
p->say = man_say;
p.say(); // Hello Man
p->say = woman_say;
p.say(); // Hello Woman
複製代碼

Go語言經過interface來實現多態。這裏的interface和Java裏的interface不是一個概念

; 定義interface
type Person interface {
 say()
}

type Man struct {}
type Women struct {}

func (this Man) say() {
 fmt.Println("Man")
}

func (this Women) area() {
 fmt.Println("Women")
}

func main() {
 m := Man{}
 w := Women{}
 exec(m) // Man say
 exec(w) // Women say
}

func exec(a Person) {
 a.say()
}
複製代碼
  • Man和Women並無像在Java裏同樣實現了interface,而是定義了和在Person裏相同的方法
  • exec函數接收參數爲interface

Clojure除了能夠經過高階函數來實現多態(上面的例子就是高階函數的例子)。還能夠經過「多重方法」來實現多態。

(defmulti say (fn [t] t))

(defmethod run
 :Man
 [t]
 (println "Man"))

(defmethod run
 :Women
 [t]
 (println "Women"))

(rsay :Man) ; 打印Man,結合Ad-hoc,能夠實現相似Java的多態
複製代碼

四種語言一樣都能實現多態

問題的解決

從上面的對比可知,「封裝」、「繼承」和「多態」並非面向對象所特有的

那麼當咱們說「面向XX編程時,咱們實際在說什麼呢」?

咱們從解決問題的方式來回答這個問題!

對於一些很簡單的問題,咱們通常能夠直接獲得解決方案。例如:1+1等於幾?

當咱們說面向XX編程時,咱們實際在說什麼?

可是對於比較複雜的問題,咱們不能直接獲得解決方案。例如:雞兔同籠問題,有若干只雞兔同在一個籠子裏,從上面數,有35個頭,從下面數,有94只腳。問籠中各有多少隻雞和兔?

對於這類問題,咱們的通常作法就是先對問題進行抽象建模,而後再針對抽象來尋找解決方案。

當咱們說面向XX編程時,咱們實際在說什麼?

對應到軟件開發來講,對於真實環境的問題,咱們先經過編程技術對其抽象建模,而後再解決這些抽象問題,繼而解決實際的問題。這裏的抽象方式就是:「封裝」、「繼承」、「多態」!而不管是基於類的實現、仍是基於類型的實現、仍是基於函數或方法的,都是抽象的具體實現。

當咱們說面向XX編程時,咱們實際在說什麼?

如今再回到問題:當咱們說「面向XX編程時,咱們實際在說什麼呢」?

實際就是,使用不一樣的抽象方式,來解決問題

抽象方式:只是不一樣,沒有優劣

不管是面向對象編程仍是函數式編程亦或面向過程編程,只是抽象方式的差別,而抽象方式的不一樣致使瞭解決問題方式的差別。

  • 面向對象將現實抽象爲一個個的對象,以及對象間的通訊來解決問題。
  • 函數式編程將現實抽象爲有限的數據結構,以及一個個的對這些數據結構進行操做的函數,經過函數對數據結構的操做,以及函數間的調用來解決問題。
  • 面向過程編程將現實抽象爲一個個數據結構和過程方法,經過方法組合調用以及對數據結構的操做來解決問題。

每種抽象方式都既有優勢也有缺點。沒有完美的抽象方法

例如,對於面向對象來講,能夠很方便的自定義類,也就是增長類型,可是很難在不修改已定義代碼的前提下,爲既有的具體類實現一套既有的抽象方法(稱爲表達式問題)。而相對的,函數式編程能夠很方便的增長操做(也就是函數),可是很難增長一個適應各類既有操做的類型。

舉個例子,在Java裏,String這個類在1.6以前是沒有isEmpty這個方法的,若是咱們想判斷一個字符串是否爲空,咱們只能使用工具類或者就是等着官方提供,而理想的方法應該是「abc」.isEmpty()。雖然現代化的語言都提供了各類手段來解決這個問題,像Ruby這類動態語言能夠經過猴子補丁來實現;Scala能夠經過隱式轉換實現;Kotlin能夠經過intern方法實現。但這自己是面向對象這種抽象方式所須要面對的問題。

對於函數式語言來講,好比上面提到的Clojure,它若是要新增一個相似的函數,直接編寫一個對應的函數就能夠了,由於它的數據結構都實現了統一的接口:Collection,Sequence,Associative,Indexed,Stack,Set,Sorted等。這使得一組函數能夠應用到Set,List,Vector,Map。可是相應的,若是你要增長一個數據結構,那就須要實現上面全部的接口,難度可想而知了。

抽象程度與維護成本正相關

面向對象相較於其它抽象方式的優點可能就是粒度相對較大,相對的較易理解

這就像組裝電腦同樣:

  • 面向對象就像是將電腦拆分紅了主板、CPU、顯卡、機箱等,你稍微學一學就能夠組裝了。
  • 而函數式編程就像將電腦拆成了一個個的元器件。你既須要學習相關知識,還得將這些元器件組裝起來。難度可想而知了。可是組合方式和自由度則比面向對象好得多。

抽象度越高,也就越難理解。可是抽象度越高,適應性就越強,代碼相對就越簡潔

抽象程度越高,相應的抽象粒度就更細。抽象粒度越細,靈活性就更好,但也致使了維護難度越大。

總結

編程在思想!編程範式只是輔助你思考的工具!不要被編程範式所限制!你須要考慮的是「我該如何使用XX編程範式實現XXX?」,而不是「XX編程範式能實現什麼?」

每種抽象方式都有各自的優缺點。爲了取長補短,每種編程範式都有本身的最佳實踐。這些最佳實踐被收集整理,成爲了套路或模式。

例如面向對象裏的:

  • 複用代碼優先考慮組合而不是繼承
  • 爲多態而繼承
  • 設計原則
  • 23種設計模式
  • ...

參考資料

相關文章
相關標籤/搜索