文章目錄
- 面向對象設計原則
-
- 概述
- 1.單一職責原則
-
- 單一職責定義
- 單一職責舉例分析
- 2.開閉原則
-
- 開閉原則簡介
- 開閉原則的優勢
- 案例
- 3.裡氏代換原則
-
- 裡氏替換原則簡介
- 裡氏替換原則限制
- 裡氏替換原則實戰
- 4.依賴倒轉原則
-
- 依賴倒轉原則簡介
- 依賴倒轉原則執行個體
- 5.接口隔離原則
-
- 接口隔離原則簡介
- 執行個體示範
- 6.合成複用原則
-
- 合成複用原則簡介
- 7.迪米特法則
面向對象設計原則
概述
對于面向對象軟體系統的設計而言,在支援可維護性的同時,提高系統的可複用性是一個至關重要的問題,如何同時提高一個軟體系統的可維護性和可複用性是面向對象設計需要解決的核心問題之一。在面向對象設計中,可維護性的複用是以設計原則為基礎的。每一個原則都蘊含一些面向對象設計的思想,可以從不同的角度提升一個軟體結構的設計水準。 面向對象設計原則為支援可維護性複用而誕生,這些原則蘊含在很多設計模式中,它們是從許多設計方案中總結出的指導性原則。面向對象設計原則也是我們用于評價一個設計模式的使用效果的重要名額之一,在設計模式的學習中,大家經常會看到諸如“XXX模式符合XXX原則”、“XXX模式違反了XXX原則”這樣的語句。
最常見的7種面向對象設計原則如下表所示:
1.單一職責原則
單一職責定義
一個類隻負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該隻有一個引起它變化的原因
從定義中不難思考,一個類的所做的事情越多,也就越難以複用,因為一旦做的事情多了,職責的耦合度就變高了是以我們根據這個原則應該将
不同職責封裝在不同類中,不同的變化封裝在不同類中
。從我們平常的開發中不難發現,如果一個類或者方法接口等等隻做一件事,那麼可讀性很高,并且複用性也很高,并且一旦需求變化,也容易維護,假如你一個類糅雜多個職責,那麼很難維護。
單一職責舉例分析
從實際業務來剝離一個例子:現在有這麼一種情況,某租車平台個人子產品類涉及多個方法,有如下登入、注冊、支付寶押金支付、微信押金支付、支付寶套餐支付、微信套餐支付、整個結構如下:
/**
* 個人子產品
*/
@Controller
public class userController{
/**
* 登入
*/
public void login(){
}
/**
* 注冊
*/
public void register(){
}
/**
* 押金支付(阿裡)
*/
public void payAliDeposit(){
}
/**
* 押金支付(微信)
*/
public void payWXDeposit(){
}
/**
* 套餐支付(阿裡)
*/
public void payAliPackage(){
}
/**
* 套餐支付(微信)
*/
public void payWXPackage(){
}
}
我們可以看到很多功能都糅雜在一起,一個類做了那麼多事情,很臃腫,别提維護,就連找代碼都很困難,是以我們可以對這個UserController進行拆解,與此同時我們應該分包,比如這個應該在xxx.xxx.userMoudule下面,可能支付相關的有公共的方法,登入抑或也有公共的方法,那邊抽成公共服務去調用。
public class LoginController(){}
public class registerController(){}
public class depositPayController(){
// 支付寶支付
// 微信支付
}
public class packagePayController(){
// 支付寶支付
// 微信支付
}
整個方案實作的目的就是為了解決高耦合,代碼複用率低下的問題。單一職責了解起來不難,但是實際操作需要根據具體業務的糅雜度來切割,實際上很難運用。
2.開閉原則
開閉原則簡介
開閉原則是面向對象的可複用設計的第一塊基石,它是最重要的面向對象設計原則,定義如下:
一個軟體實體應當對擴充開放,對修改關閉。即軟體實體應盡量在不修改原有代碼的情況下進行擴充。
軟體實體包括以下幾個部分:
- 項目或軟體産品中按照一定的邏輯規則劃分的子產品
- 抽象和類
- 方法
注意:開閉原則是指對擴充開放,對修改關閉,并不是說不做任何的修改
。
開閉原則的優勢
- 可以使原來的測試代碼依舊可以運作,隻需要對擴充的代碼進行測試即可
- 可以提高代碼的複用性
- 可以提高系統的維護性
###如何使用開閉原則
- 抽象限制
- 通過接口或者抽象類限制擴充,對擴充進行邊界限定,不允許出現在接口或抽象類中不存在的public方法;
- 參數類型、引用對象盡量使用接口或者抽象類,而不是實作類;(針對抽象程式設計)
- 抽象層盡量保持穩定,一旦确定即不允許修改。
-
中繼資料控制子產品行為
通俗來說就是通過配置檔案來操作資料,spring的控制反轉就是一個很典型的例子。
- 約定優于配置
- 封裝變化
- 将相同的變化封裝到一個接口或者類中
- 将不同的變化封裝到不同的類或者接口中(單一職責的展現)
案例
某公司開發的租車系統有一個押金支付功能,支付方式有支付寶、阿裡支付,後期可能還有銀聯支付、易支付等等,原始的設計方案如下:

