您的位置:首頁>正文

物件導向程式設計——類與物件

周立功教授數年之心血之作《程式設計與資料結構》以及《面向AMetal框架與介面的程式設計(上)》, 電子版已無償性分享到電子工程師與高校群體, 書本內容公開後, 在電子行業掀起一片學習熱潮。 經周立功教授授權, 本公眾號特對《程式設計與資料結構》一書內容進行連載, 願共勉之。

第四章為物件導向程式設計, 本文為4.2類與物件。

4.2類與物件

亞里斯多德可能是第一個研究類型概念的人, 他提到了“魚類和鳥類”。 將具有共同的行為和特徵的所有物件歸為一個類的思想, 在第一個物件導向語言Simula-67中得到了直接應用,

其目的是為了解決類比問題。 比如, 銀行的出納業務, 包括出納部門、顧客、業務、貨幣的單位等大量的物件, 將具有相同資料結構(屬性)和行為(操作)的物件歸在一起為一個類, 屬於類的任何物件都共用該類的所有屬性, 這就是類的來源。

創建抽象資料類型是OOP的基本思想, 幾乎能象完全內建類型一樣使用。 程式師可以創建類型的變數和操作這些變數。 每個類的成員都有共性, 每個帳戶有餘額, 每個出納員都能接收存款等。 同時每個成員都有自己的狀態, 每個帳戶有不同的餘額, 每個出納員都有名字。 通常在電腦中出納員、客戶、帳戶和交易等都被描述為唯一的實體, 這個實體就是物件, 每個物件都屬於一個定義了它的行為和特性的特定類。

由此可見, 類和類的物件不是相同的概念, 與圖紙和建築的關係類似, 物件的描述依賴描述它的類。 因此可以通過創建類的實例創建物件, 即定義類的變數, 這個過程叫做產生實體。

4.2.1物件

從人類認知的抽象角度來看, 物件可以是下列事物之一:

一個可以觸摸或可以看見的東西;

在智力上可以理解的東西;

可以指導思考或行動的東西。

顯而易見, 一個物件反映了某一部分的真實存在, 因此物件是在時間和空間中存在的某種東西。 軟體中的“物件”術語首先出現在Simula語言中, 物件存在於Simula程式中, 用於類比現實世界的某個方面。

某些物件可能有明確的概念邊界, 但代表的是不可觸摸的事件或過程。

比如, 一個立方體和一個球相交, 它們的相交線是一條不規則的曲線。 雖然它離開了球體或立方體就不存在了, 但這條線仍然是一個物件, 因為它有明確定義的概念邊界。

某些物件可能是可觸摸的, 但物理邊界不太清晰。 比如, 河流、霧和人群等就屬於這種類型的物件。 雖然類似於美和色彩這樣的屬性不是物件, 愛和恨這樣的感情也不是對象, 但這些東西有可能成為其它物件的屬性, 比如, 一個男人(一個物件)愛他的妻子(另一個物件), 或者說某只貓(又一個物件)是灰色的。 由此可見, 屬性工作表示物件記憶的資訊, 且只能通過物件的操作來訪問和修改。

當傳統的過程模組或函數返回調用者時, 不會帶來任何副作用,

模組運行結束, 只將其結果返回。 當同一模組再次被調用時, 就像是第一次誕生一樣。 模組對以前的存在沒有任何記憶, 就象人類一樣對以前的存在一無所知。

就物件而言, 物件的一個重要特徵是它們充當資料的容器, 因此物件具有記憶功能, 物件知道它的過去, 通常也將包含在物件屬性中的資料值稱為物件的狀態。 當一個物件的調用者給該物件一個資訊後, 如果該調用者或其它調用者要求該物件再次提供這一資訊, 則該物件執行結束後並沒有死, 因此物件具有如何保持其狀態(狀態即物件擁有值的集合)的能力。

假設你在看一個人, 肯定會將這個人當作一個物件。 顯然, 每個人都有資料, 比如, name、birthdate和weight等;一個人還有行為,

比如, 走路、說話和呼吸等, 因此可以說物件是由“資料和行為”構成的。 在現實世界裡, 由於每個物件的狀態不一樣, 因此可以用存儲在一個物件中的資料表示物件的狀態, 資料包含了能夠區分不同物件的資訊。

在OO程式設計中, 每個物件都有唯一的標識, 標識是一個物件的屬性, 用於區分這個物件與其它所有物件。 而這個唯一的標識可以通過控制碼機制提供, 因此可以借助這個控制碼引用物件。 不同的語言實現控制碼的方式不一樣, 比如, 位址、陣列下標或人為編號。

在現實世界中一些物件有對等物, 比如, ZLG公司, 另一些物件則是概念實體, 比如, 解一元一次方程, 還有一些其它的物件, 比如, 棧、陣列變數名a, 都是為了實現而引入的,沒有對應的物理實體。

許多開發人員可能會認為“一個包含了另一個物件的物件”,其本質上與“一個具有純資料成員的物件”是完全不同的,但是那些看起來不是物件的資料成員實際上也是物件,比如,整數和雙精度數。在真正的物件導向語言中,萬事萬物都是物件,甚至內置資料也是物件,即使其行為只是運算。

由此可見,雖然物件是具有明確定義的邊界的東西,但還不足以區分不同的物件。因為同一個類的每個物件具有不同的控制碼,在任何特定時刻,每個物件可能有不同的狀態(指存儲在變數中的不同的“值”),因此物件是一個具有狀態、行為和標識的實體。

物件又分持久物件和主動物件,持久物件是指生存期可以超越程式的執行時間,而長期存在的且所有的操作都是被動執行的物件。在主動物件概念出現之前,人們所理解的物件概念只是被動物件,即物件的每個操作都是被動地回應從外部發來的消息才可以執行。

