language agnostic - composition意思 - 比繼承更喜歡構圖?




uml composition vs aggregation (20)

“比繼承更喜歡構圖”是一個設計原則,它說不要在不適合時濫用繼承。

如果兩個實體之間不存在真實世界的層次關係,請不要使用繼承,而應使用合成。 構圖代表“HAS”關係。

例如,汽車具有輪子,車身,發動機等,但是如果你繼承這裡,它就變成汽車是輪子 - 這是不正確的。

有關更多解釋,請參閱比繼承更喜歡構圖

為什麼喜歡構圖而不是繼承? 每種方法都有哪些折衷? 你應該什麼時候選擇繼承而不是組合?


因為它比以後更容易/更容易修改,但不要使用組合總是方法,所以希望組合優於繼承。 有了構圖,使用依賴注入/設置器可以輕鬆改變行為。 繼承比較嚴格,因為大多數語言不允許你從多個類型派生。 所以一旦你從TypeA派生出來,鵝就會或多或少地被煮熟了。

我對以上的酸性測試是:

  • TypeB是否希望公開TypeA的完整接口(所有公共方法都不少於),以便可以在需要TypeA的情況下使用TypeB? 表示繼承

例如塞斯納雙翼飛機將暴露飛機的完整界面,如果不是更多的話。 所以這使它適合從飛機派生。

  • TypeB是否只需要TypeA暴露的部分/部分行為? 表示需要合成。

例如,一隻鳥可能只需要一架飛機的飛行行為。 在這種情況下,將它作為接口/類/兩者提取出來並使其成為這兩個類的成員是有意義的。

更新:剛回到我的答案,現在看來,如果沒有具體提及芭芭拉·利斯科夫的Liskov替代原則 ,它是不完整的,作為'我應該繼承這種類型嗎?'的測試。


A simple way to make sense of this would be that inheritance should be used when you need an object of your class to have the same interface as its parent class, so that it can thereby be treated as an object of the parent class (upcasting). Moreover, function calls on a derived class object would remain the same everywhere in code, but the specific method to call would be determined at runtime (ie the low-level implementation differs, the high-level interface remains the same).

Composition should be used when you do not need the new class to have the same interface, ie you wish to conceal certain aspects of the class' implementation which the user of that class need not know about. So composition is more in the way of supporting encapsulation (ie concealing the implementation) while inheritance is meant to support abstraction (ie providing a simplified representation of something, in this case the same interface for a range of types with different internals).


As many people told, I will first start with the check - whether there exists an "is-a" relationship. If it exists I usually check the following:

Whether the base class can be instantiated. That is, whether the base class can be non-abstract. If it can be non-abstract I usually prefer composition

Eg 1. Accountant is an Employee. But I will not use inheritance because a Employee object can be instantiated.

