Try   HackMD
文章目錄

請用一個例子說明,全局對像是邪惡的存在。

全域變數和靜態對象經常被視為不良的程式設計實踐,因為他們讓應用程序的各個部分的相依性增加。全域變數可以在程式的任何地方被改變,這使得程式的狀態變得難以追蹤和控制,因此更容易引入錯誤和不預期的行為

以下是一個簡單的全域變數的例子:

# 全域變數 count = 0 def increment(): global count count += 1 def decrement(): global count count -= 1

在此例中,count 變數可以在兩個函式 increment()decrement() 之間共享。但問題在於,如果其他函式或程式碼也能存取或修改 count 變數,程式的行為就會變得非常難以預測。對於大型程式來說,使用全域變數會使得程式的維護變得非常困難。

為了避免這樣的問題,一種常見的解決方案是使用類別或物件來封裝資料和相關的行為,如下所示:

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 的例子。假設我們在寫一個應用程式,並需要從不同的資料源讀取資料:

class MyApp: def get_data(self): db = Database() data = db.read_data() return data

在上述的例子中,MyApp 類別直接依賴於 Database 類別,因此他們是耦合的。如果我們想要更換資料源或者在測試時使用模擬的資料源,我們必須修 改 MyApp 類別的內部實現,這會導致程式碼的維護變得困難。

相較之下,如果我們使用反轉控制,我們可以讓 MyApp 類別在運行時才決定使用哪一種資料源,並且讓這些資料源作為參數傳遞給 MyApp。這使得 MyApp 類別和具體的資料源解耦,如下所示:

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),也稱為「最少知識原則」,是一種軟體開發中的設計原則。該原則認為一個物件應該只對其直接的朋友保持有限的知識,而不應該對整個系統都了如指掌,也就是說,一個物件應該只與其直接關聯的物件進行交互。

違反德米特法則會導致程式碼之間的耦合度增加,系統的模組之間的獨立性降低,影響程式碼的可維護性和可變更性。

例如,一個違反德米特法則的狀況可能如下:

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 對象的方法,為了遵守德米特法則,我們應該這樣修改:

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)的限制和陷阱是什麼?

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

(圖片取自於Active Record vs Data Mapper)

活躍記錄設計模式(Active Record Pattern)是一種常見的數據訪問模式,對象的屬性對應於數據庫中的列,對象的實例對應於數據庫中的行。這種模式的名稱源於Martin Fowler在其著作Patterns of Enterprise Application Architecture中的描述。

活躍記錄模式對於執行簡單CRUD(創建、讀取、更新、刪除)操作來說非常直觀和方便,可以減少大量的冗餘代碼。然而,這種模式也有一些限制和陷阱:

  • 耦合度高:活躍記錄模式將數據訪問邏輯與業務邏輯緊密繫結在一起,導致高度的耦合。這使得在不影響現有代碼的情況下修改或擴展應用變得困難。
  • 適應性差:對於複雜的查詢或需要定制的數據訪問邏輯,活躍記錄模式可能不太適合。當業務邏輯變得越來越複雜時,使用活躍記錄模式可能導致代碼難以理解和維護。
  • 單一責任原則:活躍記錄模式違反了單一責任原則(Single Responsibility Principle)。這是因為每個活躍記錄對象不僅負責數據訪問邏輯,還負責業務邏輯。這導致對象過於複雜,難以測試和維護。

資料映射器與活躍記錄模式,你對這兩種模式有何看法?你會在何種情況下選擇其中一種?

資料映射器(Data Mapper)和活躍記錄(Active Record)都是處理對象和數據庫之間關係的模式,但它們的方法和適用場景有所不同。

資料映射器是一種數據訪問層,它能夠在你的對象和數據庫之間進行數據傳輸,並保持它們彼此獨立。這意味著你的對象(或稱為實體)並不需要知道數據庫,也不需要包含與數據庫交互的任何代碼。資料映射器負責讀取數據庫並將數據傳輸到對象中,反之亦然。

相比之下,活躍記錄模式在每個對象(或稱為模型)中都包含了存儲、更新、獲取和刪除數據的方法。這些對象基本上是數據庫表的直接表現,通過對象的方法可以直接操作數據庫。

選擇何種模式取決於你的具體需求:

  • 如果你的應用程序需要處理的業務邏輯複雜,或者需要清楚地將業務邏輯與數據訪問邏輯分離,那麼資料映射器模式可能是更好的選擇。資料映射器允許你將數據訪問邏輯封裝在映射器中,使得業務邏輯不受資料庫結構或特定的SQL查詢的影響。
  • 如果你的應用程序比較簡單,或者你希望能夠快速地開發出一個原型,那麼活躍記錄模式可能更適合。活躍記錄由於簡單易用,且對於簡單的CRUD操作非常直觀,常常被選擇用於快速開發。

請注意這兩種模式並不是互相排斥的,有時在同一個應用程序中混合使用也是可以的。

十億美元的錯誤,你能討論避免這種問題的技術,比如 GOF 書中介紹的 Null 物件模式,或者選擇類型 (Option types) 嗎?

Tony Hoarenull 參考的發明者,他曾經說過 我稱之為我十億美元的錯誤,因為它導致了 無數的錯誤,漏洞,和系統崩潰,這在過去的四十年裡可能已經造成了十億美元的痛苦和損失

Null 參考是一種常見的導致錯誤的原因,特別是在面向對象的程式語言中,因為它們允許變量沒有參考任何物件。這個問題的一個常見結果是 Null 參考異常,這是當我們嘗試訪問一個 Null 對象的成員時發生的。

