选择语言
durumis AI 总结的文章
- 說明了使用物件導向程式設計方法來實現電影訂票系統,介紹了合作、物件和類別的概念,並強調了遵循領域結構的程式結構。
- 特別是,它提供了一種方法,通過物件的自律性和封裝來減少外部干擾,並通過分離介面和實現來保證程式設計師的自由。
- 它還說明了如何使用合作、繼承和多型來編寫靈活且可擴展的程式碼,用於計算折扣票價,並強調了抽象的重要性以及組合用於程式碼重用的優點。
電影預訂系統
需求分析
- 要實現一個線上電影預訂系統。
- 電影表示關於電影的基本信息。
- 標題、放映時間、價格信息等。
- 放映表示觀眾實際觀看電影的事件。
- 放映日期、時間、序號等。
- 人們預訂電影,但事實上,應該說他們預訂的是特定的放映。
- 折扣條件
- 價格是否打折。
- 順序條件:利用放映順序來決定是否打折。
- 時間條件:利用電影放映開始時間來決定是否打折。
- 折扣政策
- 決定折扣費用。
- 金額折扣政策:從預訂費用中減去一定金額。
- 比率折扣政策:從原價中減去一定比例的費用。
- 每部電影可以分配一個折扣政策,也可以不分配,折扣條件可以混合多個折扣條件。
- 如果折扣政策適用,但沒有滿足折扣條件,或者沒有適用折扣政策,則不打折。
面向物件程式設計
合作、物件、類別
- 面向物件是指面向物件。
- 在決定類別之後,不應該考慮類別需要哪些屬性和方法。
- 應該決定哪些物件具有哪些狀態和行為。
- 不應該把物件看作獨立的存在,而是應該把物件看作是合作社群的一員。
遵循領域結構的程式結構
- 領域是指使用者使用程式來解決問題的領域。
- 一般來說,類別的名稱應該與對應的領域概念的名稱相同,或者至少相似。
- 類別之間的關係也應該盡可能地與領域概念之間的關係相似,以使程式結構更容易理解和預測。
實現類別
- 類別分為內部和外部,為了設計出優秀的類別,必須決定哪些部分對外公開,哪些部分隱藏。
- 物件的屬性被設為 private,並且為修改內部狀態而必要的 method 被設為 public。
- 區分類別的內部和外部,可以保證物件的自主性,從而為程式設計師提供實現自由。
自主物件
- 物件應該是具有狀態和行為的自主物件。
- 將數據和功能一起綁定到物件內部稱為封裝。
- 通過訪問控制,可以減少外部的干預,使物件能夠自主決定自己的行為。
- 介面和實現分離原則是面向物件程式設計必須遵循的主要原則。
- 公開介面:外部可以訪問的部分。
- 實現:只能在內部訪問的部分。
程式設計師的自由
- 程式設計師的角色分為類別創建者和客戶端程式設計師。
- 類別創建者新增數據類型。
- 客戶端程式設計師使用類別創建者新增的數據類型。
- 實現隱藏
- 類別創建者可以只向客戶端程式設計師提供必要的內容,隱藏內部實現。
- 客戶端程式設計師只需要知道介面,就可以減少知識量。
合作物件的社群
- 在表示金錢時,與其只簡單地宣告 Long 類型的變數,不如將其包裝成 Money 等物件。使用物件可以使意義傳達得更清楚,並且可以集中處理重複的計算。
- 為了實現系統的某個功能,物件之間的交互作用稱為合作。
關於合作的短篇故事
- 物件之間唯一可以交互作用的方式是傳送或接收訊息。
- 處理接收到的訊息的方式稱為方法。
- 區分訊息和方法很重要,從這裡開始,多態性的概念就出現了。
取得折扣費用
開始合作計算折扣費用
- Movie 類別中沒有關於折扣政策的詳細邏輯,而是委託給 DiscountPolicy 介面。繼承、多態性和抽象化非常重要。
折扣政策和折扣條件
繼承和多態性
編譯時期依賴性和執行時期依賴性
- 代碼的依賴性和執行時間的依賴性可能不同。
- 兩者之間的依賴性越大,代碼就越難理解,但代碼會更靈活,更具可擴展性。
通過差異進行程式設計
- 繼承可以基於現有類別快速輕鬆地新增新類別,並且可以重用父類別的實現。
- 通過新增與父類別不同的部分來建立新類別的方法稱為通過差異進行程式設計。
繼承和介面
- 繼承會讓子類別繼承父類別提供的所有介面。
- 介面定義了物件可以理解的訊息清單。
public class Movie {
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
- Movie 向 DiscountPolicy 傳送 calculateDiscountAmount 訊息。從 Movie 的角度來看,它不關心哪個類別的實例會發送回應,只要能成功發送回應就行了。
- 因此,AmountDiscountPolicy 和 PercentDiscountPolicy 都可以代替 DiscountPolicy 與 Movie 合作。
- 像這樣,子類別代替父類別稱為向上轉型。因為子類別似乎被自動轉型為位於頂部的父類別。
多態性
- 多態性是指接收相同訊息時,物件的類型不同,回應也會不同的能力。
- Movie 傳送相同的訊息,但實際上會執行哪個 method 取決於接收訊息的物件的類別是什麼。
- 多態性基於程式碼的編譯時期依賴性和執行時期依賴性可能不同的事實。
- 多態性可以在執行時間決定要執行的 method,因此稱為延遲綁定或動態綁定。
介面和多態性
- 如果你不需要分享實現,只需要分享介面,那麼可以使用介面而不是抽象類別。
抽象化和靈活性
抽象化的力量
- 抽象化的優點
- 單獨查看抽象化層級,就可以在較高的層級上描述需求的政策。
- 設計變得更加靈活。
靈活的設計
- 抽象化可以防止設計與具體情況結合,從而可以建立靈活的設計。
抽象類別和介面的權衡
public abstract class DiscountPolicy {
private List conditions;
public DiscountPolicy(DiscountCondition... conditions) {
this.conditions = Arrays.asList(conditions);
}
public Money calculateDiscountAmount(Screening screening) {
for (DiscountCondition condition : conditions) {
if (condition.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
public class NoneDiscountPolicy extends DiscountPolicy {
@Override
protected Money getDiscountAmount(Screening screening) {
return Money.ZERO;
}
- 目前,NoneDiscountPolicy 實際上沒有問題,因為如果 DiscountPolicy 的 calculateDiscountAmount() 方法中沒有折扣條件,則會返回 0。但使用者需要將沒有折扣政策的 None 政策作為 Movie 的參數傳入,因此添加了它。
- 因此,從概念上來說,NoneDiscountPolicy 類別會讓人感到困惑,如果像下面這樣修改,則更容易理解。
public interface DiscountPolicy {
Money calculdateDiscountAmount(Screening screening);
}
public abstract class DefaultDiscountPolicy implements DiscountPolicy {
private List conditions;
public DefaultDiscountPolicy(DiscountCondition... conditions) {
this.conditions = Arrays.asList(conditions);
}
public Money calculateDiscountAmount(Screening screening) {
for (DiscountCondition condition : conditions) {
if (condition.isSatisfiedBy(screening)) {
return getDiscountAmount(screening);
}
}
return Money.ZERO;
}
abstract protected Money getDiscountAmount(Screening screening);
}
public class NoneDiscountPolicy implements DiscountPolicy {
@Override
public Money calculateDiscountAmount(Screening screening) {
return Money.ZERO;
}
}
public class PercentDiscountPolicy extends DefaultDiscountPolicy {
private double percent;
public PercentDiscountPolicy(double percent, DiscountCondition... conditions) {
super(conditions);
this.percent = percent;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return screening.getMovieFee().times(percent);
}
}
public class AmountDiscountPolicy extends DefaultDiscountPolicy {
private Money discountAmount;
public AmountDiscountPolicy(Money discountAmount, DiscountCondition... conditions) {
super(conditions);
this.discountAmount = discountAmount;
}
@Override
protected Money getDiscountAmount(Screening screening) {
return discountAmount;
}
- 這樣,DiscountPolicy 被抽象化為介面,並將其實現分為 NoneDiscountPolicy 和代表一般折扣政策的 DefaultDiscountPolicy。
- 你可能會認為,為了 NoneDiscountPolicy 而新增介面,與增加的複雜度相比,效率低下。
- 你應該意識到,與實現相關的所有內容都可能是權衡的目標。也就是說,所有編寫的代碼都應該有合理的理由。
代碼重用
- 繼承可以用来重用代码。
- 但是,為了重用代碼,最好使用組合而不是繼承。
- 當前代碼中,Movie 重用 DiscountPolicy 代碼的方式就是組合。
- 如果將 Movie 作為上層類別,並將其分為 AmountDiscountMovie 和 PercentDiscountMovie,則可以說使用了繼承。
繼承
- 繼承的缺點
- 違反了封裝。
- 你需要了解父類別的內部結構。
- 更改父類別時,子類別也可能需要更改。
- 使設計缺乏靈活性。
- 在編譯時間決定父類別和子類別之間的關係。
- 在執行時間無法更改物件類型。
- 但組合可以使用 changeDiscountPolicy() 等方法,在執行時間替換實例。
- 違反了封裝。
組合
- 通過介面定義的訊息來重用代碼的方法稱為組合。
- 可以實現封裝,並保持鬆散耦合。
- 為了實現多態性,重用介面的情況下,需要將繼承和組合結合使用。
- 例如,DiscountPolicy 介面只能通過繼承來實現下層實現。
資料來源
- 物件