// 用戶端調用-押金支付選擇支付手段
public class DepositPay {
void pay(String type){
if(type.equals("ali")){
AliPay aliPay = new AliPay();
aliPay.pay();
}else if(type.equals("wx")){
WXPay wxPay = new WXPay();
wxPay.pay();
}
}
}
// 支付寶支付
public class AliPay {
public void pay() {
System.out.println("正在使用支付寶支付");
}
}
// 微信支付
public class WXPay{
public void pay() {
System.out.println("正在使用微信支付");
}
}
在以上代碼中,如果需要增加銀聯支付,如YLPay,那麼就必須要修改DepositPay中的pay方法的源代碼,增加新的判斷邏輯,違反了開閉原則(對修改關閉,對擴充開放,注意這邊的銀聯支付相當于擴充,是以它沒有違反規則),是以現在必須重構此代碼,讓其遵循開閉原則,做法如下:
- 增加一個接口,使得各種具體支付實作其接口
- DepositPay類針對接口程式設計,由用戶端來決定具體使用哪種支付方式
重構後的圖如下所示:
在上圖中我們引入了
接口Pay
,定義了pay方法,并且DepositPay是針對接口程式設計,通過setPayMode()由用戶端來執行個體化具體的支付方式,在DepositPay的pay()方法中調用payMode對象來支付。如果需要增加新的支付方式,比如銀聯支付,隻需要讓它也實作Pay接口,在配置檔案中配置銀聯支付即可,依賴注入是實作此開閉原則的一種手段,在這裡不贅述,源碼如下:
public interface Pay {
// 支付
void pay();
}
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("正在使用支付寶支付");
}
}
public class WXPay implements Pay{
@Override
public void pay() {
System.out.println("正在使用微信支付");
}
}
// 用戶端調用-押金支付選擇支付手段
public class DepositPay {
// 支付方式 (這邊可以通過依賴注入的方式來注入)
// 支付方式可以寫在配置檔案中
// 現在不管你選用何種方式,我都不需要更改
@Autowired
Pay payMode;
void pay(Pay payMode){
payMode.pay();
}
}
因為配置檔案可以直接編輯,且不需要編譯,是以一般不認為更改配置檔案是更改源碼。如果一個系統能做到隻需要修改配置檔案,無需修改源碼,那麼複合開閉原則。
3.裡氏代換原則
裡氏替換原則簡介
Barbara Liskov提出:
标準定義:如果對每一個類型為S的對象o1,都有類型為T的對象o2,使得以T定義的所有程式P在所有的對象o1代換o2時,程式P的行為沒有變化,那麼類型S是類型T的子類型。
上面的定義可能比較難以了解,簡單了解就是
所有引用基類(父類的)地方都可以用子類來替換,且程式不會有任何的異常
。但是
反過來就不行,所有使用子類的地方則不一定能用基類來替代
,很簡單的例子狗是動物,不能說動物是狗,因為可能還有貓。。。。
裡氏替換原則是實作開閉原則的重要方式之一,由于使用基類的所有地方都可以用子類來替換,
是以在程式中盡量使用基類來定義對象,在運作時确定其子類類型
。
裡氏替換原則限制
- 子類必須實作父類的抽象方法,但不得重寫(覆寫)父類的非抽象(已實作)方法。
- 子類中可以添加特有方法(父類中不存在),此時則無法在以父類定義的對象中使用該方法,除非在使用的時候強轉基類成子類進行調用。
- 當子類覆寫或實作父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更寬松。
- 當子類的方法實作父類的抽象方法時,方法的後置條件(即方法的傳回值)要比父類更嚴格。
是以我們在運用裡氏替換原則的時候,盡量把父類設計為抽象類或者接口,讓子類繼承父類或者實作接口并實作在父類中聲明的方法,運作時,子類執行個體替換父類執行個體,我們可以很友善地擴充系統的功能,同時無須修改原有子類的代碼,增加新的功能可以通過增加一個新的子類來實作。裡氏代換原則是開閉原則的具體實作手段之一。
裡氏替換原則實戰
某租車系統客戶分為普通使用者(customer)和VIP客戶(VIPCustomer),系統需要提供一個根據郵箱重置密碼的功能。原始設計圖:
在編寫重置密碼的時候發現,業務邏輯是一樣的,存在着大量的重複代碼,而且還可能增加新的使用者類型,為了減少代碼重複性,使用裡氏替換原則進行重構:
圖上重置密碼交由ResetPassword類去處理,隻需要傳入Customer類即可,不管任何類型的Customer類,隻要繼承自Customer,都可以使用裡氏替換原則進行替換,假如有新的類型,我們隻需要在配置檔案中注入新的類型即可。代碼如下(簡單意會一下):
// 抽象基類
public abstract class Customer {
}
public class CommonCustomer extends Customer{
}
public class VIPCustomer extends Customer{
}
// 重置密碼邏輯在這裡實作,隻需要傳入對應的類型即可
public class ResetPassword {
void resetPassword(Customer customer){
}
}
裡氏替換原則是實作開閉原則不可或缺的手段之一
,在本例中,通過傳遞參數使用基類對象,針對抽象程式設計,進而滿足開閉原則。
4.依賴倒轉原則
依賴倒轉原則簡介
依賴倒轉原則(Dependency Inversion Principle, DIP):抽象不應該依賴于細節,細節應當依賴于抽象。換言之,要針對接口程式設計,而不是針對實作程式設計。
可以通俗的定義為兩種:
- 高層次的子產品不應該依賴于低層次的子產品,他們都應該依賴于抽象。
- 抽象不應該依賴于具體實作,具體實作應該依賴于抽象。
要求我們在設計程式的時候盡量使用層次高的抽象層類,即使用接口和抽象類進行變量的聲明、參數類型聲明、方法傳回類型聲明以及資料類型轉換等等
,同時要注意一個具體類應該隻實作抽象類或者接口中存在的方法,不要給出多餘的方法,這樣抽象類将無法調用子類增加的方法.我們可以通過配置檔案來寫入具體類,這樣一旦程式行為改變,可直接改變配置檔案,而不需要更改程式,重新編譯,通過依賴倒轉原則來滿足開閉原則。
在實作依賴倒轉原則時,我們需要針對抽象層程式設計,而将具體類的對象通過
依賴注入(DependencyInjection, DI)
的方式注入到其他對象中,依賴注入是指當一個對象要與其他對象發生依賴關系時,通過抽象來注入所依賴的對象。常用的注入方式有三種,分别是:
構造注入,設值注入(Setter注入)和接口注入
依賴倒轉原則執行個體
這部分可以參照上面開閉原則案例,可以從那例子中看出,開閉原則,依賴倒轉原則,裡氏替換原則同時出現了,可以說`開閉原則是我們要實作的目标,而裡氏替換原則是實作手段之一,而同時裡氏替換原則又是依賴倒轉原則實作的基礎,因為加入沒有這個理論,依賴倒轉原則是不成立的,無法針對抽象程式設計,要注意這3個原則基本都是同時出現的。
5.接口隔離原則
接口隔離原則簡介
接口隔離原則的兩個定義:
1:使用多個專門的接口,而不使用單一的總接口,即用戶端不應該依賴那些它不需要的接口
**2:類間的依賴關系應該建立在最小的接口上 **
接口的含義:
- 一個接口代表一個角色,不應該将不同的角色都交給一個接口,因為這樣可能會形成一個臃腫的大接口;
- 特定語言的接口,表示接口僅僅是提供用戶端需要的行為,用戶端不需要的行為則隐藏起來,應當為用戶端提供盡可能小的單獨的接口,而不要提供大的總接口。
根據接口隔離原則,我們可明白,
每個接口都應隻承擔一種相對獨立的角色,不幹不該幹的事情
.
執行個體示範
場景:模拟動物平時的動作,當然也包括人,最初的設計就是一個總接口IAnimal,裡面定義動物會有的一些動作。
代碼如下:
public interface IAnimal{
/**
* 吃飯
*/
void eat();
/**
* 工作
*/
void work();
/**
* 飛行
*/
void fly();
}
public class Tony implements IAnimal{
@Override
public void eat() {
System.out.println("tony吃");
}
@Override
public void work() {
System.out.println("tony工作");
}
@Override
public void fly() {
System.out.println("tony不會飛");
}
}
public class Bird implements IAnimal{
@Override
public void eat() {
System.out.println("鳥吃");
}
@Override
public void work() {
System.out.println("鳥工作");
}
@Override
public void fly() {
System.out.println("鳥飛");
}
}
根據上面的寫法發現Tony需要實作飛的接口,這很明顯不僅僅是多餘,而且不合理,是以需要通過接口隔離原則進行重構:
/**
* 抽象動物的行為
*/
public interface IAnimal {
/**
* 吃飯
*/
void eat();
/**
* 睡覺
*/
void sleep();
}
/**
* 進階動物人 的行為
*/
public interface IAdvancedAnimalBehavior {
/**
* 打牌
*/
void playCard();
/**
* 騎車
*/
void byBike();
}
/**
* 低級動物的行為
*/
public interface IJuniorAnimalBehavior {
/**
* fly
*/
void fly();
}
/**
* 實作進階動物人的共通方法
*/
public class AbstractAdvancedAnimal implements IAnimal {
@Override
public void eat() {
System.out.println("人吃");
}
@Override
public void sleep() {
System.out.println("人睡");
}
}
/**
* 實作低級動物人的共通方法
*/
public class AbstractJuniorAnimal implements IAnimal {
@Override
public void eat() {
System.out.println("動物吃");
}
@Override
public void sleep() {
System.out.println("動物睡");
}
}
// tony
public class Tony extends AbstractAdvancedAnimal implements IAdvancedAnimalBehavior {
@Override
public void playCard() {
System.out.println("tony打牌");
}
@Override
public void byBike() {
System.out.println("tony騎車");
}
}
// 鳥
public class Bird extends AbstractJuniorAnimal implements IJuniorAnimalBehavior{
@Override
public void fly() {
System.out.println("鳥飛");
}
}
重構之後,首先定義了一個總的動物接口的大類,然後分别使用了兩個抽象類(一個是進階動物,一個是低級動物)分别去實作這些公共的方法,實作中可以抛出異常,表明繼承此抽象類的類可以選擇性的重寫,可不重寫。之後再定義了兩個行為接口表明進階動物和低級動物所特有的,這樣使得接口之間完全隔離,動物接口不再糅雜各種各樣的角色,
當然接口的大小尺度還是要靠經驗來調整,不能太小,會造成接口泛濫,也不能太大,會背離接口隔離原則。
6.合成複用原則
合成複用原則簡介
合成複用原則(Composite Reuse Principle, CRP):盡量使用對象組合,而不是繼承來達到複用的目的。
通過合成複用原則來使一些已有的對象使之成為對象的一部分,一般通過組合/聚合關系來實作,而盡量不要使用繼承。因為組合和聚合可以降低類之間的耦合度,而繼承會讓系統更加複雜,最重要的一點會破壞系統的封裝性,因為繼承會把基類的實作細節暴露給子類,同時如果基類變化,子類也必須跟着改變,而且耦合度會很高。
7.迪米特法則
參考:https://www.cnblogs.com/muzongyan/archive/2010/08/05/1793454.html
參考:https://blog.csdn.net/lovelion/article/details/7537584
參考:https://blog.csdn.net/qq_34966814/article/details/79475977
如果有小夥伴覺得我寫的不錯的話可以關注一下我的部落格,我會一直持續更新,也可以支援一下我的公衆号哦:java架構師小密圈,會分享架構師所必須深入研究的技術,比如netty,分布式,性能優化,spring源碼分析,mybatis源碼分析,等等等,同時還會分享一些賺錢理财的小套路哦,歡迎大家來支援,一起學習成長,程式員不僅僅是搬瓦工!
公衆号:分享系列好文章
交流群:群友互相分享資料