全域變數和靜態對象經常被視為不良的程式設計實踐,因為他們讓應用程序的各個部分的相依性增加。全域變數可以在程式的任何地方被改變,這使得程式的狀態變得難以追蹤和控制,因此更容易引入錯誤和不預期的行為。
以下是一個簡單的全域變數的例子:
# 全域變數
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 Pattern)是一種常見的數據訪問模式,對象的屬性對應於數據庫中的列,對象的實例對應於數據庫中的行。這種模式的名稱源於Martin Fowler
在其著作Patterns of Enterprise Application Architecture中的描述。
活躍記錄模式對於執行簡單CRUD(創建、讀取、更新、刪除)操作來說非常直觀和方便,可以減少大量的冗餘代碼。然而,這種模式也有一些限制和陷阱:
資料映射器(Data Mapper)和活躍記錄(Active Record)都是處理對象和數據庫之間關係的模式,但它們的方法和適用場景有所不同。
資料映射器是一種數據訪問層,它能夠在你的對象和數據庫之間進行數據傳輸,並保持它們彼此獨立。這意味著你的對象(或稱為實體)並不需要知道數據庫,也不需要包含與數據庫交互的任何代碼。資料映射器負責讀取數據庫並將數據傳輸到對象中,反之亦然。
相比之下,活躍記錄模式在每個對象(或稱為模型)中都包含了存儲、更新、獲取和刪除數據的方法。這些對象基本上是數據庫表的直接表現,通過對象的方法可以直接操作數據庫。
選擇何種模式取決於你的具體需求:
請注意這兩種模式並不是互相排斥的,有時在同一個應用程序中混合使用也是可以的。
Tony Hoare
是 null
參考的發明者,他曾經說過 我稱之為我十億美元的錯誤,因為它導致了 無數的錯誤,漏洞,和系統崩潰,這在過去的四十年裡可能已經造成了十億美元的痛苦和損失。
Null
參考是一種常見的導致錯誤的原因,特別是在面向對象的程式語言中,因為它們允許變量沒有參考任何物件。這個問題的一個常見結果是 Null
參考異常,這是當我們嘗試訪問一個 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
的情況,並減少由此產生的錯誤。
繼承和組合都是面向對象編程中用於重用程式碼的兩種重要技術。然而,這兩種技術各有優劣和使用場景。
繼承是一種表示是一種關係的方式,例如貓是一種動物。使用繼承時,子類別會繼承父類別的所有屬性和方法,並且可以新增或覆蓋父類別的方法。繼承的優點是可以簡單地重用和擴展程式碼,但是過度使用繼承可能導致程式碼結構複雜,並產生僵化的階層結構。
組合則是表示有一個關係,例如汽車有一個引擎。組合允許你建立複雜的物件,這些物件由其他更簡單的物件組成。這樣可以使各個物件更加獨立,每個物件都有特定的職責,並且可以獨立地更改或重用。組合的優點是增強了程式的靈活性,減少了關係的緊密度,並有助於遵守單一職責原則。
在實際的程式設計中,繼承和組合並不是相互排斥的,而是可以共同使用的。選擇何時使用繼承或組合取決於具體的需求和上下文。然而,有一句常見的軟體工程格言是:優先使用組合,而非繼承。這是因為過度依賴繼承往往導致較差的可維護性和靈活性,而組合則可以提供更高的模組化和可重用性。
反腐敗層(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
,並提供了公開的 getter
和 setter
方法來讀取和設定 radius
的值。這樣,我們就能控制對 radius
的讀取和設定,確保其值的正確性,並且如果我們決定改變 radius
的內部表示方式(比如從直徑改為半徑),我們也只需要改變 getter
和 setter
的實現,而不需要修改使用 Circle
類別的代碼。
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);
}
}
你可以看到 processStudentGrades
和 processTeacherGrades
方法中的計算平均分數的邏輯重複了。這違反了 DRY
原則,因為如果我們需要改變計算平均分數的方式,我們需要在兩個地方修改。
這個問題可以通過創建一個公共方法來計算平均分數,並在 processStudentGrades
和 processTeacherGrades
方法中調用它來解決:
修正後的程式碼:
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
方法將平均分數的計算邏輯封裝了起來,避免了重複的代碼。
依賴地獄是一個術語,指的是在軟體開發中遇到的一種常見問題,即一個項目對許多其他包或軟體庫的依賴性產生複雜的、相互矛盾的依賴關係。這可能導致包版本衝突,使得開發和部署變得困難。
以下是一些處理依賴地獄的策略:
NPM、pip
或 Maven
等,可以幫助我們解決版本衝突並保證我們的應用使用正確的庫版本。Python
的 virtualenv
、Node.js
的 Docker
和 Java
的 Maven
都提供了創建隔離環境的能力。Jenkins
、Travis CI
等)來自動構建和測試你的項目,可以更早地發現和解決依賴問題。堅固性原則,也稱為 Postel's Law
,是一種設計網路服務和軟體的重要指導原則,其中 保守地發送,開放地接受這句話意味著:
這個原則的目的是為了提高軟體和網路服務的互操作性和堅固性。然而,這個原則也有一些批評,主要是因為如果過度的寬容,可能會導致規範的混亂和不正確的實現被濫用。因此,當實踐這個原則時,需要尋找適當的平衡點,尊重規範的同時,也提供足夠的彈性以應對不可預見的情況。
關注點分離(Separation of Concerns, SoC)是一種設計原則,用於將一個程式分解成幾個各自獨立的部分,每個部分都關注於一個單一的任務或負責一個特定的功能。這個原則的目的是為了降低複雜性,提高可讀性,並且使得模組或者組件更易於重複使用和維護。
例如,如果你正在開發一個網路應用,你可能會遵循 MVC(Model-View-Controller)設計模式來將你的應用分解成三個部分:
每個部分都只關注於一個特定的任務,使得整個應用變得更易於理解和維護。在這種設計中,每個部分都可以獨立於其他部分進行變更,這也使得測試和重複使用各部分變得更容易。
在實際開發過程中,SoC
原則可以通過多種方式實現,包括物件導向設計、函數式編程、模組化設計等。通過適當的分離關注點,我們可以創建出結構清晰、可維護性強、易於擴展和測試的系統。
你可能已經聽說過 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
,我們需要確保我們的程式碼仍然保持清晰和有序。