1. 程式人生 > >設計模式中類與類之間的關係

設計模式中類與類之間的關係


在設計模式中類與類之間的關係主要有6種:依賴、關聯、聚合、組合、繼承、實現,它們之間的耦合度依次增加。
一、繼承關係       繼承是一種“is-a”關係。 繼承指的是一個類(稱為子類、子介面)繼承另外的一個類(稱為父類、父介面)的功能,並可以增加它自己的新功能的能力。在Java中繼承關係通過關鍵字extends明確標識,在設計時一般沒有爭議性。在UML類圖設計中,繼承用一條帶空心三角箭頭的實線表示,從子類指向父類,或者子介面指向父介面,是類和類之間存在父子關係。
二、實現關係
      實現指的是一個class類實現interface介面(可以是多個)的功能,實現是類與介面之間最常見的關係。在Java中此類關係通過關鍵字implements明確標識,在設計時一般沒有爭議性。在UML類圖設計中,實現用一條帶空心三角箭頭的虛線表示,從類指向實現的介面。 一個類實現一個或多個介面的方法,介面定義好操作的集合,由實現類去完成介面的具體操作
三、依賴關係       簡單的理解,依賴就是一個類A使用到了另一個類B, 對於兩個相對獨立的物件,當一個物件依賴另一個物件的服務時,這兩個物件之間主要體現為依賴關係。
而這種使用關係是具有偶然性的、臨時性的、非常弱的,但是類B的變化會影響到類A。比如某人要過河,需要借用一條船,此時人與船之間的關係就是依賴。表現在程式碼層面,為類B作為引數被類A在某個method方法中使用。在UML類圖設計中,依賴關係用由類A指向類B的帶箭頭虛線表示。 依賴關係是五種關係中耦合最小的一種關係。 一個類是另一個類的方法區域性變數,方法的引數或方法返回值。 依賴是指類之間的呼叫關係,在UML中用帶虛線的箭頭表示。如果類A訪問類B的屬性或方法或者類A負責例項化類B則類A依賴類B,無須在類A中定義類B型別屬性
四、關聯關係    關聯體現的是兩個類之間語義級別的一種強依賴關係,這種關係比依賴更強、不存在依賴關係的偶然性、關係也不是臨時性的,一般是長期性的,而且雙方的關係一般是平等的。關聯可以是單向、雙向的。表現在程式碼層面,為被關聯類B以類的屬性形式出現在關聯類A中,也可能是關聯類A引用了一個型別為被關聯類B的全域性變數。在UML類圖設計中,關聯關係用由關聯類A指向被關聯類B的帶箭頭實線表示,在關聯的兩端可以標註關聯雙方的角色和多重性標記。 
對於兩個相對獨立的物件,當一個物件與另一個物件有種某種對應關係,這兩個物件之間為關聯關係,比如老師和學生。關聯分為單向關聯,雙向關聯和自身關聯,實現方法一般是A中包含B的某個指標或者引用。類A與類B的例項之間存在特定的對應關係,在UML中用帶實線的箭頭表示。如果類A與類B關聯,類A含有B的屬性 按照類之間的數量對比,關聯可以分為3種: a.一對一關聯 b.一對多關聯 c.多對多關聯 按方向分關聯可以分為2種: a.單向關聯 b.雙向關聯
五、聚合關係       即has-a的關係, 聚合是關聯關係的一種特例,它體現的是整體與部分的關係,即has-a的關係。此時整體與部分之間是可分離的,它們可以具有各自的生命週期,部分可以屬於多個整體物件,也可以為多個整體物件共享。比如計算機與CPU、公司與員工的關係等。表現在程式碼層面,和關聯關係是一致的,只能從語義級別來區分。在UML類圖設計中,聚合關係以空心菱形加實線箭頭表示。  一個類是另一個類的屬性,是整體和部分的關係。 聚合是關聯關係的一種,耦合度比關聯關係強,他們的程式碼表現是相同的,僅僅是在語義上有所區別:關聯關係的物件間是平等關係, 不同於關聯關係的平等地位,聚合關係中兩個類的地位是不平等, 而物件之間存在著包容關係,他們之間是“整體-個體”的相互關係。 普通的關聯關係中,a類和b類沒有必然的聯絡,而聚合中,需要b類是a類的一部分,是一種”has-a“的關係,即 a has-a b;。但是,has 不是 must has,a可以有b,也可以沒有。a是整體,b是部分,整體與部分之間是可分離的,他們可以具有各自的生命週期,部分可以屬於多個整體物件,也可以為多個整體物件共享。 聚合可分為2種類型:   a.被聚集的子系統允許被拆卸或替換,這是普通聚集關係,在UML中用帶實線的空心菱形箭頭表示   b.被聚集的子系統不允許被拆卸或替換,這是強聚集關係,在UML中用帶實線的實心菱形箭頭表示,不可以通過set方法更換元件
六、組合關係       體現的是一種contains-a的關係 ,組合也是關聯關係的一種特例,這種關係比聚合更強,也稱為強聚合。它同樣體現整體與部分間的關係,但此時整體與部分是不可分的,整體的生命週期結束也就意味著部分的生命週期結束,比如人和人的大腦。表現在程式碼層面,和關聯關係是一致的, 只看程式碼,你是無法區分關聯,聚合和組合的,具體是哪一種關係,只能從語義級別來區分。 在UML類圖設計中,組合關係以實心菱形加實線箭頭表示。 一個類是另一個類的屬性,是整體不可分割的一部分,是強聚合。 組合是耦合度比聚合還強的一直關係,類和類之間的關係是“整體-部分”的關係,整體和部分具有相同的生命週期,部分不能脫離整體而獨立存在,比如:人和身體,身體是人的一部分,不能獨立存在。程式碼實現為A中包含B的成員變數。
區分依賴、關聯和聚集: 1.依賴的特徵:兩個相對獨立的系統,當一個系統負責構造另一個系統的例項或者依賴另一個系統的服務時,兩個系統之間的關係體現為依賴。 2.關聯的特徵:兩個相對獨立的系統,當一個系統的例項與另一個系統的一些特定例項存在固定的對應關係時,兩個系統之間為關聯關係。 3.聚合的特徵:當系統A被加入系統B中成為系統B的組成部分,系統B和系統A之間為聚集關係,聚集整體物件會制約組成物件的生命週期。
七、總結       對於繼承、實現這兩種關係沒多少疑問,它們體現的是一種類和類、或者類與介面間的縱向關係。其他的四種關係體現的是類和類、或者類與介面間的引用、橫向關係,是比較難區分的,有很多事物間的關係要想準確定位是很難的。前面也提到,這四種關係都是語義級別的,所以從程式碼層面並不能完全區分各種關係,但總的來說,後幾種關係所表現的強弱程度依次為:組合>聚合>關聯>依賴。 根據前面講的內容可以發現繼承的缺點遠遠多於優點,儘管繼承在學習OOP的過程中得到了大量的強調,但並不意味著應該儘可能地到處使用它。相反,使用它時要特別慎重。只有在清楚知道繼承在所有方法中最有效的前提下,才可考慮它。 繼承最大的優點就是擴充套件簡單,但大多數缺點都很致命,但是因為這個擴充套件簡單的優點太明顯了,很多人並不深入思考,所以造成了太多問題。 1、精心設計專門用於被繼承的類,繼承樹的抽象層應該比較穩定,一般不要多於三層。 2、對於不是專門用於被繼承的類,禁止其被繼承。 3、優先考慮用組合關係來提高程式碼的可重用性。 4、子類是一種特殊的型別,而不只是父類的一個角色 5、子類擴充套件,而不是覆蓋或者使父類的功能失效
討論一下組合,聚合和繼承的關係與區別
聚合與組合 聚合與組合都是一種關聯關係,只是額外具有整體-部分的意義。部件的生命週期不同,在聚合關係中,整件不會擁有部件的生命週期,所以整件刪除時,部件不會被刪除。再者,多個整件可以共享同一個部件。在組合關係中,整件擁有部件的生命週期,所以整件刪除時,部件一定會跟著刪除。而且,多個整件不可以同時間共享同一個部件。這個區別可以用來區分某個關聯關係到底是組合還是聚合。兩個類生命週期不同步,則是聚合關係,生命週期同步就是組合關係。聚合關係是【has-a】關係,組合關係是【contains-a】關係。平時我們只討論組合和繼承的時候,認為組合是【has-a 】關係,而事實上,聚合才是真正的【has-a】關係,組合是更深層次的【contains-a】關係。 由於【contains-a】關係是一種更深的【has-a】關係,所以說組合是【has-a】關係也是正確的。
組合和繼承 這個才是本文的重點。學過設計模式的都知道,要“少用繼承,多用組合”,這究竟是為什麼呢? 我們先來看一下組合和繼承各自的優缺點:
組合和繼承的優缺點
組合 優點:不破壞封裝、整體類於區域性類之間鬆耦合、彼此相對獨立、具有較好的可擴充套件性、支援動態組合,在執行時,整體物件可以選擇不同型別的區域性物件、整體類可以區域性類進行包裝,封裝區域性類的介面,提供新介面、支援動態組合,在執行時整體物件
缺點: 整體類不能自動獲得和區域性類同樣的介面、建立整體類的物件時,需要建立所有區域性類的物件
1、為什麼繼承破壞封裝性?:鴨子中不想要“飛”的方法,但因為繼承無法封裝這個無用的“飛”方法 。
2、為什麼繼承緊耦合:當作為父類的BaseTable中感覺Insert這個名字不合適時,如果希望將其修改成Create方法,那使用了子類物件Insert方法將會編譯出錯,可能你會覺得這改起來還算容易,因為有重構工具一下子就好了並且編譯錯誤改起來很容易。但如果BaseTable和子類在不同的程式集中,維護的人員不同,BaseTable程式集升級,那本來能用的程式碼忽然不能用了,這還是很難讓人接受的。
3、為什麼繼承擴充套件起來比較複雜:當 圖書 和數碼的算稅方式和數碼產品一樣時,而消費類產品的算稅方式是另一樣時,如果採用繼承方案可能會演變成如下方式:這樣如果產品繼續增加,算稅方式繼續增加,那繼承的層次會非常複雜,而且很難控制,而使用組合就能很好的解決這個問題。
4、繼承不能支援動態繼承:這個其實很好理解,因為繼承是編譯期就決定下來的,無法在執行時改變,如3例中,如果使用者需要根據當地的情況選擇計稅方式,使用繼承就解決不了,而使用組合結合反射就能很好的解決。
5、為什麼繼承,子類不能改變父類介面:因為繼承的原因無法改變
組合與繼承的區別和聯絡
在繼承結構中,父類的內部細節對於子類是可見的。所以我們通常也可以說通過繼承的程式碼複用是一種 白盒式程式碼複用。(如果基類的實現發生改變,那麼派生類的實現也將隨之改變。這樣就導致了子類行為的不可預知性) 組合是通過對現有的物件進行拼裝(組合)產生新的、更復雜的功能。因為在物件之間,各自的內部細節是不可見的,所以我們也說這種方式的程式碼複用是黑盒式程式碼複用 。(因為組合中一般都定義一個型別,所以在編譯期根本不知道具體會呼叫哪個實現類的方法) 繼承在寫程式碼的時候就要指名具體繼承哪個類,所以,在編譯期就確定了關係。(從基類繼承來的實現是無法在執行期動態改變的,因此降低了應用的靈活性。)組合,在寫程式碼的時候可以採用面向介面 程式設計 。所以,類的組合關係一般在執行期確定。 組合(has-a)關係可以顯式地獲得被包含類(繼承中稱為父類)的物件,而繼承(is-a)則是隱式地獲得父類的物件,被包含類和父類對應,而組合外部類和子類對應。組合是在組合類和被包含類之間的一種鬆耦合關係,而繼承則是父類和子類之間的一種緊耦合關係。當選擇使用組合關係時,在組合類中包含了外部類的物件,組合類可以呼叫外部類必須的方法,而使用繼承關係時,父類的所有方法和變數都被子類無條件繼承,子類不能選擇。最重要的一點,使用繼承關係時,可以實現型別的回溯,即用父類變數引用子類物件,這樣便可以實現多型,而組合沒有這個特性。還有一點需要注意,如果你確定複用另外一個類的方法永遠不需要改變時,應該使用組合,因為組合只是簡單地複用被包含類的介面,而繼承除了複用父類的介面外,它甚至還可以覆蓋這些介面,修改父類介面的預設實現,這個特性是組合所不具有的。從邏輯上看,組合最主要地體現的是一種整體和部分的思想,例如在電腦類是由記憶體類,CPU類,硬碟類等等組成的,而繼承則體現的是一種可以回溯的父子關係,子類也是父類的一個物件。這兩者的區別主要體現在類的抽象階段,在分析類之間的關係時就應該確定是採用組合還是採用繼承。引用網友的一句很經典的話應該更能讓大家分清繼承和組合的區別:組合可以被說成“我請了個老頭在我家裡幹活” ,繼承則是“我父親在家裡幫我幹活”。
繼承還是組合?
首先它們都是實現系統功能重用,程式碼複用的最常用的有效的設計技巧,都是在設計模式中的基礎結構。很多人都知道面向物件中有一個比較重要的原則『多用組合、少用繼承』或者說『組合優於繼承』。從前面的介紹已經優缺點對比中也可以看出,組合確實比繼承更加靈活,也更有助於程式碼維護。所以,建議在同樣可行的情況下,優先使用組合而不是繼承。因為組合更安全,更簡單,更靈活,更高效。注意,並不是說繼承就一點用都沒有了,前面說的是【在同樣可行的情況下】。有一些場景還是需要使用繼承的,或者是更適合使用繼承。繼承要慎用,其使用場合僅限於你確信使用該技術有效的情況。一個判斷方法是,問一問自己是否需要從新類向基類進行向上轉型。如果是必須的,則繼承是必要的。反之則應該好好考慮是否需要繼承。只有當子類真正是超類的子型別時,才適合用繼承。換句話說,對於兩個類A和B,只有當兩者之間確實存在 is-a 關係的時候,類B才應該繼承類A。