<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                [TOC] 在軟件開發中,設計常被認為是要在編程之前完成的步驟。實際上,分析、編程、設計常常是重疊的、組合的或是交織在一起的。這章,我們將討論以下問題: * 面向對象意味著什么? * 面向對象設計和面向對象編程之間的差異是什么? * 面向對象設計的基本原則 * 統一模型設計語言(UML),如果它還沒那么邪惡的話 ## 什么是面向對象 正如大家所知道的,對象是一個我們能感覺和操縱的有形的東西。我們最早接觸的對象通常是嬰兒玩具。木塊、塑料形狀和拼圖塊是最常見的對象。嬰兒很快就知道某些對象會做某些事情:鐘聲響起,按鈕被按下,拉桿被拉動。 </b> 軟件開發中對象的定義并沒有太大的不同。軟件對象通常不是你能拿得起、感覺得到的有形的東西,但是它們是可以做某些事情和在它們之上做某些事情的模型。形式上,對象是數據和相關行為的集合。 </b> 知道一個對象是什么后,那面向對象又意味著什么?面向簡單來說就是*指向*。所以面向對象意味著功能上指向建模對象。這是用于復雜系統建模的眾多技術之一,通過描述交互對象的數據和行為的集合。 </b> 如果你讀過任何宣傳,你可能會遇到面向對象的術語,面向對象分析、面向對象設計、面向對象分析和設計以及面向對象編程。這些都是一般情況下高度相關的面向對象的概念。 </b> 事實上,分析、設計和編程都是軟件開發的階段。稱之為面向對象只是意味著我們將追求某種軟件開發的風格。 </b> **面向對象分析(OOA)**,是一個觀察問題、系統或任務(某人想將之變成應用程序),辨識對象及對象之間交互的過程。分析階段決定要做什么。 </b> 分析階段的輸出是一組需求。如果我們在一個步驟中完成分析階段,我們將會得到一項任務,比如,我需要將一個網站變成一組需求。例如:網站訪問者應該可以(*斜體*表示動作、**黑體**表示對象): * *回顧*我們的**歷史** * *申請***工作** * *瀏覽*、*比較*、*訂購***產品** 在某些方面,分析是一個誤稱。嬰兒在玩拼圖游戲時沒有分析塊和如何拼圖。相反,嬰兒探索周邊的環境,操縱形狀,看看它們放在哪里比較好。更好的說法可能是面向對象的探索。在軟件開發中,初始階段分析包括采訪客戶,研究他們的流程,以及消除各種不確定性。 </b> **對象導向設計(OOD)**,是將一系列的需求轉化為實現規格。設計人員必須命名對象,指定行為,并正式指定哪些對象可以在其他對象之上做些什么。設計階段是關于如何做事情的。 </b> 設計階段的輸出是一個實現規格書。我們將把OOA中獲得的一系列需求轉換為一組類和接口,這些類和接口可以在(理想情況下)用任何面向對象的編程語言實現。 </b> **對象導向編程(OOP)**,這個過程將設計轉換為一個工作流程。如果世界很理想,我們就可以沿著理想之路,一個接一個的,以完美的順序,像所有舊教科書告訴我們那樣完成一項工作。 </b> 然而,世界像往常一樣,它其實更加黑暗。不管我們多么努力地分開這些階段,在設計時,我們總是發現有需要進一步分析的東西。當我們正在編程時,我們總是發現設計過程中沒有澄清的特性。 </b> 大多數二十一世紀的軟件開發都采用迭代開發模式。在迭代開發中,對任務的一小部分進行建模、設計,然后對程序進行評審和擴展,以改進每一個特征,同時在一系列較短開發周期中加入新特征。 </b> 本書的其余部分是關于面向對象編程的,但是在本章中,我們將在設計環境中介紹基本的面向對象原則。這使我們能夠理解這些(相當簡單的)概念而不必爭論使用什么樣的語法或python解釋器。 </b> ## 對象和類 一個對象是一組具有相關行為的數據集合。我們如何區分對象類型呢?蘋果和桔子雖然都是對象,但兩者不可比較。蘋果和桔子并不經常出現在編程語言中。假設我們正在為水果農場設計倉儲管理。為了簡化這個例子,我們可以假設將蘋果放入桶中,桔子放在籃子里。 </b> 現在,我們有四種物品:蘋果、桔子、籃子和桶。在面向對象建模里面,用于表達*對象類型*的術語是類。因此,用技術術語,我們現在有四種對象的類。 </b> 一個對象和一個類有什么區別?類描述對象。它們就像創建對象的藍圖。例如有三個桔子在你前面的桌子上,每個桔子都是不同的對象,但這三個桔子都有與一個類相關的屬性和行為:桔子的一般類。 </b> 我們庫存系統中四種對象類之間的關系,可以使用統一建模語言(通常稱為UML,因為三個字母的縮寫詞永遠不會過時)類圖來描述。這是我們的第一張類圖: ![](https://box.kancloud.cn/1f58ee365a25c28228a327f4e97b481b_263x163.png) 這張圖顯示了**桔子**與**籃子**有某種關聯,**蘋果**在與**桶**有某種關聯。關聯是最基本的類之間的關系。 </b> UML在管理人員中很受歡迎,偶爾被程序員貶損。UML圖的語法一般很明顯,不必讀教程。UML很容易畫,很直觀。畢竟,很多人在描述類之間的關系,自然會畫出有線條的方框。基于這些直觀圖的標準使程序員很容易與設計師、經理和其他人溝通。 </b> 然而,一些程序員認為UML是在浪費時間。開發過程是迭代的,圖在實現之前是多余的,而且維護這些正式的圖只會浪費時間,對任何人都沒有好處。 </b> 對于不同的公司結構,這可能是真的,也可能不是真的。然而,每個由不止一個人組成的編程團隊偶爾還是要坐下來討論當前正在開發的子系統的細節。UML是在這些頭腦風暴會議中非常有用,以便快速和輕松地溝通。甚至那些嘲笑正式類圖的組織也傾向于在設計會議或團隊討論中使用一些非正式版本的UML。 </b> 此外,你不得不與之溝通的最重要的人,恐怕是你自己。我們都認為我們能記住我們所做的設計決策,但到最后總是變成*為什么我要這樣做*?,這種遺忘一直隱藏在我們的未來之中。如果我們在開始設計時,就做一份初始的設計圖,我們最終會發現這將是一份有用的參考資料。 </b> 然而,本章并不打算成為UML中的教程。很多資料可以在互聯網上獲得,還有許多有關該主題的書籍。UML所涵蓋的不僅僅是類和對象圖;它還具有用于用例、部署、狀態更改和活動的語法。我們將在面向對象設計用到一些常用的類圖語法。你會發現你可以通過例子來了解這些結構,然后下意識地選擇在自己的團隊或個人設計過程中使用有啟發作用的UML語法。 </b> 我們最初的設計圖雖然正確,但并不能提醒我們蘋果是裝在桶里的,或是一個蘋果可以放在多少個桶里。它只告訴我們蘋果與桶有關。類與類之間的聯系往往是顯而易見的,并不需要額外的解釋,但我們可以根據需要增加進一步的解釋。 </b> UML的優點是大多數東西都是可選的。我們只需要設計圖中的信息對于當前情況有意義就好了。在白板上快速地把框之間用線連接起來。等到正式的文檔,我們可以再更詳細地討論。對于蘋果和桶,我們可以有信心的判斷,**許多蘋果可以放在一個桶里**,**一個蘋果只能放在一個桶里**,所以我們可以強化我們的類圖: ![](https://box.kancloud.cn/e0111eb7774e44a1f82fb7cc95ba1f5c_266x206.png) 這張圖告訴我們,桔子**放在**籃子里,上面有一個小箭頭,表示什么對象在什么對象里面。它還告訴我們可以在關聯的兩邊使用對象數量。一個**籃子**可以裝許多(用*表示)**桔子**。任何一個**桔子**只能放在一個**籃子**。這個被稱為對象的多重性。你還可以聽到它被描述為基數。這些是實際上有些不同的術語。基數是指集合,而多重性則表示這個數字的大小。 </b> 我經常忘記多重性是在關系的哪一邊發生的。最接近類的那個數實際指示的是類之間關系中另一端類所需要的數量。蘋果和桶的例子,從左到右閱讀,**蘋果**類的許多實例(即許多蘋果對象)可以放在任何一個**桶**中。從右到右閱讀,只有一個**桶**可以與任何一個**蘋果**相關聯。 ## 定義屬性和行為 現在我們掌握了一些面向對象的基本術語。對象是可以相互關聯的類的實例。對象實例是具有自己的數據和行為集的特殊對象,桌子上的一個特定的桔子可以說是一個普通桔子類的實例。這很簡單,但是與每個對象相關聯的數據和行為是什么呢? ### 數據描述了對象 讓我們從數據開始。數據通常代表特定對象的個體特征。一個類可以定義被所有對象共享的特定特征集。對于給定的特征,任何特定對象都可以具有不同的數據值。例如,我們桌上的三個桔子(如果我們還沒有吃掉的話),每個的重量可能都不同,那么重量可以作為桔子類的一個屬性。桔子類的所有實例都有一個重量屬性,但每個桔子重量屬性的值可能都不同。屬性值不必是唯一的,任何兩個桔子的重量可能相同。作為一個更現實的例子是,兩個表示不同客戶的姓屬性可能具有相同的值。 </b> 屬性通常被稱為**成員**或**特性**。一些作者建議這些術語具有不同的含義,通常屬性是可設置的,而特性是只讀的。在Python中,“只讀”的概念毫無意義,所以在這本書中,我們將看到這兩個術語可以互換使用。另外,正如我們將在第5章“何時使用面向對象編程”中討論的,特性關鍵字在python中對于特定類型的屬性具有特殊的含義。 ![](https://box.kancloud.cn/2bcc60967df5256f9f0183b642ee6342_384x363.png) </b> 在我們的水果倉儲應用中,果農可能想知道桔子是從哪個果園來的,采摘的時間,以及它的重量。他們可能還想跟蹤每個籃子的存放位置。蘋果可能有顏色屬性,桶可能有不同的大小。這些屬性中的一些也可能屬于多個類(我們也可能想知道什么時候摘得蘋果),但是對于這個第一個例子,我們只在類圖中添加幾個不同的屬性。 ![](https://box.kancloud.cn/51c571b12eed1f4763aae06cd7455b45_523x390.png) 根據我們的設計需要的詳細程度,我們還可以為每個屬性指定類型。對于大多數編程語言,屬性類型通常指主類型,如整數、浮點數、字符串、字節或布爾函數。但是,屬性類型也可以是數據結構,如列表、樹或圖,或者更常用的其他類。這些可以在設計階段或在編程階段定義。一種編程語言的基本類型或對象在其他語言中多少會有一些不一樣。 </b> 通常,我們不需要在設計階段太關注數據類型,而是在編程階段選擇具體的實現方法。對于設計階段,起個通用的名字就足夠了。如果我們的設計需要列表容器類型,Java程序員可以選擇`LinkedList`或`ArrayList`,而Python程序員(就是我們!)可以選擇內建的`List`或`Tuple`。 </b> 到目前為止,在我們的水果農場例子中,我們的屬性都是基本的主類型。然而,有一些隱式屬性可以使關聯顯式化。對于給定的桔子,我們可能需要一個裝這個桔子的籃子屬性。 ### 行為即動作 現在,我們知道了什么是數據,那什么是行為呢?行為是指可能發生在對象上的動作。可以在特定對象所屬的類上執行的行為稱為**方法**。就編程而言,方法類似于結構化編程中的函數,但它們可以神奇地訪問所有與這個對象關聯的數據。和函數一樣,方法也可以接受**參數**和**返回值**。 </b> 方法參數就是一個**傳入**到被調用方法的一個對象列表。(對象列表更官方一點的說法是**實參**)。這些參數被方法使用,以執行任何需要完成的行為或任務。返回值是執行完任務的結果。 </b> 我們已經將“比較蘋果和桔子”的例子擴展到一個基本的(如果牽強的話)庫存應用程序中。我們再把它擴展一點,看看它是否完蛋。 </b> 一個可以與桔子有關的動作是**pick**動作。如果你考慮實現時,**pick**通過更新一個桔子的籃子屬性將桔子放到一個籃子里。與此同時,將這個桔子添加到**籃子**的**桔子**列表中。所以,**pick**需要知道它處理的是哪一個籃子。我們通過為**pick**方法提供一個**籃子**參數做到這一點。因為我們的果農也賣果汁,我們可以在**桔子**上再加一個**squeeze**方法。當執行`squeeze`方法時,可能會返回一定數量的果汁,同時也把榨過汁的**桔子**從**籃子**里移走。 </b> **籃子**可以有一個**賣出**動作。當一個籃子售出時,我們的庫存系統可能會更新一些尚未指定對象的數據,用來會計和利潤計算。或者,我們的一籃子橙子可能在我們出售之前就壞了。所以我們添加了一個**廢棄**方法。讓我們將這些方法添加到類圖中: ![](https://box.kancloud.cn/db9db91fba216537a2bb3229d1d9b493_752x269.png) 向一組獨立的對象添加模型和方法,我們可以創建一個交互式對象**系統**。系統中的每個對象都是某個類的成員。這些類定義了對象可以使用的數據類型和對象可以調用的方法。每個對象中的數據可以與在同一類中其他對象的數據處于不同的狀態,因為狀態差異,每個對象對方法調用的反應可能不同。 </b> 面向對象的分析和設計都是為了弄清這些對象是什么。以及他們應該如何互動。下一節描述了可以使用的原則使這些交互盡可能簡單直觀。 ## 隱藏細節、創建公共接口 OOD中給對象建模的關鍵是確定該對象的公共**接口**長什么樣。接口是其他對象可以用來與該對象交互的屬性和方法的集合。它們不需要,通常也不被允許訪問對象的內部工作。一個現實世界中常見的例子是電視。我們的電視接口是一個遙控器。遙控器上的每個按鈕表示一個可以對電視對象調用的方法。當我們調用對象、訪問這些方法時,我們不關心電視是否從天線、電纜,或者衛星天線接收信號。我們不在乎發送什么電子信號來調整音量,或聲音是否發送到揚聲器或耳機。如果我們拆開電視機探究內部工作是如何進行的,例如,如何將輸出信號分配到外置揚聲器或是耳機,我們都將違法保修政策。 </b> 隱藏對象實現或功能細節的過程被稱為**信息隱藏**。有時也稱為**封裝**,但是封裝實際上是一個更全面的術語。被封裝的數據也不一定要隱藏。從字面上來說,封裝就是創造一個時間膠囊。如果你把一堆信息放進一個時間膠囊里,把它鎖起來埋起來,它就被封裝起來,信息被隱藏起來。另一方面,如果時間膠囊沒有被掩埋,并且被解鎖或包裹透明的塑料,里面的東西仍然是密封的,但信息沒有被隱藏。 </b> 封裝和信息隱藏的區別很大程度上是不相關的,特別是在設計層面。許多參考文獻認為這些術語可互換。作為Python程序員,我們實際上沒有或不需要信息隱藏,(我們將在第2章“Python中的對象”討論這個細節),因此尋求一個更為包容的封裝定義是合適的。 </b> 然而,公共接口非常重要。它需要精心設計,因為將來很難改變它。更改接口將破壞任何調用它的客戶端對象。例如,我們可以根據自己的喜好更改內部結構,使其更有效,或通過網絡和本地訪問數據,客戶端對象則不需要變更代碼,仍然能夠使用公共接口與內部機構進行對話。另一方面,如果我們通過更改公共接口中用于公開訪問的屬性名稱,或更改能被方法接受的實參類型或順序,那么客戶端對象也必須跟著修改。對于公共接口,盡可能保持簡單。永遠記住,設計一個對象接口,應該基于怎樣簡單使用對象的基礎上,沒事把代碼弄得很復雜,那是扯淡。(這個建議也適用于用戶界面)。 </b> 記住,程序對象可以代表真實的對象,但不是把它們變成真實的對象。它們是真實對象的模型。模型最偉大的天賦之一是能夠忽略不相關的細節。我小時候造的模型車看起來很像真正的雷鳥1956,但它跑不了,傳動軸也轉不起來。在我學會開車之前,這些細節過于復雜和無關緊要。模型是一個真實概念的**抽象**。 </b> **抽象**是另外一個與封裝和信息隱藏有關的面向對象概念。簡單地說,抽象意味著,我們處理細節的層次,剛好適合給定的任務。這是一個從內部細節提取公共接口的過程。司機需要與方向盤、油門踏板和剎車踏板相互作用。但電機、傳動系和制動子系統的工作對司機毫無意義。另一方面,機修工則在另外一個不同的抽象層級上工作,調教引擎和制動真空度。下面是一個有兩個不同抽象層級的汽車例子: ![](https://box.kancloud.cn/6f83c5312b3f65d58b79d3366600dd1c_508x408.png) 現在,我們有幾個新術語,它們有類似的概念。簡單說就幾句話:抽象是,使用可分離的公共接口和私有接口,將信息封裝的過程。私有接口可以實現信息隱藏。 </b> 說這么多,我們得讓模型變得容易理解,畢竟我們得考慮那些與之交互的對象的想法。這意味著我們得注意一些小細節。確保方法和屬性具有合理的名稱。我們做系統分析時,對象通常表示原始問題中的名詞,而方法通常是動詞。屬性通常可以作為形容詞使用,盡管如果屬性引用的是另一個屬于當前對象的對象,它可能還是個名詞。相應地命名類、屬性和方法。 </b> 不要試圖給所有將來可能有用的對象或動作建模。去給那些系統需要執行的任務建模,讓設計自然而然地落入一個適當的抽象級別。這不是說我們不應該考慮未來可能的設計調整。我們的設計應該是開放的,以滿足未來的需求。然而,當抽象接口的時候,嘗試給需要建模的內容建模就好,而不是更多。 </b> 在設計界面時,試著把自己放在對象的鞋子里,想象一下對象對隱私有強烈的偏好。不允許其他對象訪問對象數據,除非你覺得其他對象擁有這些數據符合你自身的利益。不要給這些對象一個接口,來強制你執行特定的任務,除非你確定你希望他們能這樣對你。 ## 組合 到目前為止,我們學會了將系統設計成一組相互作用的對象,其中每次交互包括在適當的抽象級別上查看對象。但是我們還不知道如何創建這些抽象級別。有很多種方法可以做到這一點,我們將在第8章“字符串和序列化”、第9章“迭代器模式”討論高級設計模式。但是大多數設計模式都依賴**組合**和**繼承**這兩個基本的面向對象原則。組合比較簡單,讓我們從它開始。 </b> 組合是將幾個對象集合在一起以創建一個新對象的協議。當一個對象是另一個對象的一部分時,組合通常是一個不錯的選擇。我們已經在機械示例中看到了組合的第一個例子。汽車是由發動機、變速器、起動機、前照燈和擋風玻璃組成,除此之外還有許多其他零件。發動機則由活塞、曲軸和氣門等組成。在本例中,組合是提供抽象級別的一種好方法。汽車對象可以提供駕駛員所需的接口,同時對于一個機修工來說,汽車對象提供了一個更深的抽象層,機修工可以訪問它的組件部分。當然,如果機修工需要更多信息來診斷故障或調整發動機,那么他需要一個更深的抽象層。 </b> 這是一個常見的組合介紹性示例,但當涉及到設計計算機系統時,就不太有用了。物理對象很容易分解成組件對象。至少從古希臘人開始,人們就這樣做了,人們最初假設原子是物質的最小單位(當然,那時沒有粒子加速器)。計算機系統一般都沒有物理對象那么復雜,但從系統中識別出組件對象也不是件很容易的事情。 </b> 面向對象系統中的對象只是偶爾代表物理對象,例如人、書或電話。然而,更常見的是,它們代表抽象的想法。人們有名字,書有書名,電話用來打電話。打電話、書名、賬戶、姓名、任命和付款,通常不被看作是物理世界中的對象,但它們在計算機系統是最常見的建模的組件。 </b> 讓我們嘗試建模一個實際的更面向計算機的示例。我們將研究一個計算機化的國際象棋游戲的設計。這是一個在80年代和90年代,學術界非常流行的消遣方式。人們預測,有一天,計算機將能夠打敗人類的象棋大師。這發生在1997年(IBM的深藍擊敗了世界象棋冠軍加里卡斯帕羅夫),對這個問題的興趣隨后就減弱了,盡管仍然有許多計算機和人類之間進行的象棋比賽。(計算機通常獲勝) </b> 兩個玩家之間在進行一場象棋游戲。這是使用一個棋盤的國際象棋比賽,該棋盤在8 x 8格中包含64個位置。棋盤上有兩組棋子,各16個,兩個玩家可以選擇不同的方式交替移動棋子。每一個棋子都可以吃掉另外一個棋子。每次移動棋子后,棋盤都需要在電腦屏幕上繪制更新后的局面。 </b> 我用*斜體*描述識別一些可能的對象,使用**粗體**表示關鍵方法。這是由OOA到設計常見的第一步。在這一點上,為了強調組合,我們將重點放在棋盤上,不太關心玩家或不同類型的棋子。 </b> 讓我們從可能的最高抽象級別開始。我們有兩個隊員,輪流移動棋子: ![](https://box.kancloud.cn/8b8d2a3077185cba1cb3d072234ea1ff_521x212.png) 這是什么?它不像我們以前的類圖。那是因為它不是類圖!這是一個對象圖,也稱為實例圖。它描述處于特定狀態的系統,并描述特定實例對象,而不是類之間的交互。記住,兩個玩家都是在同一個類中的成員,類圖看起來有點不同: ![](https://box.kancloud.cn/0ffcc535a419df5961fd3d9fb9ce9e4c_293x100.png) 圖中顯示兩個玩家可以與一個棋盤交互。它也表示任何一個棋手每次只能用同一副棋子。 </b> 但是,我們討論的是組合,而不是UML,所以讓我們考慮一下**象棋集合**是由什么組成的。我們現在不在乎棋手是由什么組成的。我們可以假設棋手有心臟和大腦,以及其他器官,但是這些與我們的模型無關。事實上,沒有什么能阻止深藍這位棋手,這位棋手既沒有心臟也沒有大腦。 </b> 國際象棋是由一塊棋盤和32塊棋子組成的。棋盤進一步包括64個位置。你可能會爭論棋子不是國際象棋的一部分,因為你可以用另外一組棋子來代替國際象棋中的棋子。雖然這在計算機化的國際象棋中不太可能發生,但它把我們引向另外一個概念——**聚合**。 </b> 聚合幾乎與組合完全相同。區別在于,總的來說聚合對象可以獨立存在。一個位置不可能屬于不同的棋盤,或獨立存在,它只能和其他位置組成棋盤之后,才有意義。所以我們說棋盤是由位置組合而成的。但是棋子可以獨立于國際象棋,棋子與國際象棋的關系是聚合。 </b> 區分聚合和組合的另一種方法是考慮對象的壽命。如果組合(外部)對象控制(內部)對象被創建和銷毀,組合是最合適的。如果相關對象是獨立于組合對象創建的,或者可以比該對象命長,聚合關系更有意義。另外,請記住組合是聚合;聚合只是一種更一般的組合形式。任何組合關系也是一種聚合關系,但反過來并不成立。 </b> 讓我們描述一下當前的國際象棋組合,并添加一些屬性到對象,用于形成組合關系: ![](https://box.kancloud.cn/9c2b22a51a18d6eac57bde771914c8d4_795x278.png) 組合關系用UML表示為一個實心菱形。空心菱形表示聚合關系。你會注意到棋盤和棋子被存儲為國際象棋集合的獨立部分,并以相同的方法指向同一個參考,同時作為國際象棋集合的兩個屬性被存儲起來。這表明,在實踐中,一旦你通過設計階段,組合和聚合的區別是可以忽略不計的。但是,當你的團隊討論時,它可以幫助區分這兩者不同的對象交互方式。通常,你可以把它們當作一回事來對待,當你需要區分它們時,知道它們之間的區別還是挺重要的事情。(這是工作中的抽象)。 ## 繼承 我們討論了對象之間的三種類型的關系:關聯、組合、和聚合。然而,我們還沒有完全定義我們的國際象棋,這些工具似乎沒有給我們所需要的一切力量。我們討論了一個棋手可能是人類,也可能是一個具有人工智能的軟件。所以,不能說一個棋手和人類一定有*關聯*,或者說一個人工智能是棋手對象中的*一部分*。我們真正需要的關系是,能夠說“深藍*是一個*棋手”或“加里卡斯帕羅夫*是一個*棋手”。 </b> 這種*是一個*關系就是**繼承**。繼承太出名了,是面向對象編程中已知的和過度使用的關系。繼承有點像家譜。我祖父姓菲利普斯,我父親繼承了這個姓氏。接下來是我從他那里繼承這個姓氏(還有藍眼睛和寫作的嗜好)。 </b> 在面向對象編程中,和人類繼承特征和行為類似,一個類可以從另一個類繼承屬性和方法。 </b> 例如,我們的象棋集中有32個棋子,但只有6種棋子類型(兵、車、主教、騎士、國王和王后),每種棋子移動時的行為不同。所有這些類型的類都具有一些共有的屬性,例如顏色和它們都是象棋的一部分,但它們也有獨特的屬性,例如形狀、在棋盤上做出不同的移動。讓我們看看這六個角色類型是如何從棋類繼承的: ![](https://box.kancloud.cn/27ed83f13d4cc0c54033e45b878fb075_727x335.png) 箭頭暗示這些個角色類繼承自**棋**類。所有角色類型都自動從基類繼承了**國際象棋**屬性和**顏色**屬性。每個角色提供不同的形狀屬性(將在在渲染棋盤時顯示,譯注:你得告訴計算機,你的棋子都長什么樣子,這樣計算機才知道它得在棋盤上畫些什么,這個就叫渲染),還得為每個棋子指定不同的**移動**方法。 </b> 我們實際上知道棋類的所有子類都需要一個移動方法。否則,當我們試圖移動棋子時,這會變得很混亂。假如我們想創造一個新版本的國際象棋游戲,并額外新加了一種棋子(巫師)。我們當前的設計允許我們不給這個“巫師”定義**移動**方法。當我們要求移動“巫師”的時候,“巫師”就不知所措了。 </b> 我們可以通過在**棋**類上創建一個“傻瓜”移動方法來解決這個問題。其他子類可以**重寫**這個移動方法。像這個“巫師”子類,沒有自己的移動方法,就可以默認使用棋類的“傻瓜”移動方法,這條“傻瓜”移動方法可能就是彈出一條錯誤消息:**我不能動**。 </b> 由于可以在子類中重寫方法,所以我們可以創建非常強大的面向對象系統。例如,如果我們想實現一個人工智能棋手類,我們給這個棋手類定義一個`calculate_move`方法,接受一個棋盤對象,然后決定選擇哪個棋子移動到哪里。一個非常基礎的類可能隨機選擇一個棋子和方向,接著相應地移動它。我們也可以在深藍子類中重寫這個方法。第一種類合適一個未經訓練的初學者,后者將將是一位大師。更重要的是,類中的其他方法,例如通知棋盤哪些棋子被移動,這些無論是初學者還是大師,都是一樣的方法,則無需更改,這些方法完全可以在兩個類之間分享。 </b> 在國際象棋這個例子,提供一個默認的移動方法是沒有意義的(譯注:每個子類的走法都不同,所以設計一個默認移動方法有“嘛用”哪,所以同學們哪,抽象的本質就是了解業務啊)。我們需要做的是為每個子類定義移動方法。我們可以通過將**棋子**類定義為一個帶有**抽象**移動方法的**抽象類**,來實現這個目的。抽象方法意味著“我們命令這個方法必須存在在任何非抽象子類中,但我們拒絕在這個類中實現它。” </b> 實際上,確實存在這種根本不需要實現任何方法的類。這樣一個類會簡單地告訴我們類應該做什么,但絕對沒有關于如何做的建議。在面向對象的術語中,這種類被稱為**接口**。 ### 繼承提供抽象 **多態性**是指根據要實現的子類,對類進行不同的處理。我們已經看到在我們描述的國際象棋系統里它們是怎么實現的。如果我們再往前走兩步,我們可能會看到**棋盤**對象可以接受來自玩家的移動指令,并且調用棋子的**移動**函數。棋盤不需要知道它正在處理什么類型的棋子。它所要做的就是調用`move`方法,具體怎么移動,則是由子類來控制的,如果子類是騎士,就按騎士的移動方法,如果子類是士兵,就按士兵的移動方法。 </b> 多態性很酷,但它在Python中很少使用。Python又往前走了一步。python允許對象的子類可以像父類一樣被對待。在python中棋盤對象可以接受任何有**移動**方法的對象,無論是國際象棋的象,汽車,或是鴨子。當調用**移動**方法時,國際象棋的**象**將在棋盤上沿對角線移動,汽車將行駛到某個地方,鴨子會游泳或游泳,這取決于它的心情。 </b> python中的這種多態性通常被稱為鴨子類型:“如果像鴨子一樣走路或像鴨子一樣游泳,那就是鴨子。我們不在乎它是不是真的*是一*只鴨子(繼承),只要它能游泳或散步就行了。鵝和天鵝也很容易提供我們尋找的鴨子般的行為。這使得未來的設計師能夠創建新類型的鳥,而不實際指定水生鳥類之間的繼承關系。它還允許他們創建他們從未計劃過完全不同的行為。例如,未來的設計師能夠創造一只行走的、游泳的企鵝,和鴨子共用相同的接口,但不用暗示企鵝是鴨子。 ### 多重繼承 當我們在自己的家譜中考慮繼承時,我們可以看到我們繼承了來自多個父類的功能。當陌生人告訴一位驕傲的母親,她兒子有他父親的眼睛,她通常會回答:“是的,但是他擁有我的鼻子。” </b> 面向對象的設計也可以具有這樣的**多重繼承**特性,這使得子類從多個父類繼承功能。實際上,多重繼承可能是一項棘手的業務,一些編程語言(特別是Java)嚴格禁止它。但是,多重繼承可以有很多用途。通常,它可以用于創建具有兩組不同行為的對象。例如,一個被設計用于連接掃描儀并通過傳真發送掃描文檔的對象,可以通過從兩個不同的掃描儀對象和傳真對象繼承來創建。 </b> 只要兩個類具有不同的接口,通常不會對子類從它們中繼承造成什么嚴重的影響。但是,如果我們從兩個提供重疊(相同名字)接口的類那里繼承,就會變得混亂。例如,如果我們有一個具有移動方法的摩托車類,以及一個具有移動方法的船類,我們想把它們合并成最終的兩棲車輛,當我們調用移動方法時,如何讓生成的類知道要做什么?在設計層面,這需要要解釋,在實現層面,每個編程語言都有決定調用哪種父類方法的不同方式,或者按什么順序調用。 </b> 通常,最好的方法是避免使用多重繼承。如果你有一個展示出來的設計存在多重繼承,你可能做錯了。后退一步,再次分析系統,然后看看你是否可以刪除多重繼承關系,用其他關聯或組合設計。 </b> 繼承是用于擴展行為的一個非常強大的工具。它也是面向對象設計相對于早期編程范式的一種最顯著的市場化優勢。因此,繼承通常是面向對象程序員所接觸的第一個工具。然而,它重要的是要認識到擁有錘子不會把螺絲釘變成釘子。繼承是一種完美的解決方案,顯然是針對對象之間的關系而言,所以它可能被濫用。程序員經常使用繼承在兩種對象之間共享代碼,但兩個對象之間并沒有什么關系。雖然這不一定一個糟糕的設計,這是一個可怕的機會,問他們為什么這么決定,換一種不同的關系或設計模式或許是合適的。 ## 案例研究 讓我們將所有新學到的面向對象知識,通過一組面向對象設計迭代,在一個現實世界的例子中,將它們聯系在一起。我們要構建的系統模型是圖書館目錄。幾個世紀以來,圖書館一直在跟蹤它們的庫存,最初使用卡片目錄,最近使用電子目錄。現代圖書館有基于網絡的目錄,我們可以在家里查詢。 </b> 讓我們從分析開始。當地的圖書管理員要求我們寫一個新的卡片目錄程序,因為他們古老的基于DOS的程序是丑陋和過時的。他并沒有給我們太多細節,但在我們開始詢問更多信息之前,讓我們考慮一下我們已經知道的關于圖書館目錄的內容。 </b> 目錄包含書籍列表。人們通過特定的標題或特定的作者,搜索某些主題的書籍。書籍可以通過國際標準書號(ISBN)唯一標識。每本書都有杜威十進制系統(DDS)編號,用于幫助在特定貨架上查找書籍。 </b> 這個簡單的分析告訴我們系統中存在一些明顯的對象。我們很快將**書籍**確定為最重要的對象,并提及到多個屬性,如作者、標題、主題、ISBN和DDS編號,看起來目錄有點像圖書經理。 </b> 我們還注意到一些可能需要或不需要在系統建模的對象。為了編目,我們通過作者搜索一本書,只需要這本書的`author_ name`屬性。然而,作者也是對象,我們可能希望存儲有關作者的其他數據。當我們思考這個問題時,我們可能會記得有些書有多位作者。突然間,對于圖書對象,僅僅定義單一`author_ name`屬性似乎有點傻。定義與每本書相關聯的作者列表顯然是個好主意。 </b> 作者和書之間的關系顯然是關聯關系,因為你永遠都不能說“書是作者”(不是繼承),也不能說“書有作者”,雖然在語法上是正確的,但并不意味著作者是書的一部分(這不是聚合)。實際上,任何一個作者都可能與多本書相關聯。 </b> 我們還應該注意名詞(名詞總是可以看作代表對象)*貨架*。貨架是需要在編目系統中建模的對象嗎?我們如何識別單個貨架?如果一本書存儲在一個架子的末端,另外一本書被插在這個書架的前面,它只好移到下一個架子的開始,接著會發生什么? </b> DDS的設計是為了幫助在圖書館找到實體書。因此,不管存儲在哪個書架,通過書籍的DDS屬性都應該足以定位它。所以,至少在目前,從我們的對象競爭列表中刪除貨架對象。 </b> 系統中的另一個有爭議的對象是用戶。我們需要知道關于某個特定的用戶的信息嗎?比如他們的姓名、地址或過期書籍的列表?到目前為止,圖書管理員只告訴我們他們想要一份目錄;他們并沒有提及跟蹤訂閱或過期通知。在我們的頭腦中,我們也注意到作者和用戶都是特殊類型的人;可能這里將會有一個有用的繼承關系。 </b> 為了編目的目的,我們決定暫時不需要識別用戶。我們可以假定用戶將搜索目錄,但我們不必主動在系統中為他們建模,除了提供一個允許他們搜索的接口。 </b> 我們為書籍確定了一些屬性,但是目錄需要什么屬性嗎?有沒有一個圖書館有多個目錄?我們需要單獨識別它們嗎?顯然,目錄中必須以某種方式包含書的集合,但這個列表可能并不是公共接口的一部分。 </b> 那么行為呢?目錄顯然需要一種搜索方法,或是是幾個按作者、標題和主題進行搜索的不同的方法。書籍需要什么行為嗎?會不會需要預覽方法嗎?或者通過首頁屬性來識別預覽而不是用預覽方法? </b> 前面討論的問題都是面向對象分析階段的一部分。但在回答這些問題中,我們已經確定了一些屬于設計部分的關鍵對象。事實上,你剛才看到的就是分析和設計之間的微迭代。 </b> 很可能,這些迭代都發生在與圖書管理員的初始會議上。然而,在這次會議之前,我們可以為對象勾畫出一個最基本的設計。我們已經具體確定了: ![](https://box.kancloud.cn/80711e0cfc0f7ac60e653161f6c06271_328x274.png) 有了這張基本的類圖和一支鉛筆,我們可以和圖書管理員一起,互動地改進類圖。他們告訴我們這是個好的開始,但圖書館提供的不止有書籍,他們也提供DVD、雜志和CD,這些都沒有ISBN,或DDS號碼。所有這些類型的項目都可以由UPC數字唯一標識。我們提醒圖書管理員,它們必須在貨架,這些物品可能不是按UPC組織的。圖書管理員解釋說每種類型都以不同的方式組織。CD大多是有聲讀物,它們只有幾十個存貨,所以是按作者的姓整理的。DVD分為流派,并按標題進一步組織。雜志是按標題組織的,然后按卷和發行號再細分。書和我們猜測的一樣,按DDS編號組織。 </b> 如果以前沒有面向對象的設計經驗,我們可以考慮添加將DVD、CD、雜志和書籍的列表分別放到我們的目錄中,然后按順序搜索每個目錄。問題是,除了某些擴展屬性,包括識別項目的物理位置,這些項目的很多行為都是相同的。這就是一個繼承!我們快速更新了我們的UML圖: ![](https://box.kancloud.cn/fbe4b8df80d6c270ddd8608845a55eac_497x279.png) 圖書館員理解我們草圖的要點,但對**定位**功能有一點困惑。我們通過用戶正在搜索特定“兔子”這個詞為例來解釋。用戶開始向目錄發送搜索請求。目錄查詢其內部項目列表,并發現一本書和一張DVD的標題中含有“兔子”。到目前為止,目錄不關心它所持有的類型(DVD、書籍、CD或雜志);就目錄而言,這些類型是相同的。但是,用戶想知道如何找到物理項,如果目錄只是返回標題列表,并沒什么用。因此,它對它發現的兩個項目調用**locate**方法。這本書的**locate**方法返回一個DDS編號,該編號可用于標識存放書的位置。通過返回DVD的類型和標題來定位DVD。用戶可以訪問DVD部分,查找包含該流派的部分,并查找特定的按標題排序的DVD。 </b> 正如我們所解釋的,我們繪制了一個UML序列圖來解釋對象之間如何通信: ![](https://box.kancloud.cn/5b91260a3c8e2954c97123b4c9d96cc0_382x396.png) 類圖描述了類之間的關系,而序列圖表描述了對象之間傳遞特定消息的序列。掛在每個對象上的虛線行是描述對象生存期的生命線。這個每條生命線上更寬的盒子表示該對象中處在活動狀態(沒有盒子的地方,對象基本上是閑置的,等待著發生什么)。這個生命線之間的水平箭頭表示特定的消息。實心箭頭表示正在調用的方法,而帶實心箭頭的虛線表示方法返回值。 </b> 半箭頭表示發送到對象或從對象發送的異步消息。異步消息通常意味著第一個對象調用第二個對象的方法,然后立即返回。經過一些處理后,第二個對象調用第一個對象的方法來給它一個值。這與常規調用不同,常規調用會立即返回一個值。 </b> 序列圖和所有UML圖一樣,只有需要它們時才有用。為了繪制UML圖而畫圖實際上是沒有意義的。但是,當你需要在兩個對象之間交流一系列的互動時,序列圖是一個非常有用的工具。 </b> 不幸的是,我們的類圖到目前為止仍然是一個混亂的設計。我們注意到DVD上的演員和CD上的藝術家都是各種各樣的人,但他們完全不同于書的作者。圖書管理員還提醒我們,他們的大多數CD是有聲讀物,所以有作者而不是藝術家。 </b> 我們如何處理為一個題目做出貢獻的不同類型的人?明顯的實現是創建一個具有姓名和其他相關詳細信息的`Person`類,然后為藝術家、作者和演員創建此類的子類。然而,這里真的需要繼承嗎?為了搜索和編目的目的,我們并不關心表演和寫作是兩種截然不同的活動。如果我們做一個經濟模擬的話,給演員和作者分別創建不同的類是有意義的,順便再加上不同的`calculate_income`和`perform_job`方法,但用于編目目的,很可能只要知道這個人是如何貢獻項目就足夠了。我們認識到所有項都有一個或多個`Contributor`對象,因此我們把書籍中的作者關系移動到父類: ![](https://box.kancloud.cn/1bf134e721c4a6179637c291b5bd61bd_505x307.png) **貢獻者**/**庫項**之間的關系是**多對多**的,因為關系兩端都有`*`字符。任何一個圖書館項可能有多個貢獻者(例如,一個DVD有幾個演員和一個導演)。許多作者寫了很多書,所以他們會被附在多個項上。 </b> 這一小小的變化,雖然看起來有點干凈和簡單,卻失去了一些至關重要的信息。我們仍然可以知道是誰對一個特殊的圖書館項目做出了貢獻,但我們不知道他們是如何貢獻的。他們是導演還是演員?他們寫了有聲讀物,或者只是敘述這本書的聲音? </b> 如果我們在**貢獻者**類加上一個`contributor_type`屬性,但在與多才多藝的人打交道時,例如既寫過書又導演過電影的人,這一類會分崩離析。 </b> 一種選擇是在**庫項**子類上加上我們需要的信息,例如**書籍**上的**作者**或**CD**上的**藝術家**,然后把這些屬性都指向**貢獻者**類。問題在于這就是我們失去了很多多態的優雅。如果我們想列出一個項目的貢獻者,我們必須在該項目上尋找特定的屬性,例如**作者**或**演員**。我們可以通過在**庫項**類上添加`getContributors`方法來緩解這種情況。這樣子類可以重寫。目錄也不必知道正在查詢對象的屬性;我們抽象了公共接口: ![](https://box.kancloud.cn/17cacdb2c6e8ab52ad4036890667d873_555x456.png) 只要看一下這個類圖,就知道我們好像做錯了什么。它是笨重而易碎。它可以做我們所需要的一切,但感覺很難維護或擴展。關系太多,類大多會受到任何一個類修改的影響。看起來像意大利面和肉丸。 </b> 既然我們已經知道繼承可以作為一種選項,并且發現它是我們所需要的,我們可能會回顧我們以前的基于組合的類圖,**貢獻者**直接連接到**庫項**。經過思考,我們可以看到只需再為一個全新的類添加一個用于識別貢獻者類型的關系。這是面向對象設計中的一個重要步驟。我們在設計里添加了一個新類用于*支持*其他對象,而不是作為初始要求的一部分建模。我們正在**重構**設計使系統中的對象更加便利,而不是針對現實生活中的對象。在程序或設計的維護中,重構是一個必不可少的過程。重構的目標是通過移動代碼、刪除重復代碼或復雜關系改進設計,使得設計更簡單,更優雅。 </b> 這個新類由一個**貢獻者**類和一個額外的類型屬性(貢獻者對給定**庫項**所做的貢獻類型)組成。有可能對于一個特定的**庫項**有很多這樣的貢獻者,并且一個貢獻者可以以同樣的方式為不同的項目做出貢獻。設計的更好的類圖如下: ![](https://box.kancloud.cn/2b8c86822b36fcdd91ecff14a94d18da_577x290.png) 乍看起來,這種組合關系并不像繼承關系那么自然。但是,它的優點是允許我們添加新的貢獻者類型,而不用在設計中添加新類。當子類具有某種特殊性時,繼承是最有用的。特殊性意味著可以創造或者改變子類的屬性或行為,使其有所不同于父類。僅僅為識別不同類型的對象創建空類是愚蠢的(在Java程序員和其他“一切皆對象”的程序員,這是一種不太普遍的態度,但在更務實的python設計師那里是很常見的)。如果我們查看類圖的繼承版本,我們可以看到一堆實際上不做任何事情的子類: ![](https://box.kancloud.cn/763d0aed5c5aff82d3e5d129f0a5d0a4_638x298.png) 有時侯,認識到何時不使用面向對象原則很重要。這個不使用繼承的例子很好地提醒我們,對象只是工具,而不是規則。 </b> ## 總結 在本章中,我們對面向對象范式的術語進行了一次旋風式的介紹,重點介紹了面向對象的設計。我們可以把對象分為不同的類,并通過類接口描述這些對象的屬性和行為。類描述了對象、抽象, 封裝和信息隱藏等高度相關的概念。有很多對象之間的各種關系,包括關聯、組合以及繼承。UML語法對于娛樂和溝通很有用。 </b> 在下一章中,我們將探討如何在Python中實現類和方法。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看