這樣理解面向對象的封裝,繼承,多態是否正確?
這樣理解面向對象的封裝,繼承,多態是否正確?
用人民幣來舉例首先有一個初始模板,即長方形,左上角印著中國人民銀行。再有一個二級模板,即長方形,左上角印著中國人民銀行,又加印了偉人頭像。最後就是具體的錢了,即長方形,左上角印著中國人民銀行,印著偉人頭像,還有具體的面值和相印的花色。封裝就是模板設計出來就是固定死的,不能改變,弄成三角形不行,你把中國人民銀行印在右上角不行,印上美國人民銀行也不行,印上你爸頭像更不行。
繼承就是,二級模板具有初始模板的全部特徵,並且有了自己的新特徵,具體的錢具有二級和初始模板的所有特徵。多態就是具體的錢有1塊,5塊,10塊,20塊,50塊,100塊多種面額。
不管這些概念在別的語言裡面怎麼樣,局限到C++里,多態 == 覆蓋和調用虛函數。如果你不是為了虛函數去多態,或者為了多態去虛函數,那麼你多半最後會陷進dynamic_cast的泥潭裡不能自拔。
當然具體到這個印刷錢的例子里,雖然不是很合適,但是你一定要用多態來做,也不是不行。我想引用 @invalid s在這個問題
面向對象編程的弊端是什麼? - 計算機科學 - 知乎下面的一段回答:封裝:封裝的意義,在於明確標識出允許外部使用的所有成員函數和數據項,或者叫介面。
有了封裝,就可以明確區分內外,使得類實現者可以修改封裝內的東西而不影響外部調用者;而外部調用者也可以知道自己不可以碰哪裡。這就提供一個良好的合作基礎——或者說,只要介面這個基礎約定不變,則代碼改變不足為慮。繼承+多態:繼承和多態必須一起說。一旦割裂,就說明理解上已經誤入歧途了。
先說繼承:繼承同時具有兩種含義:其一是繼承基類的方法,並做出自己的改變和/或擴展——號稱解決了代碼重用問題;其二是聲明某個子類兼容於某基類(或者說,介面上完全兼容於基類),外部調用者可無需關注其差別(內部機制會自動把請求派發[dispatch]到合適的邏輯)。再說多態:基於對象所屬類的不同,外部對同一個方法的調用,實際執行的邏輯不同。很顯然,多態實際上是依附於繼承的兩種含義的:「改變」和「擴展」本身就意味著必須有機制去自動選用你改變/擴展過的版本,故無多態,則兩種含義就不可能實現。所以,多態實質上是繼承的實現細節;那麼讓多態與封裝、繼承這兩個概念並列,顯然是不符合邏輯的。不假思索的就把它們當作可並列概念使用的人,顯然是從一開始就被誤導了——正是這種誤導,使得大多數人把注意力過多集中在多態這個戰術層面的問題上,甚至達到近乎惡意利用的程度;同時卻忽略了戰略層面的問題,這就致使軟體很容易被他們設計成一灘稀屎(後面會詳細談論這個)。
先說封裝,封裝的目的就是「互不影響」,模塊的設計者可以隨便修改模塊裡面的實現而不影響調用者,調用者也能知道自己能碰哪些東西,只要遵守這個協議,自己的代碼就不會因為模塊內部的實現變化而跟著完蛋。題主說「封裝就是模板設計出來就是固定死的,不能改變,弄成三角形不行,你把中國人民銀行印在右上角不行,印上美國人民銀行也不行,印上你爸頭像更不行。」說對了一部分,還有另一個部分是,假如有一天印錢的人真打算把「中國人民銀行」改成「美國人民銀行」了,也不會影響用它的人。
再說繼承和多態,就像上面引用內容所說的,我在任何時候都反對把這兩個概念拆開來看,這兩個事情合起來才是和「封裝」等價的。一般來講,我們用繼承和多態,只有在你希望某一個函數的行為不同的時候才需要。像5元,10元,20元RMB這個例子的話,只要你在成員裡面搞一個value就好了,所以嚴格來說不能算是多態。除非這些紙幣對於一個同樣的方法各自獨特的行為(即虛函數的作用),那麼多態才是有價值的。
如果真寫成代碼,可能是這樣的:
abstract class Money
{
private final string bankName = "中國人民銀行";
public string getBankName(){return bankName;}
public abstract int value();
}
class NoteFiveYuan extends Money
{
public int value(){return 5;}
}
class NoteTenYuen extends Money
{
public int value(){return 10;}
}
static int main()
{
ArrayList&
wallet.Add(new NoteFiveYuan());
wallet.Add(new NoteTenYuan());
for(Money money : wallet)
{
println(money.getBankName());
println(money.value());
}
}
如果Money的設計者把「中國人民銀行」改成了「美國人民銀行」,你會發現其他地方完全不需要修改,這就是封裝的好處;尤其是如果getBankName裡面有一些複雜操作的話,好處會更明顯。
如果沒有繼承和多態,你就做不到把兩個類的實例全扔到同一個wallet裡面,並且在面對每一個Money的時候,你還必須得去手動判斷他到底是哪種Money。現在有了繼承和多態,只要你採用覆蓋虛函數的方法,根本不用操心。更關鍵的是,如果以後還有30元的貨幣類,只要也繼承Money並重寫value,那麼主函數裡面數錢的那一段根本就不用動。
題主可以參考這段代碼再來理解一下。
在你理解一個東西需要依靠打比方的方式的時候,說明你還是不理解它~找到封裝繼承多態的代碼實例,想一想如果不用這些方法你怎麼實現這些功能,自己設計一個能用上這些概念的場景,用面向對象的代碼實現出來,然後再來知乎問自己的代碼是不是體現了對這三個概念足夠深入的理解。
封裝:錢能防偽,你不用管具體是什麼黑科技,你只要知道可以通過一定的手段鑒別假幣。繼承:繼承本質上是「A 繼承 B,那麼 A 是一個 B」,所以紙鈔是貨幣,硬幣也是貨幣,這倆都繼承貨幣;刷卡走銀行餘額不一定是,但我們可以定義一個支付介面,貨幣可以實現支付,銀行卡也可以實現。如果你用鴨子類型,那麼銀行卡也是(正色多態:你拿錢去自動售貨機買東西,都是付錢,紙鈔走紙鈔口,硬幣走投幣口,你只要管錢夠不夠就行。
多態是目的,繼承是手段。
輪子哥的回答可能有點專業了. 參照樓主的例子,用JAVA做解釋, 人民幣(紙幣)有一些屬性, 這些是所有人民幣共有的, 比如是用紙做的, 長方形的.
繼承: 當你要寫一個10元人民幣和100元的, 因為他們都屬於人民幣, 沒必要挨個再寫下共有屬性, 就可以讓他們繼承人民幣這個類.
多態: 當你需要印刷的時候, 10元用的材料和方式可能和100元的有些不同, 這樣10元和100元可以各自寫自己的印刷方法, 這樣就是多臺.
一點自己的理解, 如有不對, 請指出
剛看到這一章,大體說一下我的理解。
封裝的目的是軟體分工,一個人寫程序封裝的意義不大,但當程序大的時候,你不可能掌握所有類的代碼,所以每個人只需要寫一個類,寫完之後做個表,告訴使用這個類的人,這個類都有哪些方法,每個方法能實現啥功能就行了,至於內部怎麼寫的,你不需要知道。
繼承的目的是降低代碼重寫率,父類有了的內容,子類直接用就行,方便寫也方便改。否則假如沒有繼承,你每個類都得寫一遍setname,然後一旦有變化得每個類改一遍……
多態是基於封裝和繼承的,是在封裝的情況下方便寫代碼的方式,而且可能是僅有的方式……
我原來學過點面向過程編程,如果要寫個遊戲,遊戲的怪物要攻擊,那我在設計其攻擊的時候,如果僅僅是攻擊力不同、名字不同還好,一個數組就解決了,什麼怪物有什麼屬性,這很簡單。
但如果怪的攻擊方式不同,演算法不同,傷害公式不同,這就麻煩了……一般用if從句……
if 怪=冰女 then
調用技能「寒冰箭」
else if 怪=德瑪西亞 then
調用技能「劍刃風暴」
else if……
end
關鍵這部分還得在「攻擊」這個方法里寫,做攻擊的哥們表示「我哪裡知道都有啥攻擊技能啊……那是設計英雄技能的哥們的事兒啊……」
當然也可以調用到別的函數里,專門寫個函數來做這個,但那個函數還是得來個if 表,然後一群設計技能的個人把技能寫進來……
多態解決了這個問題,放技能的時候只需要「英雄.放技能」,就可以了,至於放哪些技能,怎麼放,那是寫在每個英雄自己的類里的方法,跟寫底層那哥們沒關係,他只要寫個抽象方法,要求所有設計的英雄都必須把技能寫清楚就好了。
所以從面相過程走過來之後,一看面相對象我都壓抑不住自己的興奮……
當然了,代碼完全不會寫……
ps:關於封裝,我忽然想起來還有個重要作用——數據驗證。
封裝主要對屬性而說的,當然有些類內部的方法也被封裝不讓用,其實是不需要用。
但改屬性是不行的,因為很多屬性不能亂改,改得不是正常值的話會導致其他方法出問題的,所以要改屬性必須用方法,通過類自帶的方法改屬性,寫這個類的人可以對屬性進行校驗,確保不會被亂改。
多態就是父親可以打自己,也可以打兒子,但是兒子不可以打父親。
你這理解明顯就是複雜化。
這些概念不用管他,寫多了自然會明白,你只要知道把寫好的類new一個拿來用就行。
那啥……其實有個技術叫 套印……
有手工師傅魯班,調用手段是付款,封裝就是你等著收成品,他絕不會在你面前做東西。繼承是魯班師傅會把基本的東西教給徒弟,多態是每個徒弟又有自己特色的東西。
http://m.blog.csdn.net/article/details?id=53585009
多態是行為封裝是閉包繼承是行為或屬性傳遞
我覺得如上 @柯偉辰 的答案中,正是我打算說的,我在此基礎補充一下,本打算在評論區里說的,但因為要寫的稍微詳細點,乾脆另開一個答案。
封裝:封裝的意義,在於明確標識出允許外部使用的所有成員函數和數據項,或者叫介面。
有了封裝,就可以明確區分內外,使得類實現者可以修改封裝內的東西而不影響外部調用者;而外部調用者也可以知道自己不可以碰哪裡。這就提供一個良好的合作基礎——或者說,只要介面這個基礎約定不變,則代碼改變不足為慮
例如,設計一個類Circle,求面積:
circle.area();
既可以用實現,也可以用
實現,其中r為半徑,d為直徑,區別無非是內部數據到底選擇用半徑還是直徑的問題。
即使某位使用者是這樣用的:
double d = circle.getDiameter(); //獲取直徑
double area = PI * d * d;
修改內部實現時,其行為也依然不受影響:
double getDiameter() {
// return d; // 以前內部實現用直徑
return 2.0 * r; // 現在內部實現用的是半徑
}
特意列舉了這兩個例子是想說明,在合理地設計介面的情況下能夠使得後期修改更為方便。另外,並不意味著可以隨便改,必須得保證介面的原本含義不變。
例如,對於 @柯偉辰 例子將「中國人民銀行」改成「美國人民銀行」(可以換個例子么?這個例子有點。。。),可能匯率變了,那麼只改動這一個地方可能會造成介面含義的不一致,需要改動其他地方以保證介面意義的一致性。從private,protected,friend(以C++為例) 的角度考慮封裝,將修改private變數的函數被限定在幾個有限的函數內,好處比方說有利於維持類的不變式,以及便於調試等。
關於多態:
再說多態:基於對象所屬類的不同,外部對同一個方法的調用,實際執行的邏輯不同。
如上例子1塊,5塊,10塊,20塊,50塊,100塊多種面額,通常沒必要設計成多態,因為直接用一個枚舉變數保存,即可,因為其行為並沒有不一致的地方,只是值不一樣。
倒是人民幣,美元,歐元比較適合設計為多態,因為匯率不一樣,可以使用的國家也不一樣。
另外都說一個就是為什麼設計多態,一個常見的場合是:
double totalArea = 0;
for (Shape shape: Shapes) {
totalArea += shape.area();
}
這裡shape基類定義虛函數介面area(),子類矩形、三角形、圓形、線段等各自實現自己的面積計算公式,就能在忽略不同形狀差異的情況下,十分簡潔的求解面積之和。
其他常見例子比方說GUI渲染,或者生活中的例子,指揮家指揮樂隊演奏,鋼琴,小提琴演奏方式不一樣,行為不一樣(多態),但從指揮家的角度,他更注重控制整體而非細節。
多說一點是,虛函數,繼承只是實現多態的一種方式,用函數指針也可以實現運行期多態。
關於繼承:這個我認為繼承並不是面向對象的優勢,由於繼承帶來了基類與子類的強關係,使得類之間的關係更加複雜,限制條件更多。對象是對現實生活中事物的抽象,根據研究領域的不同,抽象的層次不同。在一些領域,錢只需要一個數值就可以了;而在其它一些領域,需要有尺寸,重量,顏色,哪年產,面值等,這些都是根據研究領域來決定的。
推薦閱讀:
※如何理解「在面向對象編程的時候,方法或者函數的參數最好是介面或者抽象類」?
※封裝和抽象的區別是什麼?
※怎麼通俗的解釋COM組件?
※如何理解「面向對象編程的精髓在於將操作綁定在數據上」?
TAG:面向對象編程 |
