:::spoiler 文章目錄 [toc] ::: ## 請用一個例子說明,全局對像是邪惡的存在。 全域變數和靜態對象經常被視為不良的程式設計實踐,因為他們讓應用程序的各個部分的相依性增加。全域變數可以在程式的任何地方被改變,這使得程式的狀態變得難以追蹤和控制,因此**更容易引入錯誤和不預期的行為**。 以下是一個簡單的全域變數的例子: ```python= # 全域變數 count = 0 def increment(): global count count += 1 def decrement(): global count count -= 1 ``` 在此例中,`count` 變數可以在兩個函式 `increment()` 和 `decrement()` 之間共享。但問題在於,如果其他函式或程式碼也能存取或修改 `count` 變數,程式的行為就會變得非常難以預測。對於大型程式來說,使用全域變數會使得程式的維護變得非常困難。 為了避免這樣的問題,一種常見的解決方案是使用類別或物件來封裝資料和相關的行為,如下所示: ```python= class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 def decrement(self): self.count -= 1 ``` 這樣,`count` 變數就只能透過 `Counter` 類別的方法來存取,這降低了不預期修改 `count` 的風險。 ## 反轉控制是什麼?它如何提升程式碼設計? 反轉控制(Inversion of Control,縮寫為 IoC)是一種程式設計原則和模式,它將傳統的控制流程反轉過來,使得自定義的程式碼被框架調用,而不是由自定義的程式碼來調用框架的代碼。這可以帶來許多好處,包括程式碼解耦、模組化和測試容易性。 要理解 `IoC`,我們可以先看一個沒有使用 `IoC` 的例子。假設我們在寫一個應用程式,並需要從不同的資料源讀取資料: ```python= class MyApp: def get_data(self): db = Database() data = db.read_data() return data ``` 在上述的例子中,`MyApp` 類別直接依賴於 `Database` 類別,因此他們是耦合的。如果我們想要更換資料源或者在測試時使用模擬的資料源,我們必須修 改 `MyApp` 類別的內部實現,這會導致程式碼的維護變得困難。 相較之下,如果我們使用反轉控制,我們可以讓 `MyApp` 類別在運行時才決定使用哪一種資料源,並且讓這些資料源作為參數傳遞給 `MyApp`。這使得 `MyApp` 類別和具體的資料源解耦,如下所示: ```python= class MyApp: def __init__(self, datasource): self.datasource = datasource def get_data(self): data = self.datasource.read_data() return data ``` 在上述的例子中,`MyApp` 不再關心資料源是什麼,只需關心它可以從資料源中讀取資料。這讓我們能更容易地替換資料源,並在測試時使用模擬的資料源。這就是 `IoC` 如何提升程式碼設計的一個例子。 ## 德米特法則是什麼?如果違反了它,為什麼會是一種糟糕的設計?並請給出修改之後的代碼示例。 德米特法則(Law of Demeter),也稱為「最少知識原則」,是一種軟體開發中的設計原則。該原則認為一個物件應該只對其直接的朋友保持有限的知識,而不應該對整個系統都了如指掌,也就是說,一個物件應該只與其直接關聯的物件進行交互。 違反德米特法則會導致程式碼之間的耦合度增加,系統的模組之間的獨立性降低,影響程式碼的可維護性和可變更性。 例如,一個違反德米特法則的狀況可能如下: ```java= class Customer { Wallet wallet; Wallet getWallet() { return wallet; } } class Seller { void charge(Customer customer, float amount) { Wallet wallet = customer.getWallet(); wallet.subtractMoney(amount); } } ``` 在這個例子中,`Seller` 類別違反了德米特法則,因為它調用了 `Customer` 中的 `Wallet` 對象的方法,為了遵守德米特法則,我們應該這樣修改: ```java= class Customer { Wallet wallet; void pay(float amount) { wallet.subtractMoney(amount); } } class Seller { void charge(Customer customer, float amount) { customer.pay(amount); } } ``` 在修改後的程式碼中,`Seller` 只知道 `Customer` 擁有 `pay` 的能力,而不需要知道 `Customer` 是如何支付的。這就是遵守德米特法則的程式設計。 ## 活躍記錄設計模式(Active-Record)的限制和陷阱是什麼?  (圖片取自於[Active Record vs Data Mapper](https://www.infinitypp.com/software-patterns/activerecord-vs-datamapper-pattern-php-laravel/)) 活躍記錄設計模式(Active Record Pattern)是一種常見的數據訪問模式,對象的屬性對應於數據庫中的列,對象的實例對應於數據庫中的行。這種模式的名稱源於`Martin Fowler`在其著作[Patterns of Enterprise Application Architecture](https://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420)中的描述。 活躍記錄模式對於執行簡單CRUD(創建、讀取、更新、刪除)操作來說非常直觀和方便,可以減少大量的冗餘代碼。然而,這種模式也有一些限制和陷阱: * 耦合度高:活躍記錄模式將數據訪問邏輯與業務邏輯緊密繫結在一起,導致高度的耦合。這使得在不影響現有代碼的情況下修改或擴展應用變得困難。 * 適應性差:對於複雜的查詢或需要定制的數據訪問邏輯,活躍記錄模式可能不太適合。當業務邏輯變得越來越複雜時,使用活躍記錄模式可能導致代碼難以理解和維護。 * 單一責任原則:活躍記錄模式違反了單一責任原則(Single Responsibility Principle)。這是因為每個活躍記錄對象不僅負責數據訪問邏輯,還負責業務邏輯。這導致對象過於複雜,難以測試和維護。 ## 資料映射器與活躍記錄模式,你對這兩種模式有何看法?你會在何種情況下選擇其中一種? 資料映射器(Data Mapper)和活躍記錄(Active Record)都是處理對象和數據庫之間關係的模式,但它們的方法和適用場景有所不同。 資料映射器是一種數據訪問層,它能夠在你的對象和數據庫之間進行數據傳輸,並保持它們彼此獨立。這意味著你的對象(或稱為實體)並不需要知道數據庫,也不需要包含與數據庫交互的任何代碼。資料映射器負責讀取數據庫並將數據傳輸到對象中,反之亦然。 相比之下,活躍記錄模式在每個對象(或稱為模型)中都包含了存儲、更新、獲取和刪除數據的方法。這些對象基本上是數據庫表的直接表現,通過對象的方法可以直接操作數據庫。 選擇何種模式取決於你的具體需求: * 如果你的應用程序需要處理的業務邏輯複雜,或者需要清楚地將業務邏輯與數據訪問邏輯分離,那麼資料映射器模式可能是更好的選擇。資料映射器允許你將數據訪問邏輯封裝在映射器中,使得業務邏輯不受資料庫結構或特定的SQL查詢的影響。 * 如果你的應用程序比較簡單,或者你希望能夠快速地開發出一個原型,那麼活躍記錄模式可能更適合。活躍記錄由於簡單易用,且對於簡單的CRUD操作非常直觀,常常被選擇用於快速開發。 請注意這兩種模式並不是互相排斥的,有時在同一個應用程序中混合使用也是可以的。 ## 十億美元的錯誤,你能討論避免這種問題的技術,比如 GOF 書中介紹的 Null 物件模式,或者選擇類型 (Option types) 嗎? `Tony Hoare` 是 `null` 參考的發明者,他曾經說過 **我稱之為我十億美元的錯誤**,因為它導致了 **無數的錯誤,漏洞,和系統崩潰,這在過去的四十年裡可能已經造成了十億美元的痛苦和損失**。 `Null` 參考是一種常見的導致錯誤的原因,特別是在面向對象的程式語言中,因為它們允許變量沒有參考任何物件。這個問題的一個常見結果是 `Null` 參考異常,這是當我們嘗試訪問一個 `Null` 對象的成員時發生的。 以下是一些常見的處理 `Null` 參考的策略: 1. Null 物件模式(Null Object Pattern):這種模式下,Null 不再是一個代表無、空或者非的東西,而是代表一種特定行為的對象。舉例來說,如果你有一個代表動物的類別,並且該類別有一個叫做 makeNoise() 的方法。在一般情況下,你可能需要檢查這個物件是否為 Null,然後再決定是否要呼叫 makeNoise()。然而,如果你使用 Null 物件模式,你可以創建一個靜默的動物類別,並且在這個類別中重寫 makeNoise() 方法使得它不做任何事情。這樣的話,你就可以在任何情況下調用 makeNoise(),而不需要擔心 Null 參考異常。 1. 選擇類型(Option Types):這是在許多函數式程式語言中使用的策略,它使得函數可以顯示地回傳一個可能為 Null 的結果。選擇類型有兩種可能的狀態:有值(Some)和無值(None)。這強制了程式設計師在操作這個結果之前,必須先檢查它是否有值。這與 Null 物件模式不同的地方在於,它強調的是使能夠為 Null 的情況顯式化,而不是隱藏它。 以下是一個使用 `Java Optionals` 的範例(`Java` 中的選擇類型): ```java= Optional<String> optional = getOptionalString(); if (optional.isPresent()) { String value = optional.get(); // 使用 value 的代碼... } else { // 當 value 為 Null 時的代碼... } ``` 這種情況下,當 `getOptionalString()` 返回一個沒有值的 `Optional` 時,並不會出現 `Null` 參考異常。相反,我們明確地檢查了 `Optional` 是否有值,並根據這個結果做出決策。這樣可以防止不經意地引入 `Null` 參考,並導致程式在運行時崩潰。 雖然這些策略不能完全消除 `Null` 參考,但它們可以幫助我們更安全地處理可能為 `Null` 的情況,並減少由此產生的錯誤。 ## 為什麼組合(Composition) vs. 繼承(Inheritance) 繼承和組合都是面向對象編程中用於重用程式碼的兩種重要技術。然而,這兩種技術各有優劣和使用場景。 繼承是一種表示**是一種**關係的方式,例如**貓是一種動物**。使用繼承時,子類別會繼承父類別的所有屬性和方法,並且可以新增或覆蓋父類別的方法。繼承的優點是可以簡單地重用和擴展程式碼,但是過度使用繼承可能導致程式碼結構複雜,並產生僵化的階層結構。 組合則是表示**有一個**關係,例如**汽車有一個引擎**。組合允許你建立複雜的物件,這些物件由其他更簡單的物件組成。這樣可以使各個物件更加獨立,每個物件都有特定的職責,並且可以獨立地更改或重用。組合的優點是增強了程式的靈活性,減少了關係的緊密度,並有助於遵守**單一職責原則**。 在實際的程式設計中,繼承和組合並不是相互排斥的,而是可以共同使用的。選擇何時使用繼承或組合取決於具體的需求和上下文。然而,有一句常見的軟體工程格言是:**優先使用組合,而非繼承**。這是因為過度依賴繼承往往導致較差的可維護性和靈活性,而組合則可以提供更高的模組化和可重用性。 ## 反腐敗層是什麼? 反腐敗層(Anti-corruption Layer,ACL)是一種設計模式,主要用於防止一個系統的設計模式、風格或其他特性影響另一個系統。這在使用微服務、服務導向架構(SOA)或與舊系統互動時特別有用。 反腐敗層的主要作用是作為兩個系統之間的轉換器或者說是適配器。它負責將一個系統的請求轉換成另一個系統可以理解的形式,並且將回應再進行轉換。這樣,每個系統可以根據自己的模型和規則運行,而不需要關心其他系統的內部結構。 例如,假設你有一個現代的、使用 `RESTful API` 的應用程式,需要與一個遺留系統互動,這個遺留系統使用完全不同的數據格式和通信協議。在這種情況下,你可以建立一個反腐敗層,將現代應用的請求轉換為遺留系統可以理解的形式,並將遺留系統的回應轉換為現代應用可以處理的形式。 這種方式允許你保護你的應用程式免受遺留系統的影響,並且可以讓你在不影響現有應用的情況下,逐步升級或替換遺留系統。這就是為什麼它被稱為**反腐敗**層:它防止了腐敗的設計污染你的應用程式。 ## 單例設計模式 單例是一種設計模式,限制一個類別只能有一個實例,並提供一個全域的訪問點。 要寫一個線程安全的單例類別並非顯而易見的事情,但以下是一個簡單的例子: ```java= public class Singleton { private static volatile Singleton instance = null; private Singleton() { // 防止通過反射來實例化 if (instance != null) { throw new RuntimeException("Use getInstance() method to get the single instance of this class."); } } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } } ``` 上述程式碼中的 `Singleton` 類別就是一個單例設計模式的實現。這裡使用了一種稱為 **double-checked locking**的技巧,來確保只有在第一次訪問時才創建單例。這種方法可以減少同步的開銷,提高效能。 此外,為了避免透過反射來創建新的實例,我們在建構函數中檢查實例是否已經存在。如果存在,則拋出異常。 注意,這只是單例模式的一種實現方式,還有其他的方法可以實現單例,具體的實現方式可能會因語言和應用場景的不同而異。 ## 資料抽象是什麼? 資料抽象是一種強調將實現細節隱藏,僅暴露出與操作數據相關的方法與介面的程式設計技巧。資料抽象能讓開發者改變內部實現而不影響使用者的使用,同時也提供了更好的代碼結構和更低的維護成本。 以下是一個違反資料抽象原則的例子,以及修正之後的程式碼: 違反資料抽象原則的程式碼: ```java= public class Circle { public double radius; public Circle(double radius) { this.radius = radius; } public double calculateArea() { return Math.PI * radius * radius; } } ``` 在這個例子中,`Circle` 類別的 `radius` 屬性是 `public` 的,這意味著外部的代碼可以直接存取和修改 `radius`。這就違反了資料抽象原則,因為這允許外部代碼可以隨意改變 `radius` 的值,可能導致不可預期的結果。 修正後的程式碼: ```java= public cla```java=Circle { private double radius; public Circle(double radius) { this.radius = radius; } public double getRadius() { return radius; } public void setRadius(double radius) { this.radius = radius; } public double calculateArea() { return Math.PI * radius * radius; } } ``` 在修正後的程式碼中,我們將 `radius` 屬性改為 `private`,並提供了公開的 `getter` 和 `setter` 方法來讀取和設定 `radius` 的值。這樣,我們就能控制對 `radius` 的讀取和設定,確保其值的正確性,並且如果我們決定改變 `radius` 的內部表示方式(比如從直徑改為半徑),我們也只需要改變 `getter` 和 `setter` 的實現,而不需要修改使用 `Circle` 類別的代碼。 ## 違反 Don't Repeat Yourself(DRY)原則的程式碼 `Don't Repeat Yourself(DRY)`原則是一種程式設計理念,主張每一個資訊都應該在系統中有一個唯一、明確、權威的表示。違反 `DRY` 原則會導致代碼的重複,這不利於維護和擴展。 以下是一個違反 `DRY` 原則的程式碼: ```java= public void processStudentGrades() { List<Student> students = getStudents(); for(Student student: students) { double gradeSum = 0; for(Grade grade: student.getGrades()) { gradeSum += grade.getValue(); } double average = gradeSum / student.getGrades().size(); student.setAverageGrade(average); } } public void processTeacherGrades() { List<Teacher> teachers = getTeachers(); for(Teacher teacher: teachers) { double gradeSum = 0; for(Grade grade: teacher.getGrades()) { gradeSum += grade.getValue(); } double average = gradeSum / teacher.getGrades().size(); teacher.setAverageGrade(average); } } ``` 你可以看到 `processStudentGrades` 和 `processTeacherGrades` 方法中的計算平均分數的邏輯重複了。這違反了 `DRY` 原則,因為如果我們需要改變計算平均分數的方式,我們需要在兩個地方修改。 這個問題可以通過創建一個公共方法來計算平均分數,並在 `processStudentGrades` 和 `processTeacherGrades` 方法中調用它來解決: 修正後的程式碼: ```java= public void processStudentGrades() { List<Student> students = getStudents(); for(Student student: students) { student.setAverageGrade(calculateAverage(student.getGrades())); } } public void processTeacherGrades() { List<Teacher> teachers = getTeachers(); for(Teacher teacher: teachers) { teacher.setAverageGrade(calculateAverage(teacher.getGrades())); } } public double calculateAverage(List<Grade> grades) { double gradeSum = 0; for(Grade grade: grades) { gradeSum += grade.getValue(); } return gradeSum / grades.size(); } ``` 在這個修正後的程式碼中,`calculateAverage` 方法將平均分數的計算邏輯封裝了起來,避免了重複的代碼。 ## 如何處理依賴地獄(Dependency Hell)? **依賴地獄**是一個術語,指的是在軟體開發中遇到的一種常見問題,即一個項目對許多其他包或軟體庫的依賴性產生複雜的、相互矛盾的依賴關係。這可能導致包版本衝突,使得開發和部署變得困難。 以下是一些處理依賴地獄的策略: 1. **版本管理**:使用一個能夠處理軟體版本的包管理工具,如 `NPM、pip` 或 `Maven` 等,可以幫助我們解決版本衝突並保證我們的應用使用正確的庫版本。 1. **隔離環境**:使用虛擬環境來隔離項目的依賴,以避免不同項目的依賴衝突。`Python` 的 `virtualenv`、`Node.js` 的 `Docker` 和 `Java` 的 `Maven` 都提供了創建隔離環境的能力。 1. **最小化依賴**:只引入你真正需要的依賴,並定期檢查並刪除不再需要的依賴。這不僅能減少出現版本衝突的可能性,還能降低維護的複雜性並改善項目的安全性。 1. **固定依賴版本**:如果可能,固定你的依賴版本。這能防止新版本的庫突然引入不兼容的更改。 1. **持續整合**:使用持續整合工具(如 `Jenkins`、`Travis CI` 等)來自動構建和測試你的項目,可以更早地發現和解決依賴問題。 1. **定期更新依賴**:舊版本的庫可能存在已知的安全漏洞和已經被修復的錯誤。定期更新依賴庫可以讓你的項目受益於這些修復,並防止依賴庫過於過時,從而導致升級變得困難。 ## 堅固性原則是一種通用的軟體設計指南,推薦 **在你發送的訊息上保守,在你接受的訊息上開放**。它通常被重新表述為 **成為一個寬容的讀者和一個謹慎的作者**。你能討論這個原則的理念嗎? 堅固性原則,也稱為 `Postel's Law`,是一種設計網路服務和軟體的重要指導原則,其中 **保守地發送,開放地接受**這句話意味著: * 保守地發送:當你的應用程式或服務與其他系統進行通信時,發送的數據必須嚴格遵守協議規範,不允許有任何模糊性或疏漏。這樣做可以確保其他系統可以正確理解和處理你的數據。 * 開放地接受:當你的應用程式或服務收到其他系統的數據時,應該盡可能地接受和處理它,即使它並不完全符合協議的規範。這樣做可以提高系統的彈性和互操作性,並提高錯誤恢復的能力。 這個原則的目的是為了提高軟體和網路服務的互操作性和堅固性。然而,這個原則也有一些批評,主要是因為如果過度的寬容,可能會導致規範的混亂和不正確的實現被濫用。因此,當實踐這個原則時,需要尋找適當的平衡點,尊重規範的同時,也提供足夠的彈性以應對不可預見的情況。 ## 關注點分離是將計算機程序分解成不同區域的設計原則,每個區域都處理一個單獨的關注點。實現關注點分離有許多不同的機制(如使用物件、函數、模組,或者如 MVC 這樣的設計模式)。你能討論一下這個主題嗎? 關注點分離(Separation of Concerns, SoC)是一種設計原則,用於將一個程式分解成幾個各自獨立的部分,每個部分都關注於一個單一的任務或負責一個特定的功能。這個原則的目的是為了降低複雜性,提高可讀性,並且使得模組或者組件更易於重複使用和維護。 例如,如果你正在開發一個網路應用,你可能會遵循 MVC(Model-View-Controller)設計模式來將你的應用分解成三個部分: * Model(模型):負責管理應用的數據和業務邏輯。這是應用的核心部分,與使用者介面和數據呈現無關。 * View(視圖):負責展示數據給使用者。它通常會使用模型的數據來生成用戶界面。 * Controller(控制器):處理用戶與視圖的互動,並更新模型以反映這些變更。 每個部分都只關注於一個特定的任務,使得整個應用變得更易於理解和維護。在這種設計中,每個部分都可以獨立於其他部分進行變更,這也使得測試和重複使用各部分變得更容易。 在實際開發過程中,`SoC` 原則可以通過多種方式實現,包括物件導向設計、函數式編程、模組化設計等。通過適當的分離關注點,我們可以創建出結構清晰、可維護性強、易於擴展和測試的系統。 ## goto 是邪惡的嗎? 你可能已經聽說過 `Edsger Dijkstra` 的著名論文 `Go To Statement Considered Harmful`,在其中他批評了 `goto` 語句的使用,並倡導結構化程式設計。`goto` 的使用一直以來都是有爭議的,以至於連 `Dijkstra` 的信都被批評,出現了類似 **'GOTO Considered Harmful' Considered Harmful**這樣的文章。你對 `goto` 的使用有什麼看法? 在現代程式設計中,`goto` 語句的使用通常被認為是一種壞的程式碼風格,這是因為它可以導致流程控制的混亂,使程式碼難以閱讀和理解。它能夠讓程式跳到程式中的任何一點,這種非線性的控制流可能導致複雜且難以預測的行為。 然而,這並不意味著 `goto` 總是邪惡的,或者在所有情況下都應該避免使用。在一些特定的情況下,`goto` 可能是清楚表示某種特定控制流的最簡單的方式。例如,在 `C` 語言中,`goto` 可能會被用來從深層的巢狀結構中跳出,或者在出錯時跳到錯誤處理的程式碼段。 雖然 `goto` 有其用處,但是在大多數情況下,我們應該首先考慮其他的控制流結構,例如 `if`,`for`,`while`,`switch` 等。只有當這些結構不能滿足我們的需求時,我們才應該考慮使用 `goto`。 此外,我們應該清楚,使用 `goto` 的代價是使得我們的程式碼更難理解和維護。所以,如果選擇使用 `goto`,我們需要確保我們的程式碼仍然保持清晰和有序。
×
Sign in
Email
Password
Forgot password
or
By clicking below, you agree to our
terms of service
.
Sign in via Facebook
Sign in via Twitter
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
New to HackMD?
Sign up