物件導向程式設計,看這篇就夠了

2023-12-12 21:00:38

一、物件導向程式設計的概念

物件導向程式設計,是一種程式設計正規化,也是一種程式語言的分類。它以物件作為程式的基本單元,將演演算法和資料封裝其中,程式可以存取和修改物件關聯的資料。這就像我們在真實世界中操作各種物體一樣,比如我們可以開啟電視、調整音量、切換頻道,而不需要知道電視的內部如何工作。同樣,在物件導向程式設計中,我們可以操作物件,而不需要關心物件的內部結構和實現。

物件導向程式設計的主要組成部分是類和物件。類是一組具有相同屬性和功能的物件的抽象,就好比我們說的「汽車」這個概念,它具有顏色、型號、速度等屬性,有啟動、加速、剎車等功能。而物件則是類的範例,它是具體的,就像你家那輛紅色的賓士車,它就是汽車這個類的一個範例。

二、物件導向程式設計的特性

物件導向程式設計有三大特性,封裝、繼承和多型。

1. 封裝

封裝是把客觀事物封裝成抽象的類,並隱藏實現細節,使得程式碼模組化。比如,我們可以把「汽車」這個客觀事物封裝成一個類,這個類有顏色、型號等屬性,有啟動、加速、剎車等方法,而這些屬性和方法的具體實現則被隱藏起來,使用者只需要知道這個類有哪些屬性和方法,不需要知道這些方法是如何實現的。

2. 繼承

繼承是物件導向程式設計的另一個重要特性,它提供了一種無需重新編寫,使用現有類的所有功能並進行擴充套件的能力。比如,我們可以定義一個「電動車」類,它繼承了「汽車」類,就自動擁有了「汽車」類的所有屬性和方法,比如顏色、型號等屬性,啟動、加速、剎車等方法,然後我們還可以在「電動車」類上增加一些新的屬性和方法,比如電池容量、充電方法等。

3. 多型

多型是指同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。比如,我們定義了一個「汽車」類,它有一個「啟動」方法,然後我們又定義了一個「電動車」類,它繼承了「汽車」類,也有一個「啟動」方法,但是「電動車」類的「啟動」方法的實現可能與「汽車」類的不同,這就是多型。

三、物件導向程式設計的理念

物件導向程式設計有兩個主要的理念,基於介面程式設計和組合優於繼承。

1. 基於介面程式設計

基於介面程式設計的理念是,使用者不需要知道資料型別、結構和演演算法的細節,只需要知道呼叫介面能夠實現功能。這就像我們使用電視遙控器一樣,我們不需要知道遙控器內部的電路設計和工作原理,只需要知道按哪個按鈕可以開啟電視,按哪個按鈕可以調節音量。

基於介面程式設計有很多好處,這裡簡單列幾條。

首先,基於介面程式設計可以提高程式碼的靈活性。因為我們的程式碼不依賴於具體的實現,所以當實現變化時,我們的呼叫程式碼不需要做任何修改。比如有一個程式需要讀取資料,資料可能來自於資料庫、檔案或者網路,無論資料來自哪裡,呼叫方只存取「資料讀取」介面,實現可以根據場景任意調整。

其次,基於介面程式設計可以提高程式碼的可測試性。因為介面只是一個規範,沒有具體的實現,所以我們可以方便地為介面建立模擬物件(Mock Object),這樣就可以在沒有實際環境的情況下進行單元測試。比如說,我們可以建立一個模擬的「資料讀取」介面,讓它返回一些預設的資料,然後我們就可以在沒有資料庫或者檔案的情況下測試我們的程式碼。

最後,基於介面程式設計也可以提高程式碼的可讀性。因為介面清晰地定義了功能,所以只要看介面,就可以知道程式碼應該做什麼,而不需要關心程式碼是怎麼做的。這就像我們使用電視遙控器,我們不需要知道遙控器是怎麼工作的,只需要知道按這個按鈕可以換臺,按那個按鈕可以調節音量。

使用介面有利於抽象、封裝和多型。

2. 組合優於繼承

儘管繼承可以使我們更容易地重用和擴充套件程式碼,但是如果繼承層次過深、繼承關係過於複雜,就會嚴重影響程式碼的可讀性和可維護性。比如我們修改了基礎類別,就可能影響到繼承它的子類,這會增加迭代的風險。因此,我們更傾向於使用組合而不是繼承。比如,我們可以定義一個「電動車」類,它包含「電池系統」、「制動系統」、「車身系統」、「轉向系統」等元件,而不是繼承「汽車」類。