在開發一個具有多工併發執行的系統時,如果僅有被動物件的概念,則很難描述系統中的多個任務。其實併發不僅僅存在作業系統中,如今多個任務併發可以說無處不在。每個任務在實現時應該成為一個可以併發執行的主動程式單位,那麼如何描述呢?

如果用被動物件將無法描述那些不接收任何消息也要主動工作的物件,比如,交通燈控制系統中的信號燈,溫控器中的感測器,它們的行為都是主動發起的,即主動物件至少有一個操作不需要接收消息就就能主動執行的物件。

儘管發現物件的活動是從具體事物出發分析和認識問題的,但人們在進行這種活動時實際上並不局限於對個別事物的認識,而是尋找一類事物的共同特徵,將物件抽象為類。

4.2.2類

類的概念早在柏拉圖之前就出現了,物件導向程式設計就像柏拉圖之後的西方哲學家一樣延續了這種思維。類的概念與物件的概念是緊密交織在一起,因為在討論一個物件時不得不提到它的類,但是這兩個術語又存在重要的差別。物件是存在於時間和空間中的具體實體,而類僅代表一種抽象,因此可以說Validator類代表了所有校驗器的共同特徵。要確定這個類中的某個具體的校驗器,則必須說“範圍值校驗器”或“同位器”。

在物件導向分析與設計的上下文中,將類定義為——類是對現實世界中事物的描述,類描述了擁有相同屬性、行為和關係類別的一組物件,一個物件就是類的一個實例,因此沒有共同的屬性和行為的物件不能劃分為一個類。比如,一個相當高層的抽象,一個GUI框架,一個資料庫和整個系統在概念上都是獨立的物件,因此不能將它們表示為一個單獨的類。相反應該將這些抽象表示為一組類,通過這些類的實例互相協作,提供我們期望的功能。

通常將這樣的一組類稱為一個元件,而元件是預先創建好的程式模組,可與其它模組一起構成一個程式。通常元件以二進位形式發佈,其實現對使用者來說是隱藏的。如果元件設計良好,使用者甚至不需要知道這個元件使用什麼語言編寫的。但元件必須至少暴露一個介面才能使用,通常元件會有暴露多個介面。從使用者的角度來看,一個元件是一些前端介面的後端服務者,程式師通過元件介面所暴露的函數操作該元件。由此可見,元件擴展了物件導向中物件作為服務提供者通過高層介面提供服務的概念。

在現實世界裡,餅乾也是物件,必須先有模子(類),才能做出你想要的形狀的餅乾,因此可以認為類是物件的範本。比如,只有符合一定條件的數值才能push到棧中,那麼Validator校驗器類就是由RangeValidator範圍值校驗器類、OddEvenValidator同位器類和PrimerValidator質數校驗器類等具體校驗器類的物件構成的一個集合體。屬於類的任何物件都共用該類的所有屬性,比如,所有的具體校驗器都有這樣的屬性——校驗參數。

在OO程式設計中,一個類就是一種抽象資料類型,用戶也可以創建一個自己的類,而且可以將這個類當做資料類型使用。一旦有了類,就可以象使用普通的資料類型那樣用類定義變數,如果定義了RangeValidator類,即可用它定義變數rangeValidator。RangeValidator類的變數rangeValidator可以擁有成員變數或域,代表不同校驗器的屬性或特性,通常將這些成員變數稱為資料成員。

(1)值和屬性

值是一段資料,屬性描述了類的每個物件都擁有的一個值,可以這樣類比——物件之于類如同值之於屬性。比如,name、birthdate和weight都是Person物件的屬性,color、modelYear和weight都是Car物件的屬性。對於每個物件,每個屬性都有一個值,比如,物件ZhangSan的屬性birthdate的值是“21 October 1983”,也就是說,ZhangSan生於1983年10月21日。對於一個特定的屬性,不同的物件可能會有相同或不同的取值。在一個類中,雖然每個屬性的名字都是唯一的,但在所有的類中不一定是唯一的,比如,類Person和類Car都可能有一個名為weight的屬性。

下面將介紹一種通過屬性詳細描述類的UML建模語言,一種用於視覺化表示、指定、構造和描述軟體密集系統中部件的圖形化語言,它提供了一種以圖形化方式表示和管理物件導向軟體系統的方法。其不僅是系統設計的表示,而且是一種有助於完成系統設計的工具。類圖定義了3個不同的部分,即類名、屬性和方法,用於解釋所構建的類。當用UML創建物件模型時,盡可能不要在類圖中包含太多的資訊,這樣就能集中注意力於整體設計,而不會將重點放在細節上。

如圖 4.1所示展示了類建模標記法,顯示了一個類(左圖)和它所描述的物件(右圖),物件ZhangSan和LiMing都是類Person的實例。物件的UML標記法是一個方框,方框裡面是物件名後加冒號和類名,對象名和類名都有底線,並約定用黑體字表示物件名和類名。類的UML標記法也是一個方框,也約定用黑體字表示類名,將名字放在方框的正中央,首字元大寫,且用單數名詞表示類名。類Person有屬性name和birthdate,name是string(字串),birthdate是date(日期)。類Person中一個物件的名字取值是"Zhang San",生日取值是“21 October 1983”;另一個對象的名字取值是"Li Ming",生日取值是“16 March 1950”。

圖 4.1屬性和值的UML標記法

UML標記法會在框的第二格裡列舉屬性,每個屬性後面都可以有可選項,比如,類型和預設值。在類之前有一個冒號,在預設值之前有一個等號。約定以常規字體顯示屬性名,方框中的名稱左對齊,首字母使用小寫。在物件方框的第二格裡,也可能會包含屬性值,其標記法是列出每個屬性名,之後跟著等號和取值,同樣屬性值也是左對齊,使用常規字體。雖然有些實現要求物件有唯一的識別字,但這些識別字在類模型中是隱含的,即不需要也不應該顯式地將它們列舉出來,比如,PersonID:ID。因為大多數OO開發語言會自動生成識別字,可以使用這些識別字來引用物件;反之,則可能需要顯式地列舉出來,否則無法引用物件。但是不要將內部識別字和現實世界的屬性混淆了,內部識別字純粹是一種便於實現的做法,沒有應用意義。相反,納稅人編號、汽車牌照號碼和電話號碼都不是內部識別字,因為它們在實現世界有真實的意義,屬於合法的屬性。