以下是一些常見的處理 Null 參考的策略:

  1. Null 物件模式(Null Object Pattern):這種模式下,Null 不再是一個代表無、空或者非的東西,而是代表一種特定行為的對象。舉例來說,如果你有一個代表動物的類別,並且該類別有一個叫做 makeNoise() 的方法。在一般情況下,你可能需要檢查這個物件是否為 Null,然後再決定是否要呼叫 makeNoise()。然而,如果你使用 Null 物件模式,你可以創建一個靜默的動物類別,並且在這個類別中重寫 makeNoise() 方法使得它不做任何事情。這樣的話,你就可以在任何情況下調用 makeNoise(),而不需要擔心 Null 參考異常。
  2. 選擇類型(Option Types):這是在許多函數式程式語言中使用的策略,它使得函數可以顯示地回傳一個可能為 Null 的結果。選擇類型有兩種可能的狀態:有值(Some)和無值(None)。這強制了程式設計師在操作這個結果之前,必須先檢查它是否有值。這與 Null 物件模式不同的地方在於,它強調的是使能夠為 Null 的情況顯式化,而不是隱藏它。

以下是一個使用 Java Optionals 的範例(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 的應用程式,需要與一個遺留系統互動,這個遺留系統使用完全不同的數據格式和通信協議。在這種情況下,你可以建立一個反腐敗層,將現代應用的請求轉換為遺留系統可以理解的形式,並將遺留系統的回應轉換為現代應用可以處理的形式。

這種方式允許你保護你的應用程式免受遺留系統的影響,並且可以讓你在不影響現有應用的情況下,逐步升級或替換遺留系統。這就是為什麼它被稱為反腐敗層:它防止了腐敗的設計污染你的應用程式。

單例設計模式

單例是一種設計模式,限制一個類別只能有一個實例,並提供一個全域的訪問點。

要寫一個線程安全的單例類別並非顯而易見的事情,但以下是一個簡單的例子:

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的技巧,來確保只有在第一次訪問時才創建單例。這種方法可以減少同步的開銷,提高效能。

此外,為了避免透過反射來創建新的實例,我們在建構函數中檢查實例是否已經存在。如果存在,則拋出異常。

注意,這只是單例模式的一種實現方式,還有其他的方法可以實現單例,具體的實現方式可能會因語言和應用場景的不同而異。

資料抽象是什麼?

資料抽象是一種強調將實現細節隱藏,僅暴露出與操作數據相關的方法與介面的程式設計技巧。資料抽象能讓開發者改變內部實現而不影響使用者的使用,同時也提供了更好的代碼結構和更低的維護成本。

以下是一個違反資料抽象原則的例子,以及修正之後的程式碼:

違反資料抽象原則的程式碼:

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 的值,可能導致不可預期的結果。

修正後的程式碼:

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,並提供了公開的 gettersetter 方法來讀取和設定 radius 的值。這樣,我們就能控制對 radius 的讀取和設定,確保其值的正確性,並且如果我們決定改變 radius 的內部表示方式(比如從直徑改為半徑),我們也只需要改變 gettersetter 的實現,而不需要修改使用 Circle 類別的代碼。

違反 Don't Repeat Yourself(DRY)原則的程式碼

Don't Repeat Yourself(DRY)原則是一種程式設計理念,主張每一個資訊都應該在系統中有一個唯一、明確、權威的表示。違反 DRY 原則會導致代碼的重複,這不利於維護和擴展。

以下是一個違反 DRY 原則的程式碼:

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); } }

你可以看到 processStudentGradesprocessTeacherGrades 方法中的計算平均分數的邏輯重複了。這違反了 DRY 原則,因為如果我們需要改變計算平均分數的方式,我們需要在兩個地方修改。

這個問題可以通過創建一個公共方法來計算平均分數,並在 processStudentGradesprocessTeacherGrades 方法中調用它來解決:

修正後的程式碼:

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、pipMaven 等,可以幫助我們解決版本衝突並保證我們的應用使用正確的庫版本。
  2. 隔離環境:使用虛擬環境來隔離項目的依賴,以避免不同項目的依賴衝突。PythonvirtualenvNode.jsDockerJavaMaven 都提供了創建隔離環境的能力。
  3. 最小化依賴:只引入你真正需要的依賴,並定期檢查並刪除不再需要的依賴。這不僅能減少出現版本衝突的可能性,還能降低維護的複雜性並改善項目的安全性。
  4. 固定依賴版本:如果可能,固定你的依賴版本。這能防止新版本的庫突然引入不兼容的更改。
  5. 持續整合:使用持續整合工具(如 JenkinsTravis CI 等)來自動構建和測試你的項目,可以更早地發現和解決依賴問題。
  6. 定期更新依賴:舊版本的庫可能存在已知的安全漏洞和已經被修復的錯誤。定期更新依賴庫可以讓你的項目受益於這些修復,並防止依賴庫過於過時,從而導致升級變得困難。

堅固性原則是一種通用的軟體設計指南,推薦 在你發送的訊息上保守,在你接受的訊息上開放。它通常被重新表述為 成為一個寬容的讀者和一個謹慎的作者。你能討論這個原則的理念嗎?

堅固性原則,也稱為 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 有其用處,但是在大多數情況下,我們應該首先考慮其他的控制流結構,例如 ifforwhileswitch 等。只有當這些結構不能滿足我們的需求時,我們才應該考慮使用 goto

此外,我們應該清楚,使用 goto 的代價是使得我們的程式碼更難理解和維護。所以,如果選擇使用 goto,我們需要確保我們的程式碼仍然保持清晰和有序。