這裡我們再列舉下組合的幾個好處:

首先,組合可以讓我們的程式碼更加靈活。因為我們可以隨時新增、刪除或者替換元件,而不需要修改元件的內部實現。比如,如果我們想要改變汽車的發動機,只需要換掉髮動機這個元件就可以了,而不需要修改汽車或者發動機的程式碼。

其次,組合可以讓我們的程式碼更容易理解。因為每個元件都是獨立的,有明確的功能,所以我們可以分別理解和測試每個元件,而不需要理解整個系統。

最後,組合可以減少程式碼的複雜性。因為我們不需要建立複雜的類層次結構,所以我們的程式碼會更簡單,更易於維護。

總的來說,「組合優於繼承」是一種程式設計實踐,它鼓勵我們使用更簡單、更靈活的組合,而不是更復雜、更脆弱的繼承。這並不是說繼承是壞的,而是說在許多情況下,組合可能是一個更好的選擇。

3.控制反轉程式碼範例

具體到程式設計中,很多同學可能使用過控制反轉或者依賴注入,控制反轉就是一種基於介面的組合程式設計思想。在傳統的程式設計模式中,我們通常是在需要的地方建立物件,然後呼叫物件的方法來完成一些任務。但是在使用了控制反轉之後,物件的建立和管理工作不再由我們自己控制,而是交給了一個外部的容器(也就是所謂的平臺),我們只需要在需要的地方宣告我們需要什麼,然後容器會自動為我們建立和注入需要的物件。這就是所謂的依賴注入(Dependency Injection,簡稱DI),它是實現控制反轉的一種方法。

為了讓大家更好理解依賴注入,我這裡貼一個Java的例子,程式基於 Spring Boot 框架。

在這個例子中,我們有一個 MessageService 介面和一個實現類 EmailService。然後我們有一個MessageClient類,它依賴於MessageService來傳送訊息。

首先,定義一個MessageService介面:

public interface MessageService {
    void sendMessage(String message, String receiver);
}

然後,建立實現類,在Spring Boot中,我們可以使用@Component或@Service等註解來讓Spring自動建立Bean。然後在需要注入的地方,使用@Autowired註解來自動注入Bean。

我們將MessageService的實現類標記為@Service:

@Service
public class EmailService implements MessageService {
    public void sendMessage(String message, String receiver) {
        System.out.println("Email sent to " + receiver + " with Message=" + message);
    }
}

我們在MessageClient中使用@Autowired來注入MessageService:

@Component
public class MessageClient {
    private MessageService messageService;

    @Autowired
    public MessageClient(MessageService messageService) {
        this.messageService = messageService;
    }

    public void processMessage(String message, String receiver){
        this.messageService.sendMessage(message, receiver);
    }
}

最後,在主程式中,我們可以直接獲取MessageClient的Bean,而不需要手動建立:

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(Main.class, args);
        MessageClient emailClient = context.getBean(MessageClient.class);
        emailClient.processMessage("Hello", "[email protected]");
    }
}

在這個例子中,Spring Boot會自動掃描@Service和@Component註解的類,並建立對應的Bean。然後在需要注入的地方,Spring Boot會自動找到對應的Bean並注入。

控制反轉是一種非常強大的設計原則,它可以幫助我們寫出更靈活、更易於維護和測試的程式碼。如果你還沒有嘗試過,我強烈建議你試試!

四、物件導向程式設計的原則

物件導向程式設計有五個基本原則,也被稱為SOLID原則。

1. 單一原則

單一原則是指一個類應該僅具有隻與他職責相關的東西,這樣可以降低類的複雜度,提高可讀性和可維護性。

這個原則就像是你在廚房裡做飯,你有各種各樣的廚具,每個廚具都有它特定的用途,比如刀用來切菜,鍋用來煮食物,勺子用來攪拌。你不會用刀去攪拌,也不會用勺子去切菜。這樣每個廚具都只負責一項任務,使得廚房的運作更加順暢。

2. 開閉原則

開閉原則是指軟體中的類、屬性和函數對擴充套件是開放的,對修改是封閉的。這樣可以避免對原有程式碼的修改導致的很多工程工作。