(2)操作與方法

操作是一個函數或過程,比如,open和close都是Windows類的操作,類中所有的物件都共用相同的操作,因此將物件能夠做什麼的行為稱為操作,通常將相同的操作應用於許多不同的類稱為多態。

方法是對操作的實現,其表現為OOP某個類的成員函數。比如,類Validator有一個操作validate,其校驗過程是通過validate調用不同的函數實現的。比如,範圍值校驗和同位。雖然這些方法在邏輯上都執行相同的任務——資料校驗,但是每種方法的實現代碼會有所不同。

如圖 4.2所示RangeValidator類有min和max屬性,以及validate操作,min、max和validate都是RangeValidator的特徵。特徵是描述屬性或操作的類屬詞彙,類似地OddEvenValidator有isEven屬性和validate操作。

圖 4.2操作的UML標記法

注意,validate省略了括弧中的輸入參數,即“validate(pThis:void *, value:int):bool”。validate的一個參數是pThis,其類型是void *;它的另一個參數value,其類型是int。當一項操作在幾個類上都有方法時,這些方法都要有相同的簽名,即相同的參數數量和類型,以及返回值的類型。

UML的方框表示類,最多有三格,從上到下每個格裡分別包含了類名、屬性清單和操作清單。類方框中的屬性和操作的框格可以選擇顯示或隱藏,缺少屬性說明沒有指定屬性,缺少操作框說明沒有指定操作。相反,空框格意味著屬性是指定的,只是沒有顯示屬性而已。

操作清單約定用常規字體列出操作名,左對齊,首字母小寫。比如,參數列表和操作結果的類型,用括弧將參數列表括起來,並用逗號分隔參數。結果類型之前有一個冒號,除非括弧中空的參數列表明確表示沒有參數,否則就不能下結論。

(3)客戶和伺服器模式

在OOP中,如果一個類公開了一些方法供其它類調用,那麼這個類被稱為伺服器,公開的這些方法被稱為服務,而調用這些服務的類就是客戶。理論上客戶類調用伺服器類的服務,即客戶向伺服器發送了一條消息。而客戶和伺服器的概念是相對而言的,當A類向B類提供了功能介面時,則類A是伺服器,B類是客戶;如果類B也同時為類A提供了功能介面,則類B是伺服器,類A是客戶。

設計良好的伺服器應該將其實現細節隱藏起來,客戶僅需知道伺服器提供的介面即可。介面就是客戶所能調用的那些函數,這些函數將消息發給伺服器,那麼伺服器就知道客戶需要什麼樣的服務,伺服器會返回一些資料給客戶,或執行客戶所需的任務等。

(4)消息傳遞和方法調用

在OOP中,類和物件表現為伺服器,使用類和物件的模組表現為客戶。客戶通過特殊的方式請求服務。那麼到底如何讓物件為我們做有用的事情呢?必須有一種方法能向物件做出請求,使得它能做某件事情,比如,完成交易、在螢幕上畫圖或打開開關。可以向物件發出的請求是由它的介面定義的,而介面是由類型定義的。

雖然介面規定了我們能向特定的物件發出什麼請求,但必須有代碼滿足這種請求,再加上隱藏的資料就組成了實現。類型對每個可能的請求都有一個相關的函數,當向物件發出請求時,就調用這個函數。這個過程被概括為向物件“發送消息”(提出請求),物件根據這個消息確定做什麼(執行代碼)。

物件之間的邏輯介面通過消息傳遞實現,消息是對物件之間通信的的抽象。常見的消息傳遞方法是直接調用定義于接收方物件中的操作,比如,當物件A調用物件B的一個方法時,物件A就是在向物件B發送一個消息,物件B的響應由其返回值定義,但只有物件的公共方法才能由另一個物件調用。

使用消息傳遞可以實現松耦合,特別在分析階段,不用指定介面的細節,比如,同步、函式呼叫格式和超時等。當全面理解了所有的問題後,接下來就可以決定設計和實現的細節了。通常物件介面可以看成物件與外部世界之間制定的契約,契約是由一組協定定義的,物件參與到這些協定中。介面協定包括前置條件、後置條件和不變數。

前置條件是當該操作被調用時,必須成立的條件。即在調用之前應該校驗傳入參數是否正確,只有正確才能執行該方法。也就是說,必須在消息發送或接收之前保證為真的條件,這是消息發送者的職責。一旦通過前置條件的校驗方法必須執行,且必須保證執行結果符合契約,這就是後置條件。也就是說,後置條件是當該操作完成時,必須成立的條件。即在處理消息時必須保證為真,這是消息接收者的職責。不變數是指在任何時刻都必須成立的條件,包括操作執行前、執行時和執行後。

(5)屬性抽象與行為抽象

OO程式設計思想可以採用抽象的方法,對現實世界中的多個具體物件進行概括分析,得到這類物件所具有的共同屬性和行為,加以描述就形成了類。雖然都是同一個類的物件,但每個物件的屬性不同,於是就形成了不同的具體物件實體。

抽象一般分為屬性抽象和行為抽象兩種,屬性抽象是尋找一類物件共有的屬性,比如,在範圍值校驗器RangeValidator類中,使用整型變數min和max來描述push到棧中的數值範圍,然後將min和max變數作為類的成員變數描述物件的屬性,即“屬性是包含在物件中的變數”。而行為抽象則是尋找這類物件所具有的共同行為特徵,比如,對push到棧中的值進行範圍值校驗,同樣,也可以為這個類添加相應的函數,最終將該函數作為類的成員函數描述物件的行為,即“方法是包含在物件中的函數”。

