「什麼是面向對象?」這個問題每每會問到剛畢業的新手or實習生上,也是每每做爲一個技術面試的開頭題。在這裏咱們不去談如何答(fu)好(yan)問(guo)題(qu),僅談談我所理解的面向對象。
從歷史上看,從20世紀60年代末期到70年代,分別有幾個不一樣領域都發展了面向對象的思想。好比數據抽象的研究、人工智能領域中的知識表現(框架模型)、仿真對象的管理方法(Simula)、並行計算模型(Actor)以及在結構化編程思想影響下而產生的面向對象方法。
框架模型是現實世界的模型化。從這個角度來看,「對象是對現實世界中具體事物的反映」這個觀點並無錯。javascript
可是無論過去怎樣,如今對面向對象最好的理解是,面向對象編程是結構化編程的延伸。html
結構化編程基本上實現了控制流程的結構化。可是程序流程雖然結構化了,要處理的數據卻並無被結構化。面向對象的設計方法是在結構化編程對控制流程實現告終構化後,又加上了對數據的結構化。
衆多面向對象的編程思想雖不盡一致,可是不管哪一種面向對象編程語言都具備如下的共通功能。
1 . 不須要知道內部的詳細處理就能夠進行操做(封裝、數據抽象)。
2 . 根據不一樣的數據類型自動選擇適當的方法(多態性)。java
最先的時候是面向過程。想象一下一堆C語言or彙編堆砌在一塊兒的函數互相調(shang)用(hai)的場景————什麼?你說你沒學過C語言?那麼你就想象一下一個複雜的SQL語句吧,有點像。程序員
評論中有人提到了C語言並不是徹底不支持面向對象,Struct就是一個不錯的選擇。的確,可是C語言對面向對象的支持並非那麼的好。在絕大多數語言中都爲Class(C++同時支持Struct和Class),但也有小部分語言沿用了這個經典的名字——好比Go語言。 在這裏特別說明是爲了防止誤導新手
咱們以「把大象裝進冰箱須要幾步」這個經典的腦經急轉彎來舉個例子吧:面試
打開冰箱,裝入大象,關上冰箱。這三步就是面向過程的思考方式,這種思想強調的是過程,也能夠叫作動做。編程
open(icebox); putIn(icebox,elephant); close(icebox);
冰箱打開,冰箱存儲,冰箱關閉。這就是面向對象的思考方式,這種方式強調是對象,也能夠說是實例。設計模式
//咱們有一個冰箱 Icebox iceBox = new iceBox(); //可不能忘記大象,就叫它jake吧 Elephant jake = new Elephant(); icebox.open(); icebox.save(jake); icebox.close();
一種編程範式,相對於面向過程。爲了方便在編程中更接近地去描述現實世界中的萬物(萬物皆對象),咱們將對一個事物的描述稱之爲類,而對象則是該事物的實例。安全
而面向對象的編程方法常見的有三種:框架
在類中,咱們把事物的屬性轉變爲編程中的變量,把事物的行爲轉變爲方法。編程語言
Class Elephant{ public String name; public int age; public double weight; //更多的屬性...... //在這裏的方法爲了方便演示都是void public void eat(Food food){ //吃東西 } //更多的行爲....... }
//咱們再次召喚了jake Elephant jake = new Elephant(); //他隨便吃了點什麼 jake.eat(new Something);
可使子類複用父類公開的變量、方法
//幾百年後,jake和它的子孫們進化成了更強的大象 //它們被稱爲:飛象 Class FlyElephant extends Elephant{ public void fly(){ //i belive i can fly~~ } } //其中有一頭飛象叫jason FlyElephant jason = new FlyElephant(); //不要問大象爲何能飛! jason.fly(); //並且還能夠像其餘大象同樣正常的吃東西 jason.eat(new Something);
若是把類看成模塊,繼承就是利用模塊的方法。繼承的思想好像有其現實的知識基礎,可是把它看作純粹的模塊利用方法則更恰當。
由於繼承只不過是抽象的功能利用方法,因此沒必要把對繼承的理解束縛在「繼承是對現實事物的分類的反映」。實際上這樣的想法反而妨礙了咱們對繼承的理解。
屏蔽一系列的細節。使外部調用時只要知道這個方法的存在
父類的方法繼承的到子類之後能夠有不一樣的實現方式
以類爲中心的傳統面向對象編程,是以類爲基礎生成新對象。類和對象的關係能夠類比成鑄模和鑄件的關係。
而原型模式的面向對象編程語言沒有類這樣一個概念。
以JavaScript爲例。須要生成新的對象時,只要給對象追加屬性。設置函數對象做爲屬性的話,就成爲方法。當訪問對象中不存在的屬性時,JavaScript 會去搜索該對象 prototype 屬性所指向的對象。
JavaScript 利用這個功能,使用「委派」而非「繼承」來實現面向對象編程。
// 生成Doge。...(1) function Doge(){ this.sit = function () {return "I'm the king of the world"} } // 從Doge 生成對象dog...(2) var doge = new Doge() // doge 是狗,因此能 sit...(3) alert(doge.sit()) // 生成新型myDoge...(4) function MyDoge () {} // 指定委派原型 MyDoge.prototype = new Dog() // 從MyDoge 生成新對象myDoge...(5) var myDoge = new MyDoge() document.write(myDoge.sit())
從原型生成對象:
和以前的Java經過類模板來實現面向對象的編程方式相比,原型對象系統支持一個更爲直接的對象建立方法。例如,在 JavaScript 中,一個對象是一個簡單的屬性列表。每一個對象包含另外一個父類或原型 的一個特別引用,對象從父類或原型中繼承行爲。
傳統對象系統和原型對象系統有本質的區別。傳統對象被抽象地定義爲概念組的一部分,從對象的其餘類或組中繼承一些特性。相反,原型對象被具體地定義爲特定對象,從其餘特定對象中繼承行爲。
所以,基於類的面嚮對象語言具備雙重特性,至少須要 2 個基礎結構:類和對象。因爲這種雙重性,隨着基於類的軟件的發展,複雜的類層次結構繼承也將逐漸開發出來。一般沒法預測出將來類須要使用的方法,所以,類層次結構須要不斷重構,讓更改變得更輕鬆。
基於原型的語言會減小上述雙重性需求,促進對象的直接建立和操做。若是沒有經過類來束縛對象,則會建立更爲鬆散的類系統,這有助於維護模塊性並減小重構需求。
然而話雖這麼講,一大串原型鏈仍是會讓人頭痛不已的,特別仍是在動態語言中。
繼承(inheritance)是實現代碼重用的有力手段,但它並不是永遠是完成這項任務的最佳工做。使用不當會致使軟件變得很脆弱。在包的內部使用繼承是很是安全的,在那裏,子類和超類的實現都處於同一個程序員的控制下。對於專門爲了繼承而設計的而且具備很好的文檔說明的類來講,使用繼承也是很是安全的。然而,對於普通的具體類進行跨超包邊界的繼承則是很是危險的。本條目並不適用於接口繼承(一個類實現一個接口,或者一個接口擴展另外一個接口)。
方法調用不一樣的是,繼承打破了封裝性。子類信賴於其超類中特定功能的實現細節。超類的實現有可能會隨着發行版本的不一樣而有變化,子類有可能會被破壞。
在Java中,咱們老是推薦使用interface
而不是abstract class
,這樣可使代碼更加的靈活。在Java8後interface
也是獲得了加強——能夠提供默認的方法實現。
另外,Go語言也是將組合發揮到極致的語言。
一種相對於面向過程的編程範式。
面向對象設計原則是 OOPS(Object-Oriented Programming System,面向對象的程序設計系統)編程的核心,但大多數 Java 程序員追逐像 Singleton、Decorator、Observer 這樣的設計模式,而不重視面向對象的分析和設計。甚至還有經驗豐富的 Java 程序員沒有據說過 OOPS 和 SOLID設計原則,他們根本不知道設計原則的好處,也不知道如何依照這些原則來進行編程。
衆所周知,Java 編程最基本的原則就是要追求高內聚和低耦合的解決方案和代碼模塊設計。查看 Apache 和 Sun 的開放源代碼能幫助你發現其餘 Java 設計原則在這些代碼中的實際運用。Java Development Kit 則遵循如下模式:BorderFactory 類中的工廠模式、Runtime 類中的單件模式。你能夠經過 Joshua Bloch 的《Effective Java》一書來了解更多信息。我我的偏向的另外一種面向對象的設計模式是 Kathy Sierra 的 《Head First設計模式》 以及 《Head First Object Oriented Analysis and Design》。
雖然實際案例是學習設計原則或模式的最佳途徑,但經過本文的介紹,沒有接觸過這些原則或還在學習階段的 Java 程序員也可以瞭解這 10 個面向對象的設計原則。其實每條原則都須要大量的篇幅才能講清楚,但我會盡力作到言簡意賅。
即不要寫重複的代碼,而是用「abstraction」類來抽象公有的東西。若是你須要屢次用到一個硬編碼值,那麼能夠設爲公共常量;若是你要 在兩個以上的地方使用一個代碼塊,那麼能夠將它設爲一個獨立的方法。SOLID 設計原則的優勢是易於維護,但要注意,不要濫用,duplicate 不是針對代碼,而是針對功能。這意味着,即便用公共代碼來驗證 OrderID 和 SSN,兩者也不會是相同的。使用公共代碼來實現兩個不一樣的功能,其實就是近似地把這兩個功能永遠捆綁到了一塊兒,若是 OrderID 改變了其格式,SSN 驗證代碼也會中斷。所以要慎用這種組合,不要隨意捆綁相似但不相關的功能。
在軟件領域中惟一不變的就是「Change」,所以封裝你認爲或猜想將來將發生變化的代碼。OOPS 設計模式的優勢在於易於測試和維護封轉的代碼。若是你使用 Java 編碼,能夠默認私有化變量和方法,並逐步增長訪問權限,好比從 private 到 protected 和 not public。有幾種 Java 設計模式也使用封裝,好比 Factory 設計模式是封裝「對象建立」,其靈活性使得以後引進新代碼不會對現有的代碼形成影響。
即對擴展開放,對修改關閉。這是另外一種很是棒的設計原則,能夠防止其餘人更改已經測試好的代碼。理論上,能夠在不修改原有的模塊的基礎上,擴展功能。這也是開閉原則的宗旨。
類被修改的概率很大,所以應該專一於單一的功能。若是你把多個功能放在同一個類中,功能之間就造成了關聯,改變其中一個功能,有可能停止另外一個功能,這時就須要新一輪的測試來避免可能出現的問題。
這個設計原則的亮點在於任何被 DI 框架注入的類很容易用 mock 對象進行測試和維護,由於對象建立代碼集中在框架中,客戶端代碼也不混亂。有不少方式能夠實現依賴倒置,好比像 AspectJ 等的 AOP(Aspect Oriented programming)框架使用的字節碼技術,或 Spring 框架使用的代理等。
若是可能的話,優先利用組合而不是繼承。一些人可能會質疑,但我發現,組合比繼承靈活得多。組合容許在運行期間經過設置類的屬性來改變類的行爲,也能夠經過使用接口來組合一個類,它提供了更高的靈活性,並能夠隨時實現。《Effective Java》也推薦此原則。
根據該原則,子類必須可以替換掉它們的基類,也就是說使用基類的方法或函數可以順利地引用子類對象。LSP 原則與單一職責原則和接口分離原則密切相關,若是一個類比子類具有更多功能,頗有可能某些功能會失效,這就違反了 LSP 原則。爲了遵循該設計原則,派生類或子類必須加強功能。
採用多個與特定客戶類有關的接口比採用一個通用的涵蓋多個業務方法的接口要好。設計接口很棘手,由於一旦釋放接口,你就沒法在不中斷執行的狀況 下改變它。在 Java 中,該原則的另外一個優點在於,在任何類使用接口以前,接口不利於實現全部的方法,因此單一的功能意味着更少的實現方法。
該原則可使代碼更加靈活,以即可以在任何接口實現中使用。所以,在 Java 中最好使用變量接口類型、方法返回類型、方法參數類型等。《Effective Java》 和《Head First Design Pattern》書中也有提到。
該原則最典型的例子是 Java 中的 equals () 和 hashCode () 方法。爲了平等地比較兩個對象,咱們用類自己而不是客戶端類來作比較。這個設計原則的好處是沒有重複的代碼,並且很容易對其進行修改。
總之,但願這些面向對象的設計原則能幫助你寫出更靈活更好的代碼。理論是第一步,更重要的是須要開發者在實踐中去運用和體會。