# 【程式設計】物件導向筆記 ## Introduction 物件導向是程式設計中的一種概念,將程式中的元素視為一種物件,這些物件可以被賦予屬性、方法等等。此物件可以是抽象或現實中的實體概念。例如,車子具備汽油、座位等屬性,而其方法包括向前、向後等,基本上作為程式設計師在撰寫程式碼的時候,我們必須不斷思考所設計物件是否滿足某些條件,如果滿足後,就有可能重新設計類別,例如重新設計基類與衍生類別 ## Definition ### Object 對資料或行為的一種封裝,封裝成一個基本的模塊。物件可以是實體或抽象概念。 ### Class 描述物件的抽象模板,基本上會定義一個物件有甚麼樣的屬性或方法,其實就是設計圖啦。 ### Instance 類別內實體化時的名稱,或者說宣告成立時,對於相同的 class 可以實例化不同的類別屬性。每一個實例的狀態都可以不一樣或相同,彼此獨立。 ## 3 Concepts in OOP ### Inheritance 繼承 繼承能夠擴展一個類別的屬性的同時保有原始類別屬性的一個方法。例如,球有很多種,如籃球、棒球、橄欖球,但它們背後都有一個抽象概念,即形狀或大小。因此,可以定義一個類別為形狀,而這些球就是其擴展後的結果。我們稱作子類,而形狀我們就稱作父類。 * 當 `class B` 為 `class A` 的衍生類別,我可以稱作 `B is-a A` * 當 `class B` 為 `class A` 的屬性,稱作 `A has-a B` ### Encapsulation 封裝 將類別或物件屬性隱藏起來,並且設計可以控制的介面,確保內部狀態的改變必須透過介面才能更改,確保了內部狀態一致性,將方法或屬性設置存取權限,在 C++ 中可以使用 `public` / `private` / `protected` 來控制權限。 `C++` 中藉由 `Access modifiers` ( `public` / `private` / `protected`) 來控制 `struct` / `class`/ `union` 的權限 Access specifiers give the author of the class the ability to decide which class members are accessible to the users of the class (that is, the _interface_) and which members are for internal use of the class (the _implementation_). ![image](https://hackmd.io/_uploads/rk4rAlt6A.png) * `public` : 任何地方都可以存取類別的公有成員,包含使用者、衍生類別、以及類別自身 * `private` : 私有成員只能在類別自身內部存取,無法被外部使用者或衍生類別存取 * `protected` : 保護只能在類別內部及其衍生類別中存取,使用者無法存取保護成員 ### Polymorphism 多型 對於一個方法,是否有不同的結果。在射擊遊戲中,左鍵為射擊,右鍵為開鏡。同樣是滑鼠點擊的事件,但效果卻不一樣,這就是多型 * **Function Overriding (覆寫)** 允許子類重新覆蓋父類方法。 * **Function Overloading (重載)** 允許函數可以有多個不同輸入。 * **Virtual Function(虛擬函數)** 函數可以在執行時期被覆寫、與動態調度使用 ## SOLID Principles * 五種字母組成 * **[S](#)RP** 單一職責原則 * **[O](#)CP** 開放-封閉原則 * **[L](#)SP** 里氏替換原則 * **[I](#)SP** 介面隔離原則 * **[D](#)IP** 依賴反向原則 * 軟體設計需要符合兩件事 * 讓電腦的行為符合需求 * 讓電腦的行為可以被工程師設計與改變 * 也就是城市要寫得夠簡單,操作與介面分離 ### SRP 單一職責原則 (Single Responsibility Principle) 一個**模組**應該只對唯一的一個**角色負責**,基本上就是專責,舉例來說,在公司中,會計就是做帳務的任務,不會帳務弄一弄就跑去修電腦、修網路 * ❌ 壞例子 : 一個 `Report` 類別同時負責產生報表和把報表寄出 * ✅ 好例子 : 建立 `ReportGenerator` 負責產生報表,`ReportSender` 負責寄送報表 ### OCP 開放-封閉原則 (Open-Closed Principle) 軟體實體 ( 類別、模組、函數等 ),對於擴展必須是開放,但對於修改是封閉的,當你需要根據不同需求修改方法時,就可能需要重構方法然使其符合 OCP 原則,軟體實體之間是具備低偶合性質的,相依性是低的,假設我們出國,當地插頭與本國插頭不一樣,我們一定是買一個轉接頭來接上插頭,而不是直接把插座拆掉換成本國插頭吧,這就是 OCP 最核心概念 * ❌ 壞例子 : 如果要計算形狀的面積,每次新增形狀 ( 圓形、三角形、矩形 ) 都要回去修改 `AreaCalculator` * ✅ 好例子 : 建立 `Shape` 介面,每個形狀自己實作 `getArea()`,這樣 `AreaCalculator `不用改,只要新增形狀類別即可。 ### 里氏替換原則(Liskov Substitution Principle,LSP) 子類別應該可以替換其父類別並且不會破壞系統的正確性,也就是不能違反父類別當初的設計原因,如果你有一個「交通工具」的程式,`Car`、`Bus`、`Bike` 都應該能安全替換「交通工具」使用。如果新增一個 Horse 結果沒油門卻還要 `startEngine()`,就違反了 LSP ! ### 介面隔離原則(Interface Segregation Principle,ISP) 客戶端不應該被迫依賴於它不使用的接口,假設你去健身房辦會員卡,結果規定你必須同時繳「游泳池費用」,但你根本不游泳。這就是「被迫依賴不需要的功能」 * ❌ 壞例子 : `Printer` 介面同時有 `print()`、`scan()`、`fax()`,如果一台印表機只會印刷,卻被迫實作 `scan()`、`fax()`,就違反了 ISP。 * ✅ 好例子 : 拆成 Printable、Scannable、Faxable 三個小介面,依需求實作。 ### 依賴反轉原則(Dependency Inversion Principle,DIP) 高層模組不應該依賴於低層模組,高層與低層都應該依賴於抽象,藉此降低類別之間的耦合性,舉例來說就像你用「插座」而不是直接接電線,因為插座(抽象)能支援各種電器。如果你直接把電線接到冰箱,換一台洗衣機就得重接線 ## Abstraction 抽象 Abstraction,將一個程式或資料的操作方式隱藏起來,不讓使用者知道其實作細節,只要知道怎麼操作就好,在如同 `C++` 中 STL library一樣,我們不需要知道其怎麼實作的,只要能夠學會操作他就好 ## Design Pattern 設計模式 ### Strategy Pattern 策略設計 > In computer programming, the strategy pattern (also known as the policy pattern) is a behavioral software design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives runtime instructions as to which in a family of algorithms to use > 維基百科上說明已經蠻清楚的,也就是可以在執行程式碼時,可以自由地選擇需要的演算法,而不是透過指令或更改程式碼來達成 ![image](https://hackmd.io/_uploads/Sycmw-Ly1l.png) ### State Pattern 狀態設計模式 ![image](https://hackmd.io/_uploads/Bkbxl6iqge.png) ### Singleton Pattern 單例模式 將某個類別限制在整個程式中實體化時只有一個實例 1. Ensure they only have one instance 2. Provide easy access to that instance 3. Control their instantiation ## 靜態工廠函數 ( Static Factory Method Pattern ) 在 base class 中建立靜態創建函數,用於創建不同 derived class ,這種方式可以讓創建邏輯集中在 base class 中,能夠簡化創建類別時的複雜度 ```cpp Source *Source::Create(const std::string &name) { if (...) { return new ImageSource(absPathStr); } else if (...) { return new VideoSource(absPathStr); } else { throw std::invalid_argument("Unsupported source type"); } } ``` ## 依賴注入 (Dependency Injection, DI) 依賴注入就是不要讓物件自己去建立它需要的東西,而是「從外部」把需要的東西給它,以車子來說,車子都會有引擎吧,我們在要開車的時候才會請師傅裝引擎嗎 ? 肯定不會吧,一定是在拿到車子的時候就裝好了,這個過程可以理解為在建構車物件的時候將引擎就設定好,也就是讓我們在建構車子時,就先決定好引擎是什麼,有點像替換某個類別中的屬性等等,可以做到自由替換 ```cpp class Engine { public: virtual void start() = 0; // 讓引擎可替換 virtual ~Engine() {} }; class GasEngine : public Engine { public: void start() override { std::cout << "Gas engine starts\n"; } }; class ElectricEngine : public Engine { public: void start() override { std::cout << "Electric engine starts silently\n"; } }; class Car { Engine* engine; // 依賴抽象,不自己 new public: // Constructor Injection:從外部傳入依賴 Car(Engine* eng) : engine(eng) {} void drive() { engine->start(); std::cout << "Car is driving\n"; } }; ``` ## [Mixin](https://zh.wikipedia.org/zh-tw/Mixin) Mixin 是一種類別,提供了方法的實現,可以很好讓其他類別繼承後來使用該實現方法,與其他類別關係為 include 或者 -able 關係,通常做為功能模組使用,就是額外開一個類別繼承,具備擴展功能性質 ## Dispatch 在 pytorch 與 torchvision 中,在 nn 模組、或者 transform 模組,都會看到他們將核心功能與邏輯功能分開,並透過 Dispatch 讓函數可以針對不同 input 來自動選擇適合的方法 Example [torchvision/transforms/functional.py](https://github.com/pytorch/vision/blob/main/torchvision/transforms/functional.py) ```python! from . import _functional_pil as F_pil, _functional_tensor as F_t def solarize(img: Tensor, threshold: float) -> Tensor: """ Solarize an RGB/grayscale image by inverting all pixel values above a threshold. """ if not torch.jit.is_scripting() and not torch.jit.is_tracing(): _log_api_usage_once(solarize) if not isinstance(img, torch.Tensor): return F_pil.solarize(img, threshold) return F_t.solarize(img, threshold) ```