在面向過程的程式設計中,程式是由模組組成的,一個模組就是一個過程,通常採用自頂而下的設計方法。而物件導向的程式設計與設計著眼於解決面向過程的程式設計和自頂而下設計中出現的一些問題,由於在物件導向的程式設計中構成模組的基本單元是類,而不是過程,因此物件導向設計是物件導向程式設計的設計方法,它著重於類的設計,通過類的設計完成對實體的建模任務,類建模的目的是描述物件。

在面向過程的程式設計中,描述一個物體時,資料和方法是分開的。比如,當通過網路發送資訊時,則只會發送相關的資料,並認為網路另一端的程式知道如何進行處理。也就是說,如果兩者之間沒有握手協定,則網路另一端的程式不知道如何處理。而物件可以定義為“同時包含”資料和行為的一個實例,即通過封裝機制將資料和行為捆綁在一起,形成一個完整的、具有屬性和行為的物件。比如,當通過網路傳送物件時,則傳送的是整個物件。因此使用OO技術的程式實際上就是多個物件的集合,這裡的“同時包含”正是OO程式設計與面向過程程式設計方法的重要區別。

由此可見,以後在分析新的物件時,都要從屬性和行為兩個方面進行抽象和概括,提取物件的共同特徵,而整個抽象過程是一個從具體到一般的過程。如果說抽象是將很多物件的共有特徵提取出來成為類的成員屬性和成員函數,那麼封裝機制則是將這些特徵進行有機地結合形成一個完整的類。

4.2.3封裝

類和物件既是獨立的概念,又密切相關。每個物件都是某個類的一個實例,每個類都有0或多個實例。對於所有的應用來說,類幾乎都是靜態的。這就意味著,物件一旦被創建,它的類就確定了。

雖然最具挑戰的是如何確定類和物件,但只要正確使用物件導向分析(Ogject Oriented

Analysis,OOA)和物件導向設計(Object Oriented Design,OOD)就能得到具有價值的領域模型和設計模型。OOA、OOD與OOP到底是什麼關係?OOA的結果可以作為OOD開始的模型,OOD的結果可以作為藍圖,利用OOP方法實現一個系統。

在OOA和OOD中,不需要考慮特定的語言機制,“關鍵是尋找並解決業務問題,完成概念分析和設計。在OOA和OOD的早期,開發者的主要任務有兩項:

從需求的詞彙表中確定類;

創建一些結構,讓多組物件一起工作,提供滿足需求的行為。

通常我們將這樣的類和物件統稱為問題域的關鍵抽象,即關鍵抽象反映了問題域的詞彙表,可以從問題域中發現,也可以作為設計的一部分發明;將這些協作結構稱為實現的機制,其考慮的是許多不同類型的物件之間的協作活動。

確定關鍵抽象包括兩個過程:發現和發明,通過與領域專家(用戶)交流,將會發現領域專家所使用的抽象。如果領域專家提及它,那麼這個抽象通常是很重要的,比如,範圍值校驗器RangeValidator。而發明就是創造新的類和物件的過程,雖然它們不一定是問題域的組成部分,但在設計或實現中也是很重要的。比如,微型資料庫、鏈表、棧、佇列等。這些關鍵抽象是具體設計的結果,不屬於問題域。因此在設計過程中,開發者不僅需要考慮單個類的設計,還要考慮這些類的實例如何一起工作,並使用場景驅動分析過程。由此可見,關鍵抽象反映了業務領域的抽象,機制是設計的靈魂。

假設希望對push到棧中的值,既可以進行範圍值校驗,也可以進行偶校驗。從物件導向的角度來看,首先要從問題的描述中發現物件,當找到物件後,接著開始通過共性和差異性分析這些物件所具有的屬性和行為,然後利用物件導向的封裝機制將其封裝成類。

根據問題的描述,範圍值校驗器就是一個RangeValidator具體類,其屬性是範圍值校驗參數min和max,其行為就是將符合範圍要求的數值push到棧中。因此只要將RangeValidator的屬性和行為作為成員封裝到結構體中,就形成了RangeValidator類,這是面向過程程式設計的C程式師最容易想到,也最容易理解的方法。

為了支持這種風格,C允許將方法作為某個結構體的一部分來聲明,那麼操作存儲在結構體中的資料就很容易了,詳見程式清單 4.1。

程式清單 4.1範圍值校驗器類介面

其中,類名字的首字母為大寫,物件名字的首字母為小寫。由此可見,通過擴展已有結構體的概念創造了一個全新的概念——類,類如同種類一樣,定義一個類就是在創造一個新的資料類型。雖然聲明一個類的變數如同聲明一個結構體的變數一樣,但聲明一個類的變數被稱為物件,因此有了類即可聲明一個RangeValidator類的物件rangeValidator。通常也稱rangeValidator物件是RangeValidator類的一個實例,就是創建類的一個實例的過程。

在進行範圍值校驗時,首先需要判斷value值是否符合要求?validateRange函數介面的實現詳見程式清單 4.2。

程式清單 4.2範圍值校驗器介面函數的實現

偶校驗器OddEvenValidator具體類和物件oddEvenValidator的定義詳見程式清單 4.3。

程式清單 4.3偶校驗器類介面

在進行偶校驗時,同樣需要判斷value值是否符合要求?validateOddEven函數介面的實現詳見程式清單 4.4。

程式清單 4.4偶校驗器介面函數的實現

顯然,無論是什麼校驗器,其共性是value值合法性判斷,因此可以共用一個函數指標,即特殊的函數指標類型RangeValida te和OddEvenValidate被泛化成了一般的函數指標類型Validate。其次,由於每個函數都有一個指向當前物件的pThis指標,因此特殊的結構體類型RangeValidator *和OddEvenValidator *被泛化成了void *類型,即可接受任何類型的資料:

校驗器泛化介面的實現詳見程式清單 4.5。