Eg 2. Book is a SellingItem. A SellingItem cannot be instantiated - it is abstract concept. Hence I will use inheritacne. The SellingItem is an abstract base class (or interface in C#)

What do you think about this approach?

Also, I support @anon answer in Why use inheritance at all?

The main reason for using inheritance is not as a form of composition - it is so you can get polymorphic behaviour. If you don't need polymorphism, you probably should not be using inheritance.

@MatthieuM. says in https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448

The issue with inheritance is that it can be used for two orthogonal purposes:

interface (for polymorphism)

implementation (for code reuse)

REFERENCE

  1. Which class design is better?
  2. Inheritance vs. Aggregation

Composition v/s Inheritance is a wide subject. There is no real answer for what is better as I think it all depends on the design of the system.

Generally type of relationship between object provide better information to choose one of them.

If relation type is "IS-A" relation then Inheritance is better approach. otherwise relation type is "HAS-A" relation then composition will better approach.

Its totally depend on entity relationship.


Even though Composition is preferred, I would like to highlight pros of Inheritance and cons of Composition .

Pros of Inheritance:

  1. It establishes a logical " IS A" relation. If Car and Truck are two types of Vehicle ( base class), child class IS A base class.

    Car is a Vehicle

    Truck is a Vehicle

  2. With inheritance, you can define/modify/extend a capability

    1. Base class provides no implementation and sub-class has to override complete method (abstract) => You can implement a contract
    2. Base class provides default implementation and sub-class can change the behaviour => You can re-define contract
    3. Sub-class adds extension to base class implementation by calling super.methodName() as first statement => You can extend a contract
    4. Base class defines structure of the algorithm and sub-class will override a part of algorithm => You can implement Template_method without change in base class skeleton

Cons of Composition:

  1. In inheritance, subclass can directly invoke base class method even though it's not implementing base class method because of IS A relation. If you use composition, you have to add methods in container class to expose contained class API

eg If Car contains Vehicle and if you have to get price of the Car , which has been defined in Vehicle , your code will be like this

class Vehicle{
     protected double getPrice(){
          // return price
     }
} 

class Car{
     Vehicle vehicle;
     protected double getPrice(){
          return vehicle.getPrice();
     }
} 

Inheritance is a very powerfull machanism for code reuse. But needs to be used properly. I would say that inheritance is used correctly if the subclass is also a subtype of the parent class. As mentioned above, the Liskov Substitution Principle is the key point here.

Subclass is not the same as subtype. You might create subclasses that are not subtypes (and this is when you should use composition). To understand what a subtype is, lets start giving an explanation of what a type is.

When we say that the number 5 is of type integer, we are stating that 5 belongs to a set of possible values (as an example, see the possible values for the Java primitive types). We are also stating that there is a valid set of methods I can perform on the value like addition and subtraction. And finally we are stating that there are a set of properties that are always satisfied, for example, if I add the values 3 and 5, I will get 8 as a result.

To give another example, think about the abstract data types, Set of integers and List of integers, the values they can hold are restricted to integers. They both support a set of methods, like add(newValue) and size(). And they both have different properties (class invariant), Sets does not allow duplicates while List does allow duplicates (of course there are other properties that they both satisfy).

Subtype is also a type, which has a relation to another type, called parent type (or supertype). The subtype must satisfy the features (values, methods and properties) of the parent type. The relation means that in any context where the supertype is expected, it can be substitutable by a subtype, without affecting the behaviour of the execution. Let's go to see some code to exemplify what I'm saying. Suppose I write a List of integers (in some sort of pseudo language):

class List {
  data = new Array();

  Integer size() {
    return data.length;
  }

  add(Integer anInteger) {
    data[data.length] = anInteger;
  }
}

Then, I write the Set of integers as a subclass of the List of integers:

class Set, inheriting from: List {
  add(Integer anInteger) {
     if (data.notContains(anInteger)) {
       super.add(anInteger);
     }
  }
}

Our Set of integers class is a subclass of List of Integers, but is not a subtype, due to it is not satisfying all the features of the List class. The values, and the signature of the methods are satisfied but the properties are not. The behaviour of the add(Integer) method has been clearly changed, not preserving the properties of the parent type. Think from the point of view of the client of your classes. They might receive a Set of integers where a List of integers is expected. The client might want to add a value and get that value added to the List even if that value already exist in the List. But her wont get that behaviour if the value exists. A big suprise for her!

This is a classic example of an improper use of inheritance. Use composition in this case.

(a fragment from: use inheritance properly ).


Subtyping is appropriate and more powerful where the invariants can be enumerated , else use function composition for extensibility.


When you have an is-a relation between two classes (example dog is a canine), you go for inheritance.

On the other hand when you have has-a or some adjective relationship between two classes (student has courses) or (teacher studies courses), you chose composition.



另一個非常實用的理由是,偏好合成而不是繼承,這與你的領域模型有關,並將其映射到關係數據庫。 將繼承映射到SQL模型非常困難(您最終會遇到各種各樣的變通辦法,比如創建並不總是使用的列,使用視圖等)。 一些ORML試圖解決這個問題,但它總是很快變得複雜。 組合可以通過兩個表之間的外鍵關係輕鬆建模,但繼承難得多。


在Java或C#中,一旦對像被實例化,對象就不能改變它的類型。

因此,如果您的對象需要根據對象狀態或條件出現不同對像或行為不同,請使用“ 合成” :請參閱StateStrategy設計模式。

如果對象需要具有相同的類型,則使用繼承或實現接口。


如果你了解其中的差異,則更容易解釋。

程序代碼

一個例子是沒有使用類的PHP(特別是在PHP5之前)。 所有的邏輯都被編碼成一組函數。 您可以包含其他包含助手函數等的文件,並通過在函數中傳遞數據來執行業務邏輯。 隨著應用程序的增長,這可能非常難以管理。 PHP5試圖通過提供更多面向對象的設計來彌補這一點。

遺產

這鼓勵使用類。 繼承是面向對象設計的三個原則之一(繼承,多態,封裝)。

class Person {
   String Title;
   String Name;
   Int Age
}

class Employee : Person {
   Int Salary;
   String Title;
}

這是工作中的繼承。 員工“是”人或繼承人。 所有的繼承關係都是“是一種”關係。 員工還會從Person中隱藏Title屬性,這意味著Employee.Title將為Employee而非Person返回Title。

組成

構成比繼承更受青睞。 簡單地說,你會有:

class Person {
   String Title;
   String Name;
   Int Age;

   public Person(String title, String name, String age) {
      this.Title = title;
      this.Name = name;
      this.Age = age;
   }

}

class Employee {
   Int Salary;
   private Person person;

   public Employee(Person p, Int salary) {
       this.person = p;
       this.Salary = salary;
   }
}

Person johnny = new Person ("Mr.", "John", 25);
Employee john = new Employee (johnny, 50000);

構圖通常是“擁有”或“使用某種”關係。 這裡Employee類有一個Person。 它不會從Person繼承,而是將Person對像傳遞給它,這就是為什麼它具有“Person”。

繼承構成

現在說你想創建一個管理器類型,所以你最終得到:

class Manager : Person, Employee {
   ...
}

這個例子可以正常工作,但是,如果Person和Employee都聲明了Title怎麼辦? Manager.Title應該返回“運營經理”還是“Mr.”? 在構圖下,這種模糊性得到更好的處理:

Class Manager {
   public Title;
   public Manager(Person p, Employee e)
   {
      this.Title = e.Title;
   }
}

管理器對象由員工和人員組成。 標題行為取自員工。 這種明確的組合消除了其他內容中的歧義,並且您會遇到更少的錯誤。


就我個人而言,我學會了總是比繼承更喜歡構圖。 沒有程序上的問題,你可以用繼承來解決,而你不能用組合來解決; 儘管在某些情況下您可能不得不使用接口(Java)或協議(Obj-C)。 由於C ++不知道任何這樣的事情,你將不得不使用抽象基類,這意味著你不能完全擺脫C ++中的繼承。

組合通常更符合邏輯,它提供了更好的抽象,更好的封裝,更好的代碼重用(特別是在非常大的項目中),並且不太可能因為您在代碼中的任何地方進行了單獨的更改而在遠處破壞任何東西。 它還使維護“ 單一責任原則 ”變得更加容易,這個原則經常被概括為“ 一個階級改變不應該有一個以上的理由 ”,並且這意味著每一個階級都存在一個特定的目的,它應該只有與其目的直接相關的方法。 此外,使用非常淺的繼承樹,即使您的項目開始變得非常龐大,也可以更輕鬆地保持概覽。 許多人認為繼承代表我們的現實世界相當好,但事實並非如此。 現實世界比繼承使用更多的構圖。 幾乎每個可以握在手中的真實世界對像都是由其他較小的真實世界對象組成的。

不過,有構成的缺點。 如果完全忽略繼承,只關注構圖,那麼您會注意到,如果您使用了繼承,則通常必須編寫一些不必要的額外代碼行。 你有時也被迫重複自己,這違反了DRY原則 (DRY =不要重複自己)。 另外組合通常需要委託,並且一個方法只是調用另一個對象的另一個方法,而沒有其他代碼圍繞此調用。 這種“雙方法調用”(可能很容易擴展到三重或四重方法調用,甚至比這更遠)的性能比繼承性能差得多,您只需繼承父級方法即可。 調用繼承的方法可能與調用非繼承的方法的速度相同,也可能稍微慢一點,但通常比兩個連續的方法調用仍快。

您可能已經註意到大多數OO語言不允許多重繼承。 雖然有幾種情況下多重繼承可以真正為您購買某些東西,但這些都不是規則的例外。 當你遇到一種情況時,你認為“多重繼承將是一個非常酷的功能來解決這個問題”,通常你應該重新考慮繼承,因為即使它可能需要一些額外的代碼行,基於構圖的解決方案通常會變得更加優雅,靈活和麵向未來。

繼承是一個很酷的功能,但是恐怕過去幾年它已經被濫用了。 人們把繼承看作是可以釘住一切的一把錘子,不管它是釘子,螺絲釘,還是完全不同的東西。


所有現有的答案都很好的解釋了為什麼組合往往比繼承更可取。 但是,確實存在繼承更合適的情況,所以有一個很好的“測試”來知道我們是否處於這種情況很重要。

Circle-Ellipse問題很好地說明了為什麼“is-a”實際上不是檢測何時使用繼承的好測試 。 事實上, 即使一個圓是一個橢圓 ,但讓Circle類繼承Ellipse類是一個壞主意,因為有些事情可以用橢圓形,但圓形不能。 例如,橢圓可以伸展,但圓圈不能。 所以你可以使用ellipse.stretch() ,但不能使用circle.stretch()

我認為一個更好的測試是“是 - 一個”“具有多功能性” 。 換句話說,只要子類可以多態使用,最好使用繼承。 “is-a”關係是多態使用的必要條件,但這是不夠的。 由超類提供的每個功能也可以由子類提供也是非常重要的。 所以當你有一個“是某種”另一個類的類,但是增加了一些約束時,你就不應該使用繼承,因為它不能被多態使用。

換句話說, 繼承不是關於共享屬性,而是關於共享功能 。 通常情況下,“is-a”作為一個測試,測試超類的所有getter是否在子類中有意義,而“has-as-much-functional-as”用作測試超類的所有setter是否在子類中有意義。 獲取者和設置者共同定義一個類的“功能”。 吸氣人員只能定義一個班級的“屬性”。 通過“setter”,我的意思是任何改變對象狀態的函數,而不僅僅是微不足道的函數。

其他答案/評論幾乎都說了同樣的事情,但我覺得我的觀點增加了一些清晰度。


把遏制看成是一種關係。 一輛車“有一個”引擎,一個人“有一個”名字等。

認為繼承是一種關係。 汽車是“車輛”,“人”是“哺乳動物”等。

我不贊同這種方法。 我直接從史蒂夫麥康奈爾 第6.3 版的Code Complete第二版中直接獲悉


當你想“複製”/公開基類的API時,你可以使用繼承。 當您只想“複製”功能時,請使用委派。

其中一個例子:你想從列表中創建一個堆棧。 Stack只有pop,push和peek。 考慮到您不希望在堆棧中使用push_back,push_front,removeAt等類型的功能,您不應該使用繼承。


簡而言之,我會同意“比繼承更喜歡構圖”,對我來說,聽起來像“比可口可樂更喜歡馬鈴薯”。 有繼承的地方和組成的地方。 你需要了解差異,那麼這個問題就會消失。 對我來說真正意義的是“如果你打算使用繼承 - 再想一想,你很可能需要組合”。

當你想要吃東西時,你應該更喜歡土豆比可口可樂,而當你想要喝時,你應該更喜歡馬鈴薯可口可樂。

創建子類應該不僅僅是一種調用超類方法的簡便方法。 當子類“is-a”在結構上和功能上都是超類的時候,你應該使用繼承,當它可以作為超類使用時,你將使用它。 如果不是這種情況 - 這不是繼承,而是另一回事。 構圖是你的對象由另一個構成,或者與它們有某種關係。

所以對我來說,如果有人不知道他是否需要繼承或組成,真正的問題是他不知道他是否想喝酒或吃東西。 更多地考慮你的問題領域,更好地理解它。


繼承是非常強大的,但是你不能強制它(見: 圓橢圓問題 )。 如果你真的不能完全確定一個真正的“是 - 一個”亞型關係,那麼最好還是寫作。


繼承是非常誘人的,特別是來自程序性的土地,並且它看起來往往看似優雅。 我的意思是我所需要做的就是將這一點功能添加到其他類中,對吧? 那麼,其中一個問題就是這樣

繼承可能是你可以擁有的最糟糕的耦合形式

您的基類通過將實現細節以受保護成員的形式公開給子類來打破封裝。 這會讓你的系統變得僵硬脆弱。 然而,更悲慘的缺陷是新的子類帶來了繼承鏈的所有包袱和意見。

文章“ 繼承是邪惡的:DataAnnotationsModelBinder史詩般的失敗” ,在C#中演示了這個例子。 它顯示了在使用組合時以及如何重構時繼承的用法。







aggregation