這個原則就像是你的房子,你可以在房子裡面新增更多的傢俱,比如椅子、桌子、床等,但你不會去改變房子的結構,比如拆掉牆壁或者增加門窗。這樣你的房子對於新增傢俱是開放的,對於修改結構是關閉的。

在計算機體系中,最符合開閉原則的就是馮諾依曼體系架構,在這個架構中,CPU是封閉的、穩定的,然後通過IO操作對外開放,支援各種無窮無盡的輸入輸出裝置。這是開閉原則的最好最基礎的體現。

3. 里氏替換原則

里氏替換原則是指子類可以實現父類別的抽象方法,但不能覆蓋父類別的非抽象方法。這樣可以讓高層次模組能夠依賴抽象類,而不是具體的實現。

這個原則就像是你的電視遙控器,無論你的電視是老款的CRT電視,還是新款的LED電視,你都可以用同一個遙控器來控制。這是因為所有的電視都遵循了同樣的介面,即遙控器可以傳送的訊號。所以你可以用新的電視來替換老的電視,而不需要改變遙控器。

4. 介面隔離原則

介面隔離原則是指類間的依賴關係應該建立在最小的介面之上,這樣可以減少類間的耦合度。

舉個例子,假設我們有一個Animal介面,它包含了eat(), sleep(), fly()等方法。現在我們要設計一個Dog類來實現這個介面,但是狗並不能飛,所以fly()方法對於Dog類來說是不需要的。如果我們按照介面隔離原則來設計,那麼我們可以將Animal介面拆分為AnimalBasic(包含eat()和sleep()方法)和AnimalFly(包含fly()方法)兩個介面,然後讓Dog類只實現AnimalBasic介面,這樣就避免了實現不需要的方法。

5. 依賴反轉原則

依賴反轉原則是指高層次模組不應該依賴於低層次模組的具體實現,兩者都應該依賴其抽象。這樣可以提高程式碼的可延伸性。

舉個例子,假設我們有一個高階模組HighLevelModule和一個低階模組LowLevelModule。HighLevelModule直接依賴於LowLevelModule的具體實現。現在,如果我們遵循依賴反轉原則,我們可以定義一個抽象的介面AbstractModule,然後讓HighLevelModule依賴於AbstractModule,同時讓LowLevelModule也實現AbstractModule。這樣,無論是HighLevelModule還是LowLevelModule,它們都只依賴於抽象,而不再直接依賴於對方的具體實現。這樣就可以提高程式碼的可延伸性和可維護性。

五、物件導向程式設計的優缺點

物件導向程式設計的優點主要有兩個:

  • 一是能和真實的世界交相呼應,符合人的直覺。物件是基於真實世界實體的抽象,比如學生、書籍、車輛等,這些物件都有其屬性(如學生的名字、年齡)和行為(如學生的學習、閱讀)。這樣的設計方式使得我們能夠更直觀地理解和操作程式碼,因為它與我們日常生活中的理解方式是一致的。
  • 二是程式碼的可重用性、可延伸性和靈活性很好。這主要得益於OOP的幾個主要特性,包括封裝、繼承和多型。封裝可以隱藏物件的內部實現,只暴露出必要的介面,這樣可以防止外部的不恰當操作。繼承允許我們建立子類來複用和擴充套件父類別的功能,這大大提高了程式碼的可重用性。多型則允許我們使用同一個介面來操作不同的物件,這提高了程式碼的靈活性。

然而,物件導向程式設計也並非完美,它也有一些缺點,比如:

  • 首先,由於程式碼需要通過物件來抽象,這就增加了一層「程式碼粘合層」,也就是我們需要建立物件、管理物件的生命週期、處理物件之間的關係等,這使得程式碼變得更加複雜。對於一些簡單的問題,使用物件導向程式設計可能會有點「殺雞用牛刀」。
  • 其次,物件導向程式設計中的物件通常都有一些內部狀態,而這些狀態在並行環境下需要被正確地管理,否則就可能會出現資料不一致、死鎖等問題。比如,如果兩個執行緒同時操作同一個物件,而這個物件的狀態沒有被正確地保護,那麼就可能會出現資料不一致的問題。

總的來說,物件導向程式設計是一種強大而靈活的程式設計正規化,它可以幫助我們更好地組織和管理程式碼,提高程式碼的可讀性和可維護性,這使得它特別適合用在大型工程專案中。然而,我們也需要注意其可能帶來的問題,尤其是在並行和複雜系統中。

關注螢火架構,技術提升不迷路!