程式清單 4.5通用校驗器介面的實現(validator.c)

為了便於閱讀,程式清單 4.6展示了範圍值校驗器和同位器的介面。

程式清單 4.6通用校驗器介面(validator.h)

這個介面主要由所有的操作聲明構成,這些操作適用於這個類的所有物件,詳見圖 4.3。

圖 4.3類圖

以範圍值校驗器為例,假設min=0,max=9,使用名為newRangeValidator的巨集將結構體初始化的使用方法如下:

注意,RangeValidator類是在編譯時定義的,而rangeValidator物件是在運行時作為類的實例創建的。宏展開後如下:

其相當於:

如果有以下定義:

即可通過pValidator引用RangeValidator的min和max。校驗函數的調用方式如下:

以上調用形式的前提是已知pValidator指向了確定的結構體類型,如果pValidator將指向未知的校驗器,顯然以上調用形式無法做到通用,那麼如何調用?

雖然pValidator與&rangeValidator.validate的類型不一樣,但它們的值相等,因此可以利用這一特性獲取validateRange函數的位址。即:

其調用形式如下:

根據OCP開閉原則,由於不允許修改push函數,因此需要編寫一個通用的擴展push功能的pushWithValidate函數,詳見程式清單 4.7。

程式清單 4.7 pushWithValidate

其中,stack是指向當前物件(棧)的指標,用於請求物件對自身執行某些操作,而結構體的成員變數就是通過stack指標找到自己所屬的物件的。pValidator為指向校驗器的指標,如果無需校驗,則將pValidator置NULL並返回true。

使用validator.h介面的通用校驗器範例程式詳見程式清單 4.8。

程式清單 4.8通用校驗器使用範例程式

由此可見,雖然在結構體內置函數指標也可以創建類,但其中的每個類都是一個獨立的單元,每個都要從頭開始。且不同類之間沒有任何關係,因為每個類的開發者都根據自己的選擇提供方法。

對於web前端的學習有不懂的,或者不知道學習路線,不知道學習方法,不知道該如何扎實能找到工作的朋友,我還是要推薦下我自己建的前端學習群:523218370,首先你要是前端黨,其次不管你是小白還是大牛,我都挺歡迎,小白嘛,主動點多問問題也就學好了,群裡每天分享乾貨,包括我自己最近花了一星期整理的一份適合2017年自學的最新web前端資料,送給大家,歡迎初學和進階中的小夥伴。

都是為了實現而引入的,沒有對應的物理實體。

許多開發人員可能會認為“一個包含了另一個物件的物件”,其本質上與“一個具有純資料成員的物件”是完全不同的,但是那些看起來不是物件的資料成員實際上也是物件,比如,整數和雙精度數。在真正的物件導向語言中,萬事萬物都是物件,甚至內置資料也是物件,即使其行為只是運算。

由此可見,雖然物件是具有明確定義的邊界的東西,但還不足以區分不同的物件。因為同一個類的每個物件具有不同的控制碼,在任何特定時刻,每個物件可能有不同的狀態(指存儲在變數中的不同的“值”),因此物件是一個具有狀態、行為和標識的實體。

物件又分持久物件和主動物件,持久物件是指生存期可以超越程式的執行時間,而長期存在的且所有的操作都是被動執行的物件。在主動物件概念出現之前,人們所理解的物件概念只是被動物件,即物件的每個操作都是被動地回應從外部發來的消息才可以執行。

在開發一個具有多工併發執行的系統時,如果僅有被動物件的概念,則很難描述系統中的多個任務。其實併發不僅僅存在作業系統中,如今多個任務併發可以說無處不在。每個任務在實現時應該成為一個可以併發執行的主動程式單位,那麼如何描述呢?

如果用被動物件將無法描述那些不接收任何消息也要主動工作的物件,比如,交通燈控制系統中的信號燈,溫控器中的感測器,它們的行為都是主動發起的,即主動物件至少有一個操作不需要接收消息就就能主動執行的物件。

儘管發現物件的活動是從具體事物出發分析和認識問題的,但人們在進行這種活動時實際上並不局限於對個別事物的認識,而是尋找一類事物的共同特徵,將物件抽象為類。

4.2.2類

類的概念早在柏拉圖之前就出現了,物件導向程式設計就像柏拉圖之後的西方哲學家一樣延續了這種思維。類的概念與物件的概念是緊密交織在一起,因為在討論一個物件時不得不提到它的類,但是這兩個術語又存在重要的差別。物件是存在於時間和空間中的具體實體,而類僅代表一種抽象,因此可以說Validator類代表了所有校驗器的共同特徵。要確定這個類中的某個具體的校驗器,則必須說“範圍值校驗器”或“同位器”。

在物件導向分析與設計的上下文中,將類定義為——類是對現實世界中事物的描述,類描述了擁有相同屬性、行為和關係類別的一組物件,一個物件就是類的一個實例,因此沒有共同的屬性和行為的物件不能劃分為一個類。比如,一個相當高層的抽象,一個GUI框架,一個資料庫和整個系統在概念上都是獨立的物件,因此不能將它們表示為一個單獨的類。相反應該將這些抽象表示為一組類,通過這些類的實例互相協作,提供我們期望的功能。

通常將這樣的一組類稱為一個元件,而元件是預先創建好的程式模組,可與其它模組一起構成一個程式。通常元件以二進位形式發佈,其實現對使用者來說是隱藏的。如果元件設計良好,使用者甚至不需要知道這個元件使用什麼語言編寫的。但元件必須至少暴露一個介面才能使用,通常元件會有暴露多個介面。從使用者的角度來看,一個元件是一些前端介面的後端服務者,程式師通過元件介面所暴露的函數操作該元件。由此可見,元件擴展了物件導向中物件作為服務提供者通過高層介面提供服務的概念。

在現實世界裡,餅乾也是物件,必須先有模子(類),才能做出你想要的形狀的餅乾,因此可以認為類是物件的範本。比如,只有符合一定條件的數值才能push到棧中,那麼Validator校驗器類就是由RangeValidator範圍值校驗器類、OddEvenValidator同位器類和PrimerValidator質數校驗器類等具體校驗器類的物件構成的一個集合體。屬於類的任何物件都共用該類的所有屬性,比如,所有的具體校驗器都有這樣的屬性——校驗參數。

在OO程式設計中,一個類就是一種抽象資料類型,用戶也可以創建一個自己的類,而且可以將這個類當做資料類型使用。一旦有了類,就可以象使用普通的資料類型那樣用類定義變數,如果定義了RangeValidator類,即可用它定義變數rangeValidator。RangeValidator類的變數rangeValidator可以擁有成員變數或域,代表不同校驗器的屬性或特性,通常將這些成員變數稱為資料成員。

(1)值和屬性

值是一段資料,屬性描述了類的每個物件都擁有的一個值,可以這樣類比——物件之于類如同值之於屬性。比如,name、birthdate和weight都是Person物件的屬性,color、modelYear和weight都是Car物件的屬性。對於每個物件,每個屬性都有一個值,比如,物件ZhangSan的屬性birthdate的值是“21 October 1983”,也就是說,ZhangSan生於1983年10月21日。對於一個特定的屬性,不同的物件可能會有相同或不同的取值。在一個類中,雖然每個屬性的名字都是唯一的,但在所有的類中不一定是唯一的,比如,類Person和類Car都可能有一個名為weight的屬性。

下面將介紹一種通過屬性詳細描述類的UML建模語言,一種用於視覺化表示、指定、構造和描述軟體密集系統中部件的圖形化語言,它提供了一種以圖形化方式表示和管理物件導向軟體系統的方法。其不僅是系統設計的表示,而且是一種有助於完成系統設計的工具。類圖定義了3個不同的部分,即類名、屬性和方法,用於解釋所構建的類。當用UML創建物件模型時,盡可能不要在類圖中包含太多的資訊,這樣就能集中注意力於整體設計,而不會將重點放在細節上。

如圖 4.1所示展示了類建模標記法,顯示了一個類(左圖)和它所描述的物件(右圖),物件ZhangSan和LiMing都是類Person的實例。物件的UML標記法是一個方框,方框裡面是物件名後加冒號和類名,對象名和類名都有底線,並約定用黑體字表示物件名和類名。類的UML標記法也是一個方框,也約定用黑體字表示類名,將名字放在方框的正中央,首字元大寫,且用單數名詞表示類名。類Person有屬性name和birthdate,name是string(字串),birthdate是date(日期)。類Person中一個物件的名字取值是"Zhang San",生日取值是“21 October 1983”;另一個對象的名字取值是"Li Ming",生日取值是“16 March 1950”。

圖 4.1屬性和值的UML標記法

UML標記法會在框的第二格裡列舉屬性,每個屬性後面都可以有可選項,比如,類型和預設值。在類之前有一個冒號,在預設值之前有一個等號。約定以常規字體顯示屬性名,方框中的名稱左對齊,首字母使用小寫。在物件方框的第二格裡,也可能會包含屬性值,其標記法是列出每個屬性名,之後跟著等號和取值,同樣屬性值也是左對齊,使用常規字體。雖然有些實現要求物件有唯一的識別字,但這些識別字在類模型中是隱含的,即不需要也不應該顯式地將它們列舉出來,比如,PersonID:ID。因為大多數OO開發語言會自動生成識別字,可以使用這些識別字來引用物件;反之,則可能需要顯式地列舉出來,否則無法引用物件。但是不要將內部識別字和現實世界的屬性混淆了,內部識別字純粹是一種便於實現的做法,沒有應用意義。相反,納稅人編號、汽車牌照號碼和電話號碼都不是內部識別字,因為它們在實現世界有真實的意義,屬於合法的屬性。

(2)操作與方法

操作是一個函數或過程,比如,open和close都是Windows類的操作,類中所有的物件都共用相同的操作,因此將物件能夠做什麼的行為稱為操作,通常將相同的操作應用於許多不同的類稱為多態。

方法是對操作的實現,其表現為OOP某個類的成員函數。比如,類Validator有一個操作validate,其校驗過程是通過validate調用不同的函數實現的。比如,範圍值校驗和同位。雖然這些方法在邏輯上都執行相同的任務——資料校驗,但是每種方法的實現代碼會有所不同。

如圖 4.2所示RangeValidator類有min和max屬性,以及validate操作,min、max和validate都是RangeValidator的特徵。特徵是描述屬性或操作的類屬詞彙,類似地OddEvenValidator有isEven屬性和validate操作。

圖 4.2操作的UML標記法

注意,validate省略了括弧中的輸入參數,即“validate(pThis:void *, value:int):bool”。validate的一個參數是pThis,其類型是void *;它的另一個參數value,其類型是int。當一項操作在幾個類上都有方法時,這些方法都要有相同的簽名,即相同的參數數量和類型,以及返回值的類型。

UML的方框表示類,最多有三格,從上到下每個格裡分別包含了類名、屬性清單和操作清單。類方框中的屬性和操作的框格可以選擇顯示或隱藏,缺少屬性說明沒有指定屬性,缺少操作框說明沒有指定操作。相反,空框格意味著屬性是指定的,只是沒有顯示屬性而已。

操作清單約定用常規字體列出操作名,左對齊,首字母小寫。比如,參數列表和操作結果的類型,用括弧將參數列表括起來,並用逗號分隔參數。結果類型之前有一個冒號,除非括弧中空的參數列表明確表示沒有參數,否則就不能下結論。

(3)客戶和伺服器模式

在OOP中,如果一個類公開了一些方法供其它類調用,那麼這個類被稱為伺服器,公開的這些方法被稱為服務,而調用這些服務的類就是客戶。理論上客戶類調用伺服器類的服務,即客戶向伺服器發送了一條消息。而客戶和伺服器的概念是相對而言的,當A類向B類提供了功能介面時,則類A是伺服器,B類是客戶;如果類B也同時為類A提供了功能介面,則類B是伺服器,類A是客戶。

設計良好的伺服器應該將其實現細節隱藏起來,客戶僅需知道伺服器提供的介面即可。介面就是客戶所能調用的那些函數,這些函數將消息發給伺服器,那麼伺服器就知道客戶需要什麼樣的服務,伺服器會返回一些資料給客戶,或執行客戶所需的任務等。

(4)消息傳遞和方法調用

在OOP中,類和物件表現為伺服器,使用類和物件的模組表現為客戶。客戶通過特殊的方式請求服務。那麼到底如何讓物件為我們做有用的事情呢?必須有一種方法能向物件做出請求,使得它能做某件事情,比如,完成交易、在螢幕上畫圖或打開開關。可以向物件發出的請求是由它的介面定義的,而介面是由類型定義的。

雖然介面規定了我們能向特定的物件發出什麼請求,但必須有代碼滿足這種請求,再加上隱藏的資料就組成了實現。類型對每個可能的請求都有一個相關的函數,當向物件發出請求時,就調用這個函數。這個過程被概括為向物件“發送消息”(提出請求),物件根據這個消息確定做什麼(執行代碼)。

物件之間的邏輯介面通過消息傳遞實現,消息是對物件之間通信的的抽象。常見的消息傳遞方法是直接調用定義于接收方物件中的操作,比如,當物件A調用物件B的一個方法時,物件A就是在向物件B發送一個消息,物件B的響應由其返回值定義,但只有物件的公共方法才能由另一個物件調用。

使用消息傳遞可以實現松耦合,特別在分析階段,不用指定介面的細節,比如,同步、函式呼叫格式和超時等。當全面理解了所有的問題後,接下來就可以決定設計和實現的細節了。通常物件介面可以看成物件與外部世界之間制定的契約,契約是由一組協定定義的,物件參與到這些協定中。介面協定包括前置條件、後置條件和不變數。

前置條件是當該操作被調用時,必須成立的條件。即在調用之前應該校驗傳入參數是否正確,只有正確才能執行該方法。也就是說,必須在消息發送或接收之前保證為真的條件,這是消息發送者的職責。一旦通過前置條件的校驗方法必須執行,且必須保證執行結果符合契約,這就是後置條件。也就是說,後置條件是當該操作完成時,必須成立的條件。即在處理消息時必須保證為真,這是消息接收者的職責。不變數是指在任何時刻都必須成立的條件,包括操作執行前、執行時和執行後。

(5)屬性抽象與行為抽象

OO程式設計思想可以採用抽象的方法,對現實世界中的多個具體物件進行概括分析,得到這類物件所具有的共同屬性和行為,加以描述就形成了類。雖然都是同一個類的物件,但每個物件的屬性不同,於是就形成了不同的具體物件實體。

抽象一般分為屬性抽象和行為抽象兩種,屬性抽象是尋找一類物件共有的屬性,比如,在範圍值校驗器RangeValidator類中,使用整型變數min和max來描述push到棧中的數值範圍,然後將min和max變數作為類的成員變數描述物件的屬性,即“屬性是包含在物件中的變數”。而行為抽象則是尋找這類物件所具有的共同行為特徵,比如,對push到棧中的值進行範圍值校驗,同樣,也可以為這個類添加相應的函數,最終將該函數作為類的成員函數描述物件的行為,即“方法是包含在物件中的函數”。

在面向過程的程式設計中,程式是由模組組成的,一個模組就是一個過程,通常採用自頂而下的設計方法。而物件導向的程式設計與設計著眼於解決面向過程的程式設計和自頂而下設計中出現的一些問題,由於在物件導向的程式設計中構成模組的基本單元是類,而不是過程,因此物件導向設計是物件導向程式設計的設計方法,它著重於類的設計,通過類的設計完成對實體的建模任務,類建模的目的是描述物件。

在面向過程的程式設計中,描述一個物體時,資料和方法是分開的。比如,當通過網路發送資訊時,則只會發送相關的資料,並認為網路另一端的程式知道如何進行處理。也就是說,如果兩者之間沒有握手協定,則網路另一端的程式不知道如何處理。而物件可以定義為“同時包含”資料和行為的一個實例,即通過封裝機制將資料和行為捆綁在一起,形成一個完整的、具有屬性和行為的物件。比如,當通過網路傳送物件時,則傳送的是整個物件。因此使用OO技術的程式實際上就是多個物件的集合,這裡的“同時包含”正是OO程式設計與面向過程程式設計方法的重要區別。

由此可見,以後在分析新的物件時,都要從屬性和行為兩個方面進行抽象和概括,提取物件的共同特徵,而整個抽象過程是一個從具體到一般的過程。如果說抽象是將很多物件的共有特徵提取出來成為類的成員屬性和成員函數,那麼封裝機制則是將這些特徵進行有機地結合形成一個完整的類。

4.2.3封裝

類和物件既是獨立的概念,又密切相關。每個物件都是某個類的一個實例,每個類都有0或多個實例。對於所有的應用來說,類幾乎都是靜態的。這就意味著,物件一旦被創建,它的類就確定了。

雖然最具挑戰的是如何確定類和物件,但只要正確使用物件導向分析(Ogject Oriented

Analysis,OOA)和物件導向設計(Object Oriented Design,OOD)就能得到具有價值的領域模型和設計模型。OOA、OOD與OOP到底是什麼關係?OOA的結果可以作為OOD開始的模型,OOD的結果可以作為藍圖,利用OOP方法實現一個系統。

在OOA和OOD中,不需要考慮特定的語言機制,“關鍵是尋找並解決業務問題,完成概念分析和設計。在OOA和OOD的早期,開發者的主要任務有兩項:

從需求的詞彙表中確定類;

創建一些結構,讓多組物件一起工作,提供滿足需求的行為。

通常我們將這樣的類和物件統稱為問題域的關鍵抽象,即關鍵抽象反映了問題域的詞彙表,可以從問題域中發現,也可以作為設計的一部分發明;將這些協作結構稱為實現的機制,其考慮的是許多不同類型的物件之間的協作活動。

確定關鍵抽象包括兩個過程:發現和發明,通過與領域專家(用戶)交流,將會發現領域專家所使用的抽象。如果領域專家提及它,那麼這個抽象通常是很重要的,比如,範圍值校驗器RangeValidator。而發明就是創造新的類和物件的過程,雖然它們不一定是問題域的組成部分,但在設計或實現中也是很重要的。比如,微型資料庫、鏈表、棧、佇列等。這些關鍵抽象是具體設計的結果,不屬於問題域。因此在設計過程中,開發者不僅需要考慮單個類的設計,還要考慮這些類的實例如何一起工作,並使用場景驅動分析過程。由此可見,關鍵抽象反映了業務領域的抽象,機制是設計的靈魂。

假設希望對push到棧中的值,既可以進行範圍值校驗,也可以進行偶校驗。從物件導向的角度來看,首先要從問題的描述中發現物件,當找到物件後,接著開始通過共性和差異性分析這些物件所具有的屬性和行為,然後利用物件導向的封裝機制將其封裝成類。

根據問題的描述,範圍值校驗器就是一個RangeValidator具體類,其屬性是範圍值校驗參數min和max,其行為就是將符合範圍要求的數值push到棧中。因此只要將RangeValidator的屬性和行為作為成員封裝到結構體中,就形成了RangeValidator類,這是面向過程程式設計的C程式師最容易想到,也最容易理解的方法。

為了支持這種風格,C允許將方法作為某個結構體的一部分來聲明,那麼操作存儲在結構體中的資料就很容易了,詳見程式清單 4.1。

程式清單 4.1範圍值校驗器類介面

其中,類名字的首字母為大寫,物件名字的首字母為小寫。由此可見,通過擴展已有結構體的概念創造了一個全新的概念——類,類如同種類一樣,定義一個類就是在創造一個新的資料類型。雖然聲明一個類的變數如同聲明一個結構體的變數一樣,但聲明一個類的變數被稱為物件,因此有了類即可聲明一個RangeValidator類的物件rangeValidator。通常也稱rangeValidator物件是RangeValidator類的一個實例,就是創建類的一個實例的過程。

在進行範圍值校驗時,首先需要判斷value值是否符合要求?validateRange函數介面的實現詳見程式清單 4.2。

程式清單 4.2範圍值校驗器介面函數的實現

偶校驗器OddEvenValidator具體類和物件oddEvenValidator的定義詳見程式清單 4.3。

程式清單 4.3偶校驗器類介面

在進行偶校驗時,同樣需要判斷value值是否符合要求?validateOddEven函數介面的實現詳見程式清單 4.4。

程式清單 4.4偶校驗器介面函數的實現

顯然,無論是什麼校驗器,其共性是value值合法性判斷,因此可以共用一個函數指標,即特殊的函數指標類型RangeValida te和OddEvenValidate被泛化成了一般的函數指標類型Validate。其次,由於每個函數都有一個指向當前物件的pThis指標,因此特殊的結構體類型RangeValidator *和OddEvenValidator *被泛化成了void *類型,即可接受任何類型的資料:

校驗器泛化介面的實現詳見程式清單 4.5。

程式清單 4.5通用校驗器介面的實現(validator.c)

為了便於閱讀,程式清單 4.6展示了範圍值校驗器和同位器的介面。

程式清單 4.6通用校驗器介面(validator.h)

這個介面主要由所有的操作聲明構成,這些操作適用於這個類的所有物件,詳見圖 4.3。

圖 4.3類圖

以範圍值校驗器為例,假設min=0,max=9,使用名為newRangeValidator的巨集將結構體初始化的使用方法如下:

注意,RangeValidator類是在編譯時定義的,而rangeValidator物件是在運行時作為類的實例創建的。宏展開後如下:

其相當於:

如果有以下定義:

即可通過pValidator引用RangeValidator的min和max。校驗函數的調用方式如下:

以上調用形式的前提是已知pValidator指向了確定的結構體類型,如果pValidator將指向未知的校驗器,顯然以上調用形式無法做到通用,那麼如何調用?

雖然pValidator與&rangeValidator.validate的類型不一樣,但它們的值相等,因此可以利用這一特性獲取validateRange函數的位址。即:

其調用形式如下:

根據OCP開閉原則,由於不允許修改push函數,因此需要編寫一個通用的擴展push功能的pushWithValidate函數,詳見程式清單 4.7。

程式清單 4.7 pushWithValidate

其中,stack是指向當前物件(棧)的指標,用於請求物件對自身執行某些操作,而結構體的成員變數就是通過stack指標找到自己所屬的物件的。pValidator為指向校驗器的指標,如果無需校驗,則將pValidator置NULL並返回true。

使用validator.h介面的通用校驗器範例程式詳見程式清單 4.8。

程式清單 4.8通用校驗器使用範例程式

由此可見,雖然在結構體內置函數指標也可以創建類,但其中的每個類都是一個獨立的單元,每個都要從頭開始。且不同類之間沒有任何關係,因為每個類的開發者都根據自己的選擇提供方法。

對於web前端的學習有不懂的,或者不知道學習路線,不知道學習方法,不知道該如何扎實能找到工作的朋友,我還是要推薦下我自己建的前端學習群:523218370,首先你要是前端黨,其次不管你是小白還是大牛,我都挺歡迎,小白嘛,主動點多問問題也就學好了,群裡每天分享乾貨,包括我自己最近花了一星期整理的一份適合2017年自學的最新web前端資料,送給大家,歡迎初學和進階中的小夥